From d604f9adfb265b9311c2b66a2027256e9cd8b064 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 13 Feb 2019 18:18:47 -0800 Subject: [PATCH 001/446] Add AudioSoloRequest, BulkAvatarTraitsAck; also decode obfuscated protocols --- tools/dissectors/1-hfudt.lua | 98 ++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/tools/dissectors/1-hfudt.lua b/tools/dissectors/1-hfudt.lua index de99c1ce3c..8179276dbb 100644 --- a/tools/dissectors/1-hfudt.lua +++ b/tools/dissectors/1-hfudt.lua @@ -152,7 +152,9 @@ local packet_types = { [97] = "OctreeDataPersist", [98] = "EntityClone", [99] = "EntityQueryInitialResultsComplete", - [100] = "BulkAvatarTraits" + [100] = "BulkAvatarTraits", + [101] = "AudioSoloRequest", + [102] = "BulkAvatarTraitsAck" } local unsourced_packet_types = { @@ -301,55 +303,53 @@ function p_hfudt.dissector(buf, pinfo, tree) -- check if we have part of a message that we need to re-assemble -- before it can be dissected - if obfuscation_bits == 0 then - if message_bit == 1 and message_position ~= 0 then - if fragments[message_number] == nil then - fragments[message_number] = {} - end - - if fragments[message_number][message_part_number] == nil then - fragments[message_number][message_part_number] = {} - end - - -- set the properties for this fragment - fragments[message_number][message_part_number] = { - payload = buf(i):bytes() - } - - -- if this is the last part, set our maximum part number - if message_position == 1 then - fragments[message_number].last_part_number = message_part_number - end - - -- if we have the last part - -- enumerate our parts for this message and see if everything is present - if fragments[message_number].last_part_number ~= nil then - local i = 0 - local has_all = true - - local finalMessage = ByteArray.new() - local message_complete = true - - while i <= fragments[message_number].last_part_number do - if fragments[message_number][i] ~= nil then - finalMessage = finalMessage .. fragments[message_number][i].payload - else - -- missing this part, have to break until we have it - message_complete = false - end - - i = i + 1 - end - - if message_complete then - debug("Message " .. message_number .. " is " .. finalMessage:len()) - payload_to_dissect = ByteArray.tvb(finalMessage, message_number) - end - end - - else - payload_to_dissect = buf(i):tvb() + if message_bit == 1 and message_position ~= 0 then + if fragments[message_number] == nil then + fragments[message_number] = {} end + + if fragments[message_number][message_part_number] == nil then + fragments[message_number][message_part_number] = {} + end + + -- set the properties for this fragment + fragments[message_number][message_part_number] = { + payload = buf(i):bytes() + } + + -- if this is the last part, set our maximum part number + if message_position == 1 then + fragments[message_number].last_part_number = message_part_number + end + + -- if we have the last part + -- enumerate our parts for this message and see if everything is present + if fragments[message_number].last_part_number ~= nil then + local i = 0 + local has_all = true + + local finalMessage = ByteArray.new() + local message_complete = true + + while i <= fragments[message_number].last_part_number do + if fragments[message_number][i] ~= nil then + finalMessage = finalMessage .. fragments[message_number][i].payload + else + -- missing this part, have to break until we have it + message_complete = false + end + + i = i + 1 + end + + if message_complete then + debug("Message " .. message_number .. " is " .. finalMessage:len()) + payload_to_dissect = ByteArray.tvb(finalMessage, message_number) + end + end + + else + payload_to_dissect = buf(i):tvb() end if payload_to_dissect ~= nil then From 7446fa9e7eb81ae3476b70baea885518b6cc54a9 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 15 Feb 2019 11:21:06 -0800 Subject: [PATCH 002/446] allow mesh shapes to use MOTION_TYPE_KINEMATIC --- libraries/physics/src/EntityMotionState.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index ce9cb20c21..91c4c43c1d 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -205,6 +205,16 @@ PhysicsMotionType EntityMotionState::computePhysicsMotionType() const { if (_entity->getShapeType() == SHAPE_TYPE_STATIC_MESH || (_body && _body->getCollisionShape()->getShapeType() == TRIANGLE_MESH_SHAPE_PROXYTYPE)) { + if (_entity->isMoving()) { + // DANGER: Bullet doesn't support non-zero velocity for these shapes --> collision details may be wrong + // in ways that allow other DYNAMIC objects to tunnel/penetrate/snag. + // However, in practice low-velocity collisions work OK most of the time, and if we enforce these objects + // to be MOTION_TYPE_STATIC then some other bugs can be worse (e.g. when Grabbing --> Grab Action fails) + // so we're making a tradeoff here. + // TODO: The Correct Solution is to NOT use btBvhTriangleMesh shape for moving objects and instead compute the convex + // decomposition and build a btCompoundShape with convex sub-shapes. + return MOTION_TYPE_KINEMATIC; + } return MOTION_TYPE_STATIC; } From 304f993391b9f187e7b079b837fb570175485a62 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 15 Feb 2019 16:50:45 -0800 Subject: [PATCH 003/446] remove enforcement of MOTION_TYPE_STATIC for mesh shapes --- libraries/physics/src/EntityMotionState.cpp | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 91c4c43c1d..4d210c96c5 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -203,21 +203,6 @@ PhysicsMotionType EntityMotionState::computePhysicsMotionType() const { } assert(entityTreeIsLocked()); - if (_entity->getShapeType() == SHAPE_TYPE_STATIC_MESH - || (_body && _body->getCollisionShape()->getShapeType() == TRIANGLE_MESH_SHAPE_PROXYTYPE)) { - if (_entity->isMoving()) { - // DANGER: Bullet doesn't support non-zero velocity for these shapes --> collision details may be wrong - // in ways that allow other DYNAMIC objects to tunnel/penetrate/snag. - // However, in practice low-velocity collisions work OK most of the time, and if we enforce these objects - // to be MOTION_TYPE_STATIC then some other bugs can be worse (e.g. when Grabbing --> Grab Action fails) - // so we're making a tradeoff here. - // TODO: The Correct Solution is to NOT use btBvhTriangleMesh shape for moving objects and instead compute the convex - // decomposition and build a btCompoundShape with convex sub-shapes. - return MOTION_TYPE_KINEMATIC; - } - return MOTION_TYPE_STATIC; - } - if (_entity->getLocked()) { if (_entity->isMoving()) { return MOTION_TYPE_KINEMATIC; From 2fdc9bce7734ff32a424576bac580a3638b4d8c9 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 15 Feb 2019 16:59:18 -0800 Subject: [PATCH 004/446] remove velocity restrictions on SHAPE_TYPE_STATIC_MESH --- libraries/entities/src/EntityItem.cpp | 78 +++++++++++---------------- 1 file changed, 32 insertions(+), 46 deletions(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 2c6d679b46..049b46ec7e 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -1912,25 +1912,19 @@ void EntityItem::setRotation(glm::quat rotation) { void EntityItem::setVelocity(const glm::vec3& value) { glm::vec3 velocity = getLocalVelocity(); if (velocity != value) { - if (getShapeType() == SHAPE_TYPE_STATIC_MESH) { - if (velocity != Vectors::ZERO) { - setLocalVelocity(Vectors::ZERO); - } - } else { - float speed = glm::length(value); - if (!glm::isnan(speed)) { - const float MIN_LINEAR_SPEED = 0.001f; - const float MAX_LINEAR_SPEED = 270.0f; // 3m per step at 90Hz - if (speed < MIN_LINEAR_SPEED) { - velocity = ENTITY_ITEM_ZERO_VEC3; - } else if (speed > MAX_LINEAR_SPEED) { - velocity = (MAX_LINEAR_SPEED / speed) * value; - } else { - velocity = value; - } - setLocalVelocity(velocity); - _flags |= Simulation::DIRTY_LINEAR_VELOCITY; + float speed = glm::length(value); + if (!glm::isnan(speed)) { + const float MIN_LINEAR_SPEED = 0.001f; + const float MAX_LINEAR_SPEED = 270.0f; // 3m per step at 90Hz + if (speed < MIN_LINEAR_SPEED) { + velocity = ENTITY_ITEM_ZERO_VEC3; + } else if (speed > MAX_LINEAR_SPEED) { + velocity = (MAX_LINEAR_SPEED / speed) * value; + } else { + velocity = value; } + setLocalVelocity(velocity); + _flags |= Simulation::DIRTY_LINEAR_VELOCITY; } } } @@ -1948,19 +1942,15 @@ void EntityItem::setDamping(float value) { void EntityItem::setGravity(const glm::vec3& value) { withWriteLock([&] { if (_gravity != value) { - if (getShapeType() == SHAPE_TYPE_STATIC_MESH) { - _gravity = Vectors::ZERO; - } else { - float magnitude = glm::length(value); - if (!glm::isnan(magnitude)) { - const float MAX_ACCELERATION_OF_GRAVITY = 10.0f * 9.8f; // 10g - if (magnitude > MAX_ACCELERATION_OF_GRAVITY) { - _gravity = (MAX_ACCELERATION_OF_GRAVITY / magnitude) * value; - } else { - _gravity = value; - } - _flags |= Simulation::DIRTY_LINEAR_VELOCITY; + float magnitude = glm::length(value); + if (!glm::isnan(magnitude)) { + const float MAX_ACCELERATION_OF_GRAVITY = 10.0f * 9.8f; // 10g + if (magnitude > MAX_ACCELERATION_OF_GRAVITY) { + _gravity = (MAX_ACCELERATION_OF_GRAVITY / magnitude) * value; + } else { + _gravity = value; } + _flags |= Simulation::DIRTY_LINEAR_VELOCITY; } } }); @@ -1969,23 +1959,19 @@ void EntityItem::setGravity(const glm::vec3& value) { void EntityItem::setAngularVelocity(const glm::vec3& value) { glm::vec3 angularVelocity = getLocalAngularVelocity(); if (angularVelocity != value) { - if (getShapeType() == SHAPE_TYPE_STATIC_MESH) { - setLocalAngularVelocity(Vectors::ZERO); - } else { - float speed = glm::length(value); - if (!glm::isnan(speed)) { - const float MIN_ANGULAR_SPEED = 0.0002f; - const float MAX_ANGULAR_SPEED = 9.0f * TWO_PI; // 1/10 rotation per step at 90Hz - if (speed < MIN_ANGULAR_SPEED) { - angularVelocity = ENTITY_ITEM_ZERO_VEC3; - } else if (speed > MAX_ANGULAR_SPEED) { - angularVelocity = (MAX_ANGULAR_SPEED / speed) * value; - } else { - angularVelocity = value; - } - setLocalAngularVelocity(angularVelocity); - _flags |= Simulation::DIRTY_ANGULAR_VELOCITY; + float speed = glm::length(value); + if (!glm::isnan(speed)) { + const float MIN_ANGULAR_SPEED = 0.0002f; + const float MAX_ANGULAR_SPEED = 9.0f * TWO_PI; // 1/10 rotation per step at 90Hz + if (speed < MIN_ANGULAR_SPEED) { + angularVelocity = ENTITY_ITEM_ZERO_VEC3; + } else if (speed > MAX_ANGULAR_SPEED) { + angularVelocity = (MAX_ANGULAR_SPEED / speed) * value; + } else { + angularVelocity = value; } + setLocalAngularVelocity(angularVelocity); + _flags |= Simulation::DIRTY_ANGULAR_VELOCITY; } } } From 50a1e07ed275fec230f771663bea8536b81317bb Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 18 Feb 2019 18:32:49 +1300 Subject: [PATCH 005/446] Stub missing MyAvatar, Avatar, and Agent functions and properties JSDoc --- .../src/avatars/ScriptableAvatar.h | 3 + interface/src/avatar/MyAvatar.h | 76 ++++++++++++++----- .../src/avatars-renderer/Avatar.h | 6 ++ 3 files changed, 68 insertions(+), 17 deletions(-) diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h index e93be897d5..4562ad6134 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.h +++ b/assignment-client/src/avatars/ScriptableAvatar.h @@ -213,6 +213,9 @@ public: Q_INVOKABLE void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) override; public slots: + /**jsdoc + * @function MyAvatar.update + */ void update(float deltatime); /**jsdoc diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 984d7b297b..4e75c93403 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -69,6 +69,7 @@ class MyAvatar : public Avatar { * @hifi-avatar * * @property {Vec3} qmlPosition - A synonym for position for use by QML. + * * @property {boolean} shouldRenderLocally=true - If true then your avatar is rendered for you in Interface, * otherwise it is not rendered for you (but it is still rendered for other users). * @property {Vec3} motorVelocity=Vec3.ZERO - The target velocity of your avatar to be achieved by a scripted motor. @@ -90,29 +91,38 @@ class MyAvatar : public Avatar { * @property {number} audioListenerModeCamera=1 - The audio listening position is at the camera. Read-only. * @property {number} audioListenerModeCustom=2 - The audio listening position is at a the position specified by set by the * customListenPosition and customListenOrientation property values. Read-only. + * @property {Vec3} customListenPosition=Vec3.ZERO - The listening position used when the audioListenerMode + * property value is audioListenerModeCustom. + * @property {Quat} customListenOrientation=Quat.IDENTITY - The listening orientation used when the + * audioListenerMode property value is audioListenerModeCustom. * @property {boolean} hasScriptedBlendshapes=false - Blendshapes will be transmitted over the network if set to true. * @property {boolean} hasProceduralBlinkFaceMovement=true - procedural blinking will be turned on if set to true. * @property {boolean} hasProceduralEyeFaceMovement=true - procedural eye movement will be turned on if set to true. * @property {boolean} hasAudioEnabledFaceMovement=true - If set to true, voice audio will move the mouth Blendshapes while MyAvatar.hasScriptedBlendshapes is enabled. - * @property {Vec3} customListenPosition=Vec3.ZERO - The listening position used when the audioListenerMode - * property value is audioListenerModeCustom. - * @property {Quat} customListenOrientation=Quat.IDENTITY - The listening orientation used when the - * audioListenerMode property value is audioListenerModeCustom. + * @property {number} rotationRecenterFilterLength + * @property {number} rotationThreshold + * @property {boolean} enableStepResetRotation + * @property {boolean} enableDrawAverageFacing + * * @property {Vec3} leftHandPosition - The position of the left hand in avatar coordinates if it's being positioned by * controllers, otherwise {@link Vec3(0)|Vec3.ZERO}. Read-only. * @property {Vec3} rightHandPosition - The position of the right hand in avatar coordinates if it's being positioned by * controllers, otherwise {@link Vec3(0)|Vec3.ZERO}. Read-only. - * @property {Vec3} leftHandTipPosition - The position 30cm offset from the left hand in avatar coordinates if it's being * positioned by controllers, otherwise {@link Vec3(0)|Vec3.ZERO}. Read-only. * @property {Vec3} rightHandTipPosition - The position 30cm offset from the right hand in avatar coordinates if it's being * positioned by controllers, otherwise {@link Vec3(0)|Vec3.ZERO}. Read-only. + * * @property {Pose} leftHandPose - The pose of the left hand as determined by the hand controllers. Read-only. * @property {Pose} rightHandPose - The pose right hand position as determined by the hand controllers. Read-only. * @property {Pose} leftHandTipPose - The pose of the left hand as determined by the hand controllers, with the position * by 30cm. Read-only. * @property {Pose} rightHandTipPose - The pose of the right hand as determined by the hand controllers, with the position * by 30cm. Read-only. + * + * @property {number} energy + * @property {boolean} isAway + * * @property {boolean} centerOfGravityModelEnabled=true - If true then the avatar hips are placed according to the center of * gravity model that balance the center of gravity over the base of support of the feet. Setting the value false * will result in the default behaviour where the hips are placed under the head. @@ -122,30 +132,38 @@ class MyAvatar : public Avatar { * @property {boolean} collisionsEnabled - Set to true to enable collisions for the avatar, false * to disable collisions. May return true even though the value was set false because the * zone may disallow collisionless avatars. + * @property {boolean} otherAvatarsCollisionsEnabled * @property {boolean} characterControllerEnabled - Synonym of collisionsEnabled. * Deprecated: Use collisionsEnabled instead. * @property {boolean} useAdvancedMovementControls - Returns and sets the value of the Interface setting, Settings > * Walking and teleporting. Note: Setting the value has no effect unless Interface is restarted. * @property {boolean} showPlayArea - Returns and sets the value of the Interface setting, Settings > Show room boundaries * while teleporting. Note: Setting the value has no effect unless Interface is restarted. + * * @property {number} yawSpeed=75 * @property {number} pitchSpeed=50 + * * @property {boolean} hmdRollControlEnabled=true - If true, the roll angle of your HMD turns your avatar * while flying. * @property {number} hmdRollControlDeadZone=8 - The amount of HMD roll, in degrees, required before your avatar turns if * hmdRollControlEnabled is enabled. * @property {number} hmdRollControlRate If hmdRollControlEnabled is true, this value determines the maximum turn rate of * your avatar when rolling your HMD in degrees per second. + * * @property {number} userHeight=1.75 - The height of the user in sensor space. * @property {number} userEyeHeight=1.65 - The estimated height of the user's eyes in sensor space. Read-only. + * * @property {Uuid} SELF_ID - UUID representing "my avatar". Only use for local-only entities in situations * where MyAvatar.sessionUUID is not available (e.g., if not connected to a domain). Note: Likely to be deprecated. * Read-only. + * * @property {number} walkSpeed * @property {number} walkBackwardSpeed * @property {number} sprintSpeed * @property {number} isInSittingState - * @property {number} userRecenterModel + * @property {MyAvatar.SitStandModelType} userRecenterModel + * @property {boolean} isSitStandStateLocked + * @property {boolean} allowTeleporting * * @property {Vec3} skeletonOffset - Can be used to apply a translation offset between the avatar's position and the * registration point of the 3D model. @@ -160,6 +178,7 @@ class MyAvatar : public Avatar { * sometimes called "elevation". * @property {number} bodyRoll - The rotation about an axis running from the chest to the back of the avatar. Roll is * sometimes called "bank". + * * @property {Quat} orientation * @property {Quat} headOrientation - The orientation of the avatar's head. * @property {number} headPitch - The rotation about an axis running from ear to ear of the avatar's head. Pitch is @@ -168,18 +187,24 @@ class MyAvatar : public Avatar { * head. Yaw is sometimes called "heading". * @property {number} headRoll - The rotation about an axis running from the nose to the back of the avatar's head. Roll is * sometimes called "bank". + * * @property {Vec3} velocity * @property {Vec3} angularVelocity + * * @property {number} audioLoudness * @property {number} audioAverageLoudness + * * @property {string} displayName * @property {string} sessionDisplayName - Sanitized, defaulted version displayName that is defined by the AvatarMixer * rather than by Interface clients. The result is unique among all avatars present at the time. * @property {boolean} lookAtSnappingEnabled * @property {string} skeletonModelURL * @property {AttachmentData[]} attachmentData + * * @property {string[]} jointNames - The list of joints in the current avatar model. Read-only. + * * @property {Uuid} sessionUUID Read-only. + * * @property {Mat4} sensorToWorldMatrix Read-only. * @property {Mat4} controllerLeftHandMatrix Read-only. * @property {Mat4} controllerRightHandMatrix Read-only. @@ -196,11 +221,11 @@ class MyAvatar : public Avatar { Q_PROPERTY(QString motorMode READ getScriptedMotorMode WRITE setScriptedMotorMode) Q_PROPERTY(QString collisionSoundURL READ getCollisionSoundURL WRITE setCollisionSoundURL) Q_PROPERTY(AudioListenerMode audioListenerMode READ getAudioListenerMode WRITE setAudioListenerMode) - Q_PROPERTY(glm::vec3 customListenPosition READ getCustomListenPosition WRITE setCustomListenPosition) - Q_PROPERTY(glm::quat customListenOrientation READ getCustomListenOrientation WRITE setCustomListenOrientation) Q_PROPERTY(AudioListenerMode audioListenerModeHead READ getAudioListenerModeHead) Q_PROPERTY(AudioListenerMode audioListenerModeCamera READ getAudioListenerModeCamera) Q_PROPERTY(AudioListenerMode audioListenerModeCustom READ getAudioListenerModeCustom) + Q_PROPERTY(glm::vec3 customListenPosition READ getCustomListenPosition WRITE setCustomListenPosition) + Q_PROPERTY(glm::quat customListenOrientation READ getCustomListenOrientation WRITE setCustomListenOrientation) Q_PROPERTY(bool hasScriptedBlendshapes READ getHasScriptedBlendshapes WRITE setHasScriptedBlendshapes) Q_PROPERTY(bool hasProceduralBlinkFaceMovement READ getHasProceduralBlinkFaceMovement WRITE setHasProceduralBlinkFaceMovement) Q_PROPERTY(bool hasProceduralEyeFaceMovement READ getHasProceduralEyeFaceMovement WRITE setHasProceduralEyeFaceMovement) @@ -277,6 +302,9 @@ public: }; Q_ENUM(DriveKeys) + /**jsdoc + * @typedef {number} MyAvatar.SitStandModelType + */ enum SitStandModelType { ForceSit = 0, ForceStand, @@ -491,6 +519,9 @@ public: // adding one of the other handlers. While any handler may change a value in animStateDictionaryIn (or supply different values in animStateDictionaryOut) // a handler must not remove properties from animStateDictionaryIn, nor change property values that it does not intend to change. // It is not specified in what order multiple handlers are called. + /**jsdoc + * @function MyAvatar.addAnimationStateHandler + */ Q_INVOKABLE QScriptValue addAnimationStateHandler(QScriptValue handler, QScriptValue propertiesList) { return _skeletonModel->getRig().addAnimationStateHandler(handler, propertiesList); } /**jsdoc @@ -530,7 +561,7 @@ public: */ Q_INVOKABLE void setHmdAvatarAlignmentType(const QString& hand); /**jsdoc - * @function MyAvatar.setHmdAvatarAlignmentType + * @function MyAvatar.getHmdAvatarAlignmentType * @returns {string} */ Q_INVOKABLE QString getHmdAvatarAlignmentType() const; @@ -649,7 +680,7 @@ public: /**jsdoc * Recenter the avatar in the horizontal direction, if {@link MyAvatar|MyAvatar.hmdLeanRecenterEnabled} is * false. - * @ function MyAvatar.triggerHorizontalRecenter + * @function MyAvatar.triggerHorizontalRecenter */ Q_INVOKABLE void triggerHorizontalRecenter(); @@ -935,7 +966,7 @@ public: /**jsdoc * Function returns list of avatar entities - * @function MyAvatar.getAvatarEntitiesVariant() + * @function MyAvatar.getAvatarEntitiesVariant * @returns {object[]} */ Q_INVOKABLE QVariantList getAvatarEntitiesVariant(); @@ -1244,7 +1275,6 @@ public slots: * @param {boolean} [shouldFaceLocation=false] - Set to true to position the avatar a short distance away from * the new position and orientate the avatar to face the position. */ - void goToFeetLocation(const glm::vec3& newPosition, bool hasOrientation, const glm::quat& newOrientation, bool shouldFaceLocation); @@ -1382,8 +1412,20 @@ public slots: */ bool getEnableMeshVisible() const override; + /**jsdoc + * @function MyAvatar.storeAvatarEntityDataPayload + */ void storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& payload) override; + + /**jsdoc + * @function MyAvatar.clearAvatarEntity + * @param {Uuid} entityID + * @param {boolean} requiresRemovalFromTree + */ void clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree = true) override; + /**jsdoc + * @function MyAvatar.sanitizeAvatarEntityProperties + */ void sanitizeAvatarEntityProperties(EntityItemProperties& properties) const; /**jsdoc @@ -1489,11 +1531,11 @@ signals: void collisionsEnabledChanged(bool enabled); /**jsdoc - * Triggered when collisions with other avatars enabled or disabled - * @function MyAvatar.otherAvatarsCollisionsEnabledChanged - * @param {boolean} enabled - * @returns {Signal} - */ + * Triggered when collisions with other avatars enabled or disabled + * @function MyAvatar.otherAvatarsCollisionsEnabledChanged + * @param {boolean} enabled + * @returns {Signal} + */ void otherAvatarsCollisionsEnabledChanged(bool enabled); /**jsdoc diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 06942a13d8..7ebb8cad01 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -499,6 +499,9 @@ public: void tearDownGrabs(); signals: + /**jsdoc + * @function MyAvatar.targetScaleChanged + */ void targetScaleChanged(float targetScale); public slots: @@ -541,6 +544,9 @@ public slots: */ glm::quat getRightPalmRotation() const; + /**jsdoc + * @function MyAvatar.setModelURLFinished + */ // hooked up to Model::setURLFinished signal void setModelURLFinished(bool success); From f58a5db0b0931ce0244007a748ed545d51196194 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 19 Feb 2019 09:47:51 +1300 Subject: [PATCH 006/446] Reorganize JSDoc inheritance for MyAvatar and Avatar --- .../src/avatars/ScriptableAvatar.h | 109 +++------- interface/src/avatar/MyAvatar.h | 146 +++++++++----- .../src/avatars-renderer/Avatar.h | 7 +- libraries/avatars/src/AvatarData.h | 189 ++++++++++++------ 4 files changed, 262 insertions(+), 189 deletions(-) diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h index 4562ad6134..b2ad4527b0 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.h +++ b/assignment-client/src/avatars/ScriptableAvatar.h @@ -22,17 +22,16 @@ * The Avatar API is used to manipulate scriptable avatars on the domain. This API is a subset of the * {@link MyAvatar} API. * - *

Note: In the examples, use "Avatar" instead of "MyAvatar".

- * * @namespace Avatar * * @hifi-assignment-client * + * @comment IMPORTANT: This group of properties is copied from AvatarData.h; they should NOT be edited here. * @property {Vec3} position - * @property {number} scale + * @property {number} scale - Returns the clamped scale of the avatar. * @property {number} density Read-only. * @property {Vec3} handPosition - * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. + * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. * Yaw is sometimes called "heading". * @property {number} bodyPitch - The rotation about an axis running from shoulder to shoulder of the avatar. Pitch is * sometimes called "elevation". @@ -63,62 +62,6 @@ * @property {Mat4} controllerRightHandMatrix Read-only. * @property {number} sensorToWorldScale Read-only. * - * @borrows MyAvatar.getDomainMinScale as getDomainMinScale - * @borrows MyAvatar.getDomainMaxScale as getDomainMaxScale - * @borrows MyAvatar.canMeasureEyeHeight as canMeasureEyeHeight - * @borrows MyAvatar.getEyeHeight as getEyeHeight - * @borrows MyAvatar.getHeight as getHeight - * @borrows MyAvatar.setHandState as setHandState - * @borrows MyAvatar.getHandState as getHandState - * @borrows MyAvatar.setRawJointData as setRawJointData - * @borrows MyAvatar.setJointData as setJointData - * @borrows MyAvatar.setJointRotation as setJointRotation - * @borrows MyAvatar.setJointTranslation as setJointTranslation - * @borrows MyAvatar.clearJointData as clearJointData - * @borrows MyAvatar.isJointDataValid as isJointDataValid - * @borrows MyAvatar.getJointRotation as getJointRotation - * @borrows MyAvatar.getJointTranslation as getJointTranslation - * @borrows MyAvatar.getJointRotations as getJointRotations - * @borrows MyAvatar.getJointTranslations as getJointTranslations - * @borrows MyAvatar.setJointRotations as setJointRotations - * @borrows MyAvatar.setJointTranslations as setJointTranslations - * @borrows MyAvatar.clearJointsData as clearJointsData - * @borrows MyAvatar.getJointIndex as getJointIndex - * @borrows MyAvatar.getJointNames as getJointNames - * @borrows MyAvatar.setBlendshape as setBlendshape - * @borrows MyAvatar.getAttachmentsVariant as getAttachmentsVariant - * @borrows MyAvatar.setAttachmentsVariant as setAttachmentsVariant - * @borrows MyAvatar.updateAvatarEntity as updateAvatarEntity - * @borrows MyAvatar.clearAvatarEntity as clearAvatarEntity - * @borrows MyAvatar.setForceFaceTrackerConnected as setForceFaceTrackerConnected - * @borrows MyAvatar.getAttachmentData as getAttachmentData - * @borrows MyAvatar.setAttachmentData as setAttachmentData - * @borrows MyAvatar.attach as attach - * @borrows MyAvatar.detachOne as detachOne - * @borrows MyAvatar.detachAll as detachAll - * @borrows MyAvatar.getAvatarEntityData as getAvatarEntityData - * @borrows MyAvatar.setAvatarEntityData as setAvatarEntityData - * @borrows MyAvatar.getSensorToWorldMatrix as getSensorToWorldMatrix - * @borrows MyAvatar.getSensorToWorldScale as getSensorToWorldScale - * @borrows MyAvatar.getControllerLeftHandMatrix as getControllerLeftHandMatrix - * @borrows MyAvatar.getControllerRightHandMatrix as getControllerRightHandMatrix - * @borrows MyAvatar.getDataRate as getDataRate - * @borrows MyAvatar.getUpdateRate as getUpdateRate - * @borrows MyAvatar.displayNameChanged as displayNameChanged - * @borrows MyAvatar.sessionDisplayNameChanged as sessionDisplayNameChanged - * @borrows MyAvatar.skeletonModelURLChanged as skeletonModelURLChanged - * @borrows MyAvatar.lookAtSnappingChanged as lookAtSnappingChanged - * @borrows MyAvatar.sessionUUIDChanged as sessionUUIDChanged - * @borrows MyAvatar.sendAvatarDataPacket as sendAvatarDataPacket - * @borrows MyAvatar.sendIdentityPacket as sendIdentityPacket - * @borrows MyAvatar.setJointMappingsFromNetworkReply as setJointMappingsFromNetworkReply - * @borrows MyAvatar.setSessionUUID as setSessionUUID - * @borrows MyAvatar.getAbsoluteJointRotationInObjectFrame as getAbsoluteJointRotationInObjectFrame - * @borrows MyAvatar.getAbsoluteJointTranslationInObjectFrame as getAbsoluteJointTranslationInObjectFrame - * @borrows MyAvatar.setAbsoluteJointRotationInObjectFrame as setAbsoluteJointRotationInObjectFrame - * @borrows MyAvatar.setAbsoluteJointTranslationInObjectFrame as setAbsoluteJointTranslationInObjectFrame - * @borrows MyAvatar.getTargetScale as getTargetScale - * @borrows MyAvatar.resetLastSent as resetLastSent */ class ScriptableAvatar : public AvatarData, public Dependency { @@ -159,23 +102,25 @@ public: Q_INVOKABLE AnimationDetails getAnimationDetails(); /**jsdoc - * Get the names of all the joints in the current avatar. - * @function MyAvatar.getJointNames - * @returns {string[]} The joint names. - * @example Report the names of all the joints in your current avatar. - * print(JSON.stringify(MyAvatar.getJointNames())); - */ + * ####### TODO: If this override changes the function use @override and do JSDoc here, otherwise @comment that uses base class's JSDoc.
+ * Get the names of all the joints in the current avatar. + * @function Avatar.getJointNames + * @returns {string[]} The joint names. + * @example Report the names of all the joints in your current avatar. + * print(JSON.stringify(Avatar.getJointNames())); + */ Q_INVOKABLE virtual QStringList getJointNames() const override; /**jsdoc - * Get the joint index for a named joint. The joint index value is the position of the joint in the array returned by - * {@link MyAvatar.getJointNames} or {@link Avatar.getJointNames}. - * @function MyAvatar.getJointIndex - * @param {string} name - The name of the joint. - * @returns {number} The index of the joint. - * @example Report the index of your avatar's left arm joint. - * print(JSON.stringify(MyAvatar.getJointIndex("LeftArm")); - */ + * ####### TODO: If this override changes the function use @override and do JSDoc here, otherwise @comment that uses base class's JSDoc.
+ * Get the joint index for a named joint. The joint index value is the position of the joint in the array returned by + * {@link Avatar.getJointNames}. + * @function Avatar.getJointIndex + * @param {string} name - The name of the joint. + * @returns {number} The index of the joint. + * @example Report the index of your avatar's left arm joint. + * print(JSON.stringify(Avatar.getJointIndex("LeftArm")); + */ /// Returns the index of the joint with the specified name, or -1 if not found/unknown. Q_INVOKABLE virtual int getJointIndex(const QString& name) const override; @@ -193,6 +138,8 @@ public: bool getHasAudioEnabledFaceMovement() const override { return _headData->getHasAudioEnabledFaceMovement(); } /**jsdoc + * ####### TODO: If this override changes the function use @override and do JSDoc here, otherwise @comment that uses base class's JSDoc.
+ * ####### Also need to resolve with MyAvatar.
* Potentially Very Expensive. Do not use. * @function Avatar.getAvatarEntityData * @returns {object} @@ -200,13 +147,15 @@ public: Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const override; /**jsdoc - * @function MyAvatar.setAvatarEntityData - * @param {object} avatarEntityData - */ + * ####### TODO: If this override changes the function use @override and do JSDoc here, otherwise @comment that uses base class's JSDoc. + * @function Avatar.setAvatarEntityData + * @param {object} avatarEntityData + */ Q_INVOKABLE void setAvatarEntityData(const AvatarEntityMap& avatarEntityData) override; /**jsdoc - * @function MyAvatar.updateAvatarEntity + * ####### TODO: If this override changes the function use @override and do JSDoc here, otherwise @comment that uses base class's JSDoc. + * @function Avatar.updateAvatarEntity * @param {Uuid} entityID * @param {string} entityData */ @@ -214,12 +163,12 @@ public: public slots: /**jsdoc - * @function MyAvatar.update + * @function Avatar.update */ void update(float deltatime); /**jsdoc - * @function MyAvatar.setJointMappingsFromNetworkReply + * @function Avatar.setJointMappingsFromNetworkReply */ void setJointMappingsFromNetworkReply(); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 4e75c93403..d689d2f215 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -68,6 +68,46 @@ class MyAvatar : public Avatar { * @hifi-client-entity * @hifi-avatar * + * @comment IMPORTANT: This group of properties is copied from AvatarData.h; they should NOT be edited here. + * @property {Vec3} position + * @property {number} scale - Returns the clamped scale of the avatar. + * @property {number} density Read-only. + * @property {Vec3} handPosition + * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. + * Yaw is sometimes called "heading". + * @property {number} bodyPitch - The rotation about an axis running from shoulder to shoulder of the avatar. Pitch is + * sometimes called "elevation". + * @property {number} bodyRoll - The rotation about an axis running from the chest to the back of the avatar. Roll is + * sometimes called "bank". + * @property {Quat} orientation + * @property {Quat} headOrientation - The orientation of the avatar's head. + * @property {number} headPitch - The rotation about an axis running from ear to ear of the avatar's head. Pitch is + * sometimes called "elevation". + * @property {number} headYaw - The rotation left or right about an axis running from the base to the crown of the avatar's + * head. Yaw is sometimes called "heading". + * @property {number} headRoll - The rotation about an axis running from the nose to the back of the avatar's head. Roll is + * sometimes called "bank". + * @property {Vec3} velocity + * @property {Vec3} angularVelocity + * @property {number} audioLoudness + * @property {number} audioAverageLoudness + * @property {string} displayName + * @property {string} sessionDisplayName - Sanitized, defaulted version displayName that is defined by the AvatarMixer + * rather than by Interface clients. The result is unique among all avatars present at the time. + * @property {boolean} lookAtSnappingEnabled + * @property {string} skeletonModelURL + * @property {AttachmentData[]} attachmentData + * @property {string[]} jointNames - The list of joints in the current avatar model. Read-only. + * @property {Uuid} sessionUUID Read-only. + * @property {Mat4} sensorToWorldMatrix Read-only. + * @property {Mat4} controllerLeftHandMatrix Read-only. + * @property {Mat4} controllerRightHandMatrix Read-only. + * @property {number} sensorToWorldScale Read-only. + * + * @comment IMPORTANT: This group of properties is copied from Avatar.h; they should NOT be edited here. + * @property {Vec3} skeletonOffset - Can be used to apply a translation offset between the avatar's position and the + * registration point of the 3D model. + * * @property {Vec3} qmlPosition - A synonym for position for use by QML. * * @property {boolean} shouldRenderLocally=true - If true then your avatar is rendered for you in Interface, @@ -165,50 +205,60 @@ class MyAvatar : public Avatar { * @property {boolean} isSitStandStateLocked * @property {boolean} allowTeleporting * - * @property {Vec3} skeletonOffset - Can be used to apply a translation offset between the avatar's position and the - * registration point of the 3D model. - * - * @property {Vec3} position - * @property {number} scale - Returns the clamped scale of the avatar. - * @property {number} density Read-only. - * @property {Vec3} handPosition - * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. - * Yaw is sometimes called "heading". - * @property {number} bodyPitch - The rotation about an axis running from shoulder to shoulder of the avatar. Pitch is - * sometimes called "elevation". - * @property {number} bodyRoll - The rotation about an axis running from the chest to the back of the avatar. Roll is - * sometimes called "bank". - * - * @property {Quat} orientation - * @property {Quat} headOrientation - The orientation of the avatar's head. - * @property {number} headPitch - The rotation about an axis running from ear to ear of the avatar's head. Pitch is - * sometimes called "elevation". - * @property {number} headYaw - The rotation left or right about an axis running from the base to the crown of the avatar's - * head. Yaw is sometimes called "heading". - * @property {number} headRoll - The rotation about an axis running from the nose to the back of the avatar's head. Roll is - * sometimes called "bank". - * - * @property {Vec3} velocity - * @property {Vec3} angularVelocity - * - * @property {number} audioLoudness - * @property {number} audioAverageLoudness - * - * @property {string} displayName - * @property {string} sessionDisplayName - Sanitized, defaulted version displayName that is defined by the AvatarMixer - * rather than by Interface clients. The result is unique among all avatars present at the time. - * @property {boolean} lookAtSnappingEnabled - * @property {string} skeletonModelURL - * @property {AttachmentData[]} attachmentData - * - * @property {string[]} jointNames - The list of joints in the current avatar model. Read-only. - * - * @property {Uuid} sessionUUID Read-only. - * - * @property {Mat4} sensorToWorldMatrix Read-only. - * @property {Mat4} controllerLeftHandMatrix Read-only. - * @property {Mat4} controllerRightHandMatrix Read-only. - * @property {number} sensorToWorldScale Read-only. + * @borrows Avatar.getDomainMinScale as getDomainMinScale + * @borrows Avatar.getDomainMaxScale as getDomainMaxScale + * @borrows Avatar.getEyeHeight as getEyeHeight + * @borrows Avatar.getHeight as getHeight + * @borrows Avatar.setHandState as setHandState + * @borrows Avatar.getHandState as getHandState + * @borrows Avatar.setRawJointData as setRawJointData + * @borrows Avatar.setJointData as setJointData + * @borrows Avatar.setJointRotation as setJointRotation + * @borrows Avatar.setJointTranslation as setJointTranslation + * @borrows Avatar.clearJointData as clearJointData + * @borrows Avatar.isJointDataValid as isJointDataValid + * @borrows Avatar.getJointRotation as getJointRotation + * @borrows Avatar.getJointTranslation as getJointTranslation + * @borrows Avatar.getJointRotations as getJointRotations + * @borrows Avatar.getJointTranslations as getJointTranslations + * @borrows Avatar.setJointRotations as setJointRotations + * @borrows Avatar.setJointTranslations as setJointTranslations + * @borrows Avatar.clearJointsData as clearJointsData + * @borrows Avatar.getJointIndex as getJointIndex + * @borrows Avatar.getJointNames as getJointNames + * @borrows Avatar.setBlendshape as setBlendshape + * @borrows Avatar.getAttachmentsVariant as getAttachmentsVariant + * @borrows Avatar.setAttachmentsVariant as setAttachmentsVariant + * @borrows Avatar.updateAvatarEntity as updateAvatarEntity + * @borrows Avatar.clearAvatarEntity as clearAvatarEntity + * @borrows Avatar.setForceFaceTrackerConnected as setForceFaceTrackerConnected + * @borrows Avatar.getAttachmentData as getAttachmentData + * @borrows Avatar.setAttachmentData as setAttachmentData + * @borrows Avatar.attach as attach + * @borrows Avatar.detachOne as detachOne + * @borrows Avatar.detachAll as detachAll + * @borrows Avatar.getAvatarEntityData as getAvatarEntityData + * @borrows Avatar.setAvatarEntityData as setAvatarEntityData + * @borrows Avatar.getSensorToWorldMatrix as getSensorToWorldMatrix + * @borrows Avatar.getSensorToWorldScale as getSensorToWorldScale + * @borrows Avatar.getControllerLeftHandMatrix as getControllerLeftHandMatrix + * @borrows Avatar.getControllerRightHandMatrix as getControllerRightHandMatrix + * @borrows Avatar.getDataRate as getDataRate + * @borrows Avatar.getUpdateRate as getUpdateRate + * @borrows Avatar.displayNameChanged as displayNameChanged + * @borrows Avatar.sessionDisplayNameChanged as sessionDisplayNameChanged + * @borrows Avatar.skeletonModelURLChanged as skeletonModelURLChanged + * @borrows Avatar.lookAtSnappingChanged as lookAtSnappingChanged + * @borrows Avatar.sessionUUIDChanged as sessionUUIDChanged + * @borrows Avatar.sendAvatarDataPacket as sendAvatarDataPacket + * @borrows Avatar.sendIdentityPacket as sendIdentityPacket + * @borrows Avatar.setSessionUUID as setSessionUUID + * @borrows Avatar.getAbsoluteJointRotationInObjectFrame as getAbsoluteJointRotationInObjectFrame + * @borrows Avatar.getAbsoluteJointTranslationInObjectFrame as getAbsoluteJointTranslationInObjectFrame + * @borrows Avatar.setAbsoluteJointRotationInObjectFrame as setAbsoluteJointRotationInObjectFrame + * @borrows Avatar.setAbsoluteJointTranslationInObjectFrame as setAbsoluteJointTranslationInObjectFrame + * @borrows Avatar.getTargetScale as getTargetScale + * @borrows Avatar.resetLastSent as resetLastSent */ // FIXME: `glm::vec3 position` is not accessible from QML, so this exposes position in a QML-native type Q_PROPERTY(QVector3D qmlPosition READ getQmlPosition) @@ -1314,7 +1364,7 @@ public slots: /**jsdoc * @function MyAvatar.restrictScaleFromDomainSettings - * @param {objecct} domainSettingsObject + * @param {object} domainSettingsObject */ void restrictScaleFromDomainSettings(const QJsonObject& domainSettingsObject); @@ -1345,6 +1395,7 @@ public slots: /**jsdoc + * ####### Why Q_INVOKABLE? * @function MyAvatar.updateMotionBehaviorFromMenu */ Q_INVOKABLE void updateMotionBehaviorFromMenu(); @@ -1413,16 +1464,19 @@ public slots: bool getEnableMeshVisible() const override; /**jsdoc + * ####### TODO; Should this really be exposed in the API? * @function MyAvatar.storeAvatarEntityDataPayload */ void storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& payload) override; /**jsdoc + * ####### Does override change functionality? If so, document here and don't borrow; if not, borrow and don't document here. * @function MyAvatar.clearAvatarEntity * @param {Uuid} entityID - * @param {boolean} requiresRemovalFromTree + * @param {boolean} [requiresRemovalFromTree] */ void clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree = true) override; + /**jsdoc * @function MyAvatar.sanitizeAvatarEntityProperties */ diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 7ebb8cad01..1aa6829160 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -127,7 +127,12 @@ private: class Avatar : public AvatarData, public scriptable::ModelProvider, public MetaModelPayload { Q_OBJECT - // This property has JSDoc in MyAvatar.h. + /*jsdoc + * @comment IMPORTANT: The JSDoc for the following properties should be copied to MyAvatar.h. + * + * @property {Vec3} skeletonOffset - Can be used to apply a translation offset between the avatar's position and the + * registration point of the 3D model. + */ Q_PROPERTY(glm::vec3 skeletonOffset READ getSkeletonOffset WRITE setSkeletonOffset) public: diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 63396a59ac..7a4d235565 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -411,7 +411,43 @@ class ClientTraitsHandler; class AvatarData : public QObject, public SpatiallyNestable { Q_OBJECT - // The following properties have JSDoc in MyAvatar.h and ScriptableAvatar.h + // IMPORTANT: The JSDoc for the following properties should be copied to MyAvatar.h and ScriptableAvatar.h. + /* + * @property {Vec3} position + * @property {number} scale - Returns the clamped scale of the avatar. + * @property {number} density Read-only. + * @property {Vec3} handPosition + * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. + * Yaw is sometimes called "heading". + * @property {number} bodyPitch - The rotation about an axis running from shoulder to shoulder of the avatar. Pitch is + * sometimes called "elevation". + * @property {number} bodyRoll - The rotation about an axis running from the chest to the back of the avatar. Roll is + * sometimes called "bank". + * @property {Quat} orientation + * @property {Quat} headOrientation - The orientation of the avatar's head. + * @property {number} headPitch - The rotation about an axis running from ear to ear of the avatar's head. Pitch is + * sometimes called "elevation". + * @property {number} headYaw - The rotation left or right about an axis running from the base to the crown of the avatar's + * head. Yaw is sometimes called "heading". + * @property {number} headRoll - The rotation about an axis running from the nose to the back of the avatar's head. Roll is + * sometimes called "bank". + * @property {Vec3} velocity + * @property {Vec3} angularVelocity + * @property {number} audioLoudness + * @property {number} audioAverageLoudness + * @property {string} displayName + * @property {string} sessionDisplayName - Sanitized, defaulted version displayName that is defined by the AvatarMixer + * rather than by Interface clients. The result is unique among all avatars present at the time. + * @property {boolean} lookAtSnappingEnabled + * @property {string} skeletonModelURL + * @property {AttachmentData[]} attachmentData + * @property {string[]} jointNames - The list of joints in the current avatar model. Read-only. + * @property {Uuid} sessionUUID Read-only. + * @property {Mat4} sensorToWorldMatrix Read-only. + * @property {Mat4} controllerLeftHandMatrix Read-only. + * @property {Mat4} controllerRightHandMatrix Read-only. + * @property {number} sensorToWorldScale Read-only. + */ Q_PROPERTY(glm::vec3 position READ getWorldPosition WRITE setPositionViaScript) Q_PROPERTY(float scale READ getDomainLimitedScale WRITE setTargetScale) Q_PROPERTY(float density READ getDensity) @@ -567,7 +603,7 @@ public: /**jsdoc * Returns the minimum scale allowed for this avatar in the current domain. * This value can change as the user changes avatars or when changing domains. - * @function MyAvatar.getDomainMinScale + * @function Avatar.getDomainMinScale * @returns {number} minimum scale allowed for this avatar in the current domain. */ Q_INVOKABLE float getDomainMinScale() const; @@ -575,7 +611,7 @@ public: /**jsdoc * Returns the maximum scale allowed for this avatar in the current domain. * This value can change as the user changes avatars or when changing domains. - * @function MyAvatar.getDomainMaxScale + * @function Avatar.getDomainMaxScale * @returns {number} maximum scale allowed for this avatar in the current domain. */ Q_INVOKABLE float getDomainMaxScale() const; @@ -591,7 +627,7 @@ public: /**jsdoc * Provides read only access to the current eye height of the avatar. * This height is only an estimate and might be incorrect for avatars that are missing standard joints. - * @function MyAvatar.getEyeHeight + * @function Avatar.getEyeHeight * @returns {number} Eye height of avatar in meters. */ Q_INVOKABLE virtual float getEyeHeight() const { return _targetScale * getUnscaledEyeHeight(); } @@ -599,7 +635,7 @@ public: /**jsdoc * Provides read only access to the current height of the avatar. * This height is only an estimate and might be incorrect for avatars that are missing standard joints. - * @function MyAvatar.getHeight + * @function Avatar.getHeight * @returns {number} Height of avatar in meters. */ Q_INVOKABLE virtual float getHeight() const; @@ -610,13 +646,13 @@ public: void setDomainMaximumHeight(float domainMaximumHeight); /**jsdoc - * @function MyAvatar.setHandState + * @function Avatar.setHandState * @param {string} state */ Q_INVOKABLE void setHandState(char s) { _handState = s; } /**jsdoc - * @function MyAvatar.getHandState + * @function Avatar.getHandState * @returns {string} */ Q_INVOKABLE char getHandState() const { return _handState; } @@ -624,7 +660,7 @@ public: const QVector& getRawJointData() const { return _jointData; } /**jsdoc - * @function MyAvatar.setRawJointData + * @function Avatar.setRawJointData * @param {JointData[]} data */ Q_INVOKABLE void setRawJointData(QVector data); @@ -636,7 +672,7 @@ public: * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

- * @function MyAvatar.setJointData + * @function Avatar.setJointData * @param {number} index - The index of the joint. * @param {Quat} rotation - The rotation of the joint relative to its parent. * @param {Vec3} translation - The translation of the joint relative to its parent. @@ -654,6 +690,8 @@ public: * Script.setTimeout(function () { * MyAvatar.clearJointsData(); * }, 5000); + * + * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual void setJointData(int index, const glm::quat& rotation, const glm::vec3& translation); @@ -664,7 +702,7 @@ public: * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

- * @function MyAvatar.setJointRotation + * @function Avatar.setJointRotation * @param {number} index - The index of the joint. * @param {Quat} rotation - The rotation of the joint relative to its parent. */ @@ -677,7 +715,7 @@ public: * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

- * @function MyAvatar.setJointTranslation + * @function Avatar.setJointTranslation * @param {number} index - The index of the joint. * @param {Vec3} translation - The translation of the joint relative to its parent. */ @@ -687,13 +725,13 @@ public: * Clear joint translations and rotations set by script for a specific joint. This restores all motion from the default * animation system including inverse kinematics for that joint. *

Note: This is slightly faster than the function variation that specifies the joint name.

- * @function MyAvatar.clearJointData + * @function Avatar.clearJointData * @param {number} index - The index of the joint. */ Q_INVOKABLE virtual void clearJointData(int index); /**jsdoc - * @function MyAvatar.isJointDataValid + * @function Avatar.isJointDataValid * @param {number} index * @returns {boolean} */ @@ -702,7 +740,7 @@ public: /**jsdoc * Get the rotation of a joint relative to its parent. For information on the joint hierarchy used, see * Avatar Standards. - * @function MyAvatar.getJointRotation + * @function Avatar.getJointRotation * @param {number} index - The index of the joint. * @returns {Quat} The rotation of the joint relative to its parent. */ @@ -711,7 +749,7 @@ public: /**jsdoc * Get the translation of a joint relative to its parent. For information on the joint hierarchy used, see * Avatar Standards. - * @function MyAvatar.getJointTranslation + * @function Avatar.getJointTranslation * @param {number} index - The index of the joint. * @returns {Vec3} The translation of the joint relative to its parent. */ @@ -724,7 +762,7 @@ public: * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

- * @function MyAvatar.setJointData + * @function Avatar.setJointData * @param {string} name - The name of the joint. * @param {Quat} rotation - The rotation of the joint relative to its parent. * @param {Vec3} translation - The translation of the joint relative to its parent. @@ -738,7 +776,7 @@ public: * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

- * @function MyAvatar.setJointRotation + * @function Avatar.setJointRotation * @param {string} name - The name of the joint. * @param {Quat} rotation - The rotation of the joint relative to its parent. * @example Set your avatar to its default T-pose then rotate its right arm.
@@ -759,6 +797,8 @@ public: * Script.setTimeout(function () { * MyAvatar.clearJointsData(); * }, 5000); + * + * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual void setJointRotation(const QString& name, const glm::quat& rotation); @@ -769,7 +809,7 @@ public: * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

- * @function MyAvatar.setJointTranslation + * @function Avatar.setJointTranslation * @param {string} name - The name of the joint. * @param {Vec3} translation - The translation of the joint relative to its parent. * @example Stretch your avatar's neck. Depending on the avatar you are using, you will either see a gap between @@ -782,6 +822,8 @@ public: * Script.setTimeout(function () { * MyAvatar.clearJointData("Neck"); * }, 5000); + * + * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual void setJointTranslation(const QString& name, const glm::vec3& translation); @@ -789,7 +831,7 @@ public: * Clear joint translations and rotations set by script for a specific joint. This restores all motion from the default * animation system including inverse kinematics for that joint. *

Note: This is slightly slower than the function variation that specifies the joint index.

- * @function MyAvatar.clearJointData + * @function Avatar.clearJointData * @param {string} name - The name of the joint. * @example Offset and restore the position of your avatar's head. * // Move your avatar's head up by 25cm from where it should be. @@ -799,11 +841,13 @@ public: * Script.setTimeout(function () { * MyAvatar.clearJointData("Neck"); * }, 5000); + * + * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual void clearJointData(const QString& name); /**jsdoc - * @function MyAvatar.isJointDataValid + * @function Avatar.isJointDataValid * @param {string} name * @returns {boolean} */ @@ -812,37 +856,43 @@ public: /**jsdoc * Get the rotation of a joint relative to its parent. For information on the joint hierarchy used, see * Avatar Standards. - * @function MyAvatar.getJointRotation + * @function Avatar.getJointRotation * @param {string} name - The name of the joint. * @returns {Quat} The rotation of the joint relative to its parent. * @example Report the rotation of your avatar's hips joint. * print(JSON.stringify(MyAvatar.getJointRotation("Hips"))); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual glm::quat getJointRotation(const QString& name) const; /**jsdoc * Get the translation of a joint relative to its parent. For information on the joint hierarchy used, see * Avatar Standards. - * @function MyAvatar.getJointTranslation + * @function Avatar.getJointTranslation * @param {number} name - The name of the joint. * @returns {Vec3} The translation of the joint relative to its parent. * @example Report the translation of your avatar's hips joint. * print(JSON.stringify(MyAvatar.getJointRotation("Hips"))); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual glm::vec3 getJointTranslation(const QString& name) const; /**jsdoc * Get the rotations of all joints in the current avatar. Each joint's rotation is relative to its parent joint. - * @function MyAvatar.getJointRotations + * @function Avatar.getJointRotations * @returns {Quat[]} The rotations of all joints relative to each's parent. The values are in the same order as the array * returned by {@link MyAvatar.getJointNames} or {@link Avatar.getJointNames}. * @example Report the rotations of all your avatar's joints. * print(JSON.stringify(MyAvatar.getJointRotations())); + * + * // Note: If using from the Avatar API, replace all "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual QVector getJointRotations() const; /**jsdoc - * @function MyAvatar.getJointTranslations + * @function Avatar.getJointTranslations * @returns {Vec3[]} */ Q_INVOKABLE virtual QVector getJointTranslations() const; @@ -854,7 +904,7 @@ public: * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

- * @function MyAvatar.setJointRotations + * @function Avatar.setJointRotations * @param {Quat[]} jointRotations - The rotations for all joints in the avatar. The values are in the same order as the * array returned by {@link MyAvatar.getJointNames} or {@link Avatar.getJointNames}. * @example Set your avatar to its default T-pose then rotate its right arm.
@@ -880,11 +930,13 @@ public: * Script.setTimeout(function () { * MyAvatar.clearJointsData(); * }, 5000); + * + * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual void setJointRotations(const QVector& jointRotations); /**jsdoc - * @function MyAvatar.setJointTranslations + * @function Avatar.setJointTranslations * @param {Vec3[]} translations */ Q_INVOKABLE virtual void setJointTranslations(const QVector& jointTranslations); @@ -892,7 +944,7 @@ public: /**jsdoc * Clear all joint translations and rotations that have been set by script. This restores all motion from the default * animation system including inverse kinematics for all joints. - * @function MyAvatar.clearJointsData + * @function Avatar.clearJointsData * @example Set your avatar to it's default T-pose for a while. * // Set all joint translations and rotations to defaults. * var i, length, rotation, translation; @@ -906,33 +958,39 @@ public: * Script.setTimeout(function () { * MyAvatar.clearJointsData(); * }, 5000); + * + * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual void clearJointsData(); /**jsdoc * Get the joint index for a named joint. The joint index value is the position of the joint in the array returned by * {@link MyAvatar.getJointNames} or {@link Avatar.getJointNames}. - * @function MyAvatar.getJointIndex + * @function Avatar.getJointIndex * @param {string} name - The name of the joint. * @returns {number} The index of the joint. * @example Report the index of your avatar's left arm joint. * print(JSON.stringify(MyAvatar.getJointIndex("LeftArm")); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ /// Returns the index of the joint with the specified name, or -1 if not found/unknown. Q_INVOKABLE virtual int getJointIndex(const QString& name) const; /**jsdoc * Get the names of all the joints in the current avatar. - * @function MyAvatar.getJointNames + * @function Avatar.getJointNames * @returns {string[]} The joint names. * @example Report the names of all the joints in your current avatar. * print(JSON.stringify(MyAvatar.getJointNames())); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual QStringList getJointNames() const; /**jsdoc - * @function MyAvatar.setBlendshape + * @function Avatar.setBlendshape * @param {string} name * @param {number} value */ @@ -940,14 +998,14 @@ public: /**jsdoc - * @function MyAvatar.getAttachmentsVariant + * @function Avatar.getAttachmentsVariant * @returns {object} */ // FIXME: Can this name be improved? Can it be deprecated? Q_INVOKABLE virtual QVariantList getAttachmentsVariant() const; /**jsdoc - * @function MyAvatar.setAttachmentsVariant + * @function Avatar.setAttachmentsVariant * @param {object} variant */ // FIXME: Can this name be improved? Can it be deprecated? @@ -956,21 +1014,22 @@ public: virtual void storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& payload); /**jsdoc - * @function MyAvatar.updateAvatarEntity + * @function Avatar.updateAvatarEntity * @param {Uuid} entityID * @param {string} entityData */ Q_INVOKABLE virtual void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData); /**jsdoc - * @function MyAvatar.clearAvatarEntity + * @function Avatar.clearAvatarEntity * @param {Uuid} entityID + * @param {boolean} [requiresRemovalFromTree] */ Q_INVOKABLE virtual void clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree = true); /**jsdoc - * @function MyAvatar.setForceFaceTrackerConnected + * @function Avatar.setForceFaceTrackerConnected * @param {boolean} connected */ Q_INVOKABLE void setForceFaceTrackerConnected(bool connected) { _forceFaceTrackerConnected = connected; } @@ -1023,13 +1082,15 @@ public: /**jsdoc * Get information about all models currently attached to your avatar. - * @function MyAvatar.getAttachmentData + * @function Avatar.getAttachmentData * @returns {AttachmentData[]} Information about all models attached to your avatar. * @example Report the URLs of all current attachments. * var attachments = MyAvatar.getaAttachmentData(); * for (var i = 0; i < attachments.length; i++) { * print (attachments[i].modelURL); * } + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual QVector getAttachmentData() const; @@ -1037,7 +1098,7 @@ public: * Set all models currently attached to your avatar. For example, if you retrieve attachment data using * {@link MyAvatar.getAttachmentData} or {@link Avatar.getAttachmentData}, make changes to it, and then want to update your avatar's attachments per the * changed data. You can also remove all attachments by using setting attachmentData to null. - * @function MyAvatar.setAttachmentData + * @function Avatar.setAttachmentData * @param {AttachmentData[]} attachmentData - The attachment data defining the models to have attached to your avatar. Use * null to remove all attachments. * @example Remove a hat attachment if your avatar is wearing it. @@ -1051,6 +1112,8 @@ public: * break; * } * } + * + * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual void setAttachmentData(const QVector& attachmentData); @@ -1059,7 +1122,7 @@ public: * stand on. *

Note: Attached models are models only; they are not entities and can not be manipulated using the {@link Entities} API. * Nor can you use this function to attach an entity (such as a sphere or a box) to your avatar.

- * @function MyAvatar.attach + * @function Avatar.attach * @param {string} modelURL - The URL of the model to attach. Models can be .FBX or .OBJ format. * @param {string} [jointName=""] - The name of the avatar joint (see {@link MyAvatar.getJointNames} or {@link Avatar.getJointNames}) to attach the model * to. @@ -1089,6 +1152,8 @@ public: * attachment.rotation, * attachment.scale, * attachment.isSoft); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual void attach(const QString& modelURL, const QString& jointName = QString(), const glm::vec3& translation = glm::vec3(), const glm::quat& rotation = glm::quat(), @@ -1097,7 +1162,7 @@ public: /**jsdoc * Detach the most recently attached instance of a particular model from either a specific joint or any joint. - * @function MyAvatar.detachOne + * @function Avatar.detachOne * @param {string} modelURL - The URL of the model to detach. * @param {string} [jointName=""] - The name of the joint to detach the model from. If "", then the most * recently attached model is removed from which ever joint it was attached to. @@ -1106,7 +1171,7 @@ public: /**jsdoc * Detach all instances of a particular model from either a specific joint or all joints. - * @function MyAvatar.detachAll + * @function Avatar.detachAll * @param {string} modelURL - The URL of the model to detach. * @param {string} [jointName=""] - The name of the joint to detach the model from. If "", then the model is * detached from all joints. @@ -1136,13 +1201,13 @@ public: AABox getDefaultBubbleBox() const; /**jsdoc - * @function MyAvatar.getAvatarEntityData + * @function Avatar.getAvatarEntityData * @returns {object} */ Q_INVOKABLE virtual AvatarEntityMap getAvatarEntityData() const; /**jsdoc - * @function MyAvatar.setAvatarEntityData + * @function Avatar.setAvatarEntityData * @param {object} avatarEntityData */ Q_INVOKABLE virtual void setAvatarEntityData(const AvatarEntityMap& avatarEntityData); @@ -1151,28 +1216,28 @@ public: AvatarEntityIDs getAndClearRecentlyRemovedIDs(); /**jsdoc - * @function MyAvatar.getSensorToWorldMatrix + * @function Avatar.getSensorToWorldMatrix * @returns {Mat4} */ // thread safe Q_INVOKABLE glm::mat4 getSensorToWorldMatrix() const; /**jsdoc - * @function MyAvatar.getSensorToWorldScale + * @function Avatar.getSensorToWorldScale * @returns {number} */ // thread safe Q_INVOKABLE float getSensorToWorldScale() const; /**jsdoc - * @function MyAvatar.getControllerLeftHandMatrix + * @function Avatar.getControllerLeftHandMatrix * @returns {Mat4} */ // thread safe Q_INVOKABLE glm::mat4 getControllerLeftHandMatrix() const; /**jsdoc - * @function MyAvatar.getControllerRightHandMatrix + * @function Avatar.getControllerRightHandMatrix * @returns {Mat4} */ // thread safe @@ -1180,14 +1245,14 @@ public: /**jsdoc - * @function MyAvatar.getDataRate + * @function Avatar.getDataRate * @param {string} [rateName=""] * @returns {number} */ Q_INVOKABLE float getDataRate(const QString& rateName = QString("")) const; /**jsdoc - * @function MyAvatar.getUpdateRate + * @function Avatar.getUpdateRate * @param {string} [rateName=""] * @returns {number} */ @@ -1235,32 +1300,32 @@ public: signals: /**jsdoc - * @function MyAvatar.displayNameChanged + * @function Avatar.displayNameChanged * @returns {Signal} */ void displayNameChanged(); /**jsdoc - * @function MyAvatar.sessionDisplayNameChanged + * @function Avatar.sessionDisplayNameChanged * @returns {Signal} */ void sessionDisplayNameChanged(); /**jsdoc - * @function MyAvatar.skeletonModelURLChanged + * @function Avatar.skeletonModelURLChanged * @returns {Signal} */ void skeletonModelURLChanged(); /**jsdoc - * @function MyAvatar.lookAtSnappingChanged + * @function Avatar.lookAtSnappingChanged * @param {boolean} enabled * @returns {Signal} */ void lookAtSnappingChanged(bool enabled); /**jsdoc - * @function MyAvatar.sessionUUIDChanged + * @function Avatar.sessionUUIDChanged * @returns {Signal} */ void sessionUUIDChanged(); @@ -1268,18 +1333,18 @@ signals: public slots: /**jsdoc - * @function MyAvatar.sendAvatarDataPacket + * @function Avatar.sendAvatarDataPacket * @param {boolean} [sendAll=false] */ virtual int sendAvatarDataPacket(bool sendAll = false); /**jsdoc - * @function MyAvatar.sendIdentityPacket + * @function Avatar.sendIdentityPacket */ int sendIdentityPacket(); /**jsdoc - * @function MyAvatar.setSessionUUID + * @function Avatar.setSessionUUID * @param {Uuid} sessionUUID */ virtual void setSessionUUID(const QUuid& sessionUUID) { @@ -1294,21 +1359,21 @@ public slots: } /**jsdoc - * @function MyAvatar.getAbsoluteJointRotationInObjectFrame + * @function Avatar.getAbsoluteJointRotationInObjectFrame * @param {number} index * @returns {Quat} */ virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override; /**jsdoc - * @function MyAvatar.getAbsoluteJointTranslationInObjectFrame + * @function Avatar.getAbsoluteJointTranslationInObjectFrame * @param {number} index * @returns {Vec3} */ virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override; /**jsdoc - * @function MyAvatar.setAbsoluteJointRotationInObjectFrame + * @function Avatar.setAbsoluteJointRotationInObjectFrame * @param {number} index * @param {Quat} rotation * @returns {boolean} @@ -1316,7 +1381,7 @@ public slots: virtual bool setAbsoluteJointRotationInObjectFrame(int index, const glm::quat& rotation) override { return false; } /**jsdoc - * @function MyAvatar.setAbsoluteJointTranslationInObjectFrame + * @function Avatar.setAbsoluteJointTranslationInObjectFrame * @param {number} index * @param {Vec3} translation * @returns {boolean} @@ -1324,13 +1389,13 @@ public slots: virtual bool setAbsoluteJointTranslationInObjectFrame(int index, const glm::vec3& translation) override { return false; } /**jsdoc - * @function MyAvatar.getTargetScale + * @function Avatar.getTargetScale * @returns {number} */ float getTargetScale() const { return _targetScale; } // why is this a slot? /**jsdoc - * @function MyAvatar.resetLastSent + * @function Avatar.resetLastSent */ void resetLastSent() { _lastToByteArray = 0; } From 01119a5b5d980be426bcb20bb773d2be6591ed9e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 21 Feb 2019 11:19:29 +1300 Subject: [PATCH 007/446] Fill in Agent API JSDoc --- .../src/AgentScriptingInterface.h | 31 +++++++++++++------ .../src/avatars/ScriptableAvatar.h | 2 +- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/assignment-client/src/AgentScriptingInterface.h b/assignment-client/src/AgentScriptingInterface.h index 9fa7688778..1f592a9f18 100644 --- a/assignment-client/src/AgentScriptingInterface.h +++ b/assignment-client/src/AgentScriptingInterface.h @@ -18,16 +18,25 @@ #include "Agent.h" /**jsdoc + * The Agent API enables an assignment client to emulate an avatar. In particular, setting isAvatar = + * true connects the assignment client to the avatar and audio mixers and enables the {@link Avatar} API to be used. + * * @namespace Agent * * @hifi-assignment-client * - * @property {boolean} isAvatar - * @property {boolean} isPlayingAvatarSound Read-only. - * @property {boolean} isListeningToAudioStream - * @property {boolean} isNoiseGateEnabled - * @property {number} lastReceivedAudioLoudness Read-only. - * @property {Uuid} sessionUUID Read-only. + * @property {boolean} isAvatar - true if the assignment client script is emulating an avatar, otherwise + * false. + * @property {boolean} isPlayingAvatarSound - true if the script has a sound to play, otherwise false. + * Sounds are played when isAvatar is true, from the position and with the orientation of the + * scripted avatar's head.Read-only. + * @property {boolean} isListeningToAudioStream - true if the agent is "listening" to the audio stream from the + * domain, otherwise false. + * @property {boolean} isNoiseGateEnabled - true if the noise gate is enabled, otherwise false. When + * enabled, the input audio stream is blocked (fully attenuated) if it falls below an adaptive threshold. + * @property {number} lastReceivedAudioLoudness - The current loudness of the audio input, nominal range 0.0 (no + * sound) – 1.0 (the onset of clipping). Read-only. + * @property {Uuid} sessionUUID - The unique ID associated with the agent's current session in the domain. Read-only. */ class AgentScriptingInterface : public QObject { Q_OBJECT @@ -54,20 +63,24 @@ public: public slots: /**jsdoc + * Set whether or not the script should emulate an avatar. * @function Agent.setIsAvatar - * @param {boolean} isAvatar + * @param {boolean} isAvatar - true if the script should act as if an avatar, otherwise false. */ void setIsAvatar(bool isAvatar) const { _agent->setIsAvatar(isAvatar); } /**jsdoc + * Check whether or not the script is emulating an avatar. * @function Agent.isAvatar - * @returns {boolean} + * @returns {boolean} true if the script is acting as if an avatar, otherwise false. */ bool isAvatar() const { return _agent->isAvatar(); } /**jsdoc + * Play a sound from the position and with the orientation of the emulated avatar's head. No sound is played unless + * isAvatar == true. * @function Agent.playAvatarSound - * @param {object} avatarSound + * @param {SoundObject} avatarSound - The sound to play. */ void playAvatarSound(SharedSoundPointer avatarSound) const { _agent->playAvatarSound(avatarSound); } diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h index b2ad4527b0..6b78f666e1 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.h +++ b/assignment-client/src/avatars/ScriptableAvatar.h @@ -20,7 +20,7 @@ /**jsdoc * The Avatar API is used to manipulate scriptable avatars on the domain. This API is a subset of the - * {@link MyAvatar} API. + * {@link MyAvatar} API. To enable this API, set {@link Agent|Agent.isAvatatr} to true. * * @namespace Avatar * From 7f1ae634391eb8dce84f086ad60835b109c93b2f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 21 Feb 2019 14:30:38 +1300 Subject: [PATCH 008/446] Revise current Avatar API JSDoc --- .../src/avatars/ScriptableAvatar.h | 48 +++++++---------- libraries/avatars/src/AvatarData.h | 54 ++++++++++--------- 2 files changed, 47 insertions(+), 55 deletions(-) diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h index 6b78f666e1..3cf10cc129 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.h +++ b/assignment-client/src/avatars/ScriptableAvatar.h @@ -28,7 +28,8 @@ * * @comment IMPORTANT: This group of properties is copied from AvatarData.h; they should NOT be edited here. * @property {Vec3} position - * @property {number} scale - Returns the clamped scale of the avatar. + * @property {number} scale=1.0 - The scale of the avatar. When setting, the value is limited to between 0.005 + * and 1000.0. When getting, the value may temporarily be further limited by the domain's settings. * @property {number} density Read-only. * @property {Vec3} handPosition * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. @@ -50,8 +51,9 @@ * @property {number} audioLoudness * @property {number} audioAverageLoudness * @property {string} displayName - * @property {string} sessionDisplayName - Sanitized, defaulted version displayName that is defined by the AvatarMixer - * rather than by Interface clients. The result is unique among all avatars present at the time. + * @property {string} sessionDisplayName - Sanitized, defaulted version of displayName that is defined by the + * avatar mixer rather than by Interface clients. The result is unique among all avatars present on the domain at the + * time. * @property {boolean} lookAtSnappingEnabled * @property {string} skeletonModelURL * @property {AttachmentData[]} attachmentData @@ -102,24 +104,12 @@ public: Q_INVOKABLE AnimationDetails getAnimationDetails(); /**jsdoc - * ####### TODO: If this override changes the function use @override and do JSDoc here, otherwise @comment that uses base class's JSDoc.
- * Get the names of all the joints in the current avatar. - * @function Avatar.getJointNames - * @returns {string[]} The joint names. - * @example Report the names of all the joints in your current avatar. - * print(JSON.stringify(Avatar.getJointNames())); + * @comment Uses the base class's JSDoc. */ Q_INVOKABLE virtual QStringList getJointNames() const override; /**jsdoc - * ####### TODO: If this override changes the function use @override and do JSDoc here, otherwise @comment that uses base class's JSDoc.
- * Get the joint index for a named joint. The joint index value is the position of the joint in the array returned by - * {@link Avatar.getJointNames}. - * @function Avatar.getJointIndex - * @param {string} name - The name of the joint. - * @returns {number} The index of the joint. - * @example Report the index of your avatar's left arm joint. - * print(JSON.stringify(Avatar.getJointIndex("LeftArm")); + * @comment Uses the base class's JSDoc. */ /// Returns the index of the joint with the specified name, or -1 if not found/unknown. Q_INVOKABLE virtual int getJointIndex(const QString& name) const override; @@ -137,39 +127,37 @@ public: void setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement); bool getHasAudioEnabledFaceMovement() const override { return _headData->getHasAudioEnabledFaceMovement(); } - /**jsdoc - * ####### TODO: If this override changes the function use @override and do JSDoc here, otherwise @comment that uses base class's JSDoc.
- * ####### Also need to resolve with MyAvatar.
- * Potentially Very Expensive. Do not use. + /**jsdoc + * Get the avatar entities as binary data. + *

Warning: Potentially a very expensive call. Do not use if possible.

* @function Avatar.getAvatarEntityData - * @returns {object} + * @returns {AvatarEntityMap} */ Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const override; /**jsdoc - * ####### TODO: If this override changes the function use @override and do JSDoc here, otherwise @comment that uses base class's JSDoc. + * Set the avatar entities from binary data. + *

Warning: Potentially an expensive call. Do not use if possible.

* @function Avatar.setAvatarEntityData - * @param {object} avatarEntityData + * @param {AvatarEntityMap} avatarEntityData */ Q_INVOKABLE void setAvatarEntityData(const AvatarEntityMap& avatarEntityData) override; /**jsdoc - * ####### TODO: If this override changes the function use @override and do JSDoc here, otherwise @comment that uses base class's JSDoc. - * @function Avatar.updateAvatarEntity - * @param {Uuid} entityID - * @param {string} entityData + * @comment Uses the base class's JSDoc. */ Q_INVOKABLE void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) override; public slots: /**jsdoc * @function Avatar.update + * @param {number} deltaTime */ void update(float deltatime); /**jsdoc - * @function Avatar.setJointMappingsFromNetworkReply - */ + * @function Avatar.setJointMappingsFromNetworkReply + */ void setJointMappingsFromNetworkReply(); private: diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 7a4d235565..0a5509f46c 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -61,6 +61,10 @@ using AvatarSharedPointer = std::shared_ptr; using AvatarWeakPointer = std::weak_ptr; using AvatarHash = QHash; +/**jsdoc + * An object with the UUIDs of avatar entities as keys and binary blobs, being the entity properties, as values. + * @typedef {Object.>} AvatarEntityMap + */ using AvatarEntityMap = QMap; using PackedAvatarEntityMap = QMap; // similar to AvatarEntityMap, but different internal format using AvatarEntityIDs = QSet; @@ -414,7 +418,8 @@ class AvatarData : public QObject, public SpatiallyNestable { // IMPORTANT: The JSDoc for the following properties should be copied to MyAvatar.h and ScriptableAvatar.h. /* * @property {Vec3} position - * @property {number} scale - Returns the clamped scale of the avatar. + * @property {number} scale=1.0 - The scale of the avatar. When setting, the value is limited to between 0.005 + * and 1000.0. When getting, the value may temporarily be further limited by the domain's settings. * @property {number} density Read-only. * @property {Vec3} handPosition * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. @@ -436,8 +441,9 @@ class AvatarData : public QObject, public SpatiallyNestable { * @property {number} audioLoudness * @property {number} audioAverageLoudness * @property {string} displayName - * @property {string} sessionDisplayName - Sanitized, defaulted version displayName that is defined by the AvatarMixer - * rather than by Interface clients. The result is unique among all avatars present at the time. + * @property {string} sessionDisplayName - Sanitized, defaulted version of displayName that is defined by the + * avatar mixer rather than by Interface clients. The result is unique among all avatars present on the domain at the + * time. * @property {boolean} lookAtSnappingEnabled * @property {string} skeletonModelURL * @property {AttachmentData[]} attachmentData @@ -601,18 +607,18 @@ public: virtual bool getHasAudioEnabledFaceMovement() const { return false; } /**jsdoc - * Returns the minimum scale allowed for this avatar in the current domain. + * Get the minimum scale allowed for this avatar in the current domain. * This value can change as the user changes avatars or when changing domains. * @function Avatar.getDomainMinScale - * @returns {number} minimum scale allowed for this avatar in the current domain. + * @returns {number} The minimum scale allowed for this avatar in the current domain. */ Q_INVOKABLE float getDomainMinScale() const; /**jsdoc - * Returns the maximum scale allowed for this avatar in the current domain. + * Get the maximum scale allowed for this avatar in the current domain. * This value can change as the user changes avatars or when changing domains. * @function Avatar.getDomainMaxScale - * @returns {number} maximum scale allowed for this avatar in the current domain. + * @returns {number} The maximum scale allowed for this avatar in the current domain. */ Q_INVOKABLE float getDomainMaxScale() const; @@ -625,18 +631,18 @@ public: virtual bool canMeasureEyeHeight() const { return false; } /**jsdoc - * Provides read only access to the current eye height of the avatar. + * Get the current eye height of the avatar. * This height is only an estimate and might be incorrect for avatars that are missing standard joints. * @function Avatar.getEyeHeight - * @returns {number} Eye height of avatar in meters. + * @returns {number} The eye height of the avatar. */ Q_INVOKABLE virtual float getEyeHeight() const { return _targetScale * getUnscaledEyeHeight(); } /**jsdoc - * Provides read only access to the current height of the avatar. + * Get the current height of the avatar. * This height is only an estimate and might be incorrect for avatars that are missing standard joints. * @function Avatar.getHeight - * @returns {number} Height of avatar in meters. + * @returns {number} The height of the avatar. */ Q_INVOKABLE virtual float getHeight() const; @@ -739,7 +745,7 @@ public: /**jsdoc * Get the rotation of a joint relative to its parent. For information on the joint hierarchy used, see - * Avatar Standards. + * Avatar Standards. * @function Avatar.getJointRotation * @param {number} index - The index of the joint. * @returns {Quat} The rotation of the joint relative to its parent. @@ -748,7 +754,7 @@ public: /**jsdoc * Get the translation of a joint relative to its parent. For information on the joint hierarchy used, see - * Avatar Standards. + * Avatar Standards. * @function Avatar.getJointTranslation * @param {number} index - The index of the joint. * @returns {Vec3} The translation of the joint relative to its parent. @@ -855,7 +861,7 @@ public: /**jsdoc * Get the rotation of a joint relative to its parent. For information on the joint hierarchy used, see - * Avatar Standards. + * Avatar Standards. * @function Avatar.getJointRotation * @param {string} name - The name of the joint. * @returns {Quat} The rotation of the joint relative to its parent. @@ -868,7 +874,7 @@ public: /**jsdoc * Get the translation of a joint relative to its parent. For information on the joint hierarchy used, see - * Avatar Standards. + * Avatar Standards. * @function Avatar.getJointTranslation * @param {number} name - The name of the joint. * @returns {Vec3} The translation of the joint relative to its parent. @@ -883,7 +889,7 @@ public: * Get the rotations of all joints in the current avatar. Each joint's rotation is relative to its parent joint. * @function Avatar.getJointRotations * @returns {Quat[]} The rotations of all joints relative to each's parent. The values are in the same order as the array - * returned by {@link MyAvatar.getJointNames} or {@link Avatar.getJointNames}. + * returned by {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the Avatar API. * @example Report the rotations of all your avatar's joints. * print(JSON.stringify(MyAvatar.getJointRotations())); * @@ -906,7 +912,7 @@ public: * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

* @function Avatar.setJointRotations * @param {Quat[]} jointRotations - The rotations for all joints in the avatar. The values are in the same order as the - * array returned by {@link MyAvatar.getJointNames} or {@link Avatar.getJointNames}. + * array returned by {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the Avatar API. * @example Set your avatar to its default T-pose then rotate its right arm.
* Avatar in T-pose * // Set all joint translations and rotations to defaults. @@ -965,10 +971,10 @@ public: /**jsdoc * Get the joint index for a named joint. The joint index value is the position of the joint in the array returned by - * {@link MyAvatar.getJointNames} or {@link Avatar.getJointNames}. + * {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the Avatar API. * @function Avatar.getJointIndex * @param {string} name - The name of the joint. - * @returns {number} The index of the joint. + * @returns {number} The index of the joint if valid, otherwise -1. * @example Report the index of your avatar's left arm joint. * print(JSON.stringify(MyAvatar.getJointIndex("LeftArm")); * @@ -1016,14 +1022,14 @@ public: /**jsdoc * @function Avatar.updateAvatarEntity * @param {Uuid} entityID - * @param {string} entityData + * @param {Array.} entityData */ Q_INVOKABLE virtual void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData); /**jsdoc * @function Avatar.clearAvatarEntity * @param {Uuid} entityID - * @param {boolean} [requiresRemovalFromTree] + * @param {boolean} [requiresRemovalFromTree=true] */ Q_INVOKABLE virtual void clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree = true); @@ -1201,14 +1207,12 @@ public: AABox getDefaultBubbleBox() const; /**jsdoc - * @function Avatar.getAvatarEntityData - * @returns {object} + * @comment Documented in derived classes' JSDoc. */ Q_INVOKABLE virtual AvatarEntityMap getAvatarEntityData() const; /**jsdoc - * @function Avatar.setAvatarEntityData - * @param {object} avatarEntityData + * @comment Documented in derived classes' JSDoc. */ Q_INVOKABLE virtual void setAvatarEntityData(const AvatarEntityMap& avatarEntityData); From 7912c1b2fdaa17acb14b0c452209913921405aba Mon Sep 17 00:00:00 2001 From: Flame Soulis Date: Tue, 26 Feb 2019 08:38:07 -0500 Subject: [PATCH 009/446] Adds Ubuntu 16.04 friendly cmake instructions --- BUILD_LINUX.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/BUILD_LINUX.md b/BUILD_LINUX.md index 1559ece191..58368a168f 100644 --- a/BUILD_LINUX.md +++ b/BUILD_LINUX.md @@ -37,8 +37,14 @@ sudo apt-get -y install libpulse0 libnss3 libnspr4 libfontconfig1 libxcursor1 li Install build tools: ```bash +# For Ubuntu 18.04 sudo apt-get install cmake ``` +```bash +# For Ubuntu 16.04 +wget https://cmake.org/files/v3.9/cmake-3.9.5-Linux-x86_64.sh +sudo sh cmake-3.9.5-Linux-x86_64.sh --prefix=/usr/local --exclude-subdir +``` Install Python 3: ```bash From 79c5cfee53928b559c04b89a49a00583e54d1e8f Mon Sep 17 00:00:00 2001 From: Flame Soulis Date: Tue, 26 Feb 2019 08:38:28 -0500 Subject: [PATCH 010/446] Updates checkout tag to the latest (0.79) --- BUILD_LINUX.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILD_LINUX.md b/BUILD_LINUX.md index 58368a168f..8820bda8f6 100644 --- a/BUILD_LINUX.md +++ b/BUILD_LINUX.md @@ -67,7 +67,7 @@ git tags Then checkout last tag with: ```bash -git checkout tags/v0.71.0 +git checkout tags/v0.79.0 ``` ### Compiling From 0c59f983daacdd96ab12939e90361ce2a3186756 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 27 Feb 2019 09:34:05 +1300 Subject: [PATCH 011/446] Agent JSDoc tidying --- assignment-client/src/AgentScriptingInterface.h | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/assignment-client/src/AgentScriptingInterface.h b/assignment-client/src/AgentScriptingInterface.h index 1f592a9f18..1242634dd5 100644 --- a/assignment-client/src/AgentScriptingInterface.h +++ b/assignment-client/src/AgentScriptingInterface.h @@ -63,21 +63,27 @@ public: public slots: /**jsdoc - * Set whether or not the script should emulate an avatar. + * Sets whether or not the script should emulate an avatar. * @function Agent.setIsAvatar * @param {boolean} isAvatar - true if the script should act as if an avatar, otherwise false. + * @example Make an assignment client script emulate an avatar. + * (function () { + * Agent.setIsAvatar(true); + * Avatar.displayName = "AC avatar"; + * print("Position: " + JSON.stringify(Avatar.position)); // 0, 0, 0 + * }()); */ void setIsAvatar(bool isAvatar) const { _agent->setIsAvatar(isAvatar); } /**jsdoc - * Check whether or not the script is emulating an avatar. + * Checks whether or not the script is emulating an avatar. * @function Agent.isAvatar * @returns {boolean} true if the script is acting as if an avatar, otherwise false. */ bool isAvatar() const { return _agent->isAvatar(); } /**jsdoc - * Play a sound from the position and with the orientation of the emulated avatar's head. No sound is played unless + * Plays a sound from the position and with the orientation of the emulated avatar's head. No sound is played unless * isAvatar == true. * @function Agent.playAvatarSound * @param {SoundObject} avatarSound - The sound to play. From e79594ef53458f33f63ea5c135b48ce2d9698152 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 27 Feb 2019 09:53:11 +1300 Subject: [PATCH 012/446] Fill in and tidy Avatar JSDoc --- .../src/avatars/ScriptableAvatar.h | 82 ++++-- .../src/avatars-renderer/Avatar.h | 22 +- libraries/avatars/src/AvatarData.cpp | 85 +++++- libraries/avatars/src/AvatarData.h | 277 ++++++++++++------ libraries/shared/src/RegisteredMetaTypes.cpp | 26 +- 5 files changed, 352 insertions(+), 140 deletions(-) diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h index 3cf10cc129..2d8dce23de 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.h +++ b/assignment-client/src/avatars/ScriptableAvatar.h @@ -22,23 +22,27 @@ * The Avatar API is used to manipulate scriptable avatars on the domain. This API is a subset of the * {@link MyAvatar} API. To enable this API, set {@link Agent|Agent.isAvatatr} to true. * + *

For Interface, client entity, and avatar scripts, see {@link MyAvatar}.

+ * * @namespace Avatar * * @hifi-assignment-client * * @comment IMPORTANT: This group of properties is copied from AvatarData.h; they should NOT be edited here. - * @property {Vec3} position + * @property {Vec3} position - The position of the avatar. * @property {number} scale=1.0 - The scale of the avatar. When setting, the value is limited to between 0.005 * and 1000.0. When getting, the value may temporarily be further limited by the domain's settings. - * @property {number} density Read-only. - * @property {Vec3} handPosition + * @property {number} density - The density of the avatar in kg/m3. The density is used to work out its mass in + * the application of physics. Read-only. + * @property {Vec3} handPosition - A user-defined hand position, in world coordinates. The position moves with the avatar + * but is otherwise not used or changed by Interface. * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. * Yaw is sometimes called "heading". * @property {number} bodyPitch - The rotation about an axis running from shoulder to shoulder of the avatar. Pitch is * sometimes called "elevation". * @property {number} bodyRoll - The rotation about an axis running from the chest to the back of the avatar. Roll is * sometimes called "bank". - * @property {Quat} orientation + * @property {Quat} orientation - The orientation of the avatar. * @property {Quat} headOrientation - The orientation of the avatar's head. * @property {number} headPitch - The rotation about an axis running from ear to ear of the avatar's head. Pitch is * sometimes called "elevation". @@ -46,24 +50,31 @@ * head. Yaw is sometimes called "heading". * @property {number} headRoll - The rotation about an axis running from the nose to the back of the avatar's head. Roll is * sometimes called "bank". - * @property {Vec3} velocity - * @property {Vec3} angularVelocity - * @property {number} audioLoudness - * @property {number} audioAverageLoudness - * @property {string} displayName + * @property {Vec3} velocity - The current velocity of the avatar. + * @property {Vec3} angularVelocity - The current angular velocity of the avatar. + * @property {number} audioLoudness - The instantaneous loudness of the audio input that the avatar is injecting into the + * domain. + * @property {number} audioAverageLoudness - The rolling average loudness of the audio input that the avatar is injecting + * into the domain. + * @property {string} displayName - The avatar's display name. * @property {string} sessionDisplayName - Sanitized, defaulted version of displayName that is defined by the - * avatar mixer rather than by Interface clients. The result is unique among all avatars present on the domain at the + * avatar mixer rather than by Interface clients. The result is unique among all avatars present in the domain at the * time. - * @property {boolean} lookAtSnappingEnabled - * @property {string} skeletonModelURL - * @property {AttachmentData[]} attachmentData + * @property {boolean} lookAtSnappingEnabled=true - If true, the avatar's eyes snap to look at another avatar's + * eyes if generally in the line of sight and the other avatar also has lookAtSnappingEnabled == true. + * @property {string} skeletonModelURL - The URL of the avatar model's .fst file. + * @property {AttachmentData[]} attachmentData - Information on the attachments worn by the avatar.
+ * Deprecated: Use avatar entities instead. * @property {string[]} jointNames - The list of joints in the current avatar model. Read-only. - * @property {Uuid} sessionUUID Read-only. - * @property {Mat4} sensorToWorldMatrix Read-only. - * @property {Mat4} controllerLeftHandMatrix Read-only. - * @property {Mat4} controllerRightHandMatrix Read-only. - * @property {number} sensorToWorldScale Read-only. - * + * @property {Uuid} sessionUUID - Unique ID of the avatar in the domain. Read-only. + * @property {Mat4} sensorToWorldMatrix - The scale, rotation, and translation transform from the user's real world to the + * avatar's size, orientation, and position in the virtual world. Read-only. + * @property {Mat4} controllerLeftHandMatrix - The rotation and translation of the left hand controller relative to the + * avatar. Read-only. + * @property {Mat4} controllerRightHandMatrix - The rotation and translation of the right hand controller relative to the + * avatar. Read-only. + * @property {number} sensorToWorldScale - The scale that transforms dimensions in the user's real world to the avatar's + * size in the virtual world. Read-only. */ class ScriptableAvatar : public AvatarData, public Dependency { @@ -77,15 +88,17 @@ public: ScriptableAvatar(); /**jsdoc + * Starts playing an animation on the avatar. * @function Avatar.startAnimation - * @param {string} url - * @param {number} [fps=30] - * @param {number} [priority=1] - * @param {boolean} [loop=false] - * @param {boolean} [hold=false] - * @param {number} [firstFrame=0] - * @param {number} [lastFrame=3.403e+38] - * @param {string[]} [maskedJoints=[]] + * @param {string} url - The URL to the animation file. Animation files need to be .FBX format but only need to contain + * the avatar skeleton and animation data. + * @param {number} [fps=30] - The frames per second (FPS) rate for the animation playback. 30 FPS is normal speed. + * @param {number} [priority=1] - Not used. + * @param {boolean} [loop=false] - true if the animation should loop, false if it shouldn't. + * @param {boolean} [hold=false] - Not used. + * @param {number} [firstFrame=0] - The frame the animation should start at. + * @param {number} [lastFrame=3.403e+38] - The frame the animation should stop at. + * @param {string[]} [maskedJoints=[]] - The names of joints that should not be animated. */ /// Allows scripts to run animations. Q_INVOKABLE void startAnimation(const QString& url, float fps = 30.0f, float priority = 1.0f, bool loop = false, @@ -93,13 +106,15 @@ public: const QStringList& maskedJoints = QStringList()); /**jsdoc + * Stops playing the current animation. * @function Avatar.stopAnimation */ Q_INVOKABLE void stopAnimation(); /**jsdoc + * Gets the details of the current avatar animation that is being or was recently played. * @function Avatar.getAnimationDetails - * @returns {Avatar.AnimationDetails} + * @returns {Avatar.AnimationDetails} The current or recent avatar animation. */ Q_INVOKABLE AnimationDetails getAnimationDetails(); @@ -116,6 +131,9 @@ public: virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override; + /**jsdoc + * @comment Uses the base class's JSDoc. + */ int sendAvatarDataPacket(bool sendAll = false) override; virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking = false) override; @@ -128,7 +146,7 @@ public: bool getHasAudioEnabledFaceMovement() const override { return _headData->getHasAudioEnabledFaceMovement(); } /**jsdoc - * Get the avatar entities as binary data. + * Gets the avatar entities as binary data. *

Warning: Potentially a very expensive call. Do not use if possible.

* @function Avatar.getAvatarEntityData * @returns {AvatarEntityMap} @@ -136,7 +154,7 @@ public: Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const override; /**jsdoc - * Set the avatar entities from binary data. + * Sets the avatar entities from binary data. *

Warning: Potentially an expensive call. Do not use if possible.

* @function Avatar.setAvatarEntityData * @param {AvatarEntityMap} avatarEntityData @@ -151,12 +169,14 @@ public: public slots: /**jsdoc * @function Avatar.update - * @param {number} deltaTime + * @param {number} deltaTime - Delta time. + * @deprecated This function is deprecated and will be removed. */ void update(float deltatime); /**jsdoc * @function Avatar.setJointMappingsFromNetworkReply + * @deprecated This function is deprecated and will be removed. */ void setJointMappingsFromNetworkReply(); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 1aa6829160..98aa255641 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -219,7 +219,7 @@ public: * The default pose of the avatar is defined by the position and orientation of all bones * in the avatar's model file. Typically this is a T-pose. * @function MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame - * @param index {number} index number + * @param index {number} -The joint index. * @returns {Quat} The rotation of this joint in avatar coordinates. */ Q_INVOKABLE virtual glm::quat getAbsoluteDefaultJointRotationInObjectFrame(int index) const; @@ -229,7 +229,7 @@ public: * The default pose of the avatar is defined by the position and orientation of all bones * in the avatar's model file. Typically this is a T-pose. * @function MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame - * @param index {number} index number + * @param index {number} - The joint index. * @returns {Vec3} The position of this joint in avatar coordinates. */ Q_INVOKABLE virtual glm::vec3 getAbsoluteDefaultJointTranslationInObjectFrame(int index) const; @@ -238,7 +238,25 @@ public: virtual glm::vec3 getAbsoluteJointScaleInObjectFrame(int index) const override; virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override; virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override; + + /**jsdoc + * Sets the rotation of a joint relative to the avatar. + *

Warning: Not able to be used in the MyAvatar API.

+ * @function MyAvatar.setAbsoluteJointRotationInObjectFrame + * @param {number} index - The index of the joint. Not used. + * @param {Quat} rotation - The rotation of the joint relative to the avatar. Not used. + * @returns {boolean} false. + */ virtual bool setAbsoluteJointRotationInObjectFrame(int index, const glm::quat& rotation) override { return false; } + + /**jsdoc + * Sets the translation of a joint relative to the avatar. + *

Warning: Not able to be used in the MyAvatar API.

+ * @function MyAvatar.setAbsoluteJointTranslationInObjectFrame + * @param {number} index - The index of the joint. Not used. + * @param {Vec3} translation - The translation of the joint relative to the avatar. Not used. + * @returns {boolean} false. + */ virtual bool setAbsoluteJointTranslationInObjectFrame(int index, const glm::vec3& translation) override { return false; } // world-space to avatar-space rigconversion functions diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index c733cfa291..88949900ce 100755 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1425,6 +1425,47 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { return numBytesRead; } +/**jsdoc + * The avatar mixer data comprises different types of data, with the data rates of each being tracked in kbps. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Rate NameDescription
"globalPosition"Incoming global position.
"localPosition"Incoming local position.
"avatarBoundingBox"Incoming avatar bounding box.
"avatarOrientation"Incoming avatar orientation.
"avatarScale"Incoming avatar scale.
"lookAtPosition"Incoming look-at position.
"audioLoudness"Incoming audio loudness.
"sensorToWorkMatrix"Incoming sensor-to-world matrix.
"additionalFlags"Incoming additional avatar flags.
"parentInfo"Incoming parent information.
"faceTracker"Incoming face tracker data.
"jointData"Incoming joint data.
"jointDefaultPoseFlagsRate"Incoming joint default pose flags.
"farGrabJointRate"Incoming far grab joint.
"globalPositionOutbound"Outgoing global position.
"localPositionOutbound"Outgoing local position.
"avatarBoundingBoxOutbound"Outgoing avatar bounding box.
"avatarOrientationOutbound"Outgoing avatar orientation.
"avatarScaleOutbound"Outgoing avatar scale.
"lookAtPositionOutbound"Outgoing look-at position.
"audioLoudnessOutbound"Outgoing audio loudness.
"sensorToWorkMatrixOutbound"Outgoing sensor-to-world matrix.
"additionalFlagsOutbound"Outgoing additional avatar flags.
"parentInfoOutbound"Outgoing parent information.
"faceTrackerOutbound"Outgoing face tracker data.
"jointDataOutbound"Outgoing joint data.
"jointDefaultPoseFlagsOutbound"Outgoing joint default pose flags.
""When no rate name is specified, the total incoming data rate is provided.
+ * + * @typedef {string} AvatarDataRate + */ float AvatarData::getDataRate(const QString& rateName) const { if (rateName == "") { return _parseBufferRate.rate() / BYTES_PER_KILOBIT; @@ -1486,6 +1527,35 @@ float AvatarData::getDataRate(const QString& rateName) const { return 0.0f; } +/**jsdoc + * The avatar mixer data comprises different types of data updated at different rates, in Hz. + * + * + * + * + * + * + + * + * + * + * + * + * + * + * + * + * + * + * + * + + * + * + *
Rate NameDescription
"globalPosition"Global position.
"localPosition"Local position.
"avatarBoundingBox"Avatar bounding box.
"avatarOrientation"Avatar orientation.
"avatarScale"Avatar scale.
"lookAtPosition"Look-at position.
"audioLoudness"Audio loudness.
"sensorToWorkMatrix"Sensor-to-world matrix.
"additionalFlags"Additional avatar flags.
"parentInfo"Parent information.
"faceTracker"Face tracker data.
"jointData"Joint data.
"farGrabJointData"Far grab joint data.
""When no rate name is specified, the overall update rate is provided.
+ * + * @typedef {string} AvatarUpdateRate + */ float AvatarData::getUpdateRate(const QString& rateName) const { if (rateName == "") { return _parseBufferUpdateRate.rate(); @@ -2721,13 +2791,16 @@ glm::vec3 AvatarData::getAbsoluteJointTranslationInObjectFrame(int index) const } /**jsdoc + * Information on an attachment worn by the avatar. * @typedef {object} AttachmentData - * @property {string} modelUrl - * @property {string} jointName - * @property {Vec3} translation - * @property {Vec3} rotation - * @property {number} scale - * @property {boolean} soft + * @property {string} modelUrl - The URL of the model file. Models can be .FBX or .OBJ format. + * @property {string} jointName - The offset to apply to the model relative to the joint position. + * @property {Vec3} translation - The offset from the joint that the attachment is positioned at. + * @property {Vec3} rotation - The rotation applied to the model relative to the joint orientation. + * @property {number} scale - The scale applied to the attachment model. + * @property {boolean} soft - If true and the model has a skeleton, the bones of the attached model's skeleton are + * rotated to fit the avatar's current pose. If true, the translation, rotation, and + * scale parameters are ignored. */ QVariant AttachmentData::toVariant() const { QVariantMap result; diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 0a5509f46c..a20518076f 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -116,7 +116,24 @@ const int PROCEDURAL_EYE_FACE_MOVEMENT = 9; // 10th bit const int PROCEDURAL_BLINK_FACE_MOVEMENT = 10; // 11th bit const int COLLIDE_WITH_OTHER_AVATARS = 11; // 12th bit - +/**jsdoc + *

The pointing state of the hands is specified by the following values: +

+ * + * + * + * + * + * + * + * + * + * + *
ValueDescription
0No hand is pointing.
1The left hand is pointing.
2The right hand is pointing.
4It is the index finger that is pointing.
+ *

The values for the hand states are added together to give the HandState value. For example, if the left + * hand's finger is pointing, the value is 1 + 4 == 5. + * @typedef {number} HandState + */ const char HAND_STATE_NULL = 0; const char LEFT_HAND_POINTING_FLAG = 1; const char RIGHT_HAND_POINTING_FLAG = 2; @@ -417,18 +434,20 @@ class AvatarData : public QObject, public SpatiallyNestable { // IMPORTANT: The JSDoc for the following properties should be copied to MyAvatar.h and ScriptableAvatar.h. /* - * @property {Vec3} position + * @property {Vec3} position - The position of the avatar. * @property {number} scale=1.0 - The scale of the avatar. When setting, the value is limited to between 0.005 * and 1000.0. When getting, the value may temporarily be further limited by the domain's settings. - * @property {number} density Read-only. - * @property {Vec3} handPosition + * @property {number} density - The density of the avatar in kg/m3. The density is used to work out its mass in + * the application of physics. Read-only. + * @property {Vec3} handPosition - A user-defined hand position, in world coordinates. The position moves with the avatar + * but is otherwise not used or changed by Interface. * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. * Yaw is sometimes called "heading". * @property {number} bodyPitch - The rotation about an axis running from shoulder to shoulder of the avatar. Pitch is * sometimes called "elevation". * @property {number} bodyRoll - The rotation about an axis running from the chest to the back of the avatar. Roll is * sometimes called "bank". - * @property {Quat} orientation + * @property {Quat} orientation - The orientation of the avatar. * @property {Quat} headOrientation - The orientation of the avatar's head. * @property {number} headPitch - The rotation about an axis running from ear to ear of the avatar's head. Pitch is * sometimes called "elevation". @@ -436,23 +455,31 @@ class AvatarData : public QObject, public SpatiallyNestable { * head. Yaw is sometimes called "heading". * @property {number} headRoll - The rotation about an axis running from the nose to the back of the avatar's head. Roll is * sometimes called "bank". - * @property {Vec3} velocity - * @property {Vec3} angularVelocity - * @property {number} audioLoudness - * @property {number} audioAverageLoudness - * @property {string} displayName + * @property {Vec3} velocity - The current velocity of the avatar. + * @property {Vec3} angularVelocity - The current angular velocity of the avatar. + * @property {number} audioLoudness - The instantaneous loudness of the audio input that the avatar is injecting into the + * domain. + * @property {number} audioAverageLoudness - The rolling average loudness of the audio input that the avatar is injecting + * into the domain. + * @property {string} displayName - The avatar's display name. * @property {string} sessionDisplayName - Sanitized, defaulted version of displayName that is defined by the - * avatar mixer rather than by Interface clients. The result is unique among all avatars present on the domain at the + * avatar mixer rather than by Interface clients. The result is unique among all avatars present in the domain at the * time. - * @property {boolean} lookAtSnappingEnabled - * @property {string} skeletonModelURL - * @property {AttachmentData[]} attachmentData + * @property {boolean} lookAtSnappingEnabled=true - If true, the avatar's eyes snap to look at another avatar's + * eyes if generally in the line of sight and the other avatar also has lookAtSnappingEnabled == true. + * @property {string} skeletonModelURL - The URL of the avatar model's .fst file. + * @property {AttachmentData[]} attachmentData - Information on the attachments worn by the avatar.
+ * Deprecated: Use avatar entities instead. * @property {string[]} jointNames - The list of joints in the current avatar model. Read-only. - * @property {Uuid} sessionUUID Read-only. - * @property {Mat4} sensorToWorldMatrix Read-only. - * @property {Mat4} controllerLeftHandMatrix Read-only. - * @property {Mat4} controllerRightHandMatrix Read-only. - * @property {number} sensorToWorldScale Read-only. + * @property {Uuid} sessionUUID - Unique ID of the avatar in the domain. Read-only. + * @property {Mat4} sensorToWorldMatrix - The scale, rotation, and translation transform from the user's real world to the + * avatar's size, orientation, and position in the virtual world. Read-only. + * @property {Mat4} controllerLeftHandMatrix - The rotation and translation of the left hand controller relative to the + * avatar. Read-only. + * @property {Mat4} controllerRightHandMatrix - The rotation and translation of the right hand controller relative to the + * avatar. Read-only. + * @property {number} sensorToWorldScale - The scale that transforms dimensions in the user's real world to the avatar's + * size in the virtual world. Read-only. */ Q_PROPERTY(glm::vec3 position READ getWorldPosition WRITE setPositionViaScript) Q_PROPERTY(float scale READ getDomainLimitedScale WRITE setTargetScale) @@ -607,7 +634,7 @@ public: virtual bool getHasAudioEnabledFaceMovement() const { return false; } /**jsdoc - * Get the minimum scale allowed for this avatar in the current domain. + * Gets the minimum scale allowed for this avatar in the current domain. * This value can change as the user changes avatars or when changing domains. * @function Avatar.getDomainMinScale * @returns {number} The minimum scale allowed for this avatar in the current domain. @@ -615,7 +642,7 @@ public: Q_INVOKABLE float getDomainMinScale() const; /**jsdoc - * Get the maximum scale allowed for this avatar in the current domain. + * Gets the maximum scale allowed for this avatar in the current domain. * This value can change as the user changes avatars or when changing domains. * @function Avatar.getDomainMaxScale * @returns {number} The maximum scale allowed for this avatar in the current domain. @@ -631,7 +658,7 @@ public: virtual bool canMeasureEyeHeight() const { return false; } /**jsdoc - * Get the current eye height of the avatar. + * Gets the current eye height of the avatar. * This height is only an estimate and might be incorrect for avatars that are missing standard joints. * @function Avatar.getEyeHeight * @returns {number} The eye height of the avatar. @@ -639,7 +666,7 @@ public: Q_INVOKABLE virtual float getEyeHeight() const { return _targetScale * getUnscaledEyeHeight(); } /**jsdoc - * Get the current height of the avatar. + * Gets the current height of the avatar. * This height is only an estimate and might be incorrect for avatars that are missing standard joints. * @function Avatar.getHeight * @returns {number} The height of the avatar. @@ -652,27 +679,33 @@ public: void setDomainMaximumHeight(float domainMaximumHeight); /**jsdoc + * Sets the pointing state of the hands to control where the laser emanates from. If the right index finger is pointing, the + * laser emanates from the tip of that finger, otherwise it emanates from the palm. * @function Avatar.setHandState - * @param {string} state + * @param {HandState} state - The pointing state of the hand. */ Q_INVOKABLE void setHandState(char s) { _handState = s; } /**jsdoc + * Gets the pointing state of the hands to control where the laser emanates from. If the right index finger is pointing, the + * laser emanates from the tip of that finger, otherwise it emanates from the palm. * @function Avatar.getHandState - * @returns {string} + * @returns {HandState} The pointing state of the hand. */ Q_INVOKABLE char getHandState() const { return _handState; } const QVector& getRawJointData() const { return _jointData; } /**jsdoc + * Sets joint translations and rotations from raw joint data. * @function Avatar.setRawJointData - * @param {JointData[]} data + * @param {JointData[]} data - The raw joint data. + * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE void setRawJointData(QVector data); /**jsdoc - * Set a specific joint's rotation and position relative to its parent. + * Sets a specific joint's rotation and position relative to its parent. *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate @@ -702,7 +735,7 @@ public: Q_INVOKABLE virtual void setJointData(int index, const glm::quat& rotation, const glm::vec3& translation); /**jsdoc - * Set a specific joint's rotation relative to its parent. + * Sets a specific joint's rotation relative to its parent. *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate @@ -715,7 +748,7 @@ public: Q_INVOKABLE virtual void setJointRotation(int index, const glm::quat& rotation); /**jsdoc - * Set a specific joint's translation relative to its parent. + * Sets a specific joint's translation relative to its parent. *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate @@ -728,7 +761,7 @@ public: Q_INVOKABLE virtual void setJointTranslation(int index, const glm::vec3& translation); /**jsdoc - * Clear joint translations and rotations set by script for a specific joint. This restores all motion from the default + * Clears joint translations and rotations set by script for a specific joint. This restores all motion from the default * animation system including inverse kinematics for that joint. *

Note: This is slightly faster than the function variation that specifies the joint name.

* @function Avatar.clearJointData @@ -737,14 +770,15 @@ public: Q_INVOKABLE virtual void clearJointData(int index); /**jsdoc + * Checks that the data for a joint are valid. * @function Avatar.isJointDataValid - * @param {number} index - * @returns {boolean} + * @param {number} index - The index of the joint. + * @returns {boolean} true if the joint data is valid, false if not. */ Q_INVOKABLE bool isJointDataValid(int index) const; /**jsdoc - * Get the rotation of a joint relative to its parent. For information on the joint hierarchy used, see + * Gets the rotation of a joint relative to its parent. For information on the joint hierarchy used, see * Avatar Standards. * @function Avatar.getJointRotation * @param {number} index - The index of the joint. @@ -753,7 +787,7 @@ public: Q_INVOKABLE virtual glm::quat getJointRotation(int index) const; /**jsdoc - * Get the translation of a joint relative to its parent. For information on the joint hierarchy used, see + * Gets the translation of a joint relative to its parent. For information on the joint hierarchy used, see * Avatar Standards. * @function Avatar.getJointTranslation * @param {number} index - The index of the joint. @@ -762,7 +796,7 @@ public: Q_INVOKABLE virtual glm::vec3 getJointTranslation(int index) const; /**jsdoc - * Set a specific joint's rotation and position relative to its parent. + * Sets a specific joint's rotation and position relative to its parent. *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate @@ -776,7 +810,7 @@ public: Q_INVOKABLE virtual void setJointData(const QString& name, const glm::quat& rotation, const glm::vec3& translation); /**jsdoc - * Set a specific joint's rotation relative to its parent. + * Sets a specific joint's rotation relative to its parent. *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate @@ -809,7 +843,7 @@ public: Q_INVOKABLE virtual void setJointRotation(const QString& name, const glm::quat& rotation); /**jsdoc - * Set a specific joint's translation relative to its parent. + * Sets a specific joint's translation relative to its parent. *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate @@ -834,7 +868,7 @@ public: Q_INVOKABLE virtual void setJointTranslation(const QString& name, const glm::vec3& translation); /**jsdoc - * Clear joint translations and rotations set by script for a specific joint. This restores all motion from the default + * Clears joint translations and rotations set by script for a specific joint. This restores all motion from the default * animation system including inverse kinematics for that joint. *

Note: This is slightly slower than the function variation that specifies the joint index.

* @function Avatar.clearJointData @@ -853,14 +887,15 @@ public: Q_INVOKABLE virtual void clearJointData(const QString& name); /**jsdoc + * Checks that the data for a joint are valid. * @function Avatar.isJointDataValid - * @param {string} name - * @returns {boolean} + * @param {string} name - The name of the joint. + * @returns {boolean} true if the joint data is valid, false if not. */ Q_INVOKABLE virtual bool isJointDataValid(const QString& name) const; /**jsdoc - * Get the rotation of a joint relative to its parent. For information on the joint hierarchy used, see + * Gets the rotation of a joint relative to its parent. For information on the joint hierarchy used, see * Avatar Standards. * @function Avatar.getJointRotation * @param {string} name - The name of the joint. @@ -873,7 +908,7 @@ public: Q_INVOKABLE virtual glm::quat getJointRotation(const QString& name) const; /**jsdoc - * Get the translation of a joint relative to its parent. For information on the joint hierarchy used, see + * Gets the translation of a joint relative to its parent. For information on the joint hierarchy used, see * Avatar Standards. * @function Avatar.getJointTranslation * @param {number} name - The name of the joint. @@ -886,7 +921,7 @@ public: Q_INVOKABLE virtual glm::vec3 getJointTranslation(const QString& name) const; /**jsdoc - * Get the rotations of all joints in the current avatar. Each joint's rotation is relative to its parent joint. + * Gets the rotations of all joints in the current avatar. Each joint's rotation is relative to its parent joint. * @function Avatar.getJointRotations * @returns {Quat[]} The rotations of all joints relative to each's parent. The values are in the same order as the array * returned by {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the Avatar API. @@ -898,13 +933,15 @@ public: Q_INVOKABLE virtual QVector getJointRotations() const; /**jsdoc + * Gets the translations of all joints in the current avatar. Each joint's rotation is relative to its parent joint. * @function Avatar.getJointTranslations - * @returns {Vec3[]} + * @returns {Vec3[]} The translations of all joints relative to each's parent. The values are in the same order as the array + * returned by {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the Avatar API. */ Q_INVOKABLE virtual QVector getJointTranslations() const; /**jsdoc - * Set the rotations of all joints in the current avatar. Each joint's rotation is relative to its parent joint. + * Sets the rotations of all joints in the current avatar. Each joint's rotation is relative to its parent joint. *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate @@ -942,13 +979,20 @@ public: Q_INVOKABLE virtual void setJointRotations(const QVector& jointRotations); /**jsdoc + * Sets the translations of all joints in the current avatar. Each joint's translation is relative to its parent joint. + *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse + * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, + * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate + * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set + * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

* @function Avatar.setJointTranslations - * @param {Vec3[]} translations + * @param {Vec3[]} translations - The translations for all joints in the avatar. The values are in the same order as the + * array returned by {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the Avatar API. */ Q_INVOKABLE virtual void setJointTranslations(const QVector& jointTranslations); /**jsdoc - * Clear all joint translations and rotations that have been set by script. This restores all motion from the default + * Clears all joint translations and rotations that have been set by script. This restores all motion from the default * animation system including inverse kinematics for all joints. * @function Avatar.clearJointsData * @example Set your avatar to it's default T-pose for a while. @@ -970,7 +1014,7 @@ public: Q_INVOKABLE virtual void clearJointsData(); /**jsdoc - * Get the joint index for a named joint. The joint index value is the position of the joint in the array returned by + * Gets the joint index for a named joint. The joint index value is the position of the joint in the array returned by * {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the Avatar API. * @function Avatar.getJointIndex * @param {string} name - The name of the joint. @@ -984,7 +1028,7 @@ public: Q_INVOKABLE virtual int getJointIndex(const QString& name) const; /**jsdoc - * Get the names of all the joints in the current avatar. + * Gets the names of all the joints in the current avatar. * @function Avatar.getJointNames * @returns {string[]} The joint names. * @example Report the names of all the joints in your current avatar. @@ -996,23 +1040,32 @@ public: /**jsdoc + * Sets the value of a blendshape to animate your avatar's face. To enable other users to see the resulting animation of + * your avatar's face, use {@link Avatar.setForceFaceTrackerConnected} or {@link MyAvatar.setForceFaceTrackerConnected}. * @function Avatar.setBlendshape - * @param {string} name - * @param {number} value + * @param {string} name - The name of the blendshape, per the + * {@link https://docs.highfidelity.com/create/avatars/create-avatars/avatar-standards.html#blendshapes Avatar Standards}. + * @param {number} value - A value between 0.0 and 1.0. */ Q_INVOKABLE void setBlendshape(QString name, float val) { _headData->setBlendshape(name, val); } /**jsdoc + * Gets information about the models currently attached to your avatar. * @function Avatar.getAttachmentsVariant - * @returns {object} + * @returns {AttachmentData[]} Information about all models attached to your avatar. + * @deprecated Use avatar entities instead. */ // FIXME: Can this name be improved? Can it be deprecated? Q_INVOKABLE virtual QVariantList getAttachmentsVariant() const; /**jsdoc + * Sets all models currently attached to your avatar. For example, if you retrieve attachment data using + * {@link MyAvatar.getAttachmentsVariant} or {@link Avatar.getAttachmentsVariant}, make changes to it, and then want to + * update your avatar's attachments per the changed data. * @function Avatar.setAttachmentsVariant - * @param {object} variant + * @param {AttachmentData[]} variant - The attachment data defining the models to have attached to your avatar. + * @deprecated Use avatar entities instead. */ // FIXME: Can this name be improved? Can it be deprecated? Q_INVOKABLE virtual void setAttachmentsVariant(const QVariantList& variant); @@ -1021,22 +1074,27 @@ public: /**jsdoc * @function Avatar.updateAvatarEntity - * @param {Uuid} entityID - * @param {Array.} entityData + * @param {Uuid} entityID - The entity ID. + * @param {Array.} entityData - Entity data. + * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE virtual void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData); /**jsdoc * @function Avatar.clearAvatarEntity - * @param {Uuid} entityID - * @param {boolean} [requiresRemovalFromTree=true] + * @param {Uuid} entityID - The entity ID. + * @param {boolean} [requiresRemovalFromTree=true] - Requires removal from tree. + * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE virtual void clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree = true); /**jsdoc + * Enables blendshapes set using {@link Avatar.setBlendshape} or {@link MyAvatar.setBlendshape} to be transmitted to other + * users so that they can see the animation of your avatar's face. * @function Avatar.setForceFaceTrackerConnected - * @param {boolean} connected + * @param {boolean} connected - true to enable blendshape changes to be transmitted to other users, + * false to disable. */ Q_INVOKABLE void setForceFaceTrackerConnected(bool connected) { _forceFaceTrackerConnected = connected; } @@ -1087,9 +1145,10 @@ public: } /**jsdoc - * Get information about all models currently attached to your avatar. + * Gets information about the models currently attached to your avatar. * @function Avatar.getAttachmentData * @returns {AttachmentData[]} Information about all models attached to your avatar. + * @deprecated Use avatar entities instead. * @example Report the URLs of all current attachments. * var attachments = MyAvatar.getaAttachmentData(); * for (var i = 0; i < attachments.length; i++) { @@ -1101,12 +1160,13 @@ public: Q_INVOKABLE virtual QVector getAttachmentData() const; /**jsdoc - * Set all models currently attached to your avatar. For example, if you retrieve attachment data using + * Sets all models currently attached to your avatar. For example, if you retrieve attachment data using * {@link MyAvatar.getAttachmentData} or {@link Avatar.getAttachmentData}, make changes to it, and then want to update your avatar's attachments per the * changed data. You can also remove all attachments by using setting attachmentData to null. * @function Avatar.setAttachmentData - * @param {AttachmentData[]} attachmentData - The attachment data defining the models to have attached to your avatar. Use + * @param {AttachmentData[]} attachmentData - The attachment data defining the models to have attached to your avatar. Use * null to remove all attachments. + * @deprecated Use avatar entities instead. * @example Remove a hat attachment if your avatar is wearing it. * var hatURL = "https://s3.amazonaws.com/hifi-public/tony/cowboy-hat.fbx"; * var attachments = MyAvatar.getAttachmentData(); @@ -1124,7 +1184,7 @@ public: Q_INVOKABLE virtual void setAttachmentData(const QVector& attachmentData); /**jsdoc - * Attach a model to your avatar. For example, you can give your avatar a hat to wear, a guitar to hold, or a surfboard to + * Attaches a model to your avatar. For example, you can give your avatar a hat to wear, a guitar to hold, or a surfboard to * stand on. *

Note: Attached models are models only; they are not entities and can not be manipulated using the {@link Entities} API. * Nor can you use this function to attach an entity (such as a sphere or a box) to your avatar.

@@ -1136,12 +1196,14 @@ public: * @param {Quat} [rotation=Quat.IDENTITY] - The rotation to apply to the model relative to the joint orientation. * @param {number} [scale=1.0] - The scale to apply to the model. * @param {boolean} [isSoft=false] - If the model has a skeleton, set this to true so that the bones of the - * attached model's skeleton are be rotated to fit the avatar's current pose. isSoft is used, for example, + * attached model's skeleton are rotated to fit the avatar's current pose. isSoft is used, for example, * to have clothing that moves with the avatar.
* If true, the translation, rotation, and scale parameters are * ignored. - * @param {boolean} [allowDuplicates=false] - * @param {boolean} [useSaved=true] + * @param {boolean} [allowDuplicates=false] - If true then more than one copy of any particular model may be + * attached to the same joint; if false then the same model cannot be attached to the same joint. + * @param {boolean} [useSaved=true] - Not used. + * @deprecated Use avatar entities instead. * @example Attach a cowboy hat to your avatar's head. * var attachment = { * modelURL: "https://s3.amazonaws.com/hifi-public/tony/cowboy-hat.fbx", @@ -1167,20 +1229,22 @@ public: bool allowDuplicates = false, bool useSaved = true); /**jsdoc - * Detach the most recently attached instance of a particular model from either a specific joint or any joint. + * Detaches the most recently attached instance of a particular model from either a specific joint or any joint. * @function Avatar.detachOne * @param {string} modelURL - The URL of the model to detach. * @param {string} [jointName=""] - The name of the joint to detach the model from. If "", then the most * recently attached model is removed from which ever joint it was attached to. + * @deprecated Use avatar entities instead. */ Q_INVOKABLE virtual void detachOne(const QString& modelURL, const QString& jointName = QString()); /**jsdoc - * Detach all instances of a particular model from either a specific joint or all joints. + * Detaches all instances of a particular model from either a specific joint or all joints. * @function Avatar.detachAll * @param {string} modelURL - The URL of the model to detach. * @param {string} [jointName=""] - The name of the joint to detach the model from. If "", then the model is * detached from all joints. + * @deprecated Use avatar entities instead. */ Q_INVOKABLE virtual void detachAll(const QString& modelURL, const QString& jointName = QString()); @@ -1220,45 +1284,53 @@ public: AvatarEntityIDs getAndClearRecentlyRemovedIDs(); /**jsdoc + * Gets the transform from the user's real world to the avatar's size, orientation, and position in the virtual world. * @function Avatar.getSensorToWorldMatrix - * @returns {Mat4} + * @returns {Mat4} The scale, rotation, and translation transform from the user's real world to the avatar's size, + * orientation, and position in the virtual world. */ // thread safe Q_INVOKABLE glm::mat4 getSensorToWorldMatrix() const; /**jsdoc + * Gets the scale that transforms dimensions in the user's real world to the avatar's size in the virtual world. * @function Avatar.getSensorToWorldScale - * @returns {number} + * @returns {number} The scale that transforms dimensions in the user's real world to the avatar's size in the virtual + * world. */ // thread safe Q_INVOKABLE float getSensorToWorldScale() const; /**jsdoc + * Gets the rotation and translation of the left hand controller relative to the avatar. * @function Avatar.getControllerLeftHandMatrix - * @returns {Mat4} + * @returns {Mat4} The rotation and translation of the left hand controller relative to the avatar. */ // thread safe Q_INVOKABLE glm::mat4 getControllerLeftHandMatrix() const; /**jsdoc + * Gets the rotation and translation of the right hand controller relative to the avatar. * @function Avatar.getControllerRightHandMatrix - * @returns {Mat4} + * @returns {Mat4} The rotation and translation of the right hand controller relative to the avatar. */ // thread safe Q_INVOKABLE glm::mat4 getControllerRightHandMatrix() const; /**jsdoc + * Gets the amount of avatar mixer data being generated by the avatar. * @function Avatar.getDataRate - * @param {string} [rateName=""] - * @returns {number} + * @param {AvatarDataRate} [rateName=""] - The type of avatar mixer data to get the data rate of. + * @returns {number} The data rate in kbps. */ Q_INVOKABLE float getDataRate(const QString& rateName = QString("")) const; /**jsdoc + * Gets the update rate of avatar mixer data being generated by the avatar. * @function Avatar.getUpdateRate - * @param {string} [rateName=""] - * @returns {number} + * @param {AvatarUpdateRate} [rateName=""] - The type of avatar mixer data to get the update rate of. + * @returns {number} The update rate in Hz. */ Q_INVOKABLE float getUpdateRate(const QString& rateName = QString("")) const; @@ -1304,31 +1376,36 @@ public: signals: /**jsdoc + * Triggered when the avatar's displayName property value changes. * @function Avatar.displayNameChanged * @returns {Signal} */ void displayNameChanged(); /**jsdoc + * Triggered when the avattr's sessionDisplayName property value changes. * @function Avatar.sessionDisplayNameChanged * @returns {Signal} */ void sessionDisplayNameChanged(); /**jsdoc + * Triggered when the avatar's skeletonModelURL property value changes. * @function Avatar.skeletonModelURLChanged * @returns {Signal} */ void skeletonModelURLChanged(); /**jsdoc + * Triggered when the avatar's lookAtSnappingEnabled property value changes. * @function Avatar.lookAtSnappingChanged - * @param {boolean} enabled + * @param {boolean} enabled - true if look-at snapping is enabled, false if not. * @returns {Signal} */ void lookAtSnappingChanged(bool enabled); /**jsdoc + * Triggered when the avatar's sessionUUID property value changes. * @function Avatar.sessionUUIDChanged * @returns {Signal} */ @@ -1338,18 +1415,23 @@ public slots: /**jsdoc * @function Avatar.sendAvatarDataPacket - * @param {boolean} [sendAll=false] + * @param {boolean} [sendAll=false] - Send all. + * @returns {number} + * @deprecated This function is deprecated and will be removed. */ virtual int sendAvatarDataPacket(bool sendAll = false); /**jsdoc * @function Avatar.sendIdentityPacket + * @returns {number} + * @deprecated This function is deprecated and will be removed. */ int sendIdentityPacket(); /**jsdoc * @function Avatar.setSessionUUID - * @param {Uuid} sessionUUID + * @param {Uuid} sessionUUID - Session UUID. + * @deprecated This function is deprecated and will be removed. */ virtual void setSessionUUID(const QUuid& sessionUUID) { if (sessionUUID != getID()) { @@ -1362,44 +1444,61 @@ public slots: } } + /**jsdoc + * Gets the rotation of a joint relative to the avatar. + *

Warning: Not able to be used in the Avatar API.

* @function Avatar.getAbsoluteJointRotationInObjectFrame - * @param {number} index - * @returns {Quat} + * @param {number} index - The index of the joint. Not used. + * @returns {Quat} Quat.IDENTITY. */ virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override; /**jsdoc + * Gets the translation of a joint relative to the avatar. + *

Warning: Not able to be used in the Avatar API.

* @function Avatar.getAbsoluteJointTranslationInObjectFrame - * @param {number} index - * @returns {Vec3} + * @param {number} index - The index of the joint. Not used. + * @returns {Vec3} Vec3.ZERO. */ virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override; /**jsdoc + * Sets the rotation of a joint relative to the avatar. + *

Warning: Not able to be used in the Avatar API.

* @function Avatar.setAbsoluteJointRotationInObjectFrame - * @param {number} index - * @param {Quat} rotation - * @returns {boolean} + * @param {number} index - The index of the joint. Not used. + * @param {Quat} rotation - The rotation of the joint relative to the avatar. Not used. + * @returns {boolean} false. */ virtual bool setAbsoluteJointRotationInObjectFrame(int index, const glm::quat& rotation) override { return false; } /**jsdoc + * Sets the translation of a joint relative to the avatar. + *

Warning: Not able to be used in the Avatar API.

* @function Avatar.setAbsoluteJointTranslationInObjectFrame - * @param {number} index - * @param {Vec3} translation - * @returns {boolean} + * @param {number} index - The index of the joint. Not used. + * @param {Vec3} translation - The translation of the joint relative to the avatar. Not used. + * @returns {boolean} false. */ virtual bool setAbsoluteJointTranslationInObjectFrame(int index, const glm::vec3& translation) override { return false; } /**jsdoc + * Gets the target scale of the avatar without any restrictions on permissible values imposed by the domain. In contrast, the + * scale property's value may be limited by the domain's settings. * @function Avatar.getTargetScale - * @returns {number} + * @returns {number} The target scale of the avatar. + * @example Compare the target and current avatar scales. + * print("Current avatar scale: " + MyAvatar.scale); + * print("Target avatar scale: " + MyAvatar.getTargetScale()); + * + * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ float getTargetScale() const { return _targetScale; } // why is this a slot? /**jsdoc * @function Avatar.resetLastSent + * @deprecated This function is deprecated and will be removed. */ void resetLastSent() { _lastToByteArray = 0; } diff --git a/libraries/shared/src/RegisteredMetaTypes.cpp b/libraries/shared/src/RegisteredMetaTypes.cpp index ec1126c92f..614858a77d 100644 --- a/libraries/shared/src/RegisteredMetaTypes.cpp +++ b/libraries/shared/src/RegisteredMetaTypes.cpp @@ -1147,19 +1147,21 @@ AnimationDetails::AnimationDetails(QString role, QUrl url, float fps, float prio } /**jsdoc + * The details of an animation that is playing. * @typedef {object} Avatar.AnimationDetails - * @property {string} role - * @property {string} url - * @property {number} fps - * @property {number} priority - * @property {boolean} loop - * @property {boolean} hold - * @property {boolean} startAutomatically - * @property {number} firstFrame - * @property {number} lastFrame - * @property {boolean} running - * @property {number} currentFrame - * @property {boolean} allowTranslation + * @property {string} role - Not used. + * @property {string} url - The URL to the animation file. Animation files need to be in .FBX format but only need to contain +* the avatar skeleton and animation data. + * @property {number} fps - The frames per second(FPS) rate for the animation playback. 30 FPS is normal speed. + * @property {number} priority - Not used. + * @property {boolean} loop - true if the animation should loop, false if it shouldn't. + * @property {boolean} hold - Not used. + * @property {number} firstFrame - The frame the animation should start at. + * @property {number} lastFrame - The frame the animation should stop at. + * @property {boolean} running - Not used. + * @property {number} currentFrame - The current frame being played. + * @property {boolean} startAutomatically - Not used. + * @property {boolean} allowTranslation - Not used. */ QScriptValue animationDetailsToScriptValue(QScriptEngine* engine, const AnimationDetails& details) { QScriptValue obj = engine->newObject(); From aa53ab7492b15f183754fc11f7886539364ae29a Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 27 Feb 2019 09:54:11 +1300 Subject: [PATCH 013/446] Distinguish between Mat4 type and Mat4 API in JSDoc --- libraries/script-engine/src/Mat4.h | 33 +++++++++++----------- libraries/shared/src/RegisteredMetaTypes.h | 21 ++++++++++++++ 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/libraries/script-engine/src/Mat4.h b/libraries/script-engine/src/Mat4.h index 7ad77b9b24..0cdc70e79c 100644 --- a/libraries/script-engine/src/Mat4.h +++ b/libraries/script-engine/src/Mat4.h @@ -23,6 +23,7 @@ /**jsdoc * @namespace Mat4 + * @variation 0 * * @hifi-interface * @hifi-client-entity @@ -38,7 +39,7 @@ class Mat4 : public QObject, protected QScriptable { public slots: /**jsdoc - * @function Mat4.multiply + * @function Mat4(0).multiply * @param {Mat4} m1 * @param {Mat4} m2 * @returns {Mat4} @@ -47,7 +48,7 @@ public slots: /**jsdoc - * @function Mat4.createFromRotAndTrans + * @function Mat4(0).createFromRotAndTrans * @param {Quat} rot * @param {Vec3} trans * @returns {Mat4} @@ -55,7 +56,7 @@ public slots: glm::mat4 createFromRotAndTrans(const glm::quat& rot, const glm::vec3& trans) const; /**jsdoc - * @function Mat4.createFromScaleRotAndTrans + * @function Mat4(0).createFromScaleRotAndTrans * @param {Vec3} scale * @param {Quat} rot * @param {Vec3} trans @@ -64,7 +65,7 @@ public slots: glm::mat4 createFromScaleRotAndTrans(const glm::vec3& scale, const glm::quat& rot, const glm::vec3& trans) const; /**jsdoc - * @function Mat4.createFromColumns + * @function Mat4(0).createFromColumns * @param {Vec4} col0 * @param {Vec4} col1 * @param {Vec4} col2 @@ -74,7 +75,7 @@ public slots: glm::mat4 createFromColumns(const glm::vec4& col0, const glm::vec4& col1, const glm::vec4& col2, const glm::vec4& col3) const; /**jsdoc - * @function Mat4.createFromArray + * @function Mat4(0).createFromArray * @param {number[]} numbers * @returns {Mat4} */ @@ -82,21 +83,21 @@ public slots: /**jsdoc - * @function Mat4.extractTranslation + * @function Mat4(0).extractTranslation * @param {Mat4} m * @returns {Vec3} */ glm::vec3 extractTranslation(const glm::mat4& m) const; /**jsdoc - * @function Mat4.extractRotation + * @function Mat4(0).extractRotation * @param {Mat4} m * @returns {Vec3} */ glm::quat extractRotation(const glm::mat4& m) const; /**jsdoc - * @function Mat4.extractScale + * @function Mat4(0).extractScale * @param {Mat4} m * @returns {Vec3} */ @@ -104,7 +105,7 @@ public slots: /**jsdoc - * @function Mat4.transformPoint + * @function Mat4(0).transformPoint * @param {Mat4} m * @param {Vec3} point * @returns {Vec3} @@ -112,7 +113,7 @@ public slots: glm::vec3 transformPoint(const glm::mat4& m, const glm::vec3& point) const; /**jsdoc - * @function Mat4.transformVector + * @function Mat4(0).transformVector * @param {Mat4} m * @param {Vec3} vector * @returns {Vec3} @@ -121,7 +122,7 @@ public slots: /**jsdoc - * @function Mat4.inverse + * @function Mat4(0).inverse * @param {Mat4} m * @returns {Mat4} */ @@ -129,7 +130,7 @@ public slots: /**jsdoc - * @function Mat4.getFront + * @function Mat4(0).getFront * @param {Mat4} m * @returns {Vec3} */ @@ -137,28 +138,28 @@ public slots: glm::vec3 getFront(const glm::mat4& m) const { return getForward(m); } /**jsdoc - * @function Mat4.getForward + * @function Mat4(0).getForward * @param {Mat4} m * @returns {Vec3} */ glm::vec3 getForward(const glm::mat4& m) const; /**jsdoc - * @function Mat4.getRight + * @function Mat4(0).getRight * @param {Mat4} m * @returns {Vec3} */ glm::vec3 getRight(const glm::mat4& m) const; /**jsdoc - * @function Mat4.getUp + * @function Mat4(0).getUp * @param {Mat4} m * @returns {Vec3} */ glm::vec3 getUp(const glm::mat4& m) const; /**jsdoc - * @function Mat4.print + * @function Mat4(0).print * @param {string} label * @param {Mat4} m * @param {boolean} [transpose=false] diff --git a/libraries/shared/src/RegisteredMetaTypes.h b/libraries/shared/src/RegisteredMetaTypes.h index 1edb303455..ea2c5b8354 100644 --- a/libraries/shared/src/RegisteredMetaTypes.h +++ b/libraries/shared/src/RegisteredMetaTypes.h @@ -43,6 +43,27 @@ Q_DECLARE_METATYPE(std::function); void registerMetaTypes(QScriptEngine* engine); // Mat4 +/**jsdoc + * A 4 x 4 matrix, typically containing a scale, rotation, and translation transform. See also the {@link Mat4(0)|Mat4} object. + * + * @typedef {object} Mat4 + * @property {number} r0c0 - Row 0, column 0 value. + * @property {number} r1c0 - Row 1, column 0 value. + * @property {number} r2c0 - Row 2, column 0 value. + * @property {number} r3c0 - Row 3, column 0 value. + * @property {number} r0c1 - Row 0, column 1 value. + * @property {number} r1c1 - Row 1, column 1 value. + * @property {number} r2c1 - Row 2, column 1 value. + * @property {number} r3c1 - Row 3, column 1 value. + * @property {number} r0c2 - Row 0, column 2 value. + * @property {number} r1c2 - Row 1, column 2 value. + * @property {number} r2c2 - Row 2, column 2 value. + * @property {number} r3c2 - Row 3, column 2 value. + * @property {number} r0c3 - Row 0, column 3 value. + * @property {number} r1c3 - Row 1, column 3 value. + * @property {number} r2c3 - Row 2, column 3 value. + * @property {number} r3c3 - Row 3, column 3 value. + */ QScriptValue mat4toScriptValue(QScriptEngine* engine, const glm::mat4& mat4); void mat4FromScriptValue(const QScriptValue& object, glm::mat4& mat4); From d73ff2e8558b27b861d1602c63b05cfbffa89587 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 28 Feb 2019 10:40:10 +1300 Subject: [PATCH 014/446] Revise current MyAvatar API JSDoc --- interface/src/avatar/MyAvatar.cpp | 14 +- interface/src/avatar/MyAvatar.h | 425 +++++++++++------- .../src/avatars-renderer/Avatar.h | 99 ++-- 3 files changed, 326 insertions(+), 212 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index afb7a218f6..30e8733a42 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -2381,7 +2381,19 @@ void MyAvatar::clearWornAvatarEntities() { } } - +/**jsdoc + * Information about an avatar entity. + * + * + * + * + * + * + * + * + *
PropertyTypeDescription
idUuidEntity ID.
properties{@link Entities.EntityProperties}Entity properties.
+ * @typedef {object} MyAvatar.AvatarEntityData + */ QVariantList MyAvatar::getAvatarEntitiesVariant() { // NOTE: this method is NOT efficient QVariantList avatarEntitiesData; diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index d689d2f215..a0f1531e64 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -60,7 +60,9 @@ class MyAvatar : public Avatar { /**jsdoc * Your avatar is your in-world representation of you. The MyAvatar API is used to manipulate the avatar. * For example, you can customize the avatar's appearance, run custom avatar animations, - * change the avatar's position within the domain, or manage the avatar's collisions with other objects. + * change the avatar's position within the domain, or manage the avatar's collisions with the environment and other avatars. + * + *

For assignment client scripts, see {@link Avatar}.

* * @namespace MyAvatar * @@ -69,17 +71,20 @@ class MyAvatar : public Avatar { * @hifi-avatar * * @comment IMPORTANT: This group of properties is copied from AvatarData.h; they should NOT be edited here. - * @property {Vec3} position - * @property {number} scale - Returns the clamped scale of the avatar. - * @property {number} density Read-only. - * @property {Vec3} handPosition + * @property {Vec3} position - The position of the avatar. + * @property {number} scale=1.0 - The scale of the avatar. When setting, the value is limited to between 0.005 + * and 1000.0. When getting, the value may temporarily be further limited by the domain's settings. + * @property {number} density - The density of the avatar in kg/m3. The density is used to work out its mass in + * the application of physics. Read-only. + * @property {Vec3} handPosition - A user-defined hand position, in world coordinates. The position moves with the avatar + * but is otherwise not used or changed by Interface. * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. * Yaw is sometimes called "heading". * @property {number} bodyPitch - The rotation about an axis running from shoulder to shoulder of the avatar. Pitch is * sometimes called "elevation". * @property {number} bodyRoll - The rotation about an axis running from the chest to the back of the avatar. Roll is * sometimes called "bank". - * @property {Quat} orientation + * @property {Quat} orientation - The orientation of the avatar. * @property {Quat} headOrientation - The orientation of the avatar's head. * @property {number} headPitch - The rotation about an axis running from ear to ear of the avatar's head. Pitch is * sometimes called "elevation". @@ -87,22 +92,31 @@ class MyAvatar : public Avatar { * head. Yaw is sometimes called "heading". * @property {number} headRoll - The rotation about an axis running from the nose to the back of the avatar's head. Roll is * sometimes called "bank". - * @property {Vec3} velocity - * @property {Vec3} angularVelocity - * @property {number} audioLoudness - * @property {number} audioAverageLoudness - * @property {string} displayName - * @property {string} sessionDisplayName - Sanitized, defaulted version displayName that is defined by the AvatarMixer - * rather than by Interface clients. The result is unique among all avatars present at the time. - * @property {boolean} lookAtSnappingEnabled - * @property {string} skeletonModelURL - * @property {AttachmentData[]} attachmentData + * @property {Vec3} velocity - The current velocity of the avatar. + * @property {Vec3} angularVelocity - The current angular velocity of the avatar. + * @property {number} audioLoudness - The instantaneous loudness of the audio input that the avatar is injecting into the + * domain. + * @property {number} audioAverageLoudness - The rolling average loudness of the audio input that the avatar is injecting + * into the domain. + * @property {string} displayName - The avatar's display name. + * @property {string} sessionDisplayName - Sanitized, defaulted version of displayName that is defined by the + * avatar mixer rather than by Interface clients. The result is unique among all avatars present in the domain at the + * time. + * @property {boolean} lookAtSnappingEnabled=true - If true, the avatar's eyes snap to look at another avatar's + * eyes if generally in the line of sight and the other avatar also has lookAtSnappingEnabled == true. + * @property {string} skeletonModelURL - The URL of the avatar model's .fst file. + * @property {AttachmentData[]} attachmentData - Information on the attachments worn by the avatar.
+ * Deprecated: Use avatar entities instead. * @property {string[]} jointNames - The list of joints in the current avatar model. Read-only. - * @property {Uuid} sessionUUID Read-only. - * @property {Mat4} sensorToWorldMatrix Read-only. - * @property {Mat4} controllerLeftHandMatrix Read-only. - * @property {Mat4} controllerRightHandMatrix Read-only. - * @property {number} sensorToWorldScale Read-only. + * @property {Uuid} sessionUUID - Unique ID of the avatar in the domain. Read-only. + * @property {Mat4} sensorToWorldMatrix - The scale, rotation, and translation transform from the user's real world to the + * avatar's size, orientation, and position in the virtual world. Read-only. + * @property {Mat4} controllerLeftHandMatrix - The rotation and translation of the left hand controller relative to the + * avatar. Read-only. + * @property {Mat4} controllerRightHandMatrix - The rotation and translation of the right hand controller relative to the + * avatar. Read-only. + * @property {number} sensorToWorldScale - The scale that transforms dimensions in the user's real world to the avatar's + * size in the virtual world. Read-only. * * @comment IMPORTANT: This group of properties is copied from Avatar.h; they should NOT be edited here. * @property {Vec3} skeletonOffset - Can be used to apply a translation offset between the avatar's position and the @@ -124,9 +138,9 @@ class MyAvatar : public Avatar { * by the audio mixer, so all audio effectively plays back at a 24khz. 48kHz RAW files are also supported. * @property {number} audioListenerMode=0 - Specifies the listening position when hearing spatialized audio. Must be one * of the following property values:
- * audioListenerModeHead
- * audioListenerModeCamera
- * audioListenerModeCustom + * Myavatar.audioListenerModeHead
+ * Myavatar.audioListenerModeCamera
+ * Myavatar.audioListenerModeCustom * @property {number} audioListenerModeHead=0 - The audio listening position is at the avatar's head. Read-only. * @property {number} audioListenerModeCamera=1 - The audio listening position is at the camera. Read-only. * @property {number} audioListenerModeCustom=2 - The audio listening position is at a the position specified by set by the @@ -135,10 +149,12 @@ class MyAvatar : public Avatar { * property value is audioListenerModeCustom. * @property {Quat} customListenOrientation=Quat.IDENTITY - The listening orientation used when the * audioListenerMode property value is audioListenerModeCustom. - * @property {boolean} hasScriptedBlendshapes=false - Blendshapes will be transmitted over the network if set to true. - * @property {boolean} hasProceduralBlinkFaceMovement=true - procedural blinking will be turned on if set to true. - * @property {boolean} hasProceduralEyeFaceMovement=true - procedural eye movement will be turned on if set to true. - * @property {boolean} hasAudioEnabledFaceMovement=true - If set to true, voice audio will move the mouth Blendshapes while MyAvatar.hasScriptedBlendshapes is enabled. + * @property {boolean} hasScriptedBlendshapes=false - Blendshapes will be transmitted over the network if set to true.
+ * Note: Currently doesn't work. Use {@link MyAvatar.setForceFaceTrackerConnected} instead. + * @property {boolean} hasProceduralBlinkFaceMovement=true - If true then procedural blinking is turned on. + * @property {boolean} hasProceduralEyeFaceMovement=true - If true then procedural eye movement is turned on. + * @property {boolean} hasAudioEnabledFaceMovement=true - If true then voice audio will move the mouth + * blendshapes while MyAvatar.hasScriptedBlendshapes is enabled. * @property {number} rotationRecenterFilterLength * @property {number} rotationThreshold * @property {boolean} enableStepResetRotation @@ -169,16 +185,18 @@ class MyAvatar : public Avatar { * @property {boolean} hmdLeanRecenterEnabled=true - If true then the avatar is re-centered to be under the * head's position. In room-scale VR, this behavior is what causes your avatar to follow your HMD as you walk around * the room. Setting the value false is useful if you want to pin the avatar to a fixed position. - * @property {boolean} collisionsEnabled - Set to true to enable collisions for the avatar, false - * to disable collisions. May return true even though the value was set false because the - * zone may disallow collisionless avatars. - * @property {boolean} otherAvatarsCollisionsEnabled - * @property {boolean} characterControllerEnabled - Synonym of collisionsEnabled. + * @property {boolean} collisionsEnabled - Set to true to enable the avatar to collide with the environment, + * false to disable collisions with the environment. May return true even though the value + * was set false because the zone may disallow collisionless avatars. + * @property {boolean} otherAvatarsCollisionsEnabled - Set to true to enable the avatar to collide with other + * avatars, false to disable collisions with other avatars. + * @property {boolean} characterControllerEnabled - Synonym of collisionsEnabled.
* Deprecated: Use collisionsEnabled instead. * @property {boolean} useAdvancedMovementControls - Returns and sets the value of the Interface setting, Settings > - * Walking and teleporting. Note: Setting the value has no effect unless Interface is restarted. - * @property {boolean} showPlayArea - Returns and sets the value of the Interface setting, Settings > Show room boundaries - * while teleporting. Note: Setting the value has no effect unless Interface is restarted. + * Controls > Walking. Note: Setting the value has no effect unless Interface is restarted. + * @property {boolean} showPlayArea - Returns and sets the value of the Interface setting, Settings > Controls > Show room + * boundaries while teleporting.
+ * Note: Setting the value has no effect unless Interface is restarted. * * @property {number} yawSpeed=75 * @property {number} pitchSpeed=50 @@ -187,8 +205,8 @@ class MyAvatar : public Avatar { * while flying. * @property {number} hmdRollControlDeadZone=8 - The amount of HMD roll, in degrees, required before your avatar turns if * hmdRollControlEnabled is enabled. - * @property {number} hmdRollControlRate If hmdRollControlEnabled is true, this value determines the maximum turn rate of - * your avatar when rolling your HMD in degrees per second. + * @property {number} hmdRollControlRate If MyAvatar.hmdRollControlEnabled is true, this value determines the + * maximum turn rate of your avatar when rolling your HMD in degrees per second. * * @property {number} userHeight=1.75 - The height of the user in sensor space. * @property {number} userEyeHeight=1.65 - The estimated height of the user's eyes in sensor space. Read-only. @@ -237,8 +255,8 @@ class MyAvatar : public Avatar { * @borrows Avatar.attach as attach * @borrows Avatar.detachOne as detachOne * @borrows Avatar.detachAll as detachAll - * @borrows Avatar.getAvatarEntityData as getAvatarEntityData - * @borrows Avatar.setAvatarEntityData as setAvatarEntityData + * @comment Avatar.getAvatarEntityData as getAvatarEntityData - Don't borrow because implementation is different. + * @comment Avatar.setAvatarEntityData as setAvatarEntityData - Don't borrow because implementation is different. * @borrows Avatar.getSensorToWorldMatrix as getSensorToWorldMatrix * @borrows Avatar.getSensorToWorldScale as getSensorToWorldScale * @borrows Avatar.getControllerLeftHandMatrix as getControllerLeftHandMatrix @@ -253,10 +271,10 @@ class MyAvatar : public Avatar { * @borrows Avatar.sendAvatarDataPacket as sendAvatarDataPacket * @borrows Avatar.sendIdentityPacket as sendIdentityPacket * @borrows Avatar.setSessionUUID as setSessionUUID - * @borrows Avatar.getAbsoluteJointRotationInObjectFrame as getAbsoluteJointRotationInObjectFrame - * @borrows Avatar.getAbsoluteJointTranslationInObjectFrame as getAbsoluteJointTranslationInObjectFrame - * @borrows Avatar.setAbsoluteJointRotationInObjectFrame as setAbsoluteJointRotationInObjectFrame - * @borrows Avatar.setAbsoluteJointTranslationInObjectFrame as setAbsoluteJointTranslationInObjectFrame + * @comment Avatar.getAbsoluteJointRotationInObjectFrame as getAbsoluteJointRotationInObjectFrame - Don't borrow because implementation is different. + * @comment Avatar.getAbsoluteJointTranslationInObjectFrame as getAbsoluteJointTranslationInObjectFrame - Don't borrow because implementation is different. + * @comment Avatar.setAbsoluteJointRotationInObjectFrame as setAbsoluteJointRotationInObjectFrame - Don't borrow because implementation is different. + * @comment Avatar.setAbsoluteJointTranslationInObjectFrame as setAbsoluteJointTranslationInObjectFrame - Don't borrow because implementation is different. * @borrows Avatar.getTargetScale as getTargetScale * @borrows Avatar.resetLastSent as resetLastSent */ @@ -393,8 +411,9 @@ public: /**jsdoc - * The internal inverse-kinematics system maintains a record of which joints are "locked". Sometimes it is useful to forget this history, to prevent - * contorted joints. + * Clears inverse kinematics joint limit history. + *

The internal inverse-kinematics system maintains a record of which joints are "locked". Sometimes it is useful to + * forget this history, to prevent contorted joints.

* @function MyAvatar.clearIKJointLimitHistory */ Q_INVOKABLE void clearIKJointLimitHistory(); // thread-safe @@ -441,7 +460,7 @@ public: void setRealWorldFieldOfView(float realWorldFov) { _realWorldFieldOfView.set(realWorldFov); } /**jsdoc - * Get the position in world coordinates of the point directly between your avatar's eyes assuming your avatar was in its + * Gets the position in world coordinates of the point directly between your avatar's eyes assuming your avatar was in its * default pose. This is a reference position; it does not change as your avatar's head moves relative to the avatar * position. * @function MyAvatar.getDefaultEyePosition @@ -455,15 +474,16 @@ public: float getRealWorldFieldOfView() { return _realWorldFieldOfView.get(); } /**jsdoc - * The avatar animation system includes a set of default animations along with rules for how those animations are blended - * together with procedural data (such as look at vectors, hand sensors etc.). overrideAnimation() is used to completely - * override all motion from the default animation system (including inverse kinematics for hand and head controllers) and - * play a set of specified animations. To end these animations and restore the default animations, use - * {@link MyAvatar.restoreAnimation}.
+ * Overrides the default avatar animations. + *

The avatar animation system includes a set of default animations along with rules for how those animations are blended + * together with procedural data (such as look at vectors, hand sensors etc.). overrideAnimation() is used to + * completely override all motion from the default animation system (including inverse kinematics for hand and head + * controllers) and play a set of specified animations. To end these animations and restore the default animations, use + * {@link MyAvatar.restoreAnimation}.

*

Note: When using pre-built animation data, it's critical that the joint orientation of the source animation and target * rig are equivalent, since the animation data applies absolute values onto the joints. If the orientations are different, * the avatar will move in unpredictable ways. For more information about avatar joint orientation standards, see - * Avatar Standards.

+ * Avatar Standards.

* @function MyAvatar.overrideAnimation * @param url {string} The URL to the animation file. Animation files need to be .FBX format, but only need to contain the * avatar skeleton and animation data. @@ -482,10 +502,12 @@ public: Q_INVOKABLE void overrideAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame); /**jsdoc - * The avatar animation system includes a set of default animations along with rules for how those animations are blended together with - * procedural data (such as look at vectors, hand sensors etc.). Playing your own custom animations will override the default animations. - * restoreAnimation() is used to restore all motion from the default animation system including inverse kinematics for hand and head - * controllers. If you aren't currently playing an override animation, this function will have no effect. + * Restores the default animations. + *

The avatar animation system includes a set of default animations along with rules for how those animations are blended + * together with procedural data (such as look at vectors, hand sensors etc.). Playing your own custom animations will + * override the default animations. restoreAnimation() is used to restore all motion from the default + * animation system including inverse kinematics for hand and head controllers. If you aren't currently playing an override + * animation, this function has no effect.

* @function MyAvatar.restoreAnimation * @example Play a clapping animation on your avatar for three seconds. * // Clap your hands for 3 seconds then restore animation back to the avatar. @@ -498,10 +520,12 @@ public: Q_INVOKABLE void restoreAnimation(); /**jsdoc - * Each avatar has an avatar-animation.json file that defines which animations are used and how they are blended together with procedural data - * (such as look at vectors, hand sensors etc.). Each animation specified in the avatar-animation.json file is known as an animation role. - * Animation roles map to easily understandable actions that the avatar can perform, such as "idleStand", "idleTalk", or "walkFwd." - * getAnimationRoles() is used get the list of animation roles defined in the avatar-animation.json. + * Gets the current animation roles. + *

Each avatar has an avatar-animation.json file that defines which animations are used and how they are blended together + * with procedural data (such as look at vectors, hand sensors etc.). Each animation specified in the avatar-animation.json + * file is known as an animation role. Animation roles map to easily understandable actions that the avatar can perform, + * such as "idleStand", "idleTalk", or "walkFwd". getAnimationRoles() + * is used get the list of animation roles defined in the avatar-animation.json.

* @function MyAvatar.getAnimationRoles * @returns {string[]} Array of role strings. * @example Print the list of animation roles defined in the avatar's avatar-animation.json file to the debug log. @@ -514,17 +538,19 @@ public: Q_INVOKABLE QStringList getAnimationRoles(); /**jsdoc - * Each avatar has an avatar-animation.json file that defines a set of animation roles. Animation roles map to easily understandable actions - * that the avatar can perform, such as "idleStand", "idleTalk", or "walkFwd". To get the full list of roles, use getAnimationRoles(). - * For each role, the avatar-animation.json defines when the animation is used, the animation clip (.FBX) used, and how animations are blended - * together with procedural data (such as look at vectors, hand sensors etc.). - * overrideRoleAnimation() is used to change the animation clip (.FBX) associated with a specified animation role. To end - * the animations and restore the default animations, use {@link MyAvatar.restoreRoleAnimation}.
+ * Overrides a specific animation role. + *

Each avatar has an avatar-animation.json file that defines a set of animation roles. Animation roles map to easily + * understandable actions that the avatar can perform, such as "idleStand", "idleTalk", or + * "walkFwd". To get the full list of roles, use {@ link MyAvatar.getAnimationRoles}. + * For each role, the avatar-animation.json defines when the animation is used, the animation clip (.FBX) used, and how + * animations are blended together with procedural data (such as look at vectors, hand sensors etc.). + * overrideRoleAnimation() is used to change the animation clip (.FBX) associated with a specified animation + * role. To end the role animation and restore the default, use {@link MyAvatar.restoreRoleAnimation}.

*

Note: Hand roles only affect the hand. Other 'main' roles, like 'idleStand', 'idleTalk', 'takeoffStand' are full body.

*

Note: When using pre-built animation data, it's critical that the joint orientation of the source animation and target * rig are equivalent, since the animation data applies absolute values onto the joints. If the orientations are different, * the avatar will move in unpredictable ways. For more information about avatar joint orientation standards, see - * Avatar Standards. + * Avatar Standards. * @function MyAvatar.overrideRoleAnimation * @param role {string} The animation role to override * @param url {string} The URL to the animation file. Animation files need to be .FBX format, but only need to contain the avatar skeleton and animation data. @@ -548,13 +574,15 @@ public: Q_INVOKABLE void overrideRoleAnimation(const QString& role, const QString& url, float fps, bool loop, float firstFrame, float lastFrame); /**jsdoc - * Each avatar has an avatar-animation.json file that defines a set of animation roles. Animation roles map to easily understandable actions that - * the avatar can perform, such as "idleStand", "idleTalk", or "walkFwd". To get the full list of roles, use getAnimationRoles(). For each role, - * the avatar-animation.json defines when the animation is used, the animation clip (.FBX) used, and how animations are blended together with - * procedural data (such as look at vectors, hand sensors etc.). You can change the animation clip (.FBX) associated with a specified animation - * role using overrideRoleAnimation(). - * restoreRoleAnimation() is used to restore a specified animation role's default animation clip. If you have not specified an override animation - * for the specified role, this function will have no effect. + * Restores a default role animation. + *

Each avatar has an avatar-animation.json file that defines a set of animation roles. Animation roles map to easily + * understandable actions that the avatar can perform, such as "idleStand", "idleTalk", or + * "walkFwd". To get the full list of roles, use {#link MyAvatar.getAnimationRoles}. For each role, + * the avatar-animation.json defines when the animation is used, the animation clip (.FBX) used, and how animations are + * blended together with procedural data (such as look-at vectors, hand sensors etc.). You can change the animation clip + * (.FBX) associated with a specified animation role using {@link MyAvatar.overrideRoleAnimation}. + * restoreRoleAnimation() is used to restore a specified animation role's default animation clip. If you have + * not specified an override animation for the specified role, this function has no effect. * @function MyAvatar.restoreRoleAnimation * @param role {string} The animation role clip to restore. */ @@ -599,6 +627,7 @@ public: * @param {string} hand */ Q_INVOKABLE void setDominantHand(const QString& hand); + /**jsdoc * @function MyAvatar.getDominantHand * @returns {string} @@ -610,6 +639,7 @@ public: * @param {string} hand */ Q_INVOKABLE void setHmdAvatarAlignmentType(const QString& hand); + /**jsdoc * @function MyAvatar.getHmdAvatarAlignmentType * @returns {string} @@ -617,45 +647,61 @@ public: Q_INVOKABLE QString getHmdAvatarAlignmentType() const; /**jsdoc - * @function MyAvatar.setCenterOfGravityModelEnabled - * @param {boolean} enabled - */ + * @function MyAvatar.setCenterOfGravityModelEnabled + * @param {boolean} enabled + */ Q_INVOKABLE void setCenterOfGravityModelEnabled(bool value) { _centerOfGravityModelEnabled = value; } + /**jsdoc - * @function MyAvatar.getCenterOfGravityModelEnabled - * @returns {boolean} - */ + * @function MyAvatar.getCenterOfGravityModelEnabled + * @returns {boolean} + */ Q_INVOKABLE bool getCenterOfGravityModelEnabled() const { return _centerOfGravityModelEnabled; } /**jsdoc * @function MyAvatar.setHMDLeanRecenterEnabled * @param {boolean} enabled */ Q_INVOKABLE void setHMDLeanRecenterEnabled(bool value) { _hmdLeanRecenterEnabled = value; } + /**jsdoc * @function MyAvatar.getHMDLeanRecenterEnabled * @returns {boolean} */ Q_INVOKABLE bool getHMDLeanRecenterEnabled() const { return _hmdLeanRecenterEnabled; } + /**jsdoc - * Request to enable hand touch effect globally + * Requests that the hand touch effect is disabled for your avatar. Any resulting change in the status of the hand touch + * effect will be signaled by {@link MyAvatar.shouldDisableHandTouchChanged}. + *

The hand touch effect makes the avatar's fingers adapt to the shape of any object grabbed, creating the effect that + * it is really touching that object.

* @function MyAvatar.requestEnableHandTouch */ Q_INVOKABLE void requestEnableHandTouch(); + /**jsdoc - * Request to disable hand touch effect globally + * Requests that the hand touch effect is enabled for your avatar. Any resulting change in the status of the hand touch + * effect will be signaled by {@link MyAvatar.shouldDisableHandTouchChanged}. + *

The hand touch effect makes the avatar's fingers adapt to the shape of any object grabbed, creating the effect that + * it is really touching that object.

* @function MyAvatar.requestDisableHandTouch */ Q_INVOKABLE void requestDisableHandTouch(); + /**jsdoc - * Disables hand touch effect on a specific entity + * Disables the hand touch effect on a specific entity. + *

The hand touch effect makes the avatar's fingers adapt to the shape of any object grabbed, creating the effect that + * it is really touching that object.

* @function MyAvatar.disableHandTouchForID - * @param {Uuid} entityID - ID of the entity that will disable hand touch effect + * @param {Uuid} entityID - The entity that the hand touch effect will be disabled for. */ Q_INVOKABLE void disableHandTouchForID(const QUuid& entityID); + /**jsdoc - * Enables hand touch effect on a specific entity + * Enables the hand touch effect on a specific entity. + *

The hand touch effect makes the avatar's fingers adapt to the shape of any object grabbed, creating the effect that + * it is really touching that object.

* @function MyAvatar.enableHandTouchForID - * @param {Uuid} entityID - ID of the entity that will enable hand touch effect + * @param {Uuid} entityID - The entity that the hand touch effect will be enabled for. */ Q_INVOKABLE void enableHandTouchForID(const QUuid& entityID); @@ -712,6 +758,7 @@ public: * @param {DriveKeys} key */ Q_INVOKABLE void enableDriveKey(DriveKeys key); + /**jsdoc * @function MyAvatar.isDriveKeyDisabled * @param {DriveKeys} key @@ -741,10 +788,10 @@ public: Q_INVOKABLE void triggerRotationRecenter(); /**jsdoc - *The isRecenteringHorizontally function returns true if MyAvatar - *is translating the root of the Avatar to keep the center of gravity under the head. - *isActive(Horizontal) is returned. - *@function MyAvatar.isRecenteringHorizontally + * Gets whether or not the avatar is configured to keep its center of gravity under its head. + * @function MyAvatar.isRecenteringHorizontally + * @returns {boolean} true if the avatar is keeping its center of gravity under its head position, + * false if not. */ Q_INVOKABLE bool isRecenteringHorizontally() const; @@ -753,7 +800,7 @@ public: const MyHead* getMyHead() const; /**jsdoc - * Get the current position of the avatar's "Head" joint. + * Gets the current position of the avatar's "Head" joint. * @function MyAvatar.getHeadPosition * @returns {Vec3} The current position of the avatar's "Head" joint. * @example Report the current position of your avatar's head. @@ -786,7 +833,7 @@ public: Q_INVOKABLE float getHeadDeltaPitch() const { return getHead()->getDeltaPitch(); } /**jsdoc - * Get the current position of the point directly between the avatar's eyes. + * Gets the current position of the point directly between the avatar's eyes. * @function MyAvatar.getEyePosition * @returns {Vec3} The current position of the point directly between the avatar's eyes. * @example Report your avatar's current eye position. @@ -796,8 +843,9 @@ public: Q_INVOKABLE glm::vec3 getEyePosition() const { return getHead()->getEyePosition(); } /**jsdoc + * Gets the position of the avatar your avatar is currently looking at. * @function MyAvatar.getTargetAvatarPosition - * @returns {Vec3} The position of the avatar you're currently looking at. + * @returns {Vec3} The position of the avatar your avatar is currently looking at. * @example Report the position of the avatar you're currently looking at. * print(JSON.stringify(MyAvatar.getTargetAvatarPosition())); */ @@ -811,7 +859,7 @@ public: /**jsdoc - * Get the position of the avatar's left hand as positioned by a hand controller (e.g., Oculus Touch or Vive).
+ * Gets the position of the avatar's left hand as positioned by a hand controller (e.g., Oculus Touch or Vive).
*

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints * for hand animation.)

* @function MyAvatar.getLeftHandPosition @@ -823,7 +871,7 @@ public: Q_INVOKABLE glm::vec3 getLeftHandPosition() const; /**jsdoc - * Get the position of the avatar's right hand as positioned by a hand controller (e.g., Oculus Touch or Vive).
+ * Gets the position of the avatar's right hand as positioned by a hand controller (e.g., Oculus Touch or Vive).
*

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints * for hand animation.)

* @function MyAvatar.getRightHandPosition @@ -848,7 +896,7 @@ public: /**jsdoc - * Get the pose (position, rotation, velocity, and angular velocity) of the avatar's left hand as positioned by a + * Gets the pose (position, rotation, velocity, and angular velocity) of the avatar's left hand as positioned by a * hand controller (e.g., Oculus Touch or Vive).
*

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints * for hand animation.) If you are using the Leap Motion, the return value's valid property will be @@ -861,7 +909,7 @@ public: Q_INVOKABLE controller::Pose getLeftHandPose() const; /**jsdoc - * Get the pose (position, rotation, velocity, and angular velocity) of the avatar's left hand as positioned by a + * Gets the pose (position, rotation, velocity, and angular velocity) of the avatar's left hand as positioned by a * hand controller (e.g., Oculus Touch or Vive).
*

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints * for hand animation.) If you are using the Leap Motion, the return value's valid property will be @@ -935,7 +983,7 @@ public: Q_INVOKABLE void useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelName = QString()); /**jsdoc - * Get the complete URL for the current avatar. + * Gets the complete URL for the current avatar. * @function MyAvatar.getFullAvatarURLFromPreferences * @returns {string} The full avatar model name. * @example Report the URL for the current avatar. @@ -944,7 +992,7 @@ public: Q_INVOKABLE QUrl getFullAvatarURLFromPreferences() const { return _fullAvatarURLFromPreferences; } /**jsdoc - * Get the full avatar model name for the current avatar. + * Gets the full avatar model name for the current avatar. * @function MyAvatar.getFullAvatarModelName * @returns {string} The full avatar model name. * @example Report the current full avatar model name. @@ -1015,24 +1063,24 @@ public: bool hasDriveInput() const; /**jsdoc - * Function returns list of avatar entities - * @function MyAvatar.getAvatarEntitiesVariant - * @returns {object[]} - */ + * Gets the list of avatar entities and their properties. + * @function MyAvatar.getAvatarEntitiesVariant + * @returns {MyAvatar.AvatarEntityData[]} + */ Q_INVOKABLE QVariantList getAvatarEntitiesVariant(); + void removeWornAvatarEntity(const EntityItemID& entityID); void clearWornAvatarEntities(); /**jsdoc - * Check whether your avatar is flying or not. + * Checks whether your avatar is flying or not. * @function MyAvatar.isFlying - * @returns {boolean} true if your avatar is flying and not taking off or falling, otherwise - * false. + * @returns {boolean} true if your avatar is flying and not taking off or falling, false if not. */ Q_INVOKABLE bool isFlying(); /**jsdoc - * Check whether your avatar is in the air or not. + * Checks whether your avatar is in the air or not. * @function MyAvatar.isInAir * @returns {boolean} true if your avatar is taking off, flying, or falling, otherwise false * because your avatar is on the ground. @@ -1040,7 +1088,7 @@ public: Q_INVOKABLE bool isInAir(); /**jsdoc - * Set your preference for flying in your current desktop or HMD display mode. Note that your ability to fly also depends + * Sets your preference for flying in your current desktop or HMD display mode. Note that your ability to fly also depends * on whether the domain you're in allows you to fly. * @function MyAvatar.setFlyingEnabled * @param {boolean} enabled - Set true if you want to enable flying in your current desktop or HMD display @@ -1049,7 +1097,7 @@ public: Q_INVOKABLE void setFlyingEnabled(bool enabled); /**jsdoc - * Get your preference for flying in your current desktop or HMD display mode. Note that your ability to fly also depends + * Gets your preference for flying in your current desktop or HMD display mode. Note that your ability to fly also depends * on whether the domain you're in allows you to fly. * @function MyAvatar.getFlyingEnabled * @returns {boolean} true if your preference is to enable flying in your current desktop or HMD display mode, @@ -1058,7 +1106,7 @@ public: Q_INVOKABLE bool getFlyingEnabled(); /**jsdoc - * Set your preference for flying in desktop display mode. Note that your ability to fly also depends on whether the domain + * Sets your preference for flying in desktop display mode. Note that your ability to fly also depends on whether the domain * you're in allows you to fly. * @function MyAvatar.setFlyingDesktopPref * @param {boolean} enabled - Set true if you want to enable flying in desktop display mode, otherwise set @@ -1067,7 +1115,7 @@ public: Q_INVOKABLE void setFlyingDesktopPref(bool enabled); /**jsdoc - * Get your preference for flying in desktop display mode. Note that your ability to fly also depends on whether the domain + * Gets your preference for flying in desktop display mode. Note that your ability to fly also depends on whether the domain * you're in allows you to fly. * @function MyAvatar.getFlyingDesktopPref * @returns {boolean} true if your preference is to enable flying in desktop display mode, otherwise @@ -1076,7 +1124,7 @@ public: Q_INVOKABLE bool getFlyingDesktopPref(); /**jsdoc - * Set your preference for flying in HMD display mode. Note that your ability to fly also depends on whether the domain + * Sets your preference for flying in HMD display mode. Note that your ability to fly also depends on whether the domain * you're in allows you to fly. * @function MyAvatar.setFlyingHMDPref * @param {boolean} enabled - Set true if you want to enable flying in HMD display mode, otherwise set @@ -1085,7 +1133,7 @@ public: Q_INVOKABLE void setFlyingHMDPref(bool enabled); /**jsdoc - * Get your preference for flying in HMD display mode. Note that your ability to fly also depends on whether the domain + * Gets your preference for flying in HMD display mode. Note that your ability to fly also depends on whether the domain * you're in allows you to fly. * @function MyAvatar.getFlyingHMDPref * @returns {boolean} true if your preference is to enable flying in HMD display mode, otherwise @@ -1120,38 +1168,53 @@ public: Q_INVOKABLE bool getCollisionsEnabled(); /**jsdoc - * @function MyAvatar.setOtherAvatarsCollisionsEnabled - * @param {boolean} enabled - */ + * @function MyAvatar.setOtherAvatarsCollisionsEnabled + * @param {boolean} enabled + */ Q_INVOKABLE void setOtherAvatarsCollisionsEnabled(bool enabled); /**jsdoc - * @function MyAvatar.getOtherAvatarsCollisionsEnabled - * @returns {boolean} - */ + * @function MyAvatar.getOtherAvatarsCollisionsEnabled + * @returns {boolean} + */ Q_INVOKABLE bool getOtherAvatarsCollisionsEnabled(); /**jsdoc - * @function MyAvatar.getCollisionCapsule - * @returns {object} - */ + * @function MyAvatar.getCollisionCapsule + * @returns {object} + */ Q_INVOKABLE QVariantMap getCollisionCapsule() const; /**jsdoc * @function MyAvatar.setCharacterControllerEnabled * @param {boolean} enabled - * @deprecated + * @deprecated Use {@link MyAvatar.setCollisionsEnabled} instead. */ Q_INVOKABLE void setCharacterControllerEnabled(bool enabled); // deprecated /**jsdoc * @function MyAvatar.getCharacterControllerEnabled * @returns {boolean} - * @deprecated + * @deprecated Use {@link MyAvatar.getCollisionsEnabled} instead. */ Q_INVOKABLE bool getCharacterControllerEnabled(); // deprecated + /**jsdoc + * @comment Different behavior to the Avatar version of this method. + * Gets the rotation of a joint relative to the avatar. + * @function MyAvatar.getAbsoluteJointRotationInObjectFrame + * @param {number} index - The index of the joint. + * @returns {Quat} The rotation of the joint relative to the avatar. + */ virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override; + + /**jsdoc + * @comment Different behavior to the Avatar version of this method. + * Gets the translation of a joint relative to the avatar. + * @function MyAvatar.getAbsoluteJointTranslationInObjectFrame + * @param {number} index - The index of the joint. + * @returns {Vec3} The translation of the joint relative to the avatar. + */ virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override; // all calibration matrices are in absolute sensor space. @@ -1241,34 +1304,56 @@ public: void prepareAvatarEntityDataForReload(); /**jsdoc - * Create a new grab. + * Creates a new grab, that grabs an entity. * @function MyAvatar.grab - * @param {Uuid} targetID - id of grabbed thing - * @param {number} parentJointIndex - avatar joint being used to grab - * @param {Vec3} offset - target's positional offset from joint - * @param {Quat} rotationalOffset - target's rotational offset from joint - * @returns {Uuid} id of the new grab + * @param {Uuid} targetID - The ID of the entity to grab. + * @param {number} parentJointIndex - The avatar joint to use to grab the entity. + * @param {Vec3} offset - The target's local positional relative to the joint. + * @param {Quat} rotationalOffset - The target's local rotation relative to the joint. + * @returns {Uuid} The ID of the new grab. */ Q_INVOKABLE const QUuid grab(const QUuid& targetID, int parentJointIndex, glm::vec3 positionalOffset, glm::quat rotationalOffset); /**jsdoc - * Release (delete) a grab. + * Releases (deletes) a grab, to stop grabbing an entity. * @function MyAvatar.releaseGrab - * @param {Uuid} grabID - id of grabbed thing + * @param {Uuid} grabID - The ID of the grab to release. */ Q_INVOKABLE void releaseGrab(const QUuid& grabID); + /**jsdoc + * Gets the avatar entities as binary data. + * @function MyAvatar.getAvatarEntityData + * @override + * @returns {AvatarEntityMap} + */ AvatarEntityMap getAvatarEntityData() const override; + + /**jsdoc + * Sets the avatar entities from binary data. + * @function MyAvatar.setAvatarEntityData + * @param {AvatarEntityMap} avatarEntityData + */ void setAvatarEntityData(const AvatarEntityMap& avatarEntityData) override; + + /**jsdoc + * @comment Uses the base class's JSDoc. + */ void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) override; + void avatarEntityDataToJson(QJsonObject& root) const override; + + /**jsdoc + * @function MyAvatar.sendAvatarDataPacket + * @param {boolean} sendAll + */ int sendAvatarDataPacket(bool sendAll = false) override; public slots: /**jsdoc - * Increase the avatar's scale by five percent, up to a minimum scale of 1000. + * Increases the avatar's scale by five percent, up to a minimum scale of 1000. * @function MyAvatar.increaseSize * @example Reset your avatar's size to default then grow it 5 times. * MyAvatar.resetSize(); @@ -1281,7 +1366,7 @@ public slots: void increaseSize(); /**jsdoc - * Decrease the avatar's scale by five percent, down to a minimum scale of 0.25. + * Decreases the avatar's scale by five percent, down to a minimum scale of 0.25. * @function MyAvatar.decreaseSize * @example Reset your avatar's size to default then shrink it 5 times. * MyAvatar.resetSize(); @@ -1294,7 +1379,7 @@ public slots: void decreaseSize(); /**jsdoc - * Reset the avatar's scale back to the default scale of 1.0. + * Resets the avatar's scale back to the default scale of 1.0. * @function MyAvatar.resetSize */ void resetSize(); @@ -1317,7 +1402,7 @@ public slots: float getGravity(); /**jsdoc - * Move the avatar to a new position and/or orientation in the domain, while taking into account Avatar leg-length. + * Moves the avatar to a new position and/or orientation in the domain, while taking into account Avatar leg-length. * @function MyAvatar.goToFeetLocation * @param {Vec3} position - The new position for the avatar, in world coordinates. * @param {boolean} [hasOrientation=false] - Set to true to set the orientation of the avatar. @@ -1330,7 +1415,7 @@ public slots: bool shouldFaceLocation); /**jsdoc - * Move the avatar to a new position and/or orientation in the domain. + * Moves the avatar to a new position and/or orientation in the domain. * @function MyAvatar.goToLocation * @param {Vec3} position - The new position for the avatar, in world coordinates. * @param {boolean} [hasOrientation=false] - Set to true to set the orientation of the avatar. @@ -1395,61 +1480,70 @@ public slots: /**jsdoc - * ####### Why Q_INVOKABLE? * @function MyAvatar.updateMotionBehaviorFromMenu */ Q_INVOKABLE void updateMotionBehaviorFromMenu(); /**jsdoc - * @function MyAvatar.setToggleHips - * @param {boolean} enabled - */ + * @function MyAvatar.setToggleHips + * @param {boolean} enabled + */ void setToggleHips(bool followHead); + /**jsdoc - * @function MyAvatar.setEnableDebugDrawBaseOfSupport - * @param {boolean} enabled - */ + * @function MyAvatar.setEnableDebugDrawBaseOfSupport + * @param {boolean} enabled + */ void setEnableDebugDrawBaseOfSupport(bool isEnabled); + /**jsdoc * @function MyAvatar.setEnableDebugDrawDefaultPose * @param {boolean} enabled */ void setEnableDebugDrawDefaultPose(bool isEnabled); + /**jsdoc * @function MyAvatar.setEnableDebugDrawAnimPose * @param {boolean} enabled */ void setEnableDebugDrawAnimPose(bool isEnabled); + /**jsdoc * @function MyAvatar.setEnableDebugDrawPosition * @param {boolean} enabled */ void setEnableDebugDrawPosition(bool isEnabled); + /**jsdoc * @function MyAvatar.setEnableDebugDrawHandControllers * @param {boolean} enabled */ void setEnableDebugDrawHandControllers(bool isEnabled); + /**jsdoc * @function MyAvatar.setEnableDebugDrawSensorToWorldMatrix * @param {boolean} enabled */ void setEnableDebugDrawSensorToWorldMatrix(bool isEnabled); + /**jsdoc * @function MyAvatar.setEnableDebugDrawIKTargets * @param {boolean} enabled */ void setEnableDebugDrawIKTargets(bool isEnabled); + /**jsdoc * @function MyAvatar.setEnableDebugDrawIKConstraints * @param {boolean} enabled */ void setEnableDebugDrawIKConstraints(bool isEnabled); + /**jsdoc * @function MyAvatar.setEnableDebugDrawIKChains * @param {boolean} enabled */ void setEnableDebugDrawIKChains(bool isEnabled); + /**jsdoc * @function MyAvatar.setEnableDebugDrawDetailedCollision * @param {boolean} enabled @@ -1457,23 +1551,20 @@ public slots: void setEnableDebugDrawDetailedCollision(bool isEnabled); /**jsdoc - * Get whether or not your avatar mesh is visible. + * Gets whether or not your avatar mesh is visible. * @function MyAvatar.getEnableMeshVisible * @returns {boolean} true if your avatar's mesh is visible, otherwise false. */ bool getEnableMeshVisible() const override; /**jsdoc - * ####### TODO; Should this really be exposed in the API? * @function MyAvatar.storeAvatarEntityDataPayload + * @deprecated This function is deprecated and will be removed. */ void storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& payload) override; /**jsdoc - * ####### Does override change functionality? If so, document here and don't borrow; if not, borrow and don't document here. - * @function MyAvatar.clearAvatarEntity - * @param {Uuid} entityID - * @param {boolean} [requiresRemovalFromTree] + * @comment Uses the base class's JSDoc. */ void clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree = true) override; @@ -1483,7 +1574,7 @@ public slots: void sanitizeAvatarEntityProperties(EntityItemProperties& properties) const; /**jsdoc - * Set whether or not your avatar mesh is visible. + * Sets whether or not your avatar mesh is visible. * @function MyAvatar.setEnableMeshVisible * @param {boolean} visible - true to set your avatar mesh visible; false to set it invisible. * @example Make your avatar invisible for 10s. @@ -1577,25 +1668,27 @@ signals: void collisionWithEntity(const Collision& collision); /**jsdoc - * Triggered when collisions with avatar enabled or disabled + * Triggered when collisions with the environment are enabled or disabled. * @function MyAvatar.collisionsEnabledChanged - * @param {boolean} enabled + * @param {boolean} enabled - true if collisions with the environment are enabled, false if + * they're not. * @returns {Signal} */ void collisionsEnabledChanged(bool enabled); /**jsdoc - * Triggered when collisions with other avatars enabled or disabled + * Triggered when collisions with other avatars are enabled or disabled. * @function MyAvatar.otherAvatarsCollisionsEnabledChanged - * @param {boolean} enabled + * @param {boolean} enabled - true if collisions with other avatars are enabled, false if they're + * not. * @returns {Signal} */ void otherAvatarsCollisionsEnabledChanged(bool enabled); /**jsdoc - * Triggered when avatar's animation url changes + * Triggered when the avatar's animation changes. * @function MyAvatar.animGraphUrlChanged - * @param {url} url + * @param {url} url - The URL of the new animation. * @returns {Signal} */ void animGraphUrlChanged(const QUrl& url); @@ -1672,18 +1765,24 @@ signals: void scaleChanged(); /**jsdoc - * Triggered when hand touch is globally enabled or disabled + * Triggered when the hand touch effect is enabled or disabled for the avatar. + *

The hand touch effect makes the avatar's fingers adapt to the shape of any object grabbed, creating the effect that + * it is really touching that object.

* @function MyAvatar.shouldDisableHandTouchChanged - * @param {boolean} shouldDisable + * @param {boolean} disabled - true if the hand touch effect is disabled for the avatar, + * false if it isn't disabled. * @returns {Signal} */ void shouldDisableHandTouchChanged(bool shouldDisable); /**jsdoc - * Triggered when hand touch is enabled or disabled for an specific entity + * Triggered when the hand touch is enabled or disabled on a specific entity. + *

The hand touch effect makes the avatar's fingers adapt to the shape of any object grabbed, creating the effect that + * it is really touching that object.

* @function MyAvatar.disableHandTouchForIDChanged - * @param {Uuid} entityID - ID of the entity that will enable hand touch effect - * @param {boolean} disable + * @param {Uuid} entityID - The entity that the hand touch effect has been enabled or disabled for. + * @param {boolean} disabled - true if the hand touch effect is disabled for the entity, + * false if it isn't disabled. * @returns {Signal} */ void disableHandTouchForIDChanged(const QUuid& entityID, bool disable); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 98aa255641..11940ad76a 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -215,17 +215,17 @@ public: Q_INVOKABLE virtual glm::vec3 getDefaultJointTranslation(int index) const; /**jsdoc - * Provides read only access to the default joint rotations in avatar coordinates. + * Provides read-only access to the default joint rotations in avatar coordinates. * The default pose of the avatar is defined by the position and orientation of all bones * in the avatar's model file. Typically this is a T-pose. * @function MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame - * @param index {number} -The joint index. + * @param index {number} - The joint index. * @returns {Quat} The rotation of this joint in avatar coordinates. */ Q_INVOKABLE virtual glm::quat getAbsoluteDefaultJointRotationInObjectFrame(int index) const; /**jsdoc - * Provides read only access to the default joint translations in avatar coordinates. + * Provides read-only access to the default joint translations in avatar coordinates. * The default pose of the avatar is defined by the position and orientation of all bones * in the avatar's model file. Typically this is a T-pose. * @function MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame @@ -261,52 +261,51 @@ public: // world-space to avatar-space rigconversion functions /**jsdoc - * @function MyAvatar.worldToJointPoint - * @param {Vec3} position - * @param {number} [jointIndex=-1] - * @returns {Vec3} - */ + * @function MyAvatar.worldToJointPoint + * @param {Vec3} position + * @param {number} [jointIndex=-1] + * @returns {Vec3} + */ Q_INVOKABLE glm::vec3 worldToJointPoint(const glm::vec3& position, const int jointIndex = -1) const; /**jsdoc - * @function MyAvatar.worldToJointDirection - * @param {Vec3} direction - * @param {number} [jointIndex=-1] - * @returns {Vec3} - */ + * @function MyAvatar.worldToJointDirection + * @param {Vec3} direction + * @param {number} [jointIndex=-1] + * @returns {Vec3} + */ Q_INVOKABLE glm::vec3 worldToJointDirection(const glm::vec3& direction, const int jointIndex = -1) const; /**jsdoc - * @function MyAvatar.worldToJointRotation - * @param {Quat} rotation - * @param {number} [jointIndex=-1] - * @returns {Quat} + * @function MyAvatar.worldToJointRotation + * @param {Quat} rotation + * @param {number} [jointIndex=-1] + * @returns {Quat} */ Q_INVOKABLE glm::quat worldToJointRotation(const glm::quat& rotation, const int jointIndex = -1) const; - /**jsdoc - * @function MyAvatar.jointToWorldPoint - * @param {vec3} position - * @param {number} [jointIndex=-1] - * @returns {Vec3} - */ + * @function MyAvatar.jointToWorldPoint + * @param {vec3} position + * @param {number} [jointIndex=-1] + * @returns {Vec3} + */ Q_INVOKABLE glm::vec3 jointToWorldPoint(const glm::vec3& position, const int jointIndex = -1) const; /**jsdoc - * @function MyAvatar.jointToWorldDirection - * @param {Vec3} direction - * @param {number} [jointIndex=-1] - * @returns {Vec3} - */ + * @function MyAvatar.jointToWorldDirection + * @param {Vec3} direction + * @param {number} [jointIndex=-1] + * @returns {Vec3} + */ Q_INVOKABLE glm::vec3 jointToWorldDirection(const glm::vec3& direction, const int jointIndex = -1) const; /**jsdoc - * @function MyAvatar.jointToWorldRotation - * @param {Quat} rotation - * @param {number} [jointIndex=-1] - * @returns {Quat} - */ + * @function MyAvatar.jointToWorldRotation + * @param {Quat} rotation + * @param {number} [jointIndex=-1] + * @returns {Quat} + */ Q_INVOKABLE glm::quat jointToWorldRotation(const glm::quat& rotation, const int jointIndex = -1) const; virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override; @@ -321,7 +320,7 @@ public: float radius1, float radius2, const glm::vec4& color); /**jsdoc - * Set the offset applied to the current avatar. The offset adjusts the position that the avatar is rendered. For example, + * Sets the offset applied to the current avatar. The offset adjusts the position that the avatar is rendered. For example, * with an offset of { x: 0, y: 0.1, z: 0 }, your avatar will appear to be raised off the ground slightly. * @function MyAvatar.setSkeletonOffset * @param {Vec3} offset - The skeleton offset to set. @@ -337,7 +336,7 @@ public: Q_INVOKABLE void setSkeletonOffset(const glm::vec3& offset); /**jsdoc - * Get the offset applied to the current avatar. The offset adjusts the position that the avatar is rendered. For example, + * Gets the offset applied to the current avatar. The offset adjusts the position that the avatar is rendered. For example, * with an offset of { x: 0, y: 0.1, z: 0 }, your avatar will appear to be raised off the ground slightly. * @function MyAvatar.getSkeletonOffset * @returns {Vec3} The current skeleton offset. @@ -349,7 +348,7 @@ public: virtual glm::vec3 getSkeletonPosition() const; /**jsdoc - * Get the position of a joint in the current avatar. + * Gets the position of a joint in the current avatar. * @function MyAvatar.getJointPosition * @param {number} index - The index of the joint. * @returns {Vec3} The position of the joint in world coordinates. @@ -357,7 +356,7 @@ public: Q_INVOKABLE glm::vec3 getJointPosition(int index) const; /**jsdoc - * Get the position of a joint in the current avatar. + * Gets the position of a joint in the current avatar. * @function MyAvatar.getJointPosition * @param {string} name - The name of the joint. * @returns {Vec3} The position of the joint in world coordinates. @@ -367,7 +366,7 @@ public: Q_INVOKABLE glm::vec3 getJointPosition(const QString& name) const; /**jsdoc - * Get the position of the current avatar's neck in world coordinates. + * Gets the position of the current avatar's neck in world coordinates. * @function MyAvatar.getNeckPosition * @returns {Vec3} The position of the neck in world coordinates. * @example Report the position of your avatar's neck. @@ -401,10 +400,10 @@ public: void getCapsule(glm::vec3& start, glm::vec3& end, float& radius); float computeMass(); /**jsdoc - * Get the position of the current avatar's feet (or rather, bottom of its collision capsule) in world coordinates. + * Gets the position of the current avatar's feet (or rather, bottom of its collision capsule) in world coordinates. * @function MyAvatar.getWorldFeetPosition * @returns {Vec3} The position of the avatar's feet in world coordinates. - */ + */ Q_INVOKABLE glm::vec3 getWorldFeetPosition(); void setPositionViaScript(const glm::vec3& position) override; @@ -439,9 +438,9 @@ public: Q_INVOKABLE virtual void setParentJointIndex(quint16 parentJointIndex) override; /**jsdoc - * Returns an array of joints, where each joint is an object containing name, index, and parentIndex fields. + * Gets information on all the joints in the avatar's skeleton. * @function MyAvatar.getSkeleton - * @returns {MyAvatar.SkeletonJoint[]} A list of information about each joint in this avatar's skeleton. + * @returns {MyAvatar.SkeletonJoint[]} Information about each joint in the avatar's skeleton. */ /**jsdoc * Information about a single joint in an Avatar's skeleton hierarchy. @@ -524,6 +523,8 @@ public: signals: /**jsdoc * @function MyAvatar.targetScaleChanged + * @param {number} targetScale + * @returns Signal */ void targetScaleChanged(float targetScale); @@ -533,7 +534,7 @@ public slots: // thread safe, will return last valid palm from cache /**jsdoc - * Get the position of the left palm in world coordinates. + * Gets the position of the left palm in world coordinates. * @function MyAvatar.getLeftPalmPosition * @returns {Vec3} The position of the left palm in world coordinates. * @example Report the position of your avatar's left palm. @@ -542,15 +543,16 @@ public slots: glm::vec3 getLeftPalmPosition() const; /**jsdoc - * Get the rotation of the left palm in world coordinates. + * Gets the rotation of the left palm in world coordinates. * @function MyAvatar.getLeftPalmRotation * @returns {Quat} The rotation of the left palm in world coordinates. * @example Report the rotation of your avatar's left palm. * print(JSON.stringify(MyAvatar.getLeftPalmRotation())); */ glm::quat getLeftPalmRotation() const; + /**jsdoc - * Get the position of the right palm in world coordinates. + * Gets the position of the right palm in world coordinates. * @function MyAvatar.getRightPalmPosition * @returns {Vec3} The position of the right palm in world coordinates. * @example Report the position of your avatar's right palm. @@ -569,22 +571,23 @@ public slots: /**jsdoc * @function MyAvatar.setModelURLFinished + * @param {boolean} success */ // hooked up to Model::setURLFinished signal void setModelURLFinished(bool success); /**jsdoc * @function MyAvatar.rigReady - * @returns {Signal} + * @deprecated This function is deprecated and will be removed. */ // Hooked up to Model::rigReady signal void rigReady(); /**jsdoc * @function MyAvatar.rigReset - * @returns {Signal} + * @deprecated This function is deprecated and will be removed. */ - // Jooked up to Model::rigReset signal + // Hooked up to Model::rigReset signal void rigReset(); protected: From 18325ef5e3771f8752f2b541a8e8455dd85e99e1 Mon Sep 17 00:00:00 2001 From: Oren Hurvitz Date: Mon, 16 Apr 2018 15:26:06 +0300 Subject: [PATCH 015/446] Save the "Mute Microphone" setting --- interface/src/scripting/Audio.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 2c4c29ff65..b4e3d7913b 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -25,6 +25,8 @@ QString Audio::DESKTOP { "Desktop" }; QString Audio::HMD { "VR" }; Setting::Handle enableNoiseReductionSetting { QStringList { Audio::AUDIO, "NoiseReduction" }, true }; +Setting::Handle mutedSetting { QStringList{ Audio::AUDIO, "MuteMicrophone" }, false }; + float Audio::loudnessToLevel(float loudness) { float level = loudness * (1/32768.0f); // level in [0, 1] @@ -42,6 +44,7 @@ Audio::Audio() : _devices(_contextIsHMD) { connect(this, &Audio::contextChanged, &_devices, &AudioDevices::onContextChanged); enableNoiseReduction(enableNoiseReductionSetting.get()); onContextChanged(); + setMuted(mutedSetting.get()); } bool Audio::startRecording(const QString& filepath) { @@ -89,6 +92,15 @@ bool Audio::noiseReductionEnabled() const { }); } +void Audio::onMutedChanged() { + bool isMuted = DependencyManager::get()->isMuted(); + if (_isMuted != isMuted) { + _isMuted = isMuted; + mutedSetting.set(_isMuted); + emit mutedChanged(_isMuted); + } +} + void Audio::enableNoiseReduction(bool enable) { bool changed = false; withWriteLock([&] { From 3f523617535a991463334977b1c4c164dbcfb17d Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sun, 17 Feb 2019 14:21:23 -0800 Subject: [PATCH 016/446] rework audioMuteOverlay.js --- interface/src/scripting/Audio.cpp | 10 +-- scripts/defaultScripts.js | 3 +- scripts/system/audioMuteOverlay.js | 140 +++++++++++++---------------- 3 files changed, 63 insertions(+), 90 deletions(-) diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index b4e3d7913b..bb40f69b0b 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -76,6 +76,7 @@ void Audio::setMuted(bool isMuted) { withWriteLock([&] { if (_isMuted != isMuted) { _isMuted = isMuted; + mutedSetting.set(_isMuted); auto client = DependencyManager::get().data(); QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); changed = true; @@ -92,15 +93,6 @@ bool Audio::noiseReductionEnabled() const { }); } -void Audio::onMutedChanged() { - bool isMuted = DependencyManager::get()->isMuted(); - if (_isMuted != isMuted) { - _isMuted = isMuted; - mutedSetting.set(_isMuted); - emit mutedChanged(_isMuted); - } -} - void Audio::enableNoiseReduction(bool enable) { bool changed = false; withWriteLock([&] { diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index bd7e79dffc..e392680df9 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -32,7 +32,8 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/firstPersonHMD.js", "system/tablet-ui/tabletUI.js", "system/emote.js", - "system/miniTablet.js" + "system/miniTablet.js", + "system/audioMuteOverlay.js" ]; var DEFAULT_SCRIPTS_SEPARATE = [ "system/controllers/controllerScripts.js", diff --git a/scripts/system/audioMuteOverlay.js b/scripts/system/audioMuteOverlay.js index 731d62017d..14ac96c8c6 100644 --- a/scripts/system/audioMuteOverlay.js +++ b/scripts/system/audioMuteOverlay.js @@ -1,104 +1,84 @@ -"use strict"; -/* jslint vars: true, plusplus: true, forin: true*/ -/* globals Tablet, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, Controller, print, getControllerWorldLocation */ -/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ // // audioMuteOverlay.js // // client script that creates an overlay to provide mute feedback // // Created by Triplelexx on 17/03/09 +// Reworked by Seth Alves on 2019-2-17 // Copyright 2017 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 // +"use strict"; + +/* global Audio, Script, Overlays, Quat, MyAvatar */ + (function() { // BEGIN LOCAL_SCOPE - var utilsPath = Script.resolvePath('../developer/libraries/utils.js'); - Script.include(utilsPath); - var TWEEN_SPEED = 0.025; - var MIX_AMOUNT = 0.25; + var lastInputLoudness = 0.0; + var sampleRate = 8.0; // Hz + var attackTC = Math.exp(-1.0 / (sampleRate * 0.500)) // 500 milliseconds attack + var releaseTC = Math.exp(-1.0 / (sampleRate * 1.000)) // 1000 milliseconds release + var holdReset = 2.0 * sampleRate; // 2 seconds hold + var holdCount = 0; + var warningOverlayID = null; - var overlayPosition = Vec3.ZERO; - var tweenPosition = 0; - var startColor = { - red: 170, - green: 170, - blue: 170 - }; - var endColor = { - red: 255, - green: 0, - blue: 0 - }; - var overlayID; + function showWarning() { + if (warningOverlayID) { + return; + } + warningOverlayID = Overlays.addOverlay("text3d", { + name: "Muted-Warning", + localPosition: { x: 0.2, y: -0.35, z: -1.0 }, + localOrientation: Quat.fromVec3Degrees({ x: 0.0, y: 0.0, z: 0.0, w: 1.0 }), + text: "Warning: you are muted", + textAlpha: 1, + color: { red: 226, green: 51, blue: 77 }, + backgroundAlpha: 0, + lineHeight: 0.042, + visible: true, + ignoreRayIntersection: true, + drawInFront: true, + grabbable: false, + parentID: MyAvatar.SELF_ID, + parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX") + }); + }; - Script.update.connect(update); - Script.scriptEnding.connect(cleanup); + function hideWarning() { + if (!warningOverlayID) { + return; + } + Overlays.deleteOverlay(warningOverlayID); + warningOverlayID = null; + } - function update(dt) { - if (!Audio.muted) { - if (hasOverlay()) { - deleteOverlay(); - } - } else if (!hasOverlay()) { - createOverlay(); - } else { - updateOverlay(); - } - } + function cleanup() { + Overlays.deleteOverlay(warningOverlayID); + } - function getOffsetPosition() { - return Vec3.sum(Camera.position, Quat.getFront(Camera.orientation)); - } + Script.scriptEnding.connect(cleanup); - function createOverlay() { - overlayPosition = getOffsetPosition(); - overlayID = Overlays.addOverlay("sphere", { - position: overlayPosition, - rotation: Camera.orientation, - alpha: 0.9, - dimensions: 0.1, - solid: true, - ignoreRayIntersection: true - }); - } + Script.setInterval(function() { - function hasOverlay() { - return Overlays.getProperty(overlayID, "position") !== undefined; - } + var inputLoudness = Audio.inputLevel; + var tc = (inputLoudness > lastInputLoudness) ? attackTC : releaseTC; + inputLoudness += tc * (lastInputLoudness - inputLoudness); + lastInputLoudness = inputLoudness; - function updateOverlay() { - // increase by TWEEN_SPEED until completion - if (tweenPosition < 1) { - tweenPosition += TWEEN_SPEED; - } else { - // after tween completion reset to zero and flip values to ping pong - tweenPosition = 0; - for (var component in startColor) { - var storedColor = startColor[component]; - startColor[component] = endColor[component]; - endColor[component] = storedColor; - } - } - // mix previous position with new and mix colors - overlayPosition = Vec3.mix(overlayPosition, getOffsetPosition(), MIX_AMOUNT); - Overlays.editOverlay(overlayID, { - color: colorMix(startColor, endColor, easeIn(tweenPosition)), - position: overlayPosition, - rotation: Camera.orientation - }); - } + if (Audio.muted && inputLoudness > 0.3) { + holdCount = holdReset; + } else { + holdCount = Math.max(holdCount - 1, 0); + } - function deleteOverlay() { - Overlays.deleteOverlay(overlayID); - } + if (holdCount > 0) { + showWarning(); + } else { + hideWarning(); + } + }, 1000.0 / sampleRate); - function cleanup() { - deleteOverlay(); - Audio.muted.disconnect(onMuteToggled); - Script.update.disconnect(update); - } }()); // END LOCAL_SCOPE From 38256df0f24cd710c2e8e32683fa73de9081361f Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 19 Feb 2019 09:32:41 -0800 Subject: [PATCH 017/446] add a way to disble muted warning from audio panel. fix positioning of warning. hide warning when removing timer. --- interface/resources/qml/hifi/audio/Audio.qml | 13 ++ interface/src/scripting/Audio.cpp | 25 ++++ interface/src/scripting/Audio.h | 14 +- libraries/audio-client/src/AudioClient.cpp | 8 + libraries/audio-client/src/AudioClient.h | 5 + scripts/system/audioMuteOverlay.js | 146 ++++++++++++------- 6 files changed, 155 insertions(+), 56 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index c8dd83cd62..34ae64aee8 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -159,6 +159,19 @@ Rectangle { onXChanged: rightMostInputLevelPos = x + width } } + + RowLayout { + spacing: muteMic.spacing*2; + AudioControls.CheckBox { + spacing: muteMic.spacing + text: qsTr("Warn when muted"); + checked: AudioScriptingInterface.warnWhenMuted; + onClicked: { + AudioScriptingInterface.warnWhenMuted = checked; + checked = Qt.binding(function() { return AudioScriptingInterface.warnWhenMuted; }); // restore binding + } + } + } } Separator {} diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index bb40f69b0b..4a4b3c146b 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -25,6 +25,7 @@ QString Audio::DESKTOP { "Desktop" }; QString Audio::HMD { "VR" }; Setting::Handle enableNoiseReductionSetting { QStringList { Audio::AUDIO, "NoiseReduction" }, true }; +Setting::Handle enableWarnWhenMutedSetting { QStringList { Audio::AUDIO, "WarnWhenMuted" }, true }; Setting::Handle mutedSetting { QStringList{ Audio::AUDIO, "MuteMicrophone" }, false }; @@ -39,10 +40,12 @@ Audio::Audio() : _devices(_contextIsHMD) { auto client = DependencyManager::get().data(); connect(client, &AudioClient::muteToggled, this, &Audio::setMuted); connect(client, &AudioClient::noiseReductionChanged, this, &Audio::enableNoiseReduction); + connect(client, &AudioClient::warnWhenMutedChanged, this, &Audio::enableWarnWhenMuted); connect(client, &AudioClient::inputLoudnessChanged, this, &Audio::onInputLoudnessChanged); connect(client, &AudioClient::inputVolumeChanged, this, &Audio::setInputVolume); connect(this, &Audio::contextChanged, &_devices, &AudioDevices::onContextChanged); enableNoiseReduction(enableNoiseReductionSetting.get()); + enableWarnWhenMuted(enableWarnWhenMutedSetting.get()); onContextChanged(); setMuted(mutedSetting.get()); } @@ -109,6 +112,28 @@ void Audio::enableNoiseReduction(bool enable) { } } +bool Audio::warnWhenMutedEnabled() const { + return resultWithReadLock([&] { + return _enableWarnWhenMuted; + }); +} + +void Audio::enableWarnWhenMuted(bool enable) { + bool changed = false; + withWriteLock([&] { + if (_enableWarnWhenMuted != enable) { + _enableWarnWhenMuted = enable; + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setWarnWhenMuted", Q_ARG(bool, enable), Q_ARG(bool, false)); + enableWarnWhenMutedSetting.set(enable); + changed = true; + } + }); + if (changed) { + emit warnWhenMutedChanged(enable); + } +} + float Audio::getInputVolume() const { return resultWithReadLock([&] { return _inputVolume; diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index e4dcba9130..7e216eb0b2 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -58,6 +58,7 @@ class Audio : public AudioScriptingInterface, protected ReadWriteLockable { Q_PROPERTY(bool muted READ isMuted WRITE setMuted NOTIFY mutedChanged) Q_PROPERTY(bool noiseReduction READ noiseReductionEnabled WRITE enableNoiseReduction NOTIFY noiseReductionChanged) + Q_PROPERTY(bool warnWhenMuted READ warnWhenMutedEnabled WRITE enableWarnWhenMuted NOTIFY warnWhenMutedChanged) Q_PROPERTY(float inputVolume READ getInputVolume WRITE setInputVolume NOTIFY inputVolumeChanged) Q_PROPERTY(float inputLevel READ getInputLevel NOTIFY inputLevelChanged) Q_PROPERTY(bool clipping READ isClipping NOTIFY clippingChanged) @@ -75,6 +76,7 @@ public: bool isMuted() const; bool noiseReductionEnabled() const; + bool warnWhenMutedEnabled() const; float getInputVolume() const; float getInputLevel() const; bool isClipping() const; @@ -192,7 +194,7 @@ signals: * }); */ void mutedChanged(bool isMuted); - + /**jsdoc * Triggered when the audio input noise reduction is enabled or disabled. * @function Audio.noiseReductionChanged @@ -201,6 +203,14 @@ signals: */ void noiseReductionChanged(bool isEnabled); + /**jsdoc + * Triggered when "warn when muted" is enabled or disabled. + * @function Audio.warnWhenMutedChanged + * @param {boolean} isEnabled - true if "warn when muted" is enabled, otherwise false. + * @returns {Signal} + */ + void warnWhenMutedChanged(bool isEnabled); + /**jsdoc * Triggered when the input audio volume changes. * @function Audio.inputVolumeChanged @@ -248,6 +258,7 @@ public slots: private slots: void setMuted(bool muted); void enableNoiseReduction(bool enable); + void enableWarnWhenMuted(bool enable); void setInputVolume(float volume); void onInputLoudnessChanged(float loudness, bool isClipping); @@ -262,6 +273,7 @@ private: bool _isClipping { false }; bool _isMuted { false }; bool _enableNoiseReduction { true }; // Match default value of AudioClient::_isNoiseGateEnabled. + bool _enableWarnWhenMuted { true }; bool _contextIsHMD { false }; AudioDevices* getDevices() { return &_devices; } AudioDevices _devices; diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 8c50a195ee..1c10d24f23 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1531,6 +1531,14 @@ void AudioClient::setNoiseReduction(bool enable, bool emitSignal) { } } +void AudioClient::setWarnWhenMuted(bool enable, bool emitSignal) { + if (_warnWhenMuted != enable) { + _warnWhenMuted = enable; + if (emitSignal) { + emit warnWhenMutedChanged(_warnWhenMuted); + } + } +} bool AudioClient::setIsStereoInput(bool isStereoInput) { bool stereoInputChanged = false; diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 29036b7c71..6d3483b0f8 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -210,6 +210,9 @@ public slots: void setNoiseReduction(bool isNoiseGateEnabled, bool emitSignal = true); bool isNoiseReductionEnabled() const { return _isNoiseGateEnabled; } + void setWarnWhenMuted(bool isNoiseGateEnabled, bool emitSignal = true); + bool isWarnWhenMutedEnabled() const { return _warnWhenMuted; } + bool getLocalEcho() { return _shouldEchoLocally; } void setLocalEcho(bool localEcho) { _shouldEchoLocally = localEcho; } void toggleLocalEcho() { _shouldEchoLocally = !_shouldEchoLocally; } @@ -246,6 +249,7 @@ signals: void inputVolumeChanged(float volume); void muteToggled(bool muted); void noiseReductionChanged(bool noiseReductionEnabled); + void warnWhenMutedChanged(bool warnWhenMutedEnabled); void mutedByMixer(); void inputReceived(const QByteArray& inputSamples); void inputLoudnessChanged(float loudness, bool isClipping); @@ -365,6 +369,7 @@ private: bool _shouldEchoLocally; bool _shouldEchoToServer; bool _isNoiseGateEnabled; + bool _warnWhenMuted; bool _reverb; AudioEffectOptions _scriptReverbOptions; diff --git a/scripts/system/audioMuteOverlay.js b/scripts/system/audioMuteOverlay.js index 14ac96c8c6..d759b7d885 100644 --- a/scripts/system/audioMuteOverlay.js +++ b/scripts/system/audioMuteOverlay.js @@ -17,68 +17,104 @@ (function() { // BEGIN LOCAL_SCOPE - var lastInputLoudness = 0.0; - var sampleRate = 8.0; // Hz - var attackTC = Math.exp(-1.0 / (sampleRate * 0.500)) // 500 milliseconds attack - var releaseTC = Math.exp(-1.0 / (sampleRate * 1.000)) // 1000 milliseconds release - var holdReset = 2.0 * sampleRate; // 2 seconds hold - var holdCount = 0; - var warningOverlayID = null; + var lastInputLoudness = 0.0; + var sampleRate = 8.0; // Hz + var attackTC = Math.exp(-1.0 / (sampleRate * 0.500)); // 500 milliseconds attack + var releaseTC = Math.exp(-1.0 / (sampleRate * 1.000)); // 1000 milliseconds release + var holdReset = 2.0 * sampleRate; // 2 seconds hold + var holdCount = 0; + var warningOverlayID = null; + var pollInterval = null; + var warningText = "Muted"; + var textDimensions = { x: 100, y: 50 }; - function showWarning() { - if (warningOverlayID) { - return; - } - warningOverlayID = Overlays.addOverlay("text3d", { - name: "Muted-Warning", - localPosition: { x: 0.2, y: -0.35, z: -1.0 }, - localOrientation: Quat.fromVec3Degrees({ x: 0.0, y: 0.0, z: 0.0, w: 1.0 }), - text: "Warning: you are muted", - textAlpha: 1, - color: { red: 226, green: 51, blue: 77 }, - backgroundAlpha: 0, - lineHeight: 0.042, - visible: true, - ignoreRayIntersection: true, - drawInFront: true, - grabbable: false, - parentID: MyAvatar.SELF_ID, - parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX") - }); - }; + function showWarning() { + if (warningOverlayID) { + return; + } - function hideWarning() { - if (!warningOverlayID) { - return; - } - Overlays.deleteOverlay(warningOverlayID); - warningOverlayID = null; - } + var windowWidth; + var windowHeight; + if (HMD.active) { + var viewportDimension = Controller.getViewportDimensions(); + windowWidth = viewportDimension.x; + windowHeight = viewportDimension.y; + } else { + windowWidth = Window.innerWidth; + windowHeight = Window.innerHeight; + } - function cleanup() { - Overlays.deleteOverlay(warningOverlayID); - } + warningOverlayID = Overlays.addOverlay("text", { + name: "Muted-Warning", + font: { size: 36 }, + text: warningText, + x: windowWidth / 2 - textDimensions.x / 2, + y: windowHeight / 2 - textDimensions.y / 2, + width: textDimensions.x, + height: textDimensions.y, + textColor: { red: 226, green: 51, blue: 77 }, + backgroundAlpha: 0, + visible: true + }); + } - Script.scriptEnding.connect(cleanup); + function hideWarning() { + if (!warningOverlayID) { + return; + } + Overlays.deleteOverlay(warningOverlayID); + warningOverlayID = null; + } - Script.setInterval(function() { + function startPoll() { + if (pollInterval) { + return; + } + pollInterval = Script.setInterval(function() { + var inputLoudness = Audio.inputLevel; + var tc = (inputLoudness > lastInputLoudness) ? attackTC : releaseTC; + inputLoudness += tc * (lastInputLoudness - inputLoudness); + lastInputLoudness = inputLoudness; - var inputLoudness = Audio.inputLevel; - var tc = (inputLoudness > lastInputLoudness) ? attackTC : releaseTC; - inputLoudness += tc * (lastInputLoudness - inputLoudness); - lastInputLoudness = inputLoudness; + if (inputLoudness > 0.1) { + holdCount = holdReset; + } else { + holdCount = Math.max(holdCount - 1, 0); + } - if (Audio.muted && inputLoudness > 0.3) { - holdCount = holdReset; - } else { - holdCount = Math.max(holdCount - 1, 0); - } + if (holdCount > 0) { + showWarning(); + } else { + hideWarning(); + } + }, 1000.0 / sampleRate); + } - if (holdCount > 0) { - showWarning(); - } else { - hideWarning(); - } - }, 1000.0 / sampleRate); + function stopPoll() { + if (!pollInterval) { + return; + } + Script.clearInterval(pollInterval); + pollInterval = null; + hideWarning(); + } + + function startOrStopPoll() { + if (Audio.warnWhenMuted && Audio.muted) { + startPoll(); + } else { + stopPoll(); + } + } + + function cleanup() { + stopPoll(); + } + + Script.scriptEnding.connect(cleanup); + + startOrStopPoll(); + Audio.mutedChanged.connect(startOrStopPoll); + Audio.warnWhenMutedChanged.connect(startOrStopPoll); }()); // END LOCAL_SCOPE From 76aa6fb1b9a7416c6c8858c7fc9537a48ff6d14a Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 20 Feb 2019 12:53:43 -0800 Subject: [PATCH 018/446] keep muted warning in center of view for HMD --- scripts/system/audioMuteOverlay.js | 51 ++++++++++++++++++------------ 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/scripts/system/audioMuteOverlay.js b/scripts/system/audioMuteOverlay.js index d759b7d885..65793d1d87 100644 --- a/scripts/system/audioMuteOverlay.js +++ b/scripts/system/audioMuteOverlay.js @@ -26,36 +26,45 @@ var warningOverlayID = null; var pollInterval = null; var warningText = "Muted"; - var textDimensions = { x: 100, y: 50 }; function showWarning() { if (warningOverlayID) { return; } - var windowWidth; - var windowHeight; if (HMD.active) { - var viewportDimension = Controller.getViewportDimensions(); - windowWidth = viewportDimension.x; - windowHeight = viewportDimension.y; + warningOverlayID = Overlays.addOverlay("text3d", { + name: "Muted-Warning", + localPosition: { x: 0, y: 0, z: -1.0 }, + localOrientation: Quat.fromVec3Degrees({ x: 0.0, y: 0.0, z: 0.0, w: 1.0 }), + text: warningText, + textAlpha: 1, + textColor: { red: 226, green: 51, blue: 77 }, + backgroundAlpha: 0, + lineHeight: 0.042, + dimensions: { x: 0.11, y: 0.05 }, + visible: true, + ignoreRayIntersection: true, + drawInFront: true, + grabbable: false, + parentID: MyAvatar.SELF_ID, + parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX") + }); } else { - windowWidth = Window.innerWidth; - windowHeight = Window.innerHeight; + var textDimensions = { x: 100, y: 50 }; + warningOverlayID = Overlays.addOverlay("text", { + name: "Muted-Warning", + font: { size: 36 }, + text: warningText, + x: Window.innerWidth / 2 - textDimensions.x / 2, + y: Window.innerHeight / 2 - textDimensions.y / 2, + width: textDimensions.x, + height: textDimensions.y, + textColor: { red: 226, green: 51, blue: 77 }, + backgroundAlpha: 0, + visible: true + }); } - - warningOverlayID = Overlays.addOverlay("text", { - name: "Muted-Warning", - font: { size: 36 }, - text: warningText, - x: windowWidth / 2 - textDimensions.x / 2, - y: windowHeight / 2 - textDimensions.y / 2, - width: textDimensions.x, - height: textDimensions.y, - textColor: { red: 226, green: 51, blue: 77 }, - backgroundAlpha: 0, - visible: true - }); } function hideWarning() { From bbad6af0d692b176bef20dc87866ccda8848d04c Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 20 Feb 2019 13:07:24 -0800 Subject: [PATCH 019/446] attempt to take background noise into account when triggering mute warning --- scripts/system/audioMuteOverlay.js | 33 ++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/scripts/system/audioMuteOverlay.js b/scripts/system/audioMuteOverlay.js index 65793d1d87..96f6d636dc 100644 --- a/scripts/system/audioMuteOverlay.js +++ b/scripts/system/audioMuteOverlay.js @@ -13,14 +13,22 @@ "use strict"; -/* global Audio, Script, Overlays, Quat, MyAvatar */ +/* global Audio, Script, Overlays, Quat, MyAvatar, HMD */ (function() { // BEGIN LOCAL_SCOPE - var lastInputLoudness = 0.0; + var lastShortTermInputLoudness = 0.0; + var lastLongTermInputLoudness = 0.0; var sampleRate = 8.0; // Hz - var attackTC = Math.exp(-1.0 / (sampleRate * 0.500)); // 500 milliseconds attack - var releaseTC = Math.exp(-1.0 / (sampleRate * 1.000)); // 1000 milliseconds release + + var shortTermAttackTC = Math.exp(-1.0 / (sampleRate * 0.500)); // 500 milliseconds attack + var shortTermReleaseTC = Math.exp(-1.0 / (sampleRate * 1.000)); // 1000 milliseconds release + + var longTermAttackTC = Math.exp(-1.0 / (sampleRate * 5.0)); // 5 second attack + var longTermReleaseTC = Math.exp(-1.0 / (sampleRate * 10.0)); // 10 seconds release + + var activationThreshold = 0.05; // how much louder short-term needs to be than long-term to trigger warning + var holdReset = 2.0 * sampleRate; // 2 seconds hold var holdCount = 0; var warningOverlayID = null; @@ -80,12 +88,19 @@ return; } pollInterval = Script.setInterval(function() { - var inputLoudness = Audio.inputLevel; - var tc = (inputLoudness > lastInputLoudness) ? attackTC : releaseTC; - inputLoudness += tc * (lastInputLoudness - inputLoudness); - lastInputLoudness = inputLoudness; + var shortTermInputLoudness = Audio.inputLevel; + var longTermInputLoudness = shortTermInputLoudness; - if (inputLoudness > 0.1) { + var shortTc = (shortTermInputLoudness > lastShortTermInputLoudness) ? shortTermAttackTC : shortTermReleaseTC; + var longTc = (longTermInputLoudness > lastLongTermInputLoudness) ? longTermAttackTC : longTermReleaseTC; + + shortTermInputLoudness += shortTc * (lastShortTermInputLoudness - shortTermInputLoudness); + longTermInputLoudness += longTc * (lastLongTermInputLoudness - longTermInputLoudness); + + lastShortTermInputLoudness = shortTermInputLoudness; + lastLongTermInputLoudness = longTermInputLoudness; + + if (shortTermInputLoudness > lastLongTermInputLoudness + activationThreshold) { holdCount = holdReset; } else { holdCount = Math.max(holdCount - 1, 0); From 6f400796214c1f26a15def2f98fe619806298649 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 21 Feb 2019 14:04:08 -0800 Subject: [PATCH 020/446] added a button to enable server loopback of audio --- interface/resources/qml/hifi/audio/Audio.qml | 7 ++ .../qml/hifi/audio/LoopbackAudio.qml | 75 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 interface/resources/qml/hifi/audio/LoopbackAudio.qml diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index 34ae64aee8..e340ec5003 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -313,5 +313,12 @@ Rectangle { (bar.currentIndex === 0 && !isVR); anchors { left: parent.left; leftMargin: margins.paddings } } + LoopbackAudio { + x: margins.paddings + + visible: (bar.currentIndex === 1 && isVR) || + (bar.currentIndex === 0 && !isVR); + anchors { left: parent.left; leftMargin: margins.paddings } + } } } diff --git a/interface/resources/qml/hifi/audio/LoopbackAudio.qml b/interface/resources/qml/hifi/audio/LoopbackAudio.qml new file mode 100644 index 0000000000..6d5f8d88fd --- /dev/null +++ b/interface/resources/qml/hifi/audio/LoopbackAudio.qml @@ -0,0 +1,75 @@ +// +// LoopbackAudio.qml +// qml/hifi/audio +// +// Created by Seth Alves on 2019-2-18 +// Copyright 2019 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.7 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 + +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls + +RowLayout { + property bool audioLoopedBack: false; + function startAudioLoopback() { + if (!audioLoopedBack) { + audioLoopedBack = true; + AudioScope.setServerEcho(true); + } + } + function stopAudioLoopback () { + if (audioLoopedBack) { + audioLoopedBack = false; + AudioScope.setServerEcho(false); + } + } + + Component.onDestruction: stopAudioLoopback(); + onVisibleChanged: { + if (!visible) { + stopAudioLoopback(); + } + } + + HifiConstants { id: hifi; } + + Button { + id: control + background: Rectangle { + implicitWidth: 20; + implicitHeight: 20; + radius: hifi.buttons.radius; + gradient: Gradient { + GradientStop { + position: 0.2; + color: audioLoopedBack ? hifi.buttons.colorStart[hifi.buttons.blue] : hifi.buttons.colorStart[hifi.buttons.black]; + } + GradientStop { + position: 1.0; + color: audioLoopedBack ? hifi.buttons.colorFinish[hifi.buttons.blue] : hifi.buttons.colorFinish[hifi.buttons.black]; + } + } + } + contentItem: HiFiGlyphs { + size: 14; + color: (control.pressed || control.hovered) ? (audioLoopedBack ? "black" : hifi.colors.primaryHighlight) : "white"; + text: audioLoopedBack ? hifi.glyphs.stop_square : hifi.glyphs.playback_play; + } + + onClicked: audioLoopedBack ? stopAudioLoopback() : startAudioLoopback(); + } + + RalewayRegular { + Layout.leftMargin: 2; + size: 14; + color: "white"; + text: audioLoopedBack ? qsTr("Disable Audio Loopback") : qsTr("Enable Audio Loopback"); + } +} From 9ff99c721386cf620dc6dd26d35138f87b295d44 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 22 Feb 2019 09:33:23 -0800 Subject: [PATCH 021/446] make server-audio-loopback button work in HMDs --- interface/resources/qml/hifi/audio/LoopbackAudio.qml | 4 ++-- libraries/audio-client/src/AudioClient.h | 12 ++++++------ libraries/audio/src/AbstractAudioInterface.h | 9 ++++++++- .../script-engine/src/AudioScriptingInterface.cpp | 12 ++++++++++++ .../script-engine/src/AudioScriptingInterface.h | 12 ++++++++++++ 5 files changed, 40 insertions(+), 9 deletions(-) diff --git a/interface/resources/qml/hifi/audio/LoopbackAudio.qml b/interface/resources/qml/hifi/audio/LoopbackAudio.qml index 6d5f8d88fd..2f0dbe5950 100644 --- a/interface/resources/qml/hifi/audio/LoopbackAudio.qml +++ b/interface/resources/qml/hifi/audio/LoopbackAudio.qml @@ -21,13 +21,13 @@ RowLayout { function startAudioLoopback() { if (!audioLoopedBack) { audioLoopedBack = true; - AudioScope.setServerEcho(true); + AudioScriptingInterface.setServerEcho(true); } } function stopAudioLoopback () { if (audioLoopedBack) { audioLoopedBack = false; - AudioScope.setServerEcho(false); + AudioScriptingInterface.setServerEcho(false); } } diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 6d3483b0f8..b9648219a5 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -213,13 +213,13 @@ public slots: void setWarnWhenMuted(bool isNoiseGateEnabled, bool emitSignal = true); bool isWarnWhenMutedEnabled() const { return _warnWhenMuted; } - bool getLocalEcho() { return _shouldEchoLocally; } - void setLocalEcho(bool localEcho) { _shouldEchoLocally = localEcho; } - void toggleLocalEcho() { _shouldEchoLocally = !_shouldEchoLocally; } + virtual bool getLocalEcho() override { return _shouldEchoLocally; } + virtual void setLocalEcho(bool localEcho) override { _shouldEchoLocally = localEcho; } + virtual void toggleLocalEcho() override { _shouldEchoLocally = !_shouldEchoLocally; } - bool getServerEcho() { return _shouldEchoToServer; } - void setServerEcho(bool serverEcho) { _shouldEchoToServer = serverEcho; } - void toggleServerEcho() { _shouldEchoToServer = !_shouldEchoToServer; } + virtual bool getServerEcho() override { return _shouldEchoToServer; } + virtual void setServerEcho(bool serverEcho) override { _shouldEchoToServer = serverEcho; } + virtual void toggleServerEcho() override { _shouldEchoToServer = !_shouldEchoToServer; } void processReceivedSamples(const QByteArray& inputBuffer, QByteArray& outputBuffer); void sendMuteEnvironmentPacket(); diff --git a/libraries/audio/src/AbstractAudioInterface.h b/libraries/audio/src/AbstractAudioInterface.h index 0f075ab224..e9e40e95f9 100644 --- a/libraries/audio/src/AbstractAudioInterface.h +++ b/libraries/audio/src/AbstractAudioInterface.h @@ -45,9 +45,16 @@ public slots: virtual bool shouldLoopbackInjectors() { return false; } virtual bool setIsStereoInput(bool stereo) = 0; - virtual bool isStereoInput() = 0; + virtual bool getLocalEcho() = 0; + virtual void setLocalEcho(bool localEcho) = 0; + virtual void toggleLocalEcho() = 0; + + virtual bool getServerEcho() = 0; + virtual void setServerEcho(bool serverEcho) = 0; + virtual void toggleServerEcho() = 0; + signals: void isStereoInputChanged(bool isStereo); }; diff --git a/libraries/script-engine/src/AudioScriptingInterface.cpp b/libraries/script-engine/src/AudioScriptingInterface.cpp index 8e54d2d5de..b12b55c3f7 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.cpp +++ b/libraries/script-engine/src/AudioScriptingInterface.cpp @@ -88,3 +88,15 @@ bool AudioScriptingInterface::isStereoInput() { } return stereoEnabled; } + +void AudioScriptingInterface::setServerEcho(bool serverEcho) { + if (_localAudioInterface) { + QMetaObject::invokeMethod(_localAudioInterface, "setServerEcho", Q_ARG(bool, serverEcho)); + } +} + +void AudioScriptingInterface::setLocalEcho(bool localEcho) { + if (_localAudioInterface) { + QMetaObject::invokeMethod(_localAudioInterface, "setLocalEcho", Q_ARG(bool, localEcho)); + } +} diff --git a/libraries/script-engine/src/AudioScriptingInterface.h b/libraries/script-engine/src/AudioScriptingInterface.h index d2f886d2dd..23cc02248d 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.h +++ b/libraries/script-engine/src/AudioScriptingInterface.h @@ -66,6 +66,18 @@ public: _localAudioInterface->getAudioSolo().reset(); } + /**jsdoc + * @function Audio.setServerEcho + * @parm {boolean} serverEcho + */ + Q_INVOKABLE void setServerEcho(bool serverEcho); + + /**jsdoc + * @function Audio.setLocalEcho + * @parm {boolean} localEcho + */ + Q_INVOKABLE void setLocalEcho(bool localEcho); + protected: AudioScriptingInterface() = default; From 0523a0d06dc6fa751cfb7111c8f1730b96d548fa Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 23 Feb 2019 14:23:09 -0800 Subject: [PATCH 022/446] don't disable server echo when audio qml page is closed --- .../qml/hifi/audio/LoopbackAudio.qml | 9 +----- .../src/AudioScriptingInterface.cpp | 28 +++++++++++++++++++ .../src/AudioScriptingInterface.h | 21 ++++++++++++++ 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/interface/resources/qml/hifi/audio/LoopbackAudio.qml b/interface/resources/qml/hifi/audio/LoopbackAudio.qml index 2f0dbe5950..3ecf09c948 100644 --- a/interface/resources/qml/hifi/audio/LoopbackAudio.qml +++ b/interface/resources/qml/hifi/audio/LoopbackAudio.qml @@ -17,7 +17,7 @@ import stylesUit 1.0 import controlsUit 1.0 as HifiControls RowLayout { - property bool audioLoopedBack: false; + property bool audioLoopedBack: AudioScriptingInterface.getServerEcho(); function startAudioLoopback() { if (!audioLoopedBack) { audioLoopedBack = true; @@ -31,13 +31,6 @@ RowLayout { } } - Component.onDestruction: stopAudioLoopback(); - onVisibleChanged: { - if (!visible) { - stopAudioLoopback(); - } - } - HifiConstants { id: hifi; } Button { diff --git a/libraries/script-engine/src/AudioScriptingInterface.cpp b/libraries/script-engine/src/AudioScriptingInterface.cpp index b12b55c3f7..65d71e46e6 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.cpp +++ b/libraries/script-engine/src/AudioScriptingInterface.cpp @@ -89,14 +89,42 @@ bool AudioScriptingInterface::isStereoInput() { return stereoEnabled; } +bool AudioScriptingInterface::getServerEcho() { + bool serverEchoEnabled = false; + if (_localAudioInterface) { + serverEchoEnabled = _localAudioInterface->getServerEcho(); + } + return serverEchoEnabled; +} + void AudioScriptingInterface::setServerEcho(bool serverEcho) { if (_localAudioInterface) { QMetaObject::invokeMethod(_localAudioInterface, "setServerEcho", Q_ARG(bool, serverEcho)); } } +void AudioScriptingInterface::toggleServerEcho() { + if (_localAudioInterface) { + QMetaObject::invokeMethod(_localAudioInterface, "toggleServerEcho"); + } +} + +bool AudioScriptingInterface::getLocalEcho() { + bool localEchoEnabled = false; + if (_localAudioInterface) { + localEchoEnabled = _localAudioInterface->getLocalEcho(); + } + return localEchoEnabled; +} + void AudioScriptingInterface::setLocalEcho(bool localEcho) { if (_localAudioInterface) { QMetaObject::invokeMethod(_localAudioInterface, "setLocalEcho", Q_ARG(bool, localEcho)); } } + +void AudioScriptingInterface::toggleLocalEcho() { + if (_localAudioInterface) { + QMetaObject::invokeMethod(_localAudioInterface, "toggleLocalEcho"); + } +} diff --git a/libraries/script-engine/src/AudioScriptingInterface.h b/libraries/script-engine/src/AudioScriptingInterface.h index 23cc02248d..a6801dcdcb 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.h +++ b/libraries/script-engine/src/AudioScriptingInterface.h @@ -66,18 +66,39 @@ public: _localAudioInterface->getAudioSolo().reset(); } + /**jsdoc + * @function Audio.getServerEcho + */ + Q_INVOKABLE bool getServerEcho(); + /**jsdoc * @function Audio.setServerEcho * @parm {boolean} serverEcho */ Q_INVOKABLE void setServerEcho(bool serverEcho); + /**jsdoc + * @function Audio.toggleServerEcho + */ + Q_INVOKABLE void toggleServerEcho(); + + /**jsdoc + * @function Audio.getLocalEcho + */ + Q_INVOKABLE bool getLocalEcho(); + /**jsdoc * @function Audio.setLocalEcho * @parm {boolean} localEcho */ Q_INVOKABLE void setLocalEcho(bool localEcho); + /**jsdoc + * @function Audio.toggleLocalEcho + */ + Q_INVOKABLE void toggleLocalEcho(); + + protected: AudioScriptingInterface() = default; From 90a6e0d9b01446c4f4f0aa28be66c7961b8d3a24 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 4 Mar 2019 11:42:43 -0800 Subject: [PATCH 023/446] changing mute warning position --- scripts/system/audioMuteOverlay.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/system/audioMuteOverlay.js b/scripts/system/audioMuteOverlay.js index 96f6d636dc..cd0c99ab6e 100644 --- a/scripts/system/audioMuteOverlay.js +++ b/scripts/system/audioMuteOverlay.js @@ -43,7 +43,7 @@ if (HMD.active) { warningOverlayID = Overlays.addOverlay("text3d", { name: "Muted-Warning", - localPosition: { x: 0, y: 0, z: -1.0 }, + localPosition: { x: 0.0, y: -0.5, z: -1.0 }, localOrientation: Quat.fromVec3Degrees({ x: 0.0, y: 0.0, z: 0.0, w: 1.0 }), text: warningText, textAlpha: 1, @@ -64,8 +64,8 @@ name: "Muted-Warning", font: { size: 36 }, text: warningText, - x: Window.innerWidth / 2 - textDimensions.x / 2, - y: Window.innerHeight / 2 - textDimensions.y / 2, + x: (Window.innerWidth - textDimensions.x) / 2, + y: (Window.innerHeight - textDimensions.y), width: textDimensions.x, height: textDimensions.y, textColor: { red: 226, green: 51, blue: 77 }, From 7860db68eb6ca4f557582fc022f42873b16b4fda Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 4 Mar 2019 13:33:16 -0800 Subject: [PATCH 024/446] culling hearing oneself and testing sound --- interface/resources/qml/hifi/audio/Audio.qml | 7 ---- libraries/audio-client/src/AudioClient.h | 12 +++--- libraries/audio/src/AbstractAudioInterface.h | 8 ---- .../src/AudioScriptingInterface.cpp | 42 +------------------ .../src/AudioScriptingInterface.h | 33 --------------- 5 files changed, 7 insertions(+), 95 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index e340ec5003..34ae64aee8 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -313,12 +313,5 @@ Rectangle { (bar.currentIndex === 0 && !isVR); anchors { left: parent.left; leftMargin: margins.paddings } } - LoopbackAudio { - x: margins.paddings - - visible: (bar.currentIndex === 1 && isVR) || - (bar.currentIndex === 0 && !isVR); - anchors { left: parent.left; leftMargin: margins.paddings } - } } } diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index b9648219a5..6d3483b0f8 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -213,13 +213,13 @@ public slots: void setWarnWhenMuted(bool isNoiseGateEnabled, bool emitSignal = true); bool isWarnWhenMutedEnabled() const { return _warnWhenMuted; } - virtual bool getLocalEcho() override { return _shouldEchoLocally; } - virtual void setLocalEcho(bool localEcho) override { _shouldEchoLocally = localEcho; } - virtual void toggleLocalEcho() override { _shouldEchoLocally = !_shouldEchoLocally; } + bool getLocalEcho() { return _shouldEchoLocally; } + void setLocalEcho(bool localEcho) { _shouldEchoLocally = localEcho; } + void toggleLocalEcho() { _shouldEchoLocally = !_shouldEchoLocally; } - virtual bool getServerEcho() override { return _shouldEchoToServer; } - virtual void setServerEcho(bool serverEcho) override { _shouldEchoToServer = serverEcho; } - virtual void toggleServerEcho() override { _shouldEchoToServer = !_shouldEchoToServer; } + bool getServerEcho() { return _shouldEchoToServer; } + void setServerEcho(bool serverEcho) { _shouldEchoToServer = serverEcho; } + void toggleServerEcho() { _shouldEchoToServer = !_shouldEchoToServer; } void processReceivedSamples(const QByteArray& inputBuffer, QByteArray& outputBuffer); void sendMuteEnvironmentPacket(); diff --git a/libraries/audio/src/AbstractAudioInterface.h b/libraries/audio/src/AbstractAudioInterface.h index e9e40e95f9..dc7d25fdc2 100644 --- a/libraries/audio/src/AbstractAudioInterface.h +++ b/libraries/audio/src/AbstractAudioInterface.h @@ -47,14 +47,6 @@ public slots: virtual bool setIsStereoInput(bool stereo) = 0; virtual bool isStereoInput() = 0; - virtual bool getLocalEcho() = 0; - virtual void setLocalEcho(bool localEcho) = 0; - virtual void toggleLocalEcho() = 0; - - virtual bool getServerEcho() = 0; - virtual void setServerEcho(bool serverEcho) = 0; - virtual void toggleServerEcho() = 0; - signals: void isStereoInputChanged(bool isStereo); }; diff --git a/libraries/script-engine/src/AudioScriptingInterface.cpp b/libraries/script-engine/src/AudioScriptingInterface.cpp index 65d71e46e6..c695d67d91 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.cpp +++ b/libraries/script-engine/src/AudioScriptingInterface.cpp @@ -87,44 +87,4 @@ bool AudioScriptingInterface::isStereoInput() { stereoEnabled = _localAudioInterface->isStereoInput(); } return stereoEnabled; -} - -bool AudioScriptingInterface::getServerEcho() { - bool serverEchoEnabled = false; - if (_localAudioInterface) { - serverEchoEnabled = _localAudioInterface->getServerEcho(); - } - return serverEchoEnabled; -} - -void AudioScriptingInterface::setServerEcho(bool serverEcho) { - if (_localAudioInterface) { - QMetaObject::invokeMethod(_localAudioInterface, "setServerEcho", Q_ARG(bool, serverEcho)); - } -} - -void AudioScriptingInterface::toggleServerEcho() { - if (_localAudioInterface) { - QMetaObject::invokeMethod(_localAudioInterface, "toggleServerEcho"); - } -} - -bool AudioScriptingInterface::getLocalEcho() { - bool localEchoEnabled = false; - if (_localAudioInterface) { - localEchoEnabled = _localAudioInterface->getLocalEcho(); - } - return localEchoEnabled; -} - -void AudioScriptingInterface::setLocalEcho(bool localEcho) { - if (_localAudioInterface) { - QMetaObject::invokeMethod(_localAudioInterface, "setLocalEcho", Q_ARG(bool, localEcho)); - } -} - -void AudioScriptingInterface::toggleLocalEcho() { - if (_localAudioInterface) { - QMetaObject::invokeMethod(_localAudioInterface, "toggleLocalEcho"); - } -} +} \ No newline at end of file diff --git a/libraries/script-engine/src/AudioScriptingInterface.h b/libraries/script-engine/src/AudioScriptingInterface.h index a6801dcdcb..d2f886d2dd 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.h +++ b/libraries/script-engine/src/AudioScriptingInterface.h @@ -66,39 +66,6 @@ public: _localAudioInterface->getAudioSolo().reset(); } - /**jsdoc - * @function Audio.getServerEcho - */ - Q_INVOKABLE bool getServerEcho(); - - /**jsdoc - * @function Audio.setServerEcho - * @parm {boolean} serverEcho - */ - Q_INVOKABLE void setServerEcho(bool serverEcho); - - /**jsdoc - * @function Audio.toggleServerEcho - */ - Q_INVOKABLE void toggleServerEcho(); - - /**jsdoc - * @function Audio.getLocalEcho - */ - Q_INVOKABLE bool getLocalEcho(); - - /**jsdoc - * @function Audio.setLocalEcho - * @parm {boolean} localEcho - */ - Q_INVOKABLE void setLocalEcho(bool localEcho); - - /**jsdoc - * @function Audio.toggleLocalEcho - */ - Q_INVOKABLE void toggleLocalEcho(); - - protected: AudioScriptingInterface() = default; From 23e8a8bed4c6f086fa5718153cf6733ae218ae68 Mon Sep 17 00:00:00 2001 From: r3tk0n Date: Tue, 5 Mar 2019 17:09:54 -0800 Subject: [PATCH 025/446] Initial implementation (deadlocks still occurring in Audio.cpp) --- interface/resources/qml/hifi/audio/Audio.qml | 19 ++ interface/resources/qml/hifi/audio/MicBar.qml | 9 +- interface/src/Application.cpp | 18 ++ interface/src/Application.h | 2 + interface/src/scripting/Audio.cpp | 174 +++++++++++++++++- interface/src/scripting/Audio.h | 89 ++++++++- scripts/system/audio.js | 3 + 7 files changed, 302 insertions(+), 12 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index c8dd83cd62..45358f59a2 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -148,6 +148,25 @@ Rectangle { checked = Qt.binding(function() { return AudioScriptingInterface.noiseReduction; }); // restore binding } } + AudioControls.CheckBox { + spacing: muteMic.spacing + text: qsTr("Push To Talk"); + checked: isVR ? AudioScriptingInterface.pushToTalkHMD : AudioScriptingInterface.pushToTalkDesktop; + onClicked: { + if (isVR) { + AudioScriptingInterface.pushToTalkHMD = checked; + } else { + AudioScriptingInterface.pushToTalkDesktop = checked; + } + checked = Qt.binding(function() { + if (isVR) { + return AudioScriptingInterface.pushToTalkHMD; + } else { + return AudioScriptingInterface.pushToTalkDesktop; + } + }); // restore binding + } + } AudioControls.CheckBox { spacing: muteMic.spacing text: qsTr("Show audio level meter"); diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index 39f75a9182..f91058bc3c 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -11,10 +11,13 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 +import stylesUit 1.0 import TabletScriptingInterface 1.0 Rectangle { + HifiConstants { id: hifi; } + readonly property var level: AudioScriptingInterface.inputLevel; property bool gated: false; @@ -131,9 +134,9 @@ Rectangle { Item { id: status; - readonly property string color: AudioScriptingInterface.muted ? colors.muted : colors.unmuted; + readonly property string color: (AudioScriptingInterface.pushingToTalk && AudioScriptingInterface.muted) ? hifi.colors.blueHighlight : AudioScriptingInterface.muted ? colors.muted : colors.unmuted; - visible: AudioScriptingInterface.muted; + visible: AudioScriptingInterface.pushingToTalk || AudioScriptingInterface.muted; anchors { left: parent.left; @@ -152,7 +155,7 @@ Rectangle { color: parent.color; - text: AudioScriptingInterface.muted ? "MUTED" : "MUTE"; + text: AudioScriptingInterface.pushToTalk ? (AudioScriptingInterface.pushingToTalk ? "SPEAKING" : "PUSH TO TALK") : (AudioScriptingInterface.muted ? "MUTED" : "MUTE"); font.pointSize: 12; } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 2c8d71af00..bcd5367a89 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1431,6 +1431,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo }); connect(this, &Application::activeDisplayPluginChanged, reinterpret_cast(audioScriptingInterface.data()), &scripting::Audio::onContextChanged); + connect(this, &Application::pushedToTalk, + reinterpret_cast(audioScriptingInterface.data()), &scripting::Audio::handlePushedToTalk); } // Create the rendering engine. This can be slow on some machines due to lots of @@ -4195,6 +4197,10 @@ void Application::keyPressEvent(QKeyEvent* event) { } break; + case Qt::Key_T: + emit pushedToTalk(true); + break; + case Qt::Key_P: { if (!isShifted && !isMeta && !isOption && !event->isAutoRepeat()) { AudioInjectorOptions options; @@ -4300,6 +4306,12 @@ void Application::keyReleaseEvent(QKeyEvent* event) { if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->keyReleaseEvent(event); } + + switch (event->key()) { + case Qt::Key_T: + emit pushedToTalk(false); + break; + } } void Application::focusOutEvent(QFocusEvent* event) { @@ -5243,6 +5255,9 @@ void Application::loadSettings() { } } + auto audioScriptingInterface = reinterpret_cast(DependencyManager::get().data()); + audioScriptingInterface->loadData(); + getMyAvatar()->loadData(); _settingsLoaded = true; } @@ -5252,6 +5267,9 @@ void Application::saveSettings() const { DependencyManager::get()->saveSettings(); DependencyManager::get()->saveSettings(); + auto audioScriptingInterface = reinterpret_cast(DependencyManager::get().data()); + audioScriptingInterface->saveData(); + Menu::getInstance()->saveSettings(); getMyAvatar()->saveData(); PluginManager::getInstance()->saveSettings(); diff --git a/interface/src/Application.h b/interface/src/Application.h index a8cc9450c5..1c86326f90 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -358,6 +358,8 @@ signals: void miniTabletEnabledChanged(bool enabled); + void pushedToTalk(bool enabled); + public slots: QVector pasteEntities(float x, float y, float z); bool exportEntities(const QString& filename, const QVector& entityIDs, const glm::vec3* givenOffset = nullptr); diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 2c4c29ff65..fe04ce47ca 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -63,26 +63,163 @@ void Audio::stopRecording() { } bool Audio::isMuted() const { - return resultWithReadLock([&] { - return _isMuted; - }); + bool isHMD = qApp->isHMDMode(); + if (isHMD) { + return getMutedHMD(); + } + else { + return getMutedDesktop(); + } } void Audio::setMuted(bool isMuted) { + withWriteLock([&] { + bool isHMD = qApp->isHMDMode(); + if (isHMD) { + setMutedHMD(isMuted); + } + else { + setMutedDesktop(isMuted); + } + }); +} + +void Audio::setMutedDesktop(bool isMuted) { + bool changed = false; + if (_desktopMuted != isMuted) { + changed = true; + _desktopMuted = isMuted; + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); + } + if (changed) { + emit mutedChanged(isMuted); + emit desktopMutedChanged(isMuted); + } +} + +bool Audio::getMutedDesktop() const { + return resultWithReadLock([&] { + return _desktopMuted; + }); +} + +void Audio::setMutedHMD(bool isMuted) { + bool changed = false; + if (_hmdMuted != isMuted) { + changed = true; + _hmdMuted = isMuted; + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); + } + if (changed) { + emit mutedChanged(isMuted); + emit hmdMutedChanged(isMuted); + } +} + +bool Audio::getMutedHMD() const { + return resultWithReadLock([&] { + return _hmdMuted; + }); +} + +bool Audio::getPTT() { + bool isHMD = qApp->isHMDMode(); + if (isHMD) { + return getPTTHMD(); + } + else { + return getPTTDesktop(); + } +} + +bool Audio::getPushingToTalk() const { + return resultWithReadLock([&] { + return _pushingToTalk; + }); +} + +void Audio::setPTT(bool enabled) { + bool isHMD = qApp->isHMDMode(); + if (isHMD) { + setPTTHMD(enabled); + } + else { + setPTTDesktop(enabled); + } +} + +void Audio::setPTTDesktop(bool enabled) { bool changed = false; withWriteLock([&] { - if (_isMuted != isMuted) { - _isMuted = isMuted; - auto client = DependencyManager::get().data(); - QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); + if (_pttDesktop != enabled) { changed = true; + _pttDesktop = enabled; + if (!enabled) { + // Set to default behavior (unmuted for Desktop) on Push-To-Talk disable. + setMutedDesktop(true); + } + else { + // Should be muted when not pushing to talk while PTT is enabled. + setMutedDesktop(true); + } } }); if (changed) { - emit mutedChanged(isMuted); + emit pushToTalkChanged(enabled); + emit pushToTalkDesktopChanged(enabled); } } +bool Audio::getPTTDesktop() const { + return resultWithReadLock([&] { + return _pttDesktop; + }); +} + +void Audio::setPTTHMD(bool enabled) { + bool changed = false; + withWriteLock([&] { + if (_pttHMD != enabled) { + changed = true; + _pttHMD = enabled; + if (!enabled) { + // Set to default behavior (unmuted for HMD) on Push-To-Talk disable. + setMutedHMD(false); + } + else { + // Should be muted when not pushing to talk while PTT is enabled. + setMutedHMD(true); + } + } + }); + if (changed) { + emit pushToTalkChanged(enabled); + emit pushToTalkHMDChanged(enabled); + } +} + +bool Audio::getPTTHMD() const { + return resultWithReadLock([&] { + return _pttHMD; + }); +} + +void Audio::saveData() { + _desktopMutedSetting.set(getMutedDesktop()); + _hmdMutedSetting.set(getMutedHMD()); + _pttDesktopSetting.set(getPTTDesktop()); + _pttHMDSetting.set(getPTTHMD()); +} + +void Audio::loadData() { + _desktopMuted = _desktopMutedSetting.get(); + _hmdMuted = _hmdMutedSetting.get(); + _pttDesktop = _pttDesktopSetting.get(); + _pttHMD = _pttHMDSetting.get(); +} + bool Audio::noiseReductionEnabled() const { return resultWithReadLock([&] { return _enableNoiseReduction; @@ -179,11 +316,32 @@ void Audio::onContextChanged() { changed = true; } }); + if (isHMD) { + setMuted(getMutedHMD()); + } + else { + setMuted(getMutedDesktop()); + } if (changed) { emit contextChanged(isHMD ? Audio::HMD : Audio::DESKTOP); } } +void Audio::handlePushedToTalk(bool enabled) { + if (getPTT()) { + if (enabled) { + setMuted(false); + } + else { + setMuted(true); + } + if (_pushingToTalk != enabled) { + _pushingToTalk = enabled; + emit pushingToTalkChanged(enabled); + } + } +} + void Audio::setReverb(bool enable) { withWriteLock([&] { DependencyManager::get()->setReverb(enable); diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index e4dcba9130..6aa589e399 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -12,6 +12,7 @@ #ifndef hifi_scripting_Audio_h #define hifi_scripting_Audio_h +#include #include "AudioScriptingInterface.h" #include "AudioDevices.h" #include "AudioEffectOptions.h" @@ -19,6 +20,9 @@ #include "AudioFileWav.h" #include +using MutedGetter = std::function; +using MutedSetter = std::function; + namespace scripting { class Audio : public AudioScriptingInterface, protected ReadWriteLockable { @@ -63,6 +67,12 @@ class Audio : public AudioScriptingInterface, protected ReadWriteLockable { Q_PROPERTY(bool clipping READ isClipping NOTIFY clippingChanged) Q_PROPERTY(QString context READ getContext NOTIFY contextChanged) Q_PROPERTY(AudioDevices* devices READ getDevices NOTIFY nop) + Q_PROPERTY(bool desktopMuted READ getMutedDesktop WRITE setMutedDesktop NOTIFY desktopMutedChanged) + Q_PROPERTY(bool hmdMuted READ getMutedHMD WRITE setMutedHMD NOTIFY hmdMutedChanged) + Q_PROPERTY(bool pushToTalk READ getPTT WRITE setPTT NOTIFY pushToTalkChanged); + Q_PROPERTY(bool pushToTalkDesktop READ getPTTDesktop WRITE setPTTDesktop NOTIFY pushToTalkDesktopChanged) + Q_PROPERTY(bool pushToTalkHMD READ getPTTHMD WRITE setPTTHMD NOTIFY pushToTalkHMDChanged) + Q_PROPERTY(bool pushingToTalk READ getPushingToTalk NOTIFY pushingToTalkChanged) public: static QString AUDIO; @@ -82,6 +92,25 @@ public: void showMicMeter(bool show); + // Mute setting setters and getters + void setMutedDesktop(bool isMuted); + bool getMutedDesktop() const; + void setMutedHMD(bool isMuted); + bool getMutedHMD() const; + void setPTT(bool enabled); + bool getPTT(); + bool getPushingToTalk() const; + + // Push-To-Talk setters and getters + void setPTTDesktop(bool enabled); + bool getPTTDesktop() const; + void setPTTHMD(bool enabled); + bool getPTTHMD() const; + + // Settings handlers + void saveData(); + void loadData(); + /**jsdoc * @function Audio.setInputDevice * @param {object} device @@ -193,6 +222,46 @@ signals: */ void mutedChanged(bool isMuted); + /**jsdoc + * Triggered when desktop audio input is muted or unmuted. + * @function Audio.desktopMutedChanged + * @param {boolean} isMuted - true if the audio input is muted for desktop mode, otherwise false. + * @returns {Signal} + */ + void desktopMutedChanged(bool isMuted); + + /**jsdoc + * Triggered when HMD audio input is muted or unmuted. + * @function Audio.hmdMutedChanged + * @param {boolean} isMuted - true if the audio input is muted for HMD mode, otherwise false. + * @returns {Signal} + */ + void hmdMutedChanged(bool isMuted); + + /** + * Triggered when Push-to-Talk has been enabled or disabled. + * @function Audio.pushToTalkChanged + * @param {boolean} enabled - true if Push-to-Talk is enabled, otherwise false. + * @returns {Signal} + */ + void pushToTalkChanged(bool enabled); + + /** + * Triggered when Push-to-Talk has been enabled or disabled for desktop mode. + * @function Audio.pushToTalkDesktopChanged + * @param {boolean} enabled - true if Push-to-Talk is emabled for Desktop mode, otherwise false. + * @returns {Signal} + */ + void pushToTalkDesktopChanged(bool enabled); + + /** + * Triggered when Push-to-Talk has been enabled or disabled for HMD mode. + * @function Audio.pushToTalkHMDChanged + * @param {boolean} enabled - true if Push-to-Talk is emabled for HMD mode, otherwise false. + * @returns {Signal} + */ + void pushToTalkHMDChanged(bool enabled); + /**jsdoc * Triggered when the audio input noise reduction is enabled or disabled. * @function Audio.noiseReductionChanged @@ -237,6 +306,14 @@ signals: */ void contextChanged(const QString& context); + /**jsdoc + * Triggered when pushing to talk. + * @function Audio.pushingToTalkChanged + * @param {boolean} talking - true if broadcasting with PTT, false otherwise. + * @returns {Signal} + */ + void pushingToTalkChanged(bool talking); + public slots: /**jsdoc @@ -245,6 +322,8 @@ public slots: */ void onContextChanged(); + void handlePushedToTalk(bool enabled); + private slots: void setMuted(bool muted); void enableNoiseReduction(bool enable); @@ -260,11 +339,19 @@ private: float _inputVolume { 1.0f }; float _inputLevel { 0.0f }; bool _isClipping { false }; - bool _isMuted { false }; bool _enableNoiseReduction { true }; // Match default value of AudioClient::_isNoiseGateEnabled. bool _contextIsHMD { false }; AudioDevices* getDevices() { return &_devices; } AudioDevices _devices; + Setting::Handle _desktopMutedSetting{ QStringList { Audio::AUDIO, "desktopMuted" }, true }; + Setting::Handle _hmdMutedSetting{ QStringList { Audio::AUDIO, "hmdMuted" }, true }; + Setting::Handle _pttDesktopSetting{ QStringList { Audio::AUDIO, "pushToTalkDesktop" }, false }; + Setting::Handle _pttHMDSetting{ QStringList { Audio::AUDIO, "pushToTalkHMD" }, false }; + bool _desktopMuted{ true }; + bool _hmdMuted{ false }; + bool _pttDesktop{ false }; + bool _pttHMD{ false }; + bool _pushingToTalk{ false }; }; }; diff --git a/scripts/system/audio.js b/scripts/system/audio.js index ee82c0c6ea..51d070d8cd 100644 --- a/scripts/system/audio.js +++ b/scripts/system/audio.js @@ -28,6 +28,9 @@ var UNMUTE_ICONS = { }; function onMuteToggled() { + if (Audio.pushingToTalk) { + return; + } if (Audio.muted) { button.editProperties(MUTE_ICONS); } else { From d88235a08f577bc4145bd6b8e518e02e256ae3ce Mon Sep 17 00:00:00 2001 From: r3tk0n Date: Tue, 5 Mar 2019 17:09:54 -0800 Subject: [PATCH 026/446] Initial implementation (deadlocks still occurring in Audio.cpp) --- interface/resources/qml/hifi/audio/Audio.qml | 19 ++ interface/resources/qml/hifi/audio/MicBar.qml | 9 +- interface/src/Application.cpp | 18 ++ interface/src/Application.h | 2 + interface/src/scripting/Audio.cpp | 174 +++++++++++++++++- interface/src/scripting/Audio.h | 89 ++++++++- scripts/system/audio.js | 3 + 7 files changed, 302 insertions(+), 12 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index c8dd83cd62..45358f59a2 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -148,6 +148,25 @@ Rectangle { checked = Qt.binding(function() { return AudioScriptingInterface.noiseReduction; }); // restore binding } } + AudioControls.CheckBox { + spacing: muteMic.spacing + text: qsTr("Push To Talk"); + checked: isVR ? AudioScriptingInterface.pushToTalkHMD : AudioScriptingInterface.pushToTalkDesktop; + onClicked: { + if (isVR) { + AudioScriptingInterface.pushToTalkHMD = checked; + } else { + AudioScriptingInterface.pushToTalkDesktop = checked; + } + checked = Qt.binding(function() { + if (isVR) { + return AudioScriptingInterface.pushToTalkHMD; + } else { + return AudioScriptingInterface.pushToTalkDesktop; + } + }); // restore binding + } + } AudioControls.CheckBox { spacing: muteMic.spacing text: qsTr("Show audio level meter"); diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index 39f75a9182..f91058bc3c 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -11,10 +11,13 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 +import stylesUit 1.0 import TabletScriptingInterface 1.0 Rectangle { + HifiConstants { id: hifi; } + readonly property var level: AudioScriptingInterface.inputLevel; property bool gated: false; @@ -131,9 +134,9 @@ Rectangle { Item { id: status; - readonly property string color: AudioScriptingInterface.muted ? colors.muted : colors.unmuted; + readonly property string color: (AudioScriptingInterface.pushingToTalk && AudioScriptingInterface.muted) ? hifi.colors.blueHighlight : AudioScriptingInterface.muted ? colors.muted : colors.unmuted; - visible: AudioScriptingInterface.muted; + visible: AudioScriptingInterface.pushingToTalk || AudioScriptingInterface.muted; anchors { left: parent.left; @@ -152,7 +155,7 @@ Rectangle { color: parent.color; - text: AudioScriptingInterface.muted ? "MUTED" : "MUTE"; + text: AudioScriptingInterface.pushToTalk ? (AudioScriptingInterface.pushingToTalk ? "SPEAKING" : "PUSH TO TALK") : (AudioScriptingInterface.muted ? "MUTED" : "MUTE"); font.pointSize: 12; } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index b8ab4d10db..fa63757560 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1435,6 +1435,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo }); connect(this, &Application::activeDisplayPluginChanged, reinterpret_cast(audioScriptingInterface.data()), &scripting::Audio::onContextChanged); + connect(this, &Application::pushedToTalk, + reinterpret_cast(audioScriptingInterface.data()), &scripting::Audio::handlePushedToTalk); } // Create the rendering engine. This can be slow on some machines due to lots of @@ -4199,6 +4201,10 @@ void Application::keyPressEvent(QKeyEvent* event) { } break; + case Qt::Key_T: + emit pushedToTalk(true); + break; + case Qt::Key_P: { if (!isShifted && !isMeta && !isOption && !event->isAutoRepeat()) { AudioInjectorOptions options; @@ -4304,6 +4310,12 @@ void Application::keyReleaseEvent(QKeyEvent* event) { if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->keyReleaseEvent(event); } + + switch (event->key()) { + case Qt::Key_T: + emit pushedToTalk(false); + break; + } } void Application::focusOutEvent(QFocusEvent* event) { @@ -5235,6 +5247,9 @@ void Application::loadSettings() { } } + auto audioScriptingInterface = reinterpret_cast(DependencyManager::get().data()); + audioScriptingInterface->loadData(); + getMyAvatar()->loadData(); _settingsLoaded = true; } @@ -5244,6 +5259,9 @@ void Application::saveSettings() const { DependencyManager::get()->saveSettings(); DependencyManager::get()->saveSettings(); + auto audioScriptingInterface = reinterpret_cast(DependencyManager::get().data()); + audioScriptingInterface->saveData(); + Menu::getInstance()->saveSettings(); getMyAvatar()->saveData(); PluginManager::getInstance()->saveSettings(); diff --git a/interface/src/Application.h b/interface/src/Application.h index a8cc9450c5..1c86326f90 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -358,6 +358,8 @@ signals: void miniTabletEnabledChanged(bool enabled); + void pushedToTalk(bool enabled); + public slots: QVector pasteEntities(float x, float y, float z); bool exportEntities(const QString& filename, const QVector& entityIDs, const glm::vec3* givenOffset = nullptr); diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 2c4c29ff65..fe04ce47ca 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -63,26 +63,163 @@ void Audio::stopRecording() { } bool Audio::isMuted() const { - return resultWithReadLock([&] { - return _isMuted; - }); + bool isHMD = qApp->isHMDMode(); + if (isHMD) { + return getMutedHMD(); + } + else { + return getMutedDesktop(); + } } void Audio::setMuted(bool isMuted) { + withWriteLock([&] { + bool isHMD = qApp->isHMDMode(); + if (isHMD) { + setMutedHMD(isMuted); + } + else { + setMutedDesktop(isMuted); + } + }); +} + +void Audio::setMutedDesktop(bool isMuted) { + bool changed = false; + if (_desktopMuted != isMuted) { + changed = true; + _desktopMuted = isMuted; + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); + } + if (changed) { + emit mutedChanged(isMuted); + emit desktopMutedChanged(isMuted); + } +} + +bool Audio::getMutedDesktop() const { + return resultWithReadLock([&] { + return _desktopMuted; + }); +} + +void Audio::setMutedHMD(bool isMuted) { + bool changed = false; + if (_hmdMuted != isMuted) { + changed = true; + _hmdMuted = isMuted; + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); + } + if (changed) { + emit mutedChanged(isMuted); + emit hmdMutedChanged(isMuted); + } +} + +bool Audio::getMutedHMD() const { + return resultWithReadLock([&] { + return _hmdMuted; + }); +} + +bool Audio::getPTT() { + bool isHMD = qApp->isHMDMode(); + if (isHMD) { + return getPTTHMD(); + } + else { + return getPTTDesktop(); + } +} + +bool Audio::getPushingToTalk() const { + return resultWithReadLock([&] { + return _pushingToTalk; + }); +} + +void Audio::setPTT(bool enabled) { + bool isHMD = qApp->isHMDMode(); + if (isHMD) { + setPTTHMD(enabled); + } + else { + setPTTDesktop(enabled); + } +} + +void Audio::setPTTDesktop(bool enabled) { bool changed = false; withWriteLock([&] { - if (_isMuted != isMuted) { - _isMuted = isMuted; - auto client = DependencyManager::get().data(); - QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); + if (_pttDesktop != enabled) { changed = true; + _pttDesktop = enabled; + if (!enabled) { + // Set to default behavior (unmuted for Desktop) on Push-To-Talk disable. + setMutedDesktop(true); + } + else { + // Should be muted when not pushing to talk while PTT is enabled. + setMutedDesktop(true); + } } }); if (changed) { - emit mutedChanged(isMuted); + emit pushToTalkChanged(enabled); + emit pushToTalkDesktopChanged(enabled); } } +bool Audio::getPTTDesktop() const { + return resultWithReadLock([&] { + return _pttDesktop; + }); +} + +void Audio::setPTTHMD(bool enabled) { + bool changed = false; + withWriteLock([&] { + if (_pttHMD != enabled) { + changed = true; + _pttHMD = enabled; + if (!enabled) { + // Set to default behavior (unmuted for HMD) on Push-To-Talk disable. + setMutedHMD(false); + } + else { + // Should be muted when not pushing to talk while PTT is enabled. + setMutedHMD(true); + } + } + }); + if (changed) { + emit pushToTalkChanged(enabled); + emit pushToTalkHMDChanged(enabled); + } +} + +bool Audio::getPTTHMD() const { + return resultWithReadLock([&] { + return _pttHMD; + }); +} + +void Audio::saveData() { + _desktopMutedSetting.set(getMutedDesktop()); + _hmdMutedSetting.set(getMutedHMD()); + _pttDesktopSetting.set(getPTTDesktop()); + _pttHMDSetting.set(getPTTHMD()); +} + +void Audio::loadData() { + _desktopMuted = _desktopMutedSetting.get(); + _hmdMuted = _hmdMutedSetting.get(); + _pttDesktop = _pttDesktopSetting.get(); + _pttHMD = _pttHMDSetting.get(); +} + bool Audio::noiseReductionEnabled() const { return resultWithReadLock([&] { return _enableNoiseReduction; @@ -179,11 +316,32 @@ void Audio::onContextChanged() { changed = true; } }); + if (isHMD) { + setMuted(getMutedHMD()); + } + else { + setMuted(getMutedDesktop()); + } if (changed) { emit contextChanged(isHMD ? Audio::HMD : Audio::DESKTOP); } } +void Audio::handlePushedToTalk(bool enabled) { + if (getPTT()) { + if (enabled) { + setMuted(false); + } + else { + setMuted(true); + } + if (_pushingToTalk != enabled) { + _pushingToTalk = enabled; + emit pushingToTalkChanged(enabled); + } + } +} + void Audio::setReverb(bool enable) { withWriteLock([&] { DependencyManager::get()->setReverb(enable); diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index e4dcba9130..6aa589e399 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -12,6 +12,7 @@ #ifndef hifi_scripting_Audio_h #define hifi_scripting_Audio_h +#include #include "AudioScriptingInterface.h" #include "AudioDevices.h" #include "AudioEffectOptions.h" @@ -19,6 +20,9 @@ #include "AudioFileWav.h" #include +using MutedGetter = std::function; +using MutedSetter = std::function; + namespace scripting { class Audio : public AudioScriptingInterface, protected ReadWriteLockable { @@ -63,6 +67,12 @@ class Audio : public AudioScriptingInterface, protected ReadWriteLockable { Q_PROPERTY(bool clipping READ isClipping NOTIFY clippingChanged) Q_PROPERTY(QString context READ getContext NOTIFY contextChanged) Q_PROPERTY(AudioDevices* devices READ getDevices NOTIFY nop) + Q_PROPERTY(bool desktopMuted READ getMutedDesktop WRITE setMutedDesktop NOTIFY desktopMutedChanged) + Q_PROPERTY(bool hmdMuted READ getMutedHMD WRITE setMutedHMD NOTIFY hmdMutedChanged) + Q_PROPERTY(bool pushToTalk READ getPTT WRITE setPTT NOTIFY pushToTalkChanged); + Q_PROPERTY(bool pushToTalkDesktop READ getPTTDesktop WRITE setPTTDesktop NOTIFY pushToTalkDesktopChanged) + Q_PROPERTY(bool pushToTalkHMD READ getPTTHMD WRITE setPTTHMD NOTIFY pushToTalkHMDChanged) + Q_PROPERTY(bool pushingToTalk READ getPushingToTalk NOTIFY pushingToTalkChanged) public: static QString AUDIO; @@ -82,6 +92,25 @@ public: void showMicMeter(bool show); + // Mute setting setters and getters + void setMutedDesktop(bool isMuted); + bool getMutedDesktop() const; + void setMutedHMD(bool isMuted); + bool getMutedHMD() const; + void setPTT(bool enabled); + bool getPTT(); + bool getPushingToTalk() const; + + // Push-To-Talk setters and getters + void setPTTDesktop(bool enabled); + bool getPTTDesktop() const; + void setPTTHMD(bool enabled); + bool getPTTHMD() const; + + // Settings handlers + void saveData(); + void loadData(); + /**jsdoc * @function Audio.setInputDevice * @param {object} device @@ -193,6 +222,46 @@ signals: */ void mutedChanged(bool isMuted); + /**jsdoc + * Triggered when desktop audio input is muted or unmuted. + * @function Audio.desktopMutedChanged + * @param {boolean} isMuted - true if the audio input is muted for desktop mode, otherwise false. + * @returns {Signal} + */ + void desktopMutedChanged(bool isMuted); + + /**jsdoc + * Triggered when HMD audio input is muted or unmuted. + * @function Audio.hmdMutedChanged + * @param {boolean} isMuted - true if the audio input is muted for HMD mode, otherwise false. + * @returns {Signal} + */ + void hmdMutedChanged(bool isMuted); + + /** + * Triggered when Push-to-Talk has been enabled or disabled. + * @function Audio.pushToTalkChanged + * @param {boolean} enabled - true if Push-to-Talk is enabled, otherwise false. + * @returns {Signal} + */ + void pushToTalkChanged(bool enabled); + + /** + * Triggered when Push-to-Talk has been enabled or disabled for desktop mode. + * @function Audio.pushToTalkDesktopChanged + * @param {boolean} enabled - true if Push-to-Talk is emabled for Desktop mode, otherwise false. + * @returns {Signal} + */ + void pushToTalkDesktopChanged(bool enabled); + + /** + * Triggered when Push-to-Talk has been enabled or disabled for HMD mode. + * @function Audio.pushToTalkHMDChanged + * @param {boolean} enabled - true if Push-to-Talk is emabled for HMD mode, otherwise false. + * @returns {Signal} + */ + void pushToTalkHMDChanged(bool enabled); + /**jsdoc * Triggered when the audio input noise reduction is enabled or disabled. * @function Audio.noiseReductionChanged @@ -237,6 +306,14 @@ signals: */ void contextChanged(const QString& context); + /**jsdoc + * Triggered when pushing to talk. + * @function Audio.pushingToTalkChanged + * @param {boolean} talking - true if broadcasting with PTT, false otherwise. + * @returns {Signal} + */ + void pushingToTalkChanged(bool talking); + public slots: /**jsdoc @@ -245,6 +322,8 @@ public slots: */ void onContextChanged(); + void handlePushedToTalk(bool enabled); + private slots: void setMuted(bool muted); void enableNoiseReduction(bool enable); @@ -260,11 +339,19 @@ private: float _inputVolume { 1.0f }; float _inputLevel { 0.0f }; bool _isClipping { false }; - bool _isMuted { false }; bool _enableNoiseReduction { true }; // Match default value of AudioClient::_isNoiseGateEnabled. bool _contextIsHMD { false }; AudioDevices* getDevices() { return &_devices; } AudioDevices _devices; + Setting::Handle _desktopMutedSetting{ QStringList { Audio::AUDIO, "desktopMuted" }, true }; + Setting::Handle _hmdMutedSetting{ QStringList { Audio::AUDIO, "hmdMuted" }, true }; + Setting::Handle _pttDesktopSetting{ QStringList { Audio::AUDIO, "pushToTalkDesktop" }, false }; + Setting::Handle _pttHMDSetting{ QStringList { Audio::AUDIO, "pushToTalkHMD" }, false }; + bool _desktopMuted{ true }; + bool _hmdMuted{ false }; + bool _pttDesktop{ false }; + bool _pttHMD{ false }; + bool _pushingToTalk{ false }; }; }; diff --git a/scripts/system/audio.js b/scripts/system/audio.js index ee82c0c6ea..51d070d8cd 100644 --- a/scripts/system/audio.js +++ b/scripts/system/audio.js @@ -28,6 +28,9 @@ var UNMUTE_ICONS = { }; function onMuteToggled() { + if (Audio.pushingToTalk) { + return; + } if (Audio.muted) { button.editProperties(MUTE_ICONS); } else { From 8b4123b5dc7a92bedd7a533a2fd6f16b66a1ade5 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 6 Mar 2019 11:00:09 -0800 Subject: [PATCH 027/446] laying groundwork for audio app + fixing deadlocks --- interface/resources/qml/hifi/audio/MicBar.qml | 6 +- interface/src/scripting/Audio.cpp | 108 +++++++++--------- interface/src/scripting/Audio.h | 1 + scripts/system/audio.js | 11 +- 4 files changed, 69 insertions(+), 57 deletions(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index f91058bc3c..2ab1085408 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -134,7 +134,7 @@ Rectangle { Item { id: status; - readonly property string color: (AudioScriptingInterface.pushingToTalk && AudioScriptingInterface.muted) ? hifi.colors.blueHighlight : AudioScriptingInterface.muted ? colors.muted : colors.unmuted; + readonly property string color: AudioScriptingInterface.pushToTalk ? hifi.colors.blueHighlight : AudioScriptingInterface.muted ? colors.muted : colors.unmuted; visible: AudioScriptingInterface.pushingToTalk || AudioScriptingInterface.muted; @@ -165,7 +165,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - width: 50; + width: AudioScriptingInterface.pushToTalk ? (AudioScriptingInterface.pushingToTalk ? 45: 30) : 50; height: 4; color: parent.color; } @@ -176,7 +176,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - width: 50; + width: AudioScriptingInterface.pushToTalk ? (AudioScriptingInterface.pushingToTalk ? 45: 30) : 50; height: 4; color: parent.color; } diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index fe04ce47ca..63ce9d2b2e 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -66,32 +66,30 @@ bool Audio::isMuted() const { bool isHMD = qApp->isHMDMode(); if (isHMD) { return getMutedHMD(); - } - else { + } else { return getMutedDesktop(); } } void Audio::setMuted(bool isMuted) { - withWriteLock([&] { - bool isHMD = qApp->isHMDMode(); - if (isHMD) { - setMutedHMD(isMuted); - } - else { - setMutedDesktop(isMuted); - } - }); + bool isHMD = qApp->isHMDMode(); + if (isHMD) { + setMutedHMD(isMuted); + } else { + setMutedDesktop(isMuted); + } } void Audio::setMutedDesktop(bool isMuted) { bool changed = false; - if (_desktopMuted != isMuted) { - changed = true; - _desktopMuted = isMuted; - auto client = DependencyManager::get().data(); - QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); - } + withWriteLock([&] { + if (_desktopMuted != isMuted) { + changed = true; + _desktopMuted = isMuted; + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); + } + }); if (changed) { emit mutedChanged(isMuted); emit desktopMutedChanged(isMuted); @@ -106,12 +104,14 @@ bool Audio::getMutedDesktop() const { void Audio::setMutedHMD(bool isMuted) { bool changed = false; - if (_hmdMuted != isMuted) { - changed = true; - _hmdMuted = isMuted; - auto client = DependencyManager::get().data(); - QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); - } + withWriteLock([&] { + if (_hmdMuted != isMuted) { + changed = true; + _hmdMuted = isMuted; + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); + } + }); if (changed) { emit mutedChanged(isMuted); emit hmdMutedChanged(isMuted); @@ -128,12 +128,24 @@ bool Audio::getPTT() { bool isHMD = qApp->isHMDMode(); if (isHMD) { return getPTTHMD(); - } - else { + } else { return getPTTDesktop(); } } +void scripting::Audio::setPushingToTalk(bool pushingToTalk) { + bool changed = false; + withWriteLock([&] { + if (_pushingToTalk != pushingToTalk) { + changed = true; + _pushingToTalk = pushingToTalk; + } + }); + if (changed) { + emit pushingToTalkChanged(pushingToTalk); + } +} + bool Audio::getPushingToTalk() const { return resultWithReadLock([&] { return _pushingToTalk; @@ -144,8 +156,7 @@ void Audio::setPTT(bool enabled) { bool isHMD = qApp->isHMDMode(); if (isHMD) { setPTTHMD(enabled); - } - else { + } else { setPTTDesktop(enabled); } } @@ -156,16 +167,16 @@ void Audio::setPTTDesktop(bool enabled) { if (_pttDesktop != enabled) { changed = true; _pttDesktop = enabled; - if (!enabled) { - // Set to default behavior (unmuted for Desktop) on Push-To-Talk disable. - setMutedDesktop(true); - } - else { - // Should be muted when not pushing to talk while PTT is enabled. - setMutedDesktop(true); - } } }); + if (!enabled) { + // Set to default behavior (unmuted for Desktop) on Push-To-Talk disable. + setMutedDesktop(true); + } else { + // Should be muted when not pushing to talk while PTT is enabled. + setMutedDesktop(true); + } + if (changed) { emit pushToTalkChanged(enabled); emit pushToTalkDesktopChanged(enabled); @@ -184,16 +195,16 @@ void Audio::setPTTHMD(bool enabled) { if (_pttHMD != enabled) { changed = true; _pttHMD = enabled; - if (!enabled) { - // Set to default behavior (unmuted for HMD) on Push-To-Talk disable. - setMutedHMD(false); - } - else { - // Should be muted when not pushing to talk while PTT is enabled. - setMutedHMD(true); - } } }); + if (!enabled) { + // Set to default behavior (unmuted for HMD) on Push-To-Talk disable. + setMutedHMD(false); + } else { + // Should be muted when not pushing to talk while PTT is enabled. + setMutedHMD(true); + } + if (changed) { emit pushToTalkChanged(enabled); emit pushToTalkHMDChanged(enabled); @@ -318,8 +329,7 @@ void Audio::onContextChanged() { }); if (isHMD) { setMuted(getMutedHMD()); - } - else { + } else { setMuted(getMutedDesktop()); } if (changed) { @@ -331,14 +341,10 @@ void Audio::handlePushedToTalk(bool enabled) { if (getPTT()) { if (enabled) { setMuted(false); - } - else { + } else { setMuted(true); } - if (_pushingToTalk != enabled) { - _pushingToTalk = enabled; - emit pushingToTalkChanged(enabled); - } + setPushingToTalk(enabled); } } diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index 6aa589e399..94f8a7bf54 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -99,6 +99,7 @@ public: bool getMutedHMD() const; void setPTT(bool enabled); bool getPTT(); + void setPushingToTalk(bool pushingToTalk); bool getPushingToTalk() const; // Push-To-Talk setters and getters diff --git a/scripts/system/audio.js b/scripts/system/audio.js index 51d070d8cd..bf44cfa7cc 100644 --- a/scripts/system/audio.js +++ b/scripts/system/audio.js @@ -26,12 +26,15 @@ var UNMUTE_ICONS = { icon: "icons/tablet-icons/mic-unmute-i.svg", activeIcon: "icons/tablet-icons/mic-unmute-a.svg" }; +var PTT_ICONS = { + icon: "icons/tablet-icons/mic-unmute-i.svg", + activeIcon: "icons/tablet-icons/mic-unmute-a.svg" +}; function onMuteToggled() { if (Audio.pushingToTalk) { - return; - } - if (Audio.muted) { + button.editProperties(PTT_ICONS); + } else if (Audio.muted) { button.editProperties(MUTE_ICONS); } else { button.editProperties(UNMUTE_ICONS); @@ -71,6 +74,7 @@ onMuteToggled(); button.clicked.connect(onClicked); tablet.screenChanged.connect(onScreenChanged); Audio.mutedChanged.connect(onMuteToggled); +Audio.pushingToTalkChanged.connect(onMuteToggled); Script.scriptEnding.connect(function () { if (onAudioScreen) { @@ -79,6 +83,7 @@ Script.scriptEnding.connect(function () { button.clicked.disconnect(onClicked); tablet.screenChanged.disconnect(onScreenChanged); Audio.mutedChanged.disconnect(onMuteToggled); + Audio.pushingToTalkChanged.disconnect(onMuteToggled); tablet.removeButton(button); }); From 7f585587ffb3afb0073855150fc5026ab0976aa1 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 6 Mar 2019 11:11:47 -0800 Subject: [PATCH 028/446] Push to talk changes + rebased with master (#7) Push to talk changes + rebased with master --- interface/resources/qml/hifi/audio/MicBar.qml | 6 +- interface/src/AboutUtil.h | 14 +- interface/src/Application.cpp | 24 +- interface/src/commerce/Wallet.h | 8 +- .../AccountServicesScriptingInterface.cpp | 4 +- .../AccountServicesScriptingInterface.h | 34 +-- interface/src/scripting/Audio.cpp | 128 ++++++---- interface/src/scripting/Audio.h | 1 + .../src/scripting/WalletScriptingInterface.h | 22 +- interface/src/ui/overlays/Overlays.cpp | 239 ++++++------------ interface/src/ui/overlays/Overlays.h | 17 +- .../src/EntityTreeRenderer.cpp | 70 ++--- .../src/EntityTreeRenderer.h | 8 +- libraries/entities/src/EntityTree.cpp | 24 +- libraries/entities/src/EntityTree.h | 7 +- libraries/entities/src/EntityTreeElement.cpp | 4 +- libraries/entities/src/EntityTreeElement.h | 2 +- libraries/entities/src/ModelEntityItem.h | 2 +- libraries/fbx/src/FBXSerializer.cpp | 28 +- libraries/octree/src/Octree.h | 2 +- libraries/octree/src/OctreeProcessor.cpp | 4 +- libraries/octree/src/OctreeProcessor.h | 2 +- libraries/render-utils/src/Model.cpp | 19 +- libraries/script-engine/src/ScriptEngine.cpp | 4 +- scripts/system/audio.js | 11 +- 25 files changed, 331 insertions(+), 353 deletions(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index f91058bc3c..2ab1085408 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -134,7 +134,7 @@ Rectangle { Item { id: status; - readonly property string color: (AudioScriptingInterface.pushingToTalk && AudioScriptingInterface.muted) ? hifi.colors.blueHighlight : AudioScriptingInterface.muted ? colors.muted : colors.unmuted; + readonly property string color: AudioScriptingInterface.pushToTalk ? hifi.colors.blueHighlight : AudioScriptingInterface.muted ? colors.muted : colors.unmuted; visible: AudioScriptingInterface.pushingToTalk || AudioScriptingInterface.muted; @@ -165,7 +165,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - width: 50; + width: AudioScriptingInterface.pushToTalk ? (AudioScriptingInterface.pushingToTalk ? 45: 30) : 50; height: 4; color: parent.color; } @@ -176,7 +176,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - width: 50; + width: AudioScriptingInterface.pushToTalk ? (AudioScriptingInterface.pushingToTalk ? 45: 30) : 50; height: 4; color: parent.color; } diff --git a/interface/src/AboutUtil.h b/interface/src/AboutUtil.h index c06255aaa5..4a5074857d 100644 --- a/interface/src/AboutUtil.h +++ b/interface/src/AboutUtil.h @@ -16,8 +16,8 @@ #include /**jsdoc - * The HifiAbout API provides information about the version of Interface that is currently running. It also - * provides the ability to open a Web page in an Interface browser window. + * The HifiAbout API provides information about the version of Interface that is currently running. It also + * has the functionality to open a web page in an Interface browser window. * * @namespace HifiAbout * @@ -30,9 +30,9 @@ * @property {string} qtVersion - The Qt version used in Interface that is currently running. Read-only. * * @example Report build information for the version of Interface currently running. - * print("HiFi build date: " + HifiAbout.buildDate); // 11 Feb 2019 - * print("HiFi version: " + HifiAbout.buildVersion); // 0.78.0 - * print("Qt version: " + HifiAbout.qtVersion); // 5.10.1 + * print("HiFi build date: " + HifiAbout.buildDate); // Returns the build date of the version of Interface currently running on your machine. + * print("HiFi version: " + HifiAbout.buildVersion); // Returns the build version of Interface currently running on your machine. + * print("Qt version: " + HifiAbout.qtVersion); // Returns the Qt version details of the version of Interface currently running on your machine. */ class AboutUtil : public QObject { @@ -52,9 +52,9 @@ public: public slots: /**jsdoc - * Display a Web page in an Interface browser window. + * Display a web page in an Interface browser window. * @function HifiAbout.openUrl - * @param {string} url - The URL of the Web page to display. + * @param {string} url - The URL of the web page you want to view in Interface. */ void openUrl(const QString &url) const; private: diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index bcd5367a89..fa63757560 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1061,6 +1061,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(PluginManager::getInstance().data(), &PluginManager::inputDeviceRunningChanged, controllerScriptingInterface, &controller::ScriptingInterface::updateRunningInputDevices); + EntityTree::setEntityClicksCapturedOperator([this] { + return _controllerScriptingInterface->areEntityClicksCaptured(); + }); + _entityClipboard->createRootElement(); #ifdef Q_OS_WIN @@ -4404,7 +4408,6 @@ void Application::mouseMoveEvent(QMouseEvent* event) { if (compositor.getReticleVisible() || !isHMDMode() || !compositor.getReticleOverDesktop() || getOverlays().getOverlayAtPoint(glm::vec2(transformedPos.x(), transformedPos.y())) != UNKNOWN_ENTITY_ID) { getEntities()->mouseMoveEvent(&mappedEvent); - getOverlays().mouseMoveEvent(&mappedEvent); } _controllerScriptingInterface->emitMouseMoveEvent(&mappedEvent); // send events to any registered scripts @@ -4438,14 +4441,8 @@ void Application::mousePressEvent(QMouseEvent* event) { #endif QMouseEvent mappedEvent(event->type(), transformedPos, event->screenPos(), event->button(), event->buttons(), event->modifiers()); - std::pair entityResult; - if (!_controllerScriptingInterface->areEntityClicksCaptured()) { - entityResult = getEntities()->mousePressEvent(&mappedEvent); - } - std::pair overlayResult = getOverlays().mousePressEvent(&mappedEvent); - - QUuid focusedEntity = entityResult.first < overlayResult.first ? entityResult.second : overlayResult.second; - setKeyboardFocusEntity(getEntities()->wantsKeyboardFocus(focusedEntity) ? focusedEntity : UNKNOWN_ENTITY_ID); + QUuid result = getEntities()->mousePressEvent(&mappedEvent); + setKeyboardFocusEntity(getEntities()->wantsKeyboardFocus(result) ? result : UNKNOWN_ENTITY_ID); _controllerScriptingInterface->emitMousePressEvent(&mappedEvent); // send events to any registered scripts @@ -4484,11 +4481,7 @@ void Application::mouseDoublePressEvent(QMouseEvent* event) { transformedPos, event->screenPos(), event->button(), event->buttons(), event->modifiers()); - - if (!_controllerScriptingInterface->areEntityClicksCaptured()) { - getEntities()->mouseDoublePressEvent(&mappedEvent); - } - getOverlays().mouseDoublePressEvent(&mappedEvent); + getEntities()->mouseDoublePressEvent(&mappedEvent); // if one of our scripts have asked to capture this event, then stop processing it if (_controllerScriptingInterface->isMouseCaptured()) { @@ -4513,7 +4506,6 @@ void Application::mouseReleaseEvent(QMouseEvent* event) { event->buttons(), event->modifiers()); getEntities()->mouseReleaseEvent(&mappedEvent); - getOverlays().mouseReleaseEvent(&mappedEvent); _controllerScriptingInterface->emitMouseReleaseEvent(&mappedEvent); // send events to any registered scripts @@ -6969,7 +6961,7 @@ void Application::clearDomainOctreeDetails(bool clearAll) { }); // reset the model renderer - clearAll ? getEntities()->clear() : getEntities()->clearNonLocalEntities(); + clearAll ? getEntities()->clear() : getEntities()->clearDomainAndNonOwnedEntities(); auto skyStage = DependencyManager::get()->getSkyStage(); diff --git a/interface/src/commerce/Wallet.h b/interface/src/commerce/Wallet.h index 5e5e6c9b4f..fdd6b5e2a6 100644 --- a/interface/src/commerce/Wallet.h +++ b/interface/src/commerce/Wallet.h @@ -62,8 +62,8 @@ public: * ValueMeaningDescription * * - * 0Not logged inThe user isn't logged in. - * 1Not set upThe user's wallet isn't set up. + * 0Not logged inThe user is not logged in. + * 1Not set upThe user's wallet has not been set up. * 2Pre-existingThere is a wallet present on the server but not one * locally. * 3ConflictingThere is a wallet present on the server plus one present locally, @@ -73,8 +73,8 @@ public: * 5ReadyThe wallet is ready for use. * * - *

Wallets used to be stored locally but now they're stored on the server, unless the computer once had a wallet stored - * locally in which case the wallet may be present in both places.

+ *

Wallets used to be stored locally but now they're only stored on the server. A wallet is present in both places if + * your computer previously stored its information locally.

* @typedef {number} WalletScriptingInterface.WalletStatus */ enum WalletStatus { diff --git a/interface/src/scripting/AccountServicesScriptingInterface.cpp b/interface/src/scripting/AccountServicesScriptingInterface.cpp index a3597886e9..5f8fb065ff 100644 --- a/interface/src/scripting/AccountServicesScriptingInterface.cpp +++ b/interface/src/scripting/AccountServicesScriptingInterface.cpp @@ -115,8 +115,8 @@ DownloadInfoResult::DownloadInfoResult() : /**jsdoc * Information on the assets currently being downloaded and pending download. * @typedef {object} AccountServices.DownloadInfoResult - * @property {number[]} downloading - The percentage complete for each asset currently being downloaded. - * @property {number} pending - The number of assets waiting to be download. + * @property {number[]} downloading - The download percentage remaining of each asset currently downloading. + * @property {number} pending - The number of assets pending download. */ QScriptValue DownloadInfoResultToScriptValue(QScriptEngine* engine, const DownloadInfoResult& result) { QScriptValue object = engine->newObject(); diff --git a/interface/src/scripting/AccountServicesScriptingInterface.h b/interface/src/scripting/AccountServicesScriptingInterface.h index c08181d7c9..b188b4e63b 100644 --- a/interface/src/scripting/AccountServicesScriptingInterface.h +++ b/interface/src/scripting/AccountServicesScriptingInterface.h @@ -38,19 +38,19 @@ class AccountServicesScriptingInterface : public QObject { Q_OBJECT /**jsdoc - * The AccountServices API provides functions related to user connectivity, visibility, and asset download - * progress. + * The AccountServices API provides functions that give information on user connectivity, visibility, and + * asset download progress. * * @hifi-interface * @hifi-client-entity * @hifi-avatar * * @namespace AccountServices - * @property {string} username - The user name if the user is logged in, otherwise "Unknown user". - * Read-only. + * @property {string} username - The user name of the user logged in. If there is no user logged in, it is + * "Unknown user". Read-only. * @property {boolean} loggedIn - true if the user is logged in, otherwise false. * Read-only. - * @property {string} findableBy - The user's visibility to other people:
+ * @property {string} findableBy - The user's visibility to other users:
* "none" - user appears offline.
* "friends" - user is visible only to friends.
* "connections" - user is visible to friends and connections.
@@ -74,23 +74,23 @@ public: public slots: /**jsdoc - * Get information on the progress of downloading assets in the domain. + * Gets information on the download progress of assets in the domain. * @function AccountServices.getDownloadInfo - * @returns {AccountServices.DownloadInfoResult} Information on the progress of assets download. + * @returns {AccountServices.DownloadInfoResult} Information on the download progress of assets. */ DownloadInfoResult getDownloadInfo(); /**jsdoc - * Cause a {@link AccountServices.downloadInfoChanged|downloadInfoChanged} signal to be triggered with information on the - * current progress of the download of assets in the domain. + * Triggers a {@link AccountServices.downloadInfoChanged|downloadInfoChanged} signal with information on the current + * download progress of the assets in the domain. * @function AccountServices.updateDownloadInfo */ void updateDownloadInfo(); /**jsdoc - * Check whether the user is logged in. + * Checks whether the user is logged in. * @function AccountServices.isLoggedIn - * @returns {boolean} true if the user is logged in, false otherwise. + * @returns {boolean} true if the user is logged in, false if not. * @example Report whether you are logged in. * var isLoggedIn = AccountServices.isLoggedIn(); * print("You are logged in: " + isLoggedIn); // true or false @@ -98,9 +98,9 @@ public slots: bool isLoggedIn(); /**jsdoc - * Prompts the user to log in (the login dialog is displayed) if they're not already logged in. + * The function returns the login status of the user and prompts the user to log in (with a login dialog) if they're not already logged in. * @function AccountServices.checkAndSignalForAccessToken - * @returns {boolean} true if the user is already logged in, false otherwise. + * @returns {boolean} true if the user is logged in, false if not. */ bool checkAndSignalForAccessToken(); @@ -140,7 +140,7 @@ signals: /**jsdoc * Triggered when the username logged in with changes, i.e., when the user logs in or out. * @function AccountServices.myUsernameChanged - * @param {string} username - The username logged in with if the user is logged in, otherwise "". + * @param {string} username - The user name of the user logged in. If there is no user logged in, it is "". * @returns {Signal} * @example Report when your username changes. * AccountServices.myUsernameChanged.connect(function (username) { @@ -150,9 +150,9 @@ signals: void myUsernameChanged(const QString& username); /**jsdoc - * Triggered when the progress of the download of assets for the domain changes. + * Triggered when the download progress of the assets in the domain changes. * @function AccountServices.downloadInfoChanged - * @param {AccountServices.DownloadInfoResult} downloadInfo - Information on the progress of assets download. + * @param {AccountServices.DownloadInfoResult} downloadInfo - Information on the download progress of assets. * @returns {Signal} */ void downloadInfoChanged(DownloadInfoResult info); @@ -186,7 +186,7 @@ signals: /**jsdoc * Triggered when the login status of the user changes. * @function AccountServices.loggedInChanged - * @param {boolean} loggedIn - true if the user is logged in, otherwise false. + * @param {boolean} loggedIn - true if the user is logged in, false if not. * @returns {Signal} * @example Report when your login status changes. * AccountServices.loggedInChanged.connect(function(loggedIn) { diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index fe04ce47ca..45bb15f1a3 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -66,32 +66,30 @@ bool Audio::isMuted() const { bool isHMD = qApp->isHMDMode(); if (isHMD) { return getMutedHMD(); - } - else { + } else { return getMutedDesktop(); } } void Audio::setMuted(bool isMuted) { - withWriteLock([&] { - bool isHMD = qApp->isHMDMode(); - if (isHMD) { - setMutedHMD(isMuted); - } - else { - setMutedDesktop(isMuted); - } - }); + bool isHMD = qApp->isHMDMode(); + if (isHMD) { + setMutedHMD(isMuted); + } else { + setMutedDesktop(isMuted); + } } void Audio::setMutedDesktop(bool isMuted) { bool changed = false; - if (_desktopMuted != isMuted) { - changed = true; - _desktopMuted = isMuted; - auto client = DependencyManager::get().data(); - QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); - } + withWriteLock([&] { + if (_desktopMuted != isMuted) { + changed = true; + _desktopMuted = isMuted; + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); + } + }); if (changed) { emit mutedChanged(isMuted); emit desktopMutedChanged(isMuted); @@ -106,12 +104,14 @@ bool Audio::getMutedDesktop() const { void Audio::setMutedHMD(bool isMuted) { bool changed = false; - if (_hmdMuted != isMuted) { - changed = true; - _hmdMuted = isMuted; - auto client = DependencyManager::get().data(); - QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); - } + withWriteLock([&] { + if (_hmdMuted != isMuted) { + changed = true; + _hmdMuted = isMuted; + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); + } + }); if (changed) { emit mutedChanged(isMuted); emit hmdMutedChanged(isMuted); @@ -128,12 +128,24 @@ bool Audio::getPTT() { bool isHMD = qApp->isHMDMode(); if (isHMD) { return getPTTHMD(); - } - else { + } else { return getPTTDesktop(); } } +void scripting::Audio::setPushingToTalk(bool pushingToTalk) { + bool changed = false; + withWriteLock([&] { + if (_pushingToTalk != pushingToTalk) { + changed = true; + _pushingToTalk = pushingToTalk; + } + }); + if (changed) { + emit pushingToTalkChanged(pushingToTalk); + } +} + bool Audio::getPushingToTalk() const { return resultWithReadLock([&] { return _pushingToTalk; @@ -144,8 +156,7 @@ void Audio::setPTT(bool enabled) { bool isHMD = qApp->isHMDMode(); if (isHMD) { setPTTHMD(enabled); - } - else { + } else { setPTTDesktop(enabled); } } @@ -156,16 +167,16 @@ void Audio::setPTTDesktop(bool enabled) { if (_pttDesktop != enabled) { changed = true; _pttDesktop = enabled; - if (!enabled) { - // Set to default behavior (unmuted for Desktop) on Push-To-Talk disable. - setMutedDesktop(true); - } - else { - // Should be muted when not pushing to talk while PTT is enabled. - setMutedDesktop(true); - } } }); + if (!enabled) { + // Set to default behavior (unmuted for Desktop) on Push-To-Talk disable. + setMutedDesktop(true); + } else { + // Should be muted when not pushing to talk while PTT is enabled. + setMutedDesktop(true); + } + if (changed) { emit pushToTalkChanged(enabled); emit pushToTalkDesktopChanged(enabled); @@ -184,16 +195,16 @@ void Audio::setPTTHMD(bool enabled) { if (_pttHMD != enabled) { changed = true; _pttHMD = enabled; - if (!enabled) { - // Set to default behavior (unmuted for HMD) on Push-To-Talk disable. - setMutedHMD(false); - } - else { - // Should be muted when not pushing to talk while PTT is enabled. - setMutedHMD(true); - } } }); + if (!enabled) { + // Set to default behavior (unmuted for HMD) on Push-To-Talk disable. + setMutedHMD(false); + } else { + // Should be muted when not pushing to talk while PTT is enabled. + setMutedHMD(true); + } + if (changed) { emit pushToTalkChanged(enabled); emit pushToTalkHMDChanged(enabled); @@ -220,6 +231,26 @@ void Audio::loadData() { _pttHMD = _pttHMDSetting.get(); } +bool Audio::getPTTHMD() const { + return resultWithReadLock([&] { + return _pttHMD; + }); +} + +void Audio::saveData() { + _desktopMutedSetting.set(getMutedDesktop()); + _hmdMutedSetting.set(getMutedHMD()); + _pttDesktopSetting.set(getPTTDesktop()); + _pttHMDSetting.set(getPTTHMD()); +} + +void Audio::loadData() { + _desktopMuted = _desktopMutedSetting.get(); + _hmdMuted = _hmdMutedSetting.get(); + _pttDesktop = _pttDesktopSetting.get(); + _pttHMD = _pttHMDSetting.get(); +} + bool Audio::noiseReductionEnabled() const { return resultWithReadLock([&] { return _enableNoiseReduction; @@ -318,8 +349,7 @@ void Audio::onContextChanged() { }); if (isHMD) { setMuted(getMutedHMD()); - } - else { + } else { setMuted(getMutedDesktop()); } if (changed) { @@ -331,14 +361,10 @@ void Audio::handlePushedToTalk(bool enabled) { if (getPTT()) { if (enabled) { setMuted(false); - } - else { + } else { setMuted(true); } - if (_pushingToTalk != enabled) { - _pushingToTalk = enabled; - emit pushingToTalkChanged(enabled); - } + setPushingToTalk(enabled); } } diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index 6aa589e399..94f8a7bf54 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -99,6 +99,7 @@ public: bool getMutedHMD() const; void setPTT(bool enabled); bool getPTT(); + void setPushingToTalk(bool pushingToTalk); bool getPushingToTalk() const; // Push-To-Talk setters and getters diff --git a/interface/src/scripting/WalletScriptingInterface.h b/interface/src/scripting/WalletScriptingInterface.h index 7482b8be00..3ef9c7953a 100644 --- a/interface/src/scripting/WalletScriptingInterface.h +++ b/interface/src/scripting/WalletScriptingInterface.h @@ -41,8 +41,8 @@ public: * * @property {WalletScriptingInterface.WalletStatus} walletStatus - The status of the user's wallet. Read-only. * @property {boolean} limitedCommerce - true if Interface is running in limited commerce mode. In limited commerce - * mode, certain Interface functionality is disabled, e.g., users can't buy non-free items from the Marketplace. The Oculus - * Store version of Interface runs in limited commerce mode. Read-only. + * mode, certain Interface functionalities are disabled, e.g., users can't buy items that are not free from the Marketplace. + * The Oculus Store version of Interface runs in limited commerce mode. Read-only. */ class WalletScriptingInterface : public QObject, public Dependency { Q_OBJECT @@ -55,16 +55,16 @@ public: WalletScriptingInterface(); /**jsdoc - * Check and update the user's wallet status. + * Checks and updates the user's wallet status. * @function WalletScriptingInterface.refreshWalletStatus */ Q_INVOKABLE void refreshWalletStatus(); /**jsdoc - * Get the current status of the user's wallet. + * Gets the current status of the user's wallet. * @function WalletScriptingInterface.getWalletStatus * @returns {WalletScriptingInterface.WalletStatus} - * @example Two ways to report your wallet status. + * @example Use two methods to report your wallet's status. * print("Wallet status: " + WalletScriptingInterface.walletStatus); // Same value as next line. * print("Wallet status: " + WalletScriptingInterface.getWalletStatus()); */ @@ -74,11 +74,11 @@ public: * Check that a certified avatar entity is owned by the avatar whose entity it is. The result of the check is provided via * the {@link WalletScriptingInterface.ownershipVerificationSuccess|ownershipVerificationSuccess} and * {@link WalletScriptingInterface.ownershipVerificationFailed|ownershipVerificationFailed} signals.
- * Warning: Neither of these signals fire if the entity is not an avatar entity or it's not a certified - * entity. + * Warning: Neither of these signals are triggered if the entity is not an avatar entity or is not + * certified. * @function WalletScriptingInterface.proveAvatarEntityOwnershipVerification - * @param {Uuid} entityID - The ID of the avatar entity to check. - * @example Check ownership of all nearby certified avatar entities. + * @param {Uuid} entityID - The avatar entity's ID. + * @example Check the ownership of all nearby certified avatar entities. * // Set up response handling. * function ownershipSuccess(entityID) { * print("Ownership test succeeded for: " + entityID); @@ -118,7 +118,7 @@ public: signals: /**jsdoc - * Triggered when the status of the user's wallet changes. + * Triggered when the user's wallet status changes. * @function WalletScriptingInterface.walletStatusChanged * @returns {Signal} * @example Report when your wallet status changes, e.g., when you log in and out. @@ -136,7 +136,7 @@ signals: void limitedCommerceChanged(); /**jsdoc - * Triggered when the user rezzes a certified entity but the user's wallet is not ready and so the certified location of the + * Triggered when the user rezzes a certified entity but the user's wallet is not ready. So the certified location of the * entity cannot be updated in the metaverse. * @function WalletScriptingInterface.walletNotSetup * @returns {Signal} diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 5ae3f7d38e..dfd698f6c5 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -43,14 +43,6 @@ std::unordered_map Overlays::_entityToOverlayTypes; std::unordered_map Overlays::_overlayToEntityTypes; Overlays::Overlays() { - auto pointerManager = DependencyManager::get(); - connect(pointerManager.data(), &PointerManager::hoverBeginOverlay, this, &Overlays::hoverEnterPointerEvent); - connect(pointerManager.data(), &PointerManager::hoverContinueOverlay, this, &Overlays::hoverOverPointerEvent); - connect(pointerManager.data(), &PointerManager::hoverEndOverlay, this, &Overlays::hoverLeavePointerEvent); - connect(pointerManager.data(), &PointerManager::triggerBeginOverlay, this, &Overlays::mousePressPointerEvent); - connect(pointerManager.data(), &PointerManager::triggerContinueOverlay, this, &Overlays::mouseMovePointerEvent); - connect(pointerManager.data(), &PointerManager::triggerEndOverlay, this, &Overlays::mouseReleasePointerEvent); - ADD_TYPE_MAP(Box, cube); ADD_TYPE_MAP(Sphere, sphere); _overlayToEntityTypes["rectangle3d"] = "Shape"; @@ -81,13 +73,23 @@ void Overlays::cleanupAllOverlays() { void Overlays::init() { auto entityScriptingInterface = DependencyManager::get().data(); - auto pointerManager = DependencyManager::get(); - connect(pointerManager.data(), &PointerManager::hoverBeginOverlay, entityScriptingInterface , &EntityScriptingInterface::hoverEnterEntity); - connect(pointerManager.data(), &PointerManager::hoverContinueOverlay, entityScriptingInterface, &EntityScriptingInterface::hoverOverEntity); - connect(pointerManager.data(), &PointerManager::hoverEndOverlay, entityScriptingInterface, &EntityScriptingInterface::hoverLeaveEntity); - connect(pointerManager.data(), &PointerManager::triggerBeginOverlay, entityScriptingInterface, &EntityScriptingInterface::mousePressOnEntity); - connect(pointerManager.data(), &PointerManager::triggerContinueOverlay, entityScriptingInterface, &EntityScriptingInterface::mouseMoveOnEntity); - connect(pointerManager.data(), &PointerManager::triggerEndOverlay, entityScriptingInterface, &EntityScriptingInterface::mouseReleaseOnEntity); + auto pointerManager = DependencyManager::get().data(); + connect(pointerManager, &PointerManager::hoverBeginOverlay, entityScriptingInterface , &EntityScriptingInterface::hoverEnterEntity); + connect(pointerManager, &PointerManager::hoverContinueOverlay, entityScriptingInterface, &EntityScriptingInterface::hoverOverEntity); + connect(pointerManager, &PointerManager::hoverEndOverlay, entityScriptingInterface, &EntityScriptingInterface::hoverLeaveEntity); + connect(pointerManager, &PointerManager::triggerBeginOverlay, entityScriptingInterface, &EntityScriptingInterface::mousePressOnEntity); + connect(pointerManager, &PointerManager::triggerContinueOverlay, entityScriptingInterface, &EntityScriptingInterface::mouseMoveOnEntity); + connect(pointerManager, &PointerManager::triggerEndOverlay, entityScriptingInterface, &EntityScriptingInterface::mouseReleaseOnEntity); + + connect(entityScriptingInterface, &EntityScriptingInterface::mousePressOnEntity, this, &Overlays::mousePressOnPointerEvent); + connect(entityScriptingInterface, &EntityScriptingInterface::mousePressOffEntity, this, &Overlays::mousePressOffPointerEvent); + connect(entityScriptingInterface, &EntityScriptingInterface::mouseDoublePressOnEntity, this, &Overlays::mouseDoublePressOnPointerEvent); + connect(entityScriptingInterface, &EntityScriptingInterface::mouseDoublePressOffEntity, this, &Overlays::mouseDoublePressOffPointerEvent); + connect(entityScriptingInterface, &EntityScriptingInterface::mouseReleaseOnEntity, this, &Overlays::mouseReleasePointerEvent); + connect(entityScriptingInterface, &EntityScriptingInterface::mouseMoveOnEntity, this, &Overlays::mouseMovePointerEvent); + connect(entityScriptingInterface, &EntityScriptingInterface::hoverEnterEntity , this, &Overlays::hoverEnterPointerEvent); + connect(entityScriptingInterface, &EntityScriptingInterface::hoverOverEntity, this, &Overlays::hoverOverPointerEvent); + connect(entityScriptingInterface, &EntityScriptingInterface::hoverLeaveEntity, this, &Overlays::hoverLeavePointerEvent); } void Overlays::update(float deltatime) { @@ -1159,7 +1161,7 @@ bool Overlays::isAddedOverlay(const QUuid& id) { } void Overlays::sendMousePressOnOverlay(const QUuid& id, const PointerEvent& event) { - mousePressPointerEvent(id, event); + mousePressOnPointerEvent(id, event); } void Overlays::sendMouseReleaseOnOverlay(const QUuid& id, const PointerEvent& event) { @@ -1206,57 +1208,66 @@ float Overlays::height() { return offscreenUi->getWindow()->size().height(); } -static uint32_t toPointerButtons(const QMouseEvent& event) { - uint32_t buttons = 0; - buttons |= event.buttons().testFlag(Qt::LeftButton) ? PointerEvent::PrimaryButton : 0; - buttons |= event.buttons().testFlag(Qt::RightButton) ? PointerEvent::SecondaryButton : 0; - buttons |= event.buttons().testFlag(Qt::MiddleButton) ? PointerEvent::TertiaryButton : 0; - return buttons; -} - -static PointerEvent::Button toPointerButton(const QMouseEvent& event) { - switch (event.button()) { - case Qt::LeftButton: - return PointerEvent::PrimaryButton; - case Qt::RightButton: - return PointerEvent::SecondaryButton; - case Qt::MiddleButton: - return PointerEvent::TertiaryButton; - default: - return PointerEvent::NoButtons; - } -} - -RayToOverlayIntersectionResult getPrevPickResult() { - RayToOverlayIntersectionResult overlayResult; - overlayResult.intersects = false; - auto pickResult = DependencyManager::get()->getPrevPickResultTyped(DependencyManager::get()->getMouseRayPickID()); - if (pickResult) { - overlayResult.intersects = pickResult->type == IntersectionType::LOCAL_ENTITY; - if (overlayResult.intersects) { - overlayResult.intersection = pickResult->intersection; - overlayResult.distance = pickResult->distance; - overlayResult.surfaceNormal = pickResult->surfaceNormal; - overlayResult.overlayID = pickResult->objectID; - overlayResult.extraInfo = pickResult->extraInfo; +void Overlays::mousePressOnPointerEvent(const QUuid& id, const PointerEvent& event) { + auto keyboard = DependencyManager::get(); + // Do not send keyboard key event to scripts to prevent malignant scripts from gathering what you typed + if (!keyboard->getKeyIDs().contains(id)) { + auto entity = DependencyManager::get()->getEntity(id); + if (entity && entity->isLocalEntity()) { + emit mousePressOnOverlay(id, event); } } - return overlayResult; } -PointerEvent Overlays::calculateOverlayPointerEvent(const QUuid& id, const PickRay& ray, - const RayToOverlayIntersectionResult& rayPickResult, QMouseEvent* event, - PointerEvent::EventType eventType) { - glm::vec2 pos2D = RayPick::projectOntoEntityXYPlane(id, rayPickResult.intersection); - return PointerEvent(eventType, PointerManager::MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, - ray.direction, toPointerButton(*event), toPointerButtons(*event), event->modifiers()); +void Overlays::mousePressOffPointerEvent() { + emit mousePressOffOverlay(); +} + +void Overlays::mouseDoublePressOnPointerEvent(const QUuid& id, const PointerEvent& event) { + auto keyboard = DependencyManager::get(); + // Do not send keyboard key event to scripts to prevent malignant scripts from gathering what you typed + if (!keyboard->getKeyIDs().contains(id)) { + auto entity = DependencyManager::get()->getEntity(id); + if (entity && entity->isLocalEntity()) { + emit mouseDoublePressOnOverlay(id, event); + } + } +} + +void Overlays::mouseDoublePressOffPointerEvent() { + emit mouseDoublePressOffOverlay(); +} + +void Overlays::mouseReleasePointerEvent(const QUuid& id, const PointerEvent& event) { + auto keyboard = DependencyManager::get(); + // Do not send keyboard key event to scripts to prevent malignant scripts from gathering what you typed + if (!keyboard->getKeyIDs().contains(id)) { + auto entity = DependencyManager::get()->getEntity(id); + if (entity && entity->isLocalEntity()) { + emit mouseReleaseOnOverlay(id, event); + } + } +} + +void Overlays::mouseMovePointerEvent(const QUuid& id, const PointerEvent& event) { + auto keyboard = DependencyManager::get(); + // Do not send keyboard key event to scripts to prevent malignant scripts from gathering what you typed + if (!keyboard->getKeyIDs().contains(id)) { + auto entity = DependencyManager::get()->getEntity(id); + if (entity && entity->isLocalEntity()) { + emit mouseMoveOnOverlay(id, event); + } + } } void Overlays::hoverEnterPointerEvent(const QUuid& id, const PointerEvent& event) { auto keyboard = DependencyManager::get(); // Do not send keyboard key event to scripts to prevent malignant scripts from gathering what you typed if (!keyboard->getKeyIDs().contains(id)) { - emit hoverEnterOverlay(id, event); + auto entity = DependencyManager::get()->getEntity(id); + if (entity && entity->isLocalEntity()) { + emit hoverEnterOverlay(id, event); + } } } @@ -1264,7 +1275,10 @@ void Overlays::hoverOverPointerEvent(const QUuid& id, const PointerEvent& event) auto keyboard = DependencyManager::get(); // Do not send keyboard key event to scripts to prevent malignant scripts from gathering what you typed if (!keyboard->getKeyIDs().contains(id)) { - emit hoverOverOverlay(id, event); + auto entity = DependencyManager::get()->getEntity(id); + if (entity && entity->isLocalEntity()) { + emit hoverOverOverlay(id, event); + } } } @@ -1272,113 +1286,10 @@ void Overlays::hoverLeavePointerEvent(const QUuid& id, const PointerEvent& event auto keyboard = DependencyManager::get(); // Do not send keyboard key event to scripts to prevent malignant scripts from gathering what you typed if (!keyboard->getKeyIDs().contains(id)) { - emit hoverLeaveOverlay(id, event); - } -} - -std::pair Overlays::mousePressEvent(QMouseEvent* event) { - PerformanceTimer perfTimer("Overlays::mousePressEvent"); - - PickRay ray = qApp->computePickRay(event->x(), event->y()); - RayToOverlayIntersectionResult rayPickResult = getPrevPickResult(); - if (rayPickResult.intersects) { - _currentClickingOnOverlayID = rayPickResult.overlayID; - - PointerEvent pointerEvent = calculateOverlayPointerEvent(_currentClickingOnOverlayID, ray, rayPickResult, event, PointerEvent::Press); - mousePressPointerEvent(_currentClickingOnOverlayID, pointerEvent); - return { rayPickResult.distance, rayPickResult.overlayID }; - } - emit mousePressOffOverlay(); - return { FLT_MAX, UNKNOWN_ENTITY_ID }; -} - -void Overlays::mousePressPointerEvent(const QUuid& id, const PointerEvent& event) { - auto keyboard = DependencyManager::get(); - // Do not send keyboard key event to scripts to prevent malignant scripts from gathering what you typed - if (!keyboard->getKeyIDs().contains(id)) { - emit mousePressOnOverlay(id, event); - } -} - -bool Overlays::mouseDoublePressEvent(QMouseEvent* event) { - PerformanceTimer perfTimer("Overlays::mouseDoublePressEvent"); - - PickRay ray = qApp->computePickRay(event->x(), event->y()); - RayToOverlayIntersectionResult rayPickResult = getPrevPickResult(); - if (rayPickResult.intersects) { - _currentClickingOnOverlayID = rayPickResult.overlayID; - - auto pointerEvent = calculateOverlayPointerEvent(_currentClickingOnOverlayID, ray, rayPickResult, event, PointerEvent::Press); - emit mouseDoublePressOnOverlay(_currentClickingOnOverlayID, pointerEvent); - return true; - } - emit mouseDoublePressOffOverlay(); - return false; -} - -bool Overlays::mouseReleaseEvent(QMouseEvent* event) { - PerformanceTimer perfTimer("Overlays::mouseReleaseEvent"); - - PickRay ray = qApp->computePickRay(event->x(), event->y()); - RayToOverlayIntersectionResult rayPickResult = getPrevPickResult(); - if (rayPickResult.intersects) { - auto pointerEvent = calculateOverlayPointerEvent(rayPickResult.overlayID, ray, rayPickResult, event, PointerEvent::Release); - mouseReleasePointerEvent(rayPickResult.overlayID, pointerEvent); - } - - _currentClickingOnOverlayID = UNKNOWN_ENTITY_ID; - return false; -} - -void Overlays::mouseReleasePointerEvent(const QUuid& id, const PointerEvent& event) { - auto keyboard = DependencyManager::get(); - // Do not send keyboard key event to scripts to prevent malignant scripts from gathering what you typed - if (!keyboard->getKeyIDs().contains(id)) { - emit mouseReleaseOnOverlay(id, event); - } -} - -bool Overlays::mouseMoveEvent(QMouseEvent* event) { - PerformanceTimer perfTimer("Overlays::mouseMoveEvent"); - - PickRay ray = qApp->computePickRay(event->x(), event->y()); - RayToOverlayIntersectionResult rayPickResult = getPrevPickResult(); - if (rayPickResult.intersects) { - auto pointerEvent = calculateOverlayPointerEvent(rayPickResult.overlayID, ray, rayPickResult, event, PointerEvent::Move); - mouseMovePointerEvent(rayPickResult.overlayID, pointerEvent); - - // If previously hovering over a different overlay then leave hover on that overlay. - if (_currentHoverOverOverlayID != UNKNOWN_ENTITY_ID && rayPickResult.overlayID != _currentHoverOverOverlayID) { - auto pointerEvent = calculateOverlayPointerEvent(_currentHoverOverOverlayID, ray, rayPickResult, event, PointerEvent::Move); - hoverLeavePointerEvent(_currentHoverOverOverlayID, pointerEvent); + auto entity = DependencyManager::get()->getEntity(id); + if (entity && entity->isLocalEntity()) { + emit hoverLeaveOverlay(id, event); } - - // If hovering over a new overlay then enter hover on that overlay. - if (rayPickResult.overlayID != _currentHoverOverOverlayID) { - hoverEnterPointerEvent(rayPickResult.overlayID, pointerEvent); - } - - // Hover over current overlay. - hoverOverPointerEvent(rayPickResult.overlayID, pointerEvent); - - _currentHoverOverOverlayID = rayPickResult.overlayID; - } else { - // If previously hovering an overlay then leave hover. - if (_currentHoverOverOverlayID != UNKNOWN_ENTITY_ID) { - auto pointerEvent = calculateOverlayPointerEvent(_currentHoverOverOverlayID, ray, rayPickResult, event, PointerEvent::Move); - hoverLeavePointerEvent(_currentHoverOverOverlayID, pointerEvent); - - _currentHoverOverOverlayID = UNKNOWN_ENTITY_ID; - } - } - return false; -} - -void Overlays::mouseMovePointerEvent(const QUuid& id, const PointerEvent& event) { - auto keyboard = DependencyManager::get(); - // Do not send keyboard key event to scripts to prevent malignant scripts from gathering what you typed - if (!keyboard->getKeyIDs().contains(id)) { - emit mouseMoveOnOverlay(id, event); } } diff --git a/interface/src/ui/overlays/Overlays.h b/interface/src/ui/overlays/Overlays.h index 93efc2bc0b..0b2994b872 100644 --- a/interface/src/ui/overlays/Overlays.h +++ b/interface/src/ui/overlays/Overlays.h @@ -112,11 +112,6 @@ public: const QVector& discard, bool visibleOnly = false, bool collidableOnly = false); - std::pair mousePressEvent(QMouseEvent* event); - bool mouseDoublePressEvent(QMouseEvent* event); - bool mouseReleaseEvent(QMouseEvent* event); - bool mouseMoveEvent(QMouseEvent* event); - void cleanupAllOverlays(); mutable QScriptEngine _scriptEngine; @@ -719,9 +714,6 @@ private: PointerEvent calculateOverlayPointerEvent(const QUuid& id, const PickRay& ray, const RayToOverlayIntersectionResult& rayPickResult, QMouseEvent* event, PointerEvent::EventType eventType); - QUuid _currentClickingOnOverlayID; - QUuid _currentHoverOverOverlayID; - static QString entityToOverlayType(const QString& type); static QString overlayToEntityType(const QString& type); static std::unordered_map _entityToOverlayTypes; @@ -732,12 +724,17 @@ private: EntityItemProperties convertOverlayToEntityProperties(QVariantMap& overlayProps, std::pair& rotationToSave, const QString& type, bool add, const QUuid& id = QUuid()); private slots: - void mousePressPointerEvent(const QUuid& id, const PointerEvent& event); - void mouseMovePointerEvent(const QUuid& id, const PointerEvent& event); + void mousePressOnPointerEvent(const QUuid& id, const PointerEvent& event); + void mousePressOffPointerEvent(); + void mouseDoublePressOnPointerEvent(const QUuid& id, const PointerEvent& event); + void mouseDoublePressOffPointerEvent(); void mouseReleasePointerEvent(const QUuid& id, const PointerEvent& event); + void mouseMovePointerEvent(const QUuid& id, const PointerEvent& event); void hoverEnterPointerEvent(const QUuid& id, const PointerEvent& event); void hoverOverPointerEvent(const QUuid& id, const PointerEvent& event); void hoverLeavePointerEvent(const QUuid& id, const PointerEvent& event); + + }; #define ADD_TYPE_MAP(entity, overlay) \ diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index e54258fc3e..143c7fa377 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -73,14 +73,14 @@ EntityTreeRenderer::EntityTreeRenderer(bool wantScripts, AbstractViewStateInterf _currentHoverOverEntityID = UNKNOWN_ENTITY_ID; _currentClickingOnEntityID = UNKNOWN_ENTITY_ID; - auto entityScriptingInterface = DependencyManager::get(); + auto entityScriptingInterface = DependencyManager::get().data(); auto pointerManager = DependencyManager::get(); - connect(pointerManager.data(), &PointerManager::hoverBeginEntity, entityScriptingInterface.data(), &EntityScriptingInterface::hoverEnterEntity); - connect(pointerManager.data(), &PointerManager::hoverContinueEntity, entityScriptingInterface.data(), &EntityScriptingInterface::hoverOverEntity); - connect(pointerManager.data(), &PointerManager::hoverEndEntity, entityScriptingInterface.data(), &EntityScriptingInterface::hoverLeaveEntity); - connect(pointerManager.data(), &PointerManager::triggerBeginEntity, entityScriptingInterface.data(), &EntityScriptingInterface::mousePressOnEntity); - connect(pointerManager.data(), &PointerManager::triggerContinueEntity, entityScriptingInterface.data(), &EntityScriptingInterface::mouseMoveOnEntity); - connect(pointerManager.data(), &PointerManager::triggerEndEntity, entityScriptingInterface.data(), &EntityScriptingInterface::mouseReleaseOnEntity); + connect(pointerManager.data(), &PointerManager::hoverBeginEntity, entityScriptingInterface, &EntityScriptingInterface::hoverEnterEntity); + connect(pointerManager.data(), &PointerManager::hoverContinueEntity, entityScriptingInterface, &EntityScriptingInterface::hoverOverEntity); + connect(pointerManager.data(), &PointerManager::hoverEndEntity, entityScriptingInterface, &EntityScriptingInterface::hoverLeaveEntity); + connect(pointerManager.data(), &PointerManager::triggerBeginEntity, entityScriptingInterface, &EntityScriptingInterface::mousePressOnEntity); + connect(pointerManager.data(), &PointerManager::triggerContinueEntity, entityScriptingInterface, &EntityScriptingInterface::mouseMoveOnEntity); + connect(pointerManager.data(), &PointerManager::triggerEndEntity, entityScriptingInterface, &EntityScriptingInterface::mouseReleaseOnEntity); // Forward mouse events to web entities auto handlePointerEvent = [&](const QUuid& entityID, const PointerEvent& event) { @@ -93,10 +93,10 @@ EntityTreeRenderer::EntityTreeRenderer(bool wantScripts, AbstractViewStateInterf QMetaObject::invokeMethod(thisEntity.get(), "handlePointerEvent", Q_ARG(const PointerEvent&, event)); } }; - connect(entityScriptingInterface.data(), &EntityScriptingInterface::mousePressOnEntity, this, handlePointerEvent); - connect(entityScriptingInterface.data(), &EntityScriptingInterface::mouseMoveOnEntity, this, handlePointerEvent); - connect(entityScriptingInterface.data(), &EntityScriptingInterface::mouseReleaseOnEntity, this, handlePointerEvent); - connect(entityScriptingInterface.data(), &EntityScriptingInterface::hoverEnterEntity, this, [&](const QUuid& entityID, const PointerEvent& event) { + connect(entityScriptingInterface, &EntityScriptingInterface::mousePressOnEntity, this, handlePointerEvent); + connect(entityScriptingInterface, &EntityScriptingInterface::mouseMoveOnEntity, this, handlePointerEvent); + connect(entityScriptingInterface, &EntityScriptingInterface::mouseReleaseOnEntity, this, handlePointerEvent); + connect(entityScriptingInterface, &EntityScriptingInterface::hoverEnterEntity, this, [&](const QUuid& entityID, const PointerEvent& event) { std::shared_ptr thisEntity; auto entity = getEntity(entityID); if (entity && entity->isVisible() && entity->getType() == EntityTypes::Web) { @@ -106,8 +106,8 @@ EntityTreeRenderer::EntityTreeRenderer(bool wantScripts, AbstractViewStateInterf QMetaObject::invokeMethod(thisEntity.get(), "hoverEnterEntity", Q_ARG(const PointerEvent&, event)); } }); - connect(entityScriptingInterface.data(), &EntityScriptingInterface::hoverOverEntity, this, handlePointerEvent); - connect(entityScriptingInterface.data(), &EntityScriptingInterface::hoverLeaveEntity, this, [&](const QUuid& entityID, const PointerEvent& event) { + connect(entityScriptingInterface, &EntityScriptingInterface::hoverOverEntity, this, handlePointerEvent); + connect(entityScriptingInterface, &EntityScriptingInterface::hoverLeaveEntity, this, [&](const QUuid& entityID, const PointerEvent& event) { std::shared_ptr thisEntity; auto entity = getEntity(entityID); if (entity && entity->isVisible() && entity->getType() == EntityTypes::Web) { @@ -196,8 +196,8 @@ void EntityTreeRenderer::resetEntitiesScriptEngine() { }); } -void EntityTreeRenderer::stopNonLocalEntityScripts() { - leaveNonLocalEntities(); +void EntityTreeRenderer::stopDomainAndNonOwnedEntities() { + leaveDomainAndNonOwnedEntities(); // unload and stop the engine if (_entitiesScriptEngine) { QList entitiesWithEntityScripts = _entitiesScriptEngine->getListOfEntityScriptIDs(); @@ -206,7 +206,7 @@ void EntityTreeRenderer::stopNonLocalEntityScripts() { EntityItemPointer entityItem = getTree()->findEntityByEntityItemID(entityID); if (entityItem) { - if (!entityItem->isLocalEntity()) { + if (!(entityItem->isLocalEntity() || (entityItem->isAvatarEntity() && entityItem->getOwningAvatarID() == getTree()->getMyAvatarSessionUUID()))) { _entitiesScriptEngine->unloadEntityScript(entityID, true); } } @@ -214,8 +214,8 @@ void EntityTreeRenderer::stopNonLocalEntityScripts() { } } -void EntityTreeRenderer::clearNonLocalEntities() { - stopNonLocalEntityScripts(); +void EntityTreeRenderer::clearDomainAndNonOwnedEntities() { + stopDomainAndNonOwnedEntities(); std::unordered_map savedEntities; // remove all entities from the scene @@ -225,7 +225,7 @@ void EntityTreeRenderer::clearNonLocalEntities() { for (const auto& entry : _entitiesInScene) { const auto& renderer = entry.second; const EntityItemPointer& entityItem = renderer->getEntity(); - if (!entityItem->isLocalEntity()) { + if (!(entityItem->isLocalEntity() || (entityItem->isAvatarEntity() && entityItem->getOwningAvatarID() == getTree()->getMyAvatarSessionUUID()))) { renderer->removeFromScene(scene, transaction); } else { savedEntities[entry.first] = entry.second; @@ -239,7 +239,7 @@ void EntityTreeRenderer::clearNonLocalEntities() { _layeredZones.clearNonLocalLayeredZones(); - OctreeProcessor::clearNonLocalEntities(); + OctreeProcessor::clearDomainAndNonOwnedEntities(); } void EntityTreeRenderer::clear() { @@ -655,22 +655,22 @@ bool EntityTreeRenderer::checkEnterLeaveEntities() { return didUpdate; } -void EntityTreeRenderer::leaveNonLocalEntities() { +void EntityTreeRenderer::leaveDomainAndNonOwnedEntities() { if (_tree && !_shuttingDown) { - QVector currentLocalEntitiesInside; + QVector currentEntitiesInsideToSave; foreach (const EntityItemID& entityID, _currentEntitiesInside) { EntityItemPointer entityItem = getTree()->findEntityByEntityItemID(entityID); - if (!entityItem->isLocalEntity()) { + if (!(entityItem->isLocalEntity() || (entityItem->isAvatarEntity() && entityItem->getOwningAvatarID() == getTree()->getMyAvatarSessionUUID()))) { emit leaveEntity(entityID); if (_entitiesScriptEngine) { _entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity"); } } else { - currentLocalEntitiesInside.push_back(entityID); + currentEntitiesInsideToSave.push_back(entityID); } } - _currentEntitiesInside = currentLocalEntitiesInside; + _currentEntitiesInside = currentEntitiesInsideToSave; forceRecheckEntities(); } } @@ -792,11 +792,11 @@ static PointerEvent::Button toPointerButton(const QMouseEvent& event) { } } -std::pair EntityTreeRenderer::mousePressEvent(QMouseEvent* event) { +QUuid EntityTreeRenderer::mousePressEvent(QMouseEvent* event) { // If we don't have a tree, or we're in the process of shutting down, then don't // process these events. if (!_tree || _shuttingDown) { - return { FLT_MAX, UNKNOWN_ENTITY_ID }; + return UNKNOWN_ENTITY_ID; } PerformanceTimer perfTimer("EntityTreeRenderer::mousePressEvent"); @@ -805,11 +805,13 @@ std::pair EntityTreeRenderer::mousePressEvent(QMouseEvent* event) RayToEntityIntersectionResult rayPickResult = _getPrevRayPickResultOperator(_mouseRayPickID); EntityItemPointer entity; if (rayPickResult.intersects && (entity = getTree()->findEntityByID(rayPickResult.entityID))) { - auto properties = entity->getProperties(); - QString urlString = properties.getHref(); - QUrl url = QUrl(urlString, QUrl::StrictMode); - if (url.isValid() && !url.isEmpty()){ - DependencyManager::get()->handleLookupString(urlString); + if (!EntityTree::areEntityClicksCaptured()) { + auto properties = entity->getProperties(); + QString urlString = properties.getHref(); + QUrl url = QUrl(urlString, QUrl::StrictMode); + if (url.isValid() && !url.isEmpty()) { + DependencyManager::get()->handleLookupString(urlString); + } } glm::vec2 pos2D = projectOntoEntityXYPlane(entity, ray, rayPickResult); @@ -827,10 +829,10 @@ std::pair EntityTreeRenderer::mousePressEvent(QMouseEvent* event) _lastPointerEvent = pointerEvent; _lastPointerEventValid = true; - return { rayPickResult.distance, rayPickResult.entityID }; + return rayPickResult.entityID; } emit entityScriptingInterface->mousePressOffEntity(); - return { FLT_MAX, UNKNOWN_ENTITY_ID }; + return UNKNOWN_ENTITY_ID; } void EntityTreeRenderer::mouseDoublePressEvent(QMouseEvent* event) { diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 51568ab744..a257951ba8 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -87,14 +87,14 @@ public: virtual void init() override; /// clears the tree - virtual void clearNonLocalEntities() override; + virtual void clearDomainAndNonOwnedEntities() override; virtual void clear() override; /// reloads the entity scripts, calling unload and preload void reloadEntityScripts(); // event handles which may generate entity related events - std::pair mousePressEvent(QMouseEvent* event); + QUuid mousePressEvent(QMouseEvent* event); void mouseReleaseEvent(QMouseEvent* event); void mouseDoublePressEvent(QMouseEvent* event); void mouseMoveEvent(QMouseEvent* event); @@ -170,7 +170,7 @@ private: bool findBestZoneAndMaybeContainingEntities(QVector* entitiesContainingAvatar = nullptr); bool applyLayeredZones(); - void stopNonLocalEntityScripts(); + void stopDomainAndNonOwnedEntities(); void checkAndCallPreload(const EntityItemID& entityID, bool reload = false, bool unloadFirst = false); @@ -179,7 +179,7 @@ private: QScriptValueList createEntityArgs(const EntityItemID& entityID); bool checkEnterLeaveEntities(); - void leaveNonLocalEntities(); + void leaveDomainAndNonOwnedEntities(); void leaveAllEntities(); void forceRecheckEntities(); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 6e404ce690..d4f15fa8b2 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -78,13 +78,14 @@ OctreeElementPointer EntityTree::createNewElement(unsigned char* octalCode) { return std::static_pointer_cast(newElement); } -void EntityTree::eraseNonLocalEntities() { +void EntityTree::eraseDomainAndNonOwnedEntities() { emit clearingEntities(); if (_simulation) { // local entities are not in the simulation, so we clear ALL _simulation->clearEntities(); } + this->withWriteLock([&] { QHash savedEntities; // NOTE: lock the Tree first, then lock the _entityMap. @@ -93,10 +94,10 @@ void EntityTree::eraseNonLocalEntities() { foreach(EntityItemPointer entity, _entityMap) { EntityTreeElementPointer element = entity->getElement(); if (element) { - element->cleanupNonLocalEntities(); + element->cleanupDomainAndNonOwnedEntities(); } - if (entity->isLocalEntity()) { + if (entity->isLocalEntity() || (entity->isAvatarEntity() && entity->getOwningAvatarID() == getMyAvatarSessionUUID())) { savedEntities[entity->getEntityItemID()] = entity; } else { int32_t spaceIndex = entity->getSpaceIndex(); @@ -114,15 +115,16 @@ void EntityTree::eraseNonLocalEntities() { { QWriteLocker locker(&_needsParentFixupLock); - QVector localEntitiesNeedsParentFixup; + QVector needParentFixup; foreach (EntityItemWeakPointer entityItem, _needsParentFixup) { - if (!entityItem.expired() && entityItem.lock()->isLocalEntity()) { - localEntitiesNeedsParentFixup.push_back(entityItem); + auto entity = entityItem.lock(); + if (entity && (entity->isLocalEntity() || (entity->isAvatarEntity() && entity->getOwningAvatarID() == getMyAvatarSessionUUID()))) { + needParentFixup.push_back(entityItem); } } - _needsParentFixup = localEntitiesNeedsParentFixup; + _needsParentFixup = needParentFixup; } } @@ -2972,6 +2974,7 @@ QStringList EntityTree::getJointNames(const QUuid& entityID) const { std::function EntityTree::_getEntityObjectOperator = nullptr; std::function EntityTree::_textSizeOperator = nullptr; +std::function EntityTree::_areEntityClicksCapturedOperator = nullptr; QObject* EntityTree::getEntityObject(const QUuid& id) { if (_getEntityObjectOperator) { @@ -2987,6 +2990,13 @@ QSizeF EntityTree::textSize(const QUuid& id, const QString& text) { return QSizeF(0.0f, 0.0f); } +bool EntityTree::areEntityClicksCaptured() { + if (_areEntityClicksCapturedOperator) { + return _areEntityClicksCapturedOperator(); + } + return false; +} + void EntityTree::updateEntityQueryAACubeWorker(SpatiallyNestablePointer object, EntityEditPacketSender* packetSender, MovingEntitiesOperator& moveOperator, bool force, bool tellServer) { // if the queryBox has changed, tell the entity-server diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index dcce0e4b99..39b3dc57c7 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -75,7 +75,7 @@ public: } - virtual void eraseNonLocalEntities() override; + virtual void eraseDomainAndNonOwnedEntities() override; virtual void eraseAllOctreeElements(bool createNewRoot = true) override; virtual void readBitstreamToTree(const unsigned char* bitstream, @@ -255,6 +255,7 @@ public: QByteArray computeNonce(const QString& certID, const QString ownerKey); bool verifyNonce(const QString& certID, const QString& nonce, EntityItemID& id); + QUuid getMyAvatarSessionUUID() { return _myAvatar ? _myAvatar->getSessionUUID() : QUuid(); } void setMyAvatar(std::shared_ptr myAvatar) { _myAvatar = myAvatar; } void swapStaleProxies(std::vector& proxies) { proxies.swap(_staleProxies); } @@ -268,6 +269,9 @@ public: static void setTextSizeOperator(std::function textSizeOperator) { _textSizeOperator = textSizeOperator; } static QSizeF textSize(const QUuid& id, const QString& text); + static void setEntityClicksCapturedOperator(std::function areEntityClicksCapturedOperator) { _areEntityClicksCapturedOperator = areEntityClicksCapturedOperator; } + static bool areEntityClicksCaptured(); + std::map getNamedPaths() const { return _namedPaths; } void updateEntityQueryAACube(SpatiallyNestablePointer object, EntityEditPacketSender* packetSender, @@ -378,6 +382,7 @@ private: static std::function _getEntityObjectOperator; static std::function _textSizeOperator; + static std::function _areEntityClicksCapturedOperator; std::vector _staleProxies; diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp index ce6f20262f..aab98adb52 100644 --- a/libraries/entities/src/EntityTreeElement.cpp +++ b/libraries/entities/src/EntityTreeElement.cpp @@ -697,11 +697,11 @@ EntityItemPointer EntityTreeElement::getEntityWithEntityItemID(const EntityItemI return foundEntity; } -void EntityTreeElement::cleanupNonLocalEntities() { +void EntityTreeElement::cleanupDomainAndNonOwnedEntities() { withWriteLock([&] { EntityItems savedEntities; foreach(EntityItemPointer entity, _entityItems) { - if (!entity->isLocalEntity()) { + if (!(entity->isLocalEntity() || (entity->isAvatarEntity() && entity->getOwningAvatarID() == getTree()->getMyAvatarSessionUUID()))) { entity->preDelete(); entity->_element = NULL; } else { diff --git a/libraries/entities/src/EntityTreeElement.h b/libraries/entities/src/EntityTreeElement.h index f82eaa7fb1..f94da44138 100644 --- a/libraries/entities/src/EntityTreeElement.h +++ b/libraries/entities/src/EntityTreeElement.h @@ -190,7 +190,7 @@ public: EntityItemPointer getEntityWithEntityItemID(const EntityItemID& id) const; void getEntitiesInside(const AACube& box, QVector& foundEntities); - void cleanupNonLocalEntities(); + void cleanupDomainAndNonOwnedEntities(); void cleanupEntities(); /// called by EntityTree on cleanup this will free all entities bool removeEntityItem(EntityItemPointer entity, bool deletion = false); diff --git a/libraries/entities/src/ModelEntityItem.h b/libraries/entities/src/ModelEntityItem.h index 0efbbbb16c..08468617ba 100644 --- a/libraries/entities/src/ModelEntityItem.h +++ b/libraries/entities/src/ModelEntityItem.h @@ -164,7 +164,7 @@ protected: int _lastKnownCurrentFrame{-1}; glm::u8vec3 _color; - glm::vec3 _modelScale; + glm::vec3 _modelScale { 1.0f }; QString _modelURL; bool _relayParentJoints; bool _groupCulled { false }; diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 9e7f422b40..5246242a1e 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -167,7 +167,6 @@ glm::mat4 getGlobalTransform(const QMultiMap& _connectionParen } } } - return globalTransform; } @@ -436,6 +435,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr hfmModel.originalURL = url; float unitScaleFactor = 1.0f; + glm::quat upAxisZRotation; + bool applyUpAxisZRotation = false; glm::vec3 ambientColor; QString hifiGlobalNodeID; unsigned int meshIndex = 0; @@ -473,11 +474,22 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr if (subobject.name == propertyName) { static const QVariant UNIT_SCALE_FACTOR = QByteArray("UnitScaleFactor"); static const QVariant AMBIENT_COLOR = QByteArray("AmbientColor"); + static const QVariant UP_AXIS = QByteArray("UpAxis"); const auto& subpropName = subobject.properties.at(0); if (subpropName == UNIT_SCALE_FACTOR) { unitScaleFactor = subobject.properties.at(index).toFloat(); } else if (subpropName == AMBIENT_COLOR) { ambientColor = getVec3(subobject.properties, index); + } else if (subpropName == UP_AXIS) { + constexpr int UP_AXIS_Y = 1; + constexpr int UP_AXIS_Z = 2; + int upAxis = subobject.properties.at(index).toInt(); + if (upAxis == UP_AXIS_Y) { + // No update necessary, y up is the default + } else if (upAxis == UP_AXIS_Z) { + upAxisZRotation = glm::angleAxis(glm::radians(-90.0f), glm::vec3(1.0f, 0.0f, 0.0f)); + applyUpAxisZRotation = true; + } } } } @@ -1269,9 +1281,11 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr joint.geometricScaling = fbxModel.geometricScaling; joint.isSkeletonJoint = fbxModel.isLimbNode; hfmModel.hasSkeletonJoints = (hfmModel.hasSkeletonJoints || joint.isSkeletonJoint); - + if (applyUpAxisZRotation && joint.parentIndex == -1) { + joint.rotation *= upAxisZRotation; + joint.translation = upAxisZRotation * joint.translation; + } glm::quat combinedRotation = joint.preRotation * joint.rotation * joint.postRotation; - if (joint.parentIndex == -1) { joint.transform = hfmModel.offset * glm::translate(joint.translation) * joint.preTransform * glm::mat4_cast(combinedRotation) * joint.postTransform; @@ -1664,6 +1678,14 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } + if (applyUpAxisZRotation) { + hfmModelPtr->meshExtents.transform(glm::mat4_cast(upAxisZRotation)); + hfmModelPtr->bindExtents.transform(glm::mat4_cast(upAxisZRotation)); + for (auto &mesh : hfmModelPtr->meshes) { + mesh.modelTransform *= glm::mat4_cast(upAxisZRotation); + mesh.meshExtents.transform(glm::mat4_cast(upAxisZRotation)); + } + } return hfmModelPtr; } diff --git a/libraries/octree/src/Octree.h b/libraries/octree/src/Octree.h index aac29201f1..82076f618b 100644 --- a/libraries/octree/src/Octree.h +++ b/libraries/octree/src/Octree.h @@ -149,7 +149,7 @@ public: OctreeElementPointer getRoot() { return _rootElement; } - virtual void eraseNonLocalEntities() { _isDirty = true; }; + virtual void eraseDomainAndNonOwnedEntities() { _isDirty = true; }; virtual void eraseAllOctreeElements(bool createNewRoot = true); virtual void readBitstreamToTree(const unsigned char* bitstream, uint64_t bufferSizeBytes, ReadBitstreamToTreeParams& args); diff --git a/libraries/octree/src/OctreeProcessor.cpp b/libraries/octree/src/OctreeProcessor.cpp index 18c8630391..03c8b9ca2f 100644 --- a/libraries/octree/src/OctreeProcessor.cpp +++ b/libraries/octree/src/OctreeProcessor.cpp @@ -198,10 +198,10 @@ void OctreeProcessor::processDatagram(ReceivedMessage& message, SharedNodePointe } -void OctreeProcessor::clearNonLocalEntities() { +void OctreeProcessor::clearDomainAndNonOwnedEntities() { if (_tree) { _tree->withWriteLock([&] { - _tree->eraseNonLocalEntities(); + _tree->eraseDomainAndNonOwnedEntities(); }); } } diff --git a/libraries/octree/src/OctreeProcessor.h b/libraries/octree/src/OctreeProcessor.h index bc5618e657..40af7a39f8 100644 --- a/libraries/octree/src/OctreeProcessor.h +++ b/libraries/octree/src/OctreeProcessor.h @@ -43,7 +43,7 @@ public: virtual void init(); /// clears the tree - virtual void clearNonLocalEntities(); + virtual void clearDomainAndNonOwnedEntities(); virtual void clear(); float getAverageElementsPerPacket() const { return _elementsPerPacket.getAverage(); } diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 9bb3f72b31..dd9b0280ca 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1346,14 +1346,19 @@ void Model::updateRig(float deltaTime, glm::mat4 parentTransform) { } void Model::computeMeshPartLocalBounds() { - for (auto& part : _modelMeshRenderItems) { - const Model::MeshState& state = _meshStates.at(part->_meshIndex); - if (_useDualQuaternionSkinning) { - part->computeAdjustedLocalBound(state.clusterDualQuaternions); - } else { - part->computeAdjustedLocalBound(state.clusterMatrices); - } + render::Transaction transaction; + auto meshStates = _meshStates; + for (auto renderItem : _modelMeshRenderItemIDs) { + transaction.updateItem(renderItem, [this, meshStates](ModelMeshPartPayload& data) { + const Model::MeshState& state = meshStates.at(data._meshIndex); + if (_useDualQuaternionSkinning) { + data.computeAdjustedLocalBound(state.clusterDualQuaternions); + } else { + data.computeAdjustedLocalBound(state.clusterMatrices); + } + }); } + AbstractViewStateInterface::instance()->getMain3DScene()->enqueueTransaction(transaction); } // virtual diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 5d33a6a061..825017b1fe 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -976,7 +976,9 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& using PointerHandler = std::function; auto makePointerHandler = [this](QString eventName) -> PointerHandler { return [this, eventName](const EntityItemID& entityItemID, const PointerEvent& event) { - forwardHandlerCall(entityItemID, eventName, { entityItemID.toScriptValue(this), event.toScriptValue(this) }); + if (!EntityTree::areEntityClicksCaptured()) { + forwardHandlerCall(entityItemID, eventName, { entityItemID.toScriptValue(this), event.toScriptValue(this) }); + } }; }; diff --git a/scripts/system/audio.js b/scripts/system/audio.js index 51d070d8cd..bf44cfa7cc 100644 --- a/scripts/system/audio.js +++ b/scripts/system/audio.js @@ -26,12 +26,15 @@ var UNMUTE_ICONS = { icon: "icons/tablet-icons/mic-unmute-i.svg", activeIcon: "icons/tablet-icons/mic-unmute-a.svg" }; +var PTT_ICONS = { + icon: "icons/tablet-icons/mic-unmute-i.svg", + activeIcon: "icons/tablet-icons/mic-unmute-a.svg" +}; function onMuteToggled() { if (Audio.pushingToTalk) { - return; - } - if (Audio.muted) { + button.editProperties(PTT_ICONS); + } else if (Audio.muted) { button.editProperties(MUTE_ICONS); } else { button.editProperties(UNMUTE_ICONS); @@ -71,6 +74,7 @@ onMuteToggled(); button.clicked.connect(onClicked); tablet.screenChanged.connect(onScreenChanged); Audio.mutedChanged.connect(onMuteToggled); +Audio.pushingToTalkChanged.connect(onMuteToggled); Script.scriptEnding.connect(function () { if (onAudioScreen) { @@ -79,6 +83,7 @@ Script.scriptEnding.connect(function () { button.clicked.disconnect(onClicked); tablet.screenChanged.disconnect(onScreenChanged); Audio.mutedChanged.disconnect(onMuteToggled); + Audio.pushingToTalkChanged.disconnect(onMuteToggled); tablet.removeButton(button); }); From 72ec0ea29ad70609972b7e5657aadb96f5614b6a Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 6 Mar 2019 11:16:03 -0800 Subject: [PATCH 029/446] Update Oculus Mobile SDK to latest version --- cmake/macros/TargetOculusMobile.cmake | 2 +- hifi_android.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cmake/macros/TargetOculusMobile.cmake b/cmake/macros/TargetOculusMobile.cmake index 3eaa008b14..f5229845a9 100644 --- a/cmake/macros/TargetOculusMobile.cmake +++ b/cmake/macros/TargetOculusMobile.cmake @@ -1,6 +1,6 @@ macro(target_oculus_mobile) - set(INSTALL_DIR ${HIFI_ANDROID_PRECOMPILED}/oculus/VrApi) + set(INSTALL_DIR ${HIFI_ANDROID_PRECOMPILED}/oculus_1.22/VrApi) # Mobile SDK set(OVR_MOBILE_INCLUDE_DIRS ${INSTALL_DIR}/Include) diff --git a/hifi_android.py b/hifi_android.py index b8a606a82f..42b472e960 100644 --- a/hifi_android.py +++ b/hifi_android.py @@ -45,10 +45,10 @@ ANDROID_PACKAGES = { 'sharedLibFolder': 'lib', 'includeLibs': ['libnvtt.so', 'libnvmath.so', 'libnvimage.so', 'libnvcore.so'] }, - 'oculus': { - 'file': 'ovr_sdk_mobile_1.19.0.zip', - 'versionId': 's_RN1vlEvUi3pnT7WPxUC4pQ0RJBs27y', - 'checksum': '98f0afb62861f1f02dd8110b31ed30eb', + 'oculus_1.22': { + 'file': 'ovr_sdk_mobile_1.22.zip', + 'versionId': 'InhomR5gwkzyiLAelH3X9k4nvV3iIpA_', + 'checksum': '1ac3c5b0521e5406f287f351015daff8', 'sharedLibFolder': 'VrApi/Libs/Android/arm64-v8a/Release', 'includeLibs': ['libvrapi.so'] }, From 4467567ee6b6dd478f3baef76dc8e089b760fba8 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 6 Mar 2019 11:18:57 -0800 Subject: [PATCH 030/446] changing text display --- interface/resources/qml/hifi/audio/MicBar.qml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index 2ab1085408..9d1cbfbc6c 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -134,9 +134,9 @@ Rectangle { Item { id: status; - readonly property string color: AudioScriptingInterface.pushToTalk ? hifi.colors.blueHighlight : AudioScriptingInterface.muted ? colors.muted : colors.unmuted; + readonly property string color: AudioScriptingInterface.muted ? colors.muted : colors.unmuted; - visible: AudioScriptingInterface.pushingToTalk || AudioScriptingInterface.muted; + visible: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) || AudioScriptingInterface.muted; anchors { left: parent.left; @@ -155,7 +155,7 @@ Rectangle { color: parent.color; - text: AudioScriptingInterface.pushToTalk ? (AudioScriptingInterface.pushingToTalk ? "SPEAKING" : "PUSH TO TALK") : (AudioScriptingInterface.muted ? "MUTED" : "MUTE"); + text: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) ? "MUTED-PTT (T)" : (AudioScriptingInterface.muted ? "MUTED" : "MUTE"); font.pointSize: 12; } @@ -165,7 +165,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - width: AudioScriptingInterface.pushToTalk ? (AudioScriptingInterface.pushingToTalk ? 45: 30) : 50; + width: AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk ? 25 : 50; height: 4; color: parent.color; } @@ -176,7 +176,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - width: AudioScriptingInterface.pushToTalk ? (AudioScriptingInterface.pushingToTalk ? 45: 30) : 50; + width: AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk ? 25 : 50; height: 4; color: parent.color; } From a318b715c3c12ab66cf8c977479c7b6ae1aaadde Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 6 Mar 2019 11:45:43 -0800 Subject: [PATCH 031/446] exposing setting pushingToTalk --- interface/src/scripting/Audio.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index 94f8a7bf54..9ad4aac9c1 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -72,7 +72,7 @@ class Audio : public AudioScriptingInterface, protected ReadWriteLockable { Q_PROPERTY(bool pushToTalk READ getPTT WRITE setPTT NOTIFY pushToTalkChanged); Q_PROPERTY(bool pushToTalkDesktop READ getPTTDesktop WRITE setPTTDesktop NOTIFY pushToTalkDesktopChanged) Q_PROPERTY(bool pushToTalkHMD READ getPTTHMD WRITE setPTTHMD NOTIFY pushToTalkHMDChanged) - Q_PROPERTY(bool pushingToTalk READ getPushingToTalk NOTIFY pushingToTalkChanged) + Q_PROPERTY(bool pushingToTalk READ getPushingToTalk WRITE setPushingToTalk NOTIFY pushingToTalkChanged) public: static QString AUDIO; From 05ba515c73491ebc9660e34bbb69bfed49ad10a5 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 6 Mar 2019 13:13:45 -0800 Subject: [PATCH 032/446] Support KHR_no_error in the VR context --- libraries/oculusMobile/src/ovr/GLContext.cpp | 11 +++++------ libraries/oculusMobile/src/ovr/GLContext.h | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/libraries/oculusMobile/src/ovr/GLContext.cpp b/libraries/oculusMobile/src/ovr/GLContext.cpp index 449ba67084..8d81df42e5 100644 --- a/libraries/oculusMobile/src/ovr/GLContext.cpp +++ b/libraries/oculusMobile/src/ovr/GLContext.cpp @@ -13,10 +13,7 @@ #include #include - -#if !defined(EGL_OPENGL_ES3_BIT_KHR) -#define EGL_OPENGL_ES3_BIT_KHR 0x0040 -#endif +#include using namespace ovr; @@ -129,7 +126,7 @@ void GLContext::doneCurrent() { eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); } -bool GLContext::create(EGLDisplay display, EGLContext shareContext) { +bool GLContext::create(EGLDisplay display, EGLContext shareContext, bool noError) { this->display = display; auto config = findConfig(display); @@ -139,7 +136,9 @@ bool GLContext::create(EGLDisplay display, EGLContext shareContext) { return false; } - EGLint contextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE }; + EGLint contextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 3, + noError ? EGL_CONTEXT_OPENGL_NO_ERROR_KHR : EGL_NONE, EGL_TRUE, + EGL_NONE }; context = eglCreateContext(display, config, shareContext, contextAttribs); if (context == EGL_NO_CONTEXT) { diff --git a/libraries/oculusMobile/src/ovr/GLContext.h b/libraries/oculusMobile/src/ovr/GLContext.h index 04f96e8d47..5ccbf20c42 100644 --- a/libraries/oculusMobile/src/ovr/GLContext.h +++ b/libraries/oculusMobile/src/ovr/GLContext.h @@ -25,7 +25,7 @@ struct GLContext { static EGLConfig findConfig(EGLDisplay display); bool makeCurrent(); void doneCurrent(); - bool create(EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY), EGLContext shareContext = EGL_NO_CONTEXT); + bool create(EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY), EGLContext shareContext = EGL_NO_CONTEXT, bool noError = false); void destroy(); operator bool() const { return context != EGL_NO_CONTEXT; } static void initModule(); From b515a0cceb3633d4872911cf10ba5cf274279df1 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 6 Mar 2019 13:15:12 -0800 Subject: [PATCH 033/446] Support KHR_no_error in the VR wrapper API --- libraries/oculusMobile/src/ovr/VrHandler.cpp | 25 +++++++++++++------- libraries/oculusMobile/src/ovr/VrHandler.h | 2 +- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/libraries/oculusMobile/src/ovr/VrHandler.cpp b/libraries/oculusMobile/src/ovr/VrHandler.cpp index 3fe3901517..6cc2ec9526 100644 --- a/libraries/oculusMobile/src/ovr/VrHandler.cpp +++ b/libraries/oculusMobile/src/ovr/VrHandler.cpp @@ -42,6 +42,8 @@ struct VrSurface : public TaskQueue { uint32_t readFbo{0}; std::atomic presentIndex{1}; double displayTime{0}; + // Not currently set by anything + bool noErrorContext { false }; static constexpr float EYE_BUFFER_SCALE = 1.0f; @@ -71,7 +73,7 @@ struct VrSurface : public TaskQueue { EGLContext currentContext = eglGetCurrentContext(); EGLDisplay currentDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); - vrglContext.create(currentDisplay, currentContext); + vrglContext.create(currentDisplay, currentContext, noErrorContext); vrglContext.makeCurrent(); glm::uvec2 eyeTargetSize; @@ -91,14 +93,17 @@ struct VrSurface : public TaskQueue { } void shutdown() { + noErrorContext = false; + // Destroy the context? } - void setHandler(VrHandler *newHandler) { + void setHandler(VrHandler *newHandler, bool noError) { withLock([&] { isRenderThread = newHandler != nullptr; if (handler != newHandler) { shutdown(); handler = newHandler; + noErrorContext = noError; init(); if (handler) { updateVrMode(); @@ -142,7 +147,6 @@ struct VrSurface : public TaskQueue { __android_log_write(ANDROID_LOG_WARN, "QQQ_OVR", "vrapi_LeaveVrMode"); vrapi_SetClockLevels(session, 1, 1); vrapi_SetExtraLatencyMode(session, VRAPI_EXTRA_LATENCY_MODE_OFF); - vrapi_SetDisplayRefreshRate(session, 60); vrapi_LeaveVrMode(session); session = nullptr; @@ -153,16 +157,19 @@ struct VrSurface : public TaskQueue { ovrJava java{ vm, env, oculusActivity }; ovrModeParms modeParms = vrapi_DefaultModeParms(&java); modeParms.Flags |= VRAPI_MODE_FLAG_NATIVE_WINDOW; + if (noErrorContext) { + modeParms.Flags |= VRAPI_MODE_FLAG_CREATE_CONTEXT_NO_ERROR; + } modeParms.Display = (unsigned long long) vrglContext.display; modeParms.ShareContext = (unsigned long long) vrglContext.context; modeParms.WindowSurface = (unsigned long long) nativeWindow; session = vrapi_EnterVrMode(&modeParms); - ovrPosef trackingTransform = vrapi_GetTrackingTransform( session, VRAPI_TRACKING_TRANSFORM_SYSTEM_CENTER_EYE_LEVEL); - vrapi_SetTrackingTransform( session, trackingTransform ); - vrapi_SetPerfThread(session, VRAPI_PERF_THREAD_TYPE_RENDERER, pthread_self()); + vrapi_SetTrackingSpace( session, VRAPI_TRACKING_SPACE_LOCAL); + vrapi_SetPerfThread(session, VRAPI_PERF_THREAD_TYPE_RENDERER, gettid()); vrapi_SetClockLevels(session, 2, 4); vrapi_SetExtraLatencyMode(session, VRAPI_EXTRA_LATENCY_MODE_DYNAMIC); - vrapi_SetDisplayRefreshRate(session, 72); + // Generates a warning on the quest: "vrapi_SetDisplayRefreshRate: Dynamic Display Refresh Rate not supported" + // vrapi_SetDisplayRefreshRate(session, 72); }); } } @@ -227,8 +234,8 @@ bool VrHandler::vrActive() const { return SURFACE.session != nullptr; } -void VrHandler::setHandler(VrHandler* handler) { - SURFACE.setHandler(handler); +void VrHandler::setHandler(VrHandler* handler, bool noError) { + SURFACE.setHandler(handler, noError); } void VrHandler::pollTask() { diff --git a/libraries/oculusMobile/src/ovr/VrHandler.h b/libraries/oculusMobile/src/ovr/VrHandler.h index 3c36ee8626..46e99f7476 100644 --- a/libraries/oculusMobile/src/ovr/VrHandler.h +++ b/libraries/oculusMobile/src/ovr/VrHandler.h @@ -21,7 +21,7 @@ public: using HandlerTask = std::function; using OvrMobileTask = std::function; using OvrJavaTask = std::function; - static void setHandler(VrHandler* handler); + static void setHandler(VrHandler* handler, bool noError = false); static bool withOvrMobile(const OvrMobileTask& task); protected: From de1513a6dd57d979d67192f8a39c5fc8ee50bbd5 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 6 Mar 2019 13:22:48 -0800 Subject: [PATCH 034/446] Turn on KHR_no_error for the quest frame player --- android/apps/questFramePlayer/src/main/cpp/RenderThread.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/android/apps/questFramePlayer/src/main/cpp/RenderThread.cpp b/android/apps/questFramePlayer/src/main/cpp/RenderThread.cpp index 78a4487284..d5b87af7fa 100644 --- a/android/apps/questFramePlayer/src/main/cpp/RenderThread.cpp +++ b/android/apps/questFramePlayer/src/main/cpp/RenderThread.cpp @@ -117,7 +117,8 @@ void RenderThread::setup() { { std::unique_lock lock(_frameLock); } ovr::VrHandler::initVr(); - ovr::VrHandler::setHandler(this); + // Enable KHR_no_error for this context + ovr::VrHandler::setHandler(this, true); makeCurrent(); From e4d6d5af89674c1feb319944a49663689e2bdbbf Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Wed, 13 Feb 2019 17:35:43 -0800 Subject: [PATCH 035/446] Streamline ModelBaker initialization and URLs --- libraries/baking/src/FBXBaker.cpp | 24 +---- libraries/baking/src/FBXBaker.h | 2 - libraries/baking/src/ModelBaker.cpp | 23 +++++ libraries/baking/src/ModelBaker.h | 4 + libraries/baking/src/OBJBaker.cpp | 13 +-- libraries/baking/src/baking/BakerLibrary.cpp | 76 +++++++++++++++ libraries/baking/src/baking/BakerLibrary.h | 28 ++++++ tools/oven/src/BakerCLI.cpp | 22 +++-- tools/oven/src/DomainBaker.cpp | 98 ++++++-------------- tools/oven/src/ui/ModelBakeWidget.cpp | 85 ++++++----------- 10 files changed, 201 insertions(+), 174 deletions(-) create mode 100644 libraries/baking/src/baking/BakerLibrary.cpp create mode 100644 libraries/baking/src/baking/BakerLibrary.h diff --git a/libraries/baking/src/FBXBaker.cpp b/libraries/baking/src/FBXBaker.cpp index afaca1dd62..7c4354a2b6 100644 --- a/libraries/baking/src/FBXBaker.cpp +++ b/libraries/baking/src/FBXBaker.cpp @@ -40,8 +40,8 @@ void FBXBaker::bake() { qDebug() << "FBXBaker" << _modelURL << "bake starting"; - // setup the output folder for the results of this bake - setupOutputFolder(); + // Setup the output folders for the results of this bake + initializeOutputDirs(); if (shouldStop()) { return; @@ -78,26 +78,6 @@ void FBXBaker::bakeSourceCopy() { checkIfTexturesFinished(); } -void FBXBaker::setupOutputFolder() { - // make sure there isn't already an output directory using the same name - if (QDir(_bakedOutputDir).exists()) { - qWarning() << "Output path" << _bakedOutputDir << "already exists. Continuing."; - } else { - qCDebug(model_baking) << "Creating FBX output folder" << _bakedOutputDir; - - // attempt to make the output folder - if (!QDir().mkpath(_bakedOutputDir)) { - handleError("Failed to create FBX output folder " + _bakedOutputDir); - return; - } - // attempt to make the output folder - if (!QDir().mkpath(_originalOutputDir)) { - handleError("Failed to create FBX output folder " + _originalOutputDir); - return; - } - } -} - void FBXBaker::loadSourceFBX() { // check if the FBX is local or first needs to be downloaded if (_modelURL.isLocalFile()) { diff --git a/libraries/baking/src/FBXBaker.h b/libraries/baking/src/FBXBaker.h index 2af51b2190..88443de1c0 100644 --- a/libraries/baking/src/FBXBaker.h +++ b/libraries/baking/src/FBXBaker.h @@ -44,8 +44,6 @@ private slots: void handleFBXNetworkReply(); private: - void setupOutputFolder(); - void loadSourceFBX(); void importScene(); diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index 34f302b501..6959a5c455 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -64,6 +64,29 @@ ModelBaker::~ModelBaker() { } } +void ModelBaker::initializeOutputDirs() { + // Attempt to make the output folders + // Warn if there is an output directory using the same name + + if (QDir(_bakedOutputDir).exists()) { + qWarning() << "Output path" << _bakedOutputDir << "already exists. Continuing."; + } else { + qCDebug(model_baking) << "Creating baked output folder" << _bakedOutputDir; + if (!QDir().mkpath(_bakedOutputDir)) { + handleError("Failed to create baked output folder " + _bakedOutputDir); + } + } + + if (QDir(_originalOutputDir).exists()) { + qWarning() << "Output path" << _originalOutputDir << "already exists. Continuing."; + } else { + qCDebug(model_baking) << "Creating original output folder" << _originalOutputDir; + if (!QDir().mkpath(_originalOutputDir)) { + handleError("Failed to create original output folder " + _originalOutputDir); + } + } +} + void ModelBaker::abort() { Baker::abort(); diff --git a/libraries/baking/src/ModelBaker.h b/libraries/baking/src/ModelBaker.h index 14a182f622..dc9d43ad66 100644 --- a/libraries/baking/src/ModelBaker.h +++ b/libraries/baking/src/ModelBaker.h @@ -31,6 +31,8 @@ using TextureBakerThreadGetter = std::function; using GetMaterialIDCallback = std::function ; static const QString BAKED_FBX_EXTENSION = ".baked.fbx"; +static const QString BAKEABLE_MODEL_FBX_EXTENSION { ".fbx" }; +static const QString BAKEABLE_MODEL_OBJ_EXTENSION { ".obj" }; class ModelBaker : public Baker { Q_OBJECT @@ -40,6 +42,8 @@ public: const QString& bakedOutputDirectory, const QString& originalOutputDirectory = ""); virtual ~ModelBaker(); + void initializeOutputDirs(); + bool compressMesh(HFMMesh& mesh, bool hasDeformers, FBXNode& dracoMeshNode, GetMaterialIDCallback materialIDCallback = nullptr); QString compressTexture(QString textureFileName, image::TextureUsage::Type = image::TextureUsage::Type::DEFAULT_TEXTURE); virtual void setWasAborted(bool wasAborted) override; diff --git a/libraries/baking/src/OBJBaker.cpp b/libraries/baking/src/OBJBaker.cpp index 5a1239f88f..11cac0b4c2 100644 --- a/libraries/baking/src/OBJBaker.cpp +++ b/libraries/baking/src/OBJBaker.cpp @@ -38,6 +38,9 @@ const QByteArray MESH = "Mesh"; void OBJBaker::bake() { qDebug() << "OBJBaker" << _modelURL << "bake starting"; + // Setup the output folders for the results of this bake + initializeOutputDirs(); + // trigger bakeOBJ once OBJ is loaded connect(this, &OBJBaker::OBJLoaded, this, &OBJBaker::bakeOBJ); @@ -46,16 +49,6 @@ void OBJBaker::bake() { } void OBJBaker::loadOBJ() { - if (!QDir().mkpath(_bakedOutputDir)) { - handleError("Failed to create baked OBJ output folder " + _bakedOutputDir); - return; - } - - if (!QDir().mkpath(_originalOutputDir)) { - handleError("Failed to create original OBJ output folder " + _originalOutputDir); - return; - } - // check if the OBJ is local or it needs to be downloaded if (_modelURL.isLocalFile()) { // loading the local OBJ diff --git a/libraries/baking/src/baking/BakerLibrary.cpp b/libraries/baking/src/baking/BakerLibrary.cpp new file mode 100644 index 0000000000..a587de97eb --- /dev/null +++ b/libraries/baking/src/baking/BakerLibrary.cpp @@ -0,0 +1,76 @@ +// +// BakerLibrary.cpp +// libraries/baking/src/baking +// +// Created by Sabrina Shanman on 2019/02/14. +// Copyright 2019 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 +// + +#include "BakerLibrary.h" + +#include "../FBXBaker.h" +#include "../OBJBaker.h" + +QUrl getBakeableModelURL(const QUrl& url, bool shouldRebakeOriginals) { + // Check if the file pointed to by this URL is a bakeable model, by comparing extensions + auto modelFileName = url.fileName(); + + bool isBakedModel = modelFileName.endsWith(BAKED_FBX_EXTENSION, Qt::CaseInsensitive); + bool isBakeableFBX = modelFileName.endsWith(BAKEABLE_MODEL_FBX_EXTENSION, Qt::CaseInsensitive); + bool isBakeableOBJ = modelFileName.endsWith(BAKEABLE_MODEL_OBJ_EXTENSION, Qt::CaseInsensitive); + bool isBakeable = isBakeableFBX || isBakeableOBJ; + + if (isBakeable || (shouldRebakeOriginals && isBakedModel)) { + if (isBakedModel) { + // Grab a URL to the original, that we assume is stored a directory up, in the "original" folder + // with just the fbx extension + qDebug() << "Inferring original URL for baked model URL" << url; + + auto originalFileName = modelFileName; + originalFileName.replace(".baked", ""); + qDebug() << "Original model URL must be present at" << url; + + return url.resolved("../original/" + originalFileName); + } else { + // Grab a clean version of the URL without a query or fragment + return url.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); + } + } + + qWarning() << "Unknown model type: " << modelFileName; + return QUrl(); +} + +std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& contentOutputPath) { + auto filename = bakeableModelURL.fileName(); + + // Output in a sub-folder with the name of the model, potentially suffixed by a number to make it unique + auto baseName = filename.left(filename.lastIndexOf('.')); + auto subDirName = "/" + baseName; + int i = 1; + while (QDir(contentOutputPath + subDirName).exists()) { + subDirName = "/" + baseName + "-" + QString::number(i++); + } + + QString bakedOutputDirectory = contentOutputPath + subDirName + "/baked"; + QString originalOutputDirectory = contentOutputPath + subDirName + "/original"; + + std::unique_ptr baker; + + if (filename.endsWith(BAKEABLE_MODEL_FBX_EXTENSION, Qt::CaseInsensitive)) { + baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory); + } else if (filename.endsWith(BAKEABLE_MODEL_OBJ_EXTENSION, Qt::CaseInsensitive)) { + baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory); + } else { + qDebug() << "Could not create ModelBaker for url" << bakeableModelURL; + } + + if (baker) { + QDir(contentOutputPath).mkpath(subDirName); + } + + return baker; +} \ No newline at end of file diff --git a/libraries/baking/src/baking/BakerLibrary.h b/libraries/baking/src/baking/BakerLibrary.h new file mode 100644 index 0000000000..8739b4e947 --- /dev/null +++ b/libraries/baking/src/baking/BakerLibrary.h @@ -0,0 +1,28 @@ +// +// ModelBaker.h +// libraries/baking/src/baking +// +// Created by Sabrina Shanman on 2019/02/14. +// Copyright 2019 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 +// + +#ifndef hifi_BakerLibrary_h +#define hifi_BakerLibrary_h + +#include + +#include "../ModelBaker.h" + +// Returns either the given model URL, or, if the model is baked and shouldRebakeOriginals is true, +// the guessed location of the original model +// Returns an empty URL if no bakeable URL found +QUrl getBakeableModelURL(const QUrl& url, bool shouldRebakeOriginals); + +// Assuming the URL is valid, gets the appropriate baker for the given URL, and creates the base directory where the baker's output will later be stored +// Returns an empty pointer if a baker could not be created +std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& contentOutputPath); + +#endif hifi_BakerLibrary_h diff --git a/tools/oven/src/BakerCLI.cpp b/tools/oven/src/BakerCLI.cpp index ff672d13bf..f4e64c3015 100644 --- a/tools/oven/src/BakerCLI.cpp +++ b/tools/oven/src/BakerCLI.cpp @@ -20,7 +20,7 @@ #include "OvenCLIApplication.h" #include "ModelBakingLoggingCategory.h" -#include "FBXBaker.h" +#include "baking/BakerLibrary.h" #include "JSBaker.h" #include "TextureBaker.h" @@ -41,7 +41,7 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString& outputPath, const QString& static const QString SCRIPT_EXTENSION { "js" }; // check what kind of baker we should be creating - bool isFBX = type == MODEL_EXTENSION; + bool isModel = type == MODEL_EXTENSION; bool isScript = type == SCRIPT_EXTENSION; // If the type doesn't match the above, we assume we have a texture, and the type specified is the @@ -54,13 +54,17 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString& outputPath, const QString& _outputPath = outputPath; // create our appropiate baker - if (isFBX) { - _baker = std::unique_ptr { - new FBXBaker(inputUrl, - []() -> QThread* { return Oven::instance().getNextWorkerThread(); }, - outputPath) - }; - _baker->moveToThread(Oven::instance().getNextWorkerThread()); + if (isModel) { + QUrl bakeableModelURL = getBakeableModelURL(inputUrl, false); + if (!bakeableModelURL.isEmpty()) { + auto getWorkerThreadCallback = []() -> QThread* { + return Oven::instance().getNextWorkerThread(); + }; + _baker = getModelBaker(bakeableModelURL, getWorkerThreadCallback, outputPath); + if (_baker) { + _baker->moveToThread(Oven::instance().getNextWorkerThread()); + } + } } else if (isScript) { _baker = std::unique_ptr { new JSBaker(inputUrl, outputPath) }; _baker->moveToThread(Oven::instance().getNextWorkerThread()); diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 0a75c72f9a..11a38f2f24 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -20,8 +20,7 @@ #include "Gzip.h" #include "Oven.h" -#include "FBXBaker.h" -#include "OBJBaker.h" +#include "baking/BakerLibrary.h" DomainBaker::DomainBaker(const QUrl& localModelFileURL, const QString& domainName, const QString& baseOutputPath, const QUrl& destinationPath, @@ -163,82 +162,37 @@ void DomainBaker::enumerateEntities() { // check if this is an entity with a model URL or is a skybox texture if (entity.contains(ENTITY_MODEL_URL_KEY)) { // grab a QUrl for the model URL - QUrl modelURL { entity[ENTITY_MODEL_URL_KEY].toString() }; + QUrl bakeableModelURL = getBakeableModelURL(entity[ENTITY_MODEL_URL_KEY].toString(), _shouldRebakeOriginals); - // check if the file pointed to by this URL is a bakeable model, by comparing extensions - auto modelFileName = modelURL.fileName(); - - static const QString BAKEABLE_MODEL_FBX_EXTENSION { ".fbx" }; - static const QString BAKEABLE_MODEL_OBJ_EXTENSION { ".obj" }; - static const QString BAKED_MODEL_EXTENSION = ".baked.fbx"; - - bool isBakedModel = modelFileName.endsWith(BAKED_MODEL_EXTENSION, Qt::CaseInsensitive); - bool isBakeableFBX = modelFileName.endsWith(BAKEABLE_MODEL_FBX_EXTENSION, Qt::CaseInsensitive); - bool isBakeableOBJ = modelFileName.endsWith(BAKEABLE_MODEL_OBJ_EXTENSION, Qt::CaseInsensitive); - bool isBakeable = isBakeableFBX || isBakeableOBJ; - - if (isBakeable || (_shouldRebakeOriginals && isBakedModel)) { - - if (isBakedModel) { - // grab a URL to the original, that we assume is stored a directory up, in the "original" folder - // with just the fbx extension - qDebug() << "Re-baking original for" << modelURL; - - auto originalFileName = modelFileName; - originalFileName.replace(".baked", ""); - modelURL = modelURL.resolved("../original/" + originalFileName); - - qDebug() << "Original must be present at" << modelURL; - } else { - // grab a clean version of the URL without a query or fragment - modelURL = modelURL.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); - } + if (!bakeableModelURL.isEmpty()) { // setup a ModelBaker for this URL, as long as we don't already have one - if (!_modelBakers.contains(modelURL)) { - auto filename = modelURL.fileName(); - auto baseName = filename.left(filename.lastIndexOf('.')); - auto subDirName = "/" + baseName; - int i = 1; - while (QDir(_contentOutputPath + subDirName).exists()) { - subDirName = "/" + baseName + "-" + QString::number(i++); + if (!_modelBakers.contains(bakeableModelURL)) { + auto getWorkerThreadCallback = []() -> QThread* { + return Oven::instance().getNextWorkerThread(); + }; + QSharedPointer baker = QSharedPointer(getModelBaker(bakeableModelURL, getWorkerThreadCallback, _contentOutputPath).release(), &ModelBaker::deleteLater); + if (baker) { + // make sure our handler is called when the baker is done + connect(baker.data(), &Baker::finished, this, &DomainBaker::handleFinishedModelBaker); + + // insert it into our bakers hash so we hold a strong pointer to it + _modelBakers.insert(bakeableModelURL, baker); + + // move the baker to the baker thread + // and kickoff the bake + baker->moveToThread(Oven::instance().getNextWorkerThread()); + QMetaObject::invokeMethod(baker.data(), "bake"); + + // keep track of the total number of baking entities + ++_totalNumberOfSubBakes; + + // add this QJsonValueRef to our multi hash so that we can easily re-write + // the model URL to the baked version once the baker is complete + _entitiesNeedingRewrite.insert(bakeableModelURL, *it); } - QSharedPointer baker; - if (isBakeableFBX) { - baker = { - new FBXBaker(modelURL, []() -> QThread* { - return Oven::instance().getNextWorkerThread(); - }, _contentOutputPath + subDirName + "/baked", _contentOutputPath + subDirName + "/original"), - &FBXBaker::deleteLater - }; - } else { - baker = { - new OBJBaker(modelURL, []() -> QThread* { - return Oven::instance().getNextWorkerThread(); - }, _contentOutputPath + subDirName + "/baked", _contentOutputPath + subDirName + "/original"), - &OBJBaker::deleteLater - }; - } - - // make sure our handler is called when the baker is done - connect(baker.data(), &Baker::finished, this, &DomainBaker::handleFinishedModelBaker); - - // insert it into our bakers hash so we hold a strong pointer to it - _modelBakers.insert(modelURL, baker); - - // move the baker to the baker thread - // and kickoff the bake - baker->moveToThread(Oven::instance().getNextWorkerThread()); - QMetaObject::invokeMethod(baker.data(), "bake"); - - // keep track of the total number of baking entities - ++_totalNumberOfSubBakes; } - - // add this QJsonValueRef to our multi hash so that we can easily re-write - // the model URL to the baked version once the baker is complete - _entitiesNeedingRewrite.insert(modelURL, *it); } } else { // // We check now to see if we have either a texture for a skybox or a keylight, or both. diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp index 9fa586871e..5ac9b43348 100644 --- a/tools/oven/src/ui/ModelBakeWidget.cpp +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -26,8 +26,7 @@ #include "../Oven.h" #include "../OvenGUIApplication.h" #include "OvenMainWindow.h" -#include "FBXBaker.h" -#include "OBJBaker.h" +#include "baking/BakerLibrary.h" static const auto EXPORT_DIR_SETTING_KEY = "model_export_directory"; @@ -172,74 +171,42 @@ void ModelBakeWidget::bakeButtonClicked() { // construct a URL from the path in the model file text box QUrl modelToBakeURL(fileURLString); - // if the URL doesn't have a scheme, assume it is a local file - if (modelToBakeURL.scheme() != "http" && modelToBakeURL.scheme() != "https" && modelToBakeURL.scheme() != "ftp") { - qDebug() << modelToBakeURL.toString(); - qDebug() << modelToBakeURL.scheme(); - modelToBakeURL = QUrl::fromLocalFile(fileURLString); - qDebug() << "New url: " << modelToBakeURL; - } - - auto modelName = modelToBakeURL.fileName().left(modelToBakeURL.fileName().lastIndexOf('.')); - // make sure we have a valid output directory QDir outputDirectory(_outputDirLineEdit->text()); - QString subFolderName = modelName + "/"; - - // output in a sub-folder with the name of the fbx, potentially suffixed by a number to make it unique - int iteration = 0; - - while (outputDirectory.exists(subFolderName)) { - subFolderName = modelName + "-" + QString::number(++iteration) + "/"; - } - - outputDirectory.mkpath(subFolderName); - if (!outputDirectory.exists()) { QMessageBox::warning(this, "Unable to create directory", "Unable to create output directory. Please create it manually or choose a different directory."); return; } - outputDirectory.cd(subFolderName); + QUrl bakeableModelURL = getBakeableModelURL(QUrl(modelToBakeURL), false); - QDir bakedOutputDirectory = outputDirectory.absoluteFilePath("baked"); - QDir originalOutputDirectory = outputDirectory.absoluteFilePath("original"); + if (!bakeableModelURL.isEmpty()) { + auto getWorkerThreadCallback = []() -> QThread* { + return Oven::instance().getNextWorkerThread(); + }; - bakedOutputDirectory.mkdir("."); - originalOutputDirectory.mkdir("."); + std::unique_ptr baker = getModelBaker(bakeableModelURL, getWorkerThreadCallback, outputDirectory.path()); + if (baker) { + // everything seems to be in place, kick off a bake for this model now - std::unique_ptr baker; - auto getWorkerThreadCallback = []() -> QThread* { - return Oven::instance().getNextWorkerThread(); - }; - // everything seems to be in place, kick off a bake for this model now - if (modelToBakeURL.fileName().endsWith(".fbx")) { - baker.reset(new FBXBaker(modelToBakeURL, getWorkerThreadCallback, bakedOutputDirectory.absolutePath(), - originalOutputDirectory.absolutePath())); - } else if (modelToBakeURL.fileName().endsWith(".obj")) { - baker.reset(new OBJBaker(modelToBakeURL, getWorkerThreadCallback, bakedOutputDirectory.absolutePath(), - originalOutputDirectory.absolutePath())); - } else { - qWarning() << "Unknown model type: " << modelToBakeURL.fileName(); - continue; + // move the baker to the FBX baker thread + baker->moveToThread(Oven::instance().getNextWorkerThread()); + + // invoke the bake method on the baker thread + QMetaObject::invokeMethod(baker.get(), "bake"); + + // make sure we hear about the results of this baker when it is done + connect(baker.get(), &Baker::finished, this, &ModelBakeWidget::handleFinishedBaker); + + // add a pending row to the results window to show that this bake is in process + auto resultsWindow = OvenGUIApplication::instance()->getMainWindow()->showResultsWindow(); + auto resultsRow = resultsWindow->addPendingResultRow(modelToBakeURL.fileName(), outputDirectory); + + // keep a unique_ptr to this baker + // and remember the row that represents it in the results table + _bakers.emplace_back(std::move(baker), resultsRow); + } } - - // move the baker to the FBX baker thread - baker->moveToThread(Oven::instance().getNextWorkerThread()); - - // invoke the bake method on the baker thread - QMetaObject::invokeMethod(baker.get(), "bake"); - - // make sure we hear about the results of this baker when it is done - connect(baker.get(), &Baker::finished, this, &ModelBakeWidget::handleFinishedBaker); - - // add a pending row to the results window to show that this bake is in process - auto resultsWindow = OvenGUIApplication::instance()->getMainWindow()->showResultsWindow(); - auto resultsRow = resultsWindow->addPendingResultRow(modelToBakeURL.fileName(), outputDirectory); - - // keep a unique_ptr to this baker - // and remember the row that represents it in the results table - _bakers.emplace_back(std::move(baker), resultsRow); } } From 4ae0c79130341c89b7cb2dcef763e805a8db4876 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Wed, 20 Feb 2019 10:29:27 -0800 Subject: [PATCH 036/446] Harden model-baker Engine against random tasks being disabled --- libraries/model-baker/src/model-baker/Baker.cpp | 17 +++++++++-------- .../src/model-baker/BuildGraphicsMeshTask.cpp | 3 ++- .../CalculateBlendshapeTangentsTask.cpp | 4 ++-- .../model-baker/CalculateMeshTangentsTask.cpp | 2 +- .../model-baker/src/model-baker/ModelMath.h | 9 +++++++++ 5 files changed, 23 insertions(+), 12 deletions(-) diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp index dfb18eef86..344af1ba8a 100644 --- a/libraries/model-baker/src/model-baker/Baker.cpp +++ b/libraries/model-baker/src/model-baker/Baker.cpp @@ -14,6 +14,7 @@ #include #include "BakerTypes.h" +#include "ModelMath.h" #include "BuildGraphicsMeshTask.h" #include "CalculateMeshNormalsTask.h" #include "CalculateMeshTangentsTask.h" @@ -59,12 +60,12 @@ namespace baker { blendshapesPerMeshOut = blendshapesPerMeshIn; for (int i = 0; i < (int)blendshapesPerMeshOut.size(); i++) { - const auto& normalsPerBlendshape = normalsPerBlendshapePerMesh[i]; - const auto& tangentsPerBlendshape = tangentsPerBlendshapePerMesh[i]; + const auto& normalsPerBlendshape = safeGet(normalsPerBlendshapePerMesh, i); + const auto& tangentsPerBlendshape = safeGet(tangentsPerBlendshapePerMesh, i); auto& blendshapesOut = blendshapesPerMeshOut[i]; for (int j = 0; j < (int)blendshapesOut.size(); j++) { - const auto& normals = normalsPerBlendshape[j]; - const auto& tangents = tangentsPerBlendshape[j]; + const auto& normals = safeGet(normalsPerBlendshape, j); + const auto& tangents = safeGet(tangentsPerBlendshape, j); auto& blendshape = blendshapesOut[j]; blendshape.normals = QVector::fromStdVector(normals); blendshape.tangents = QVector::fromStdVector(tangents); @@ -90,10 +91,10 @@ namespace baker { auto meshesOut = meshesIn; for (int i = 0; i < numMeshes; i++) { auto& meshOut = meshesOut[i]; - meshOut._mesh = graphicsMeshesIn[i]; - meshOut.normals = QVector::fromStdVector(normalsPerMeshIn[i]); - meshOut.tangents = QVector::fromStdVector(tangentsPerMeshIn[i]); - meshOut.blendshapes = QVector::fromStdVector(blendshapesPerMeshIn[i]); + meshOut._mesh = safeGet(graphicsMeshesIn, i); + meshOut.normals = QVector::fromStdVector(safeGet(normalsPerMeshIn, i)); + meshOut.tangents = QVector::fromStdVector(safeGet(tangentsPerMeshIn, i)); + meshOut.blendshapes = QVector::fromStdVector(safeGet(blendshapesPerMeshIn, i)); } output = meshesOut; } diff --git a/libraries/model-baker/src/model-baker/BuildGraphicsMeshTask.cpp b/libraries/model-baker/src/model-baker/BuildGraphicsMeshTask.cpp index 370add2c2e..f329dc18f5 100644 --- a/libraries/model-baker/src/model-baker/BuildGraphicsMeshTask.cpp +++ b/libraries/model-baker/src/model-baker/BuildGraphicsMeshTask.cpp @@ -15,6 +15,7 @@ #include #include "ModelBakerLogging.h" +#include "ModelMath.h" using vec2h = glm::tvec2; @@ -385,7 +386,7 @@ void BuildGraphicsMeshTask::run(const baker::BakeContextPointer& context, const auto& graphicsMesh = graphicsMeshes[i]; // Try to create the graphics::Mesh - buildGraphicsMesh(meshes[i], graphicsMesh, normalsPerMesh[i], tangentsPerMesh[i]); + buildGraphicsMesh(meshes[i], graphicsMesh, baker::safeGet(normalsPerMesh, i), baker::safeGet(tangentsPerMesh, i)); // Choose a name for the mesh if (graphicsMesh) { diff --git a/libraries/model-baker/src/model-baker/CalculateBlendshapeTangentsTask.cpp b/libraries/model-baker/src/model-baker/CalculateBlendshapeTangentsTask.cpp index 04e05f0378..ba8fd94f09 100644 --- a/libraries/model-baker/src/model-baker/CalculateBlendshapeTangentsTask.cpp +++ b/libraries/model-baker/src/model-baker/CalculateBlendshapeTangentsTask.cpp @@ -24,7 +24,7 @@ void CalculateBlendshapeTangentsTask::run(const baker::BakeContextPointer& conte tangentsPerBlendshapePerMeshOut.reserve(normalsPerBlendshapePerMesh.size()); for (size_t i = 0; i < blendshapesPerMesh.size(); i++) { - const auto& normalsPerBlendshape = normalsPerBlendshapePerMesh[i]; + const auto& normalsPerBlendshape = baker::safeGet(normalsPerBlendshapePerMesh, i); const auto& blendshapes = blendshapesPerMesh[i]; const auto& mesh = meshes[i]; tangentsPerBlendshapePerMeshOut.emplace_back(); @@ -43,7 +43,7 @@ void CalculateBlendshapeTangentsTask::run(const baker::BakeContextPointer& conte for (size_t j = 0; j < blendshapes.size(); j++) { const auto& blendshape = blendshapes[j]; const auto& tangentsIn = blendshape.tangents; - const auto& normals = normalsPerBlendshape[j]; + const auto& normals = baker::safeGet(normalsPerBlendshape, j); tangentsPerBlendshapeOut.emplace_back(); auto& tangentsOut = tangentsPerBlendshapeOut[tangentsPerBlendshapeOut.size()-1]; diff --git a/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp b/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp index 6e12ec546d..d2144a0e30 100644 --- a/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp +++ b/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp @@ -34,7 +34,7 @@ void CalculateMeshTangentsTask::run(const baker::BakeContextPointer& context, co for (int i = 0; i < (int)meshes.size(); i++) { const auto& mesh = meshes[i]; const auto& tangentsIn = mesh.tangents; - const auto& normals = normalsPerMesh[i]; + const auto& normals = baker::safeGet(normalsPerMesh, i); tangentsPerMeshOut.emplace_back(); auto& tangentsOut = tangentsPerMeshOut[tangentsPerMeshOut.size()-1]; diff --git a/libraries/model-baker/src/model-baker/ModelMath.h b/libraries/model-baker/src/model-baker/ModelMath.h index 2a909e6eed..60ce3ad098 100644 --- a/libraries/model-baker/src/model-baker/ModelMath.h +++ b/libraries/model-baker/src/model-baker/ModelMath.h @@ -14,6 +14,15 @@ #include "BakerTypes.h" namespace baker { + template + T safeGet(const std::vector& data, size_t i) { + if (data.size() > i) { + return data[i]; + } else { + return T(); + } + } + // Returns a reference to the normal at the specified index, or nullptr if it cannot be accessed using NormalAccessor = std::function; From 9c9dc553a21cb80b052f1d0a1f5465ca2be2cc1c Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Wed, 20 Feb 2019 12:00:43 -0800 Subject: [PATCH 037/446] Fix binding to temporary when trying to safely get empty model-baker task data --- libraries/model-baker/src/model-baker/ModelMath.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libraries/model-baker/src/model-baker/ModelMath.h b/libraries/model-baker/src/model-baker/ModelMath.h index 60ce3ad098..38bb3e1b3d 100644 --- a/libraries/model-baker/src/model-baker/ModelMath.h +++ b/libraries/model-baker/src/model-baker/ModelMath.h @@ -15,11 +15,13 @@ namespace baker { template - T safeGet(const std::vector& data, size_t i) { + const T& safeGet(const std::vector& data, size_t i) { + static T t; + if (data.size() > i) { return data[i]; } else { - return T(); + return t; } } From aef696efe6c1a4df3a9af2ff19ee9d6161f4cb40 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Wed, 20 Feb 2019 14:28:44 -0800 Subject: [PATCH 038/446] Add passthrough config to PrepareJointsTask --- .../src/model-baker/PrepareJointsTask.cpp | 57 +++++++++++-------- .../src/model-baker/PrepareJointsTask.h | 15 ++++- 2 files changed, 47 insertions(+), 25 deletions(-) diff --git a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp index 3b1a57cb43..63d0408337 100644 --- a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp +++ b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp @@ -50,37 +50,46 @@ QMap getJointRotationOffsets(const QVariantHash& mapping) { return jointRotationOffsets; } +void PrepareJointsTask::configure(const Config& config) { + _passthrough = config.passthrough; +} + void PrepareJointsTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) { const auto& jointsIn = input.get0(); - const auto& mapping = input.get1(); auto& jointsOut = output.edit0(); - auto& jointRotationOffsets = output.edit1(); - auto& jointIndices = output.edit2(); - // Get joint renames - auto jointNameMapping = getJointNameMapping(mapping); - // Apply joint metadata from FST file mappings - for (const auto& jointIn : jointsIn) { - jointsOut.push_back(jointIn); - auto& jointOut = jointsOut.back(); + if (_passthrough) { + jointsOut = jointsIn; + } else { + const auto& mapping = input.get1(); + auto& jointRotationOffsets = output.edit1(); + auto& jointIndices = output.edit2(); - auto jointNameMapKey = jointNameMapping.key(jointIn.name); - if (jointNameMapping.contains(jointNameMapKey)) { - jointOut.name = jointNameMapKey; + // Get joint renames + auto jointNameMapping = getJointNameMapping(mapping); + // Apply joint metadata from FST file mappings + for (const auto& jointIn : jointsIn) { + jointsOut.push_back(jointIn); + auto& jointOut = jointsOut.back(); + + auto jointNameMapKey = jointNameMapping.key(jointIn.name); + if (jointNameMapping.contains(jointNameMapKey)) { + jointOut.name = jointNameMapKey; + } + + jointIndices.insert(jointOut.name, (int)jointsOut.size()); } - jointIndices.insert(jointOut.name, (int)jointsOut.size()); - } - - // Get joint rotation offsets from FST file mappings - auto offsets = getJointRotationOffsets(mapping); - for (auto itr = offsets.begin(); itr != offsets.end(); itr++) { - QString jointName = itr.key(); - int jointIndex = jointIndices.value(jointName) - 1; - if (jointIndex != -1) { - glm::quat rotationOffset = itr.value(); - jointRotationOffsets.insert(jointIndex, rotationOffset); - qCDebug(model_baker) << "Joint Rotation Offset added to Rig._jointRotationOffsets : " << " jointName: " << jointName << " jointIndex: " << jointIndex << " rotation offset: " << rotationOffset; + // Get joint rotation offsets from FST file mappings + auto offsets = getJointRotationOffsets(mapping); + for (auto itr = offsets.begin(); itr != offsets.end(); itr++) { + QString jointName = itr.key(); + int jointIndex = jointIndices.value(jointName) - 1; + if (jointIndex != -1) { + glm::quat rotationOffset = itr.value(); + jointRotationOffsets.insert(jointIndex, rotationOffset); + qCDebug(model_baker) << "Joint Rotation Offset added to Rig._jointRotationOffsets : " << " jointName: " << jointName << " jointIndex: " << jointIndex << " rotation offset: " << rotationOffset; + } } } } diff --git a/libraries/model-baker/src/model-baker/PrepareJointsTask.h b/libraries/model-baker/src/model-baker/PrepareJointsTask.h index e12d8ffd2c..6185d2fdad 100644 --- a/libraries/model-baker/src/model-baker/PrepareJointsTask.h +++ b/libraries/model-baker/src/model-baker/PrepareJointsTask.h @@ -18,13 +18,26 @@ #include "Engine.h" +// The property "passthrough", when enabled, will let the input joints flow to the output unmodified, unlike the disabled property, which discards the data +class PrepareJointsTaskConfig : public baker::JobConfig { + Q_OBJECT + Q_PROPERTY(bool passthrough MEMBER passthrough) +public: + bool passthrough { false }; +}; + class PrepareJointsTask { public: + using Config = PrepareJointsTaskConfig; using Input = baker::VaryingSet2, QVariantHash /*mapping*/>; using Output = baker::VaryingSet3, QMap /*jointRotationOffsets*/, QHash /*jointIndices*/>; - using JobModel = baker::Job::ModelIO; + using JobModel = baker::Job::ModelIO; + void configure(const Config& config); void run(const baker::BakeContextPointer& context, const Input& input, Output& output); + +protected: + bool _passthrough { false }; }; #endif // hifi_PrepareJointsTask_h \ No newline at end of file From 8ff212ac95dadc0bfaa9b06181703df0aa5f423a Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Thu, 21 Feb 2019 10:19:00 -0800 Subject: [PATCH 039/446] Move custom draco mesh attributes from FBX.h to HFM.h --- libraries/fbx/src/FBX.h | 5 ----- libraries/hfm/src/hfm/HFM.h | 5 +++++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/fbx/src/FBX.h b/libraries/fbx/src/FBX.h index 8ad419c7ec..48ce994ac8 100644 --- a/libraries/fbx/src/FBX.h +++ b/libraries/fbx/src/FBX.h @@ -26,11 +26,6 @@ static const QByteArray FBX_BINARY_PROLOG2("\0\x1a\0", 3); static const quint32 FBX_VERSION_2015 = 7400; static const quint32 FBX_VERSION_2016 = 7500; -static const int DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES = 1000; -static const int DRACO_ATTRIBUTE_MATERIAL_ID = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES; -static const int DRACO_ATTRIBUTE_TEX_COORD_1 = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES + 1; -static const int DRACO_ATTRIBUTE_ORIGINAL_INDEX = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES + 2; - static const int32_t FBX_PROPERTY_UNCOMPRESSED_FLAG = 0; static const int32_t FBX_PROPERTY_COMPRESSED_FLAG = 1; diff --git a/libraries/hfm/src/hfm/HFM.h b/libraries/hfm/src/hfm/HFM.h index 9f3de3302c..826c79e911 100644 --- a/libraries/hfm/src/hfm/HFM.h +++ b/libraries/hfm/src/hfm/HFM.h @@ -53,6 +53,11 @@ using ColorType = glm::vec3; const int MAX_NUM_PIXELS_FOR_FBX_TEXTURE = 2048 * 2048; +static const int DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES = 1000; +static const int DRACO_ATTRIBUTE_MATERIAL_ID = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES; +static const int DRACO_ATTRIBUTE_TEX_COORD_1 = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES + 1; +static const int DRACO_ATTRIBUTE_ORIGINAL_INDEX = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES + 2; + // High Fidelity Model namespace namespace hfm { From 2af17015d3e31e68d9fcef288384b8477f0d80d9 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Mon, 25 Feb 2019 12:04:26 -0800 Subject: [PATCH 040/446] Convert serializers and FBX.h to use HifiTypes.h --- libraries/fbx/src/FBX.h | 9 +- libraries/fbx/src/FBXSerializer.cpp | 150 +++++++++---------- libraries/fbx/src/FBXSerializer.h | 14 +- libraries/fbx/src/FBXSerializer_Material.cpp | 3 +- libraries/fbx/src/FBXSerializer_Mesh.cpp | 8 +- libraries/fbx/src/FBXSerializer_Node.cpp | 14 +- libraries/fbx/src/GLTFSerializer.cpp | 46 +++--- libraries/fbx/src/GLTFSerializer.h | 26 ++-- libraries/fbx/src/OBJSerializer.cpp | 56 +++---- libraries/fbx/src/OBJSerializer.h | 30 ++-- 10 files changed, 177 insertions(+), 179 deletions(-) diff --git a/libraries/fbx/src/FBX.h b/libraries/fbx/src/FBX.h index 48ce994ac8..342d605337 100644 --- a/libraries/fbx/src/FBX.h +++ b/libraries/fbx/src/FBX.h @@ -13,16 +13,17 @@ #define hifi_FBX_h_ #include -#include #include #include #include +#include + // See comment in FBXSerializer::parseFBX(). static const int FBX_HEADER_BYTES_BEFORE_VERSION = 23; -static const QByteArray FBX_BINARY_PROLOG("Kaydara FBX Binary "); -static const QByteArray FBX_BINARY_PROLOG2("\0\x1a\0", 3); +static const hifi::ByteArray FBX_BINARY_PROLOG("Kaydara FBX Binary "); +static const hifi::ByteArray FBX_BINARY_PROLOG2("\0\x1a\0", 3); static const quint32 FBX_VERSION_2015 = 7400; static const quint32 FBX_VERSION_2016 = 7500; @@ -36,7 +37,7 @@ using FBXNodeList = QList; /// A node within an FBX document. class FBXNode { public: - QByteArray name; + hifi::ByteArray name; QVariantList properties; FBXNodeList children; }; diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 9e7f422b40..d5a1f9a562 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -179,7 +179,7 @@ public: void printNode(const FBXNode& node, int indentLevel) { int indentLength = 2; - QByteArray spaces(indentLevel * indentLength, ' '); + hifi::ByteArray spaces(indentLevel * indentLength, ' '); QDebug nodeDebug = qDebug(modelformat); nodeDebug.nospace() << spaces.data() << node.name.data() << ": "; @@ -309,7 +309,7 @@ public: }; bool checkMaterialsHaveTextures(const QHash& materials, - const QHash& textureFilenames, const QMultiMap& _connectionChildMap) { + const QHash& textureFilenames, const QMultiMap& _connectionChildMap) { foreach (const QString& materialID, materials.keys()) { foreach (const QString& childID, _connectionChildMap.values(materialID)) { if (textureFilenames.contains(childID)) { @@ -376,7 +376,7 @@ HFMLight extractLight(const FBXNode& object) { return light; } -QByteArray fileOnUrl(const QByteArray& filepath, const QString& url) { +hifi::ByteArray fileOnUrl(const hifi::ByteArray& filepath, const QString& url) { // in order to match the behaviour when loading models from remote URLs // we assume that all external textures are right beside the loaded model // ignoring any relative paths or absolute paths inside of models @@ -384,7 +384,7 @@ QByteArray fileOnUrl(const QByteArray& filepath, const QString& url) { return filepath.mid(filepath.lastIndexOf('/') + 1); } -HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QString& url) { +HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const QString& url) { const FBXNode& node = _rootNode; QMap meshes; QHash modelIDsToNames; @@ -407,11 +407,11 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr std::map lights; - QVariantHash blendshapeMappings = mapping.value("bs").toHash(); + hifi::VariantHash blendshapeMappings = mapping.value("bs").toHash(); - QMultiHash blendshapeIndices; + QMultiHash blendshapeIndices; for (int i = 0;; i++) { - QByteArray blendshapeName = FACESHIFT_BLENDSHAPES[i]; + hifi::ByteArray blendshapeName = FACESHIFT_BLENDSHAPES[i]; if (blendshapeName.isEmpty()) { break; } @@ -454,7 +454,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } else if (subobject.name == "Properties70") { foreach (const FBXNode& subsubobject, subobject.children) { - static const QVariant APPLICATION_NAME = QVariant(QByteArray("Original|ApplicationName")); + static const QVariant APPLICATION_NAME = QVariant(hifi::ByteArray("Original|ApplicationName")); if (subsubobject.name == "P" && subsubobject.properties.size() >= 5 && subsubobject.properties.at(0) == APPLICATION_NAME) { hfmModel.applicationName = subsubobject.properties.at(4).toString(); @@ -471,8 +471,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr int index = 4; foreach (const FBXNode& subobject, object.children) { if (subobject.name == propertyName) { - static const QVariant UNIT_SCALE_FACTOR = QByteArray("UnitScaleFactor"); - static const QVariant AMBIENT_COLOR = QByteArray("AmbientColor"); + static const QVariant UNIT_SCALE_FACTOR = hifi::ByteArray("UnitScaleFactor"); + static const QVariant AMBIENT_COLOR = hifi::ByteArray("AmbientColor"); const auto& subpropName = subobject.properties.at(0); if (subpropName == UNIT_SCALE_FACTOR) { unitScaleFactor = subobject.properties.at(index).toFloat(); @@ -528,7 +528,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr QVector blendshapes; foreach (const FBXNode& subobject, object.children) { bool properties = false; - QByteArray propertyName; + hifi::ByteArray propertyName; int index; if (subobject.name == "Properties60") { properties = true; @@ -541,27 +541,27 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr index = 4; } if (properties) { - static const QVariant ROTATION_ORDER = QByteArray("RotationOrder"); - static const QVariant GEOMETRIC_TRANSLATION = QByteArray("GeometricTranslation"); - static const QVariant GEOMETRIC_ROTATION = QByteArray("GeometricRotation"); - static const QVariant GEOMETRIC_SCALING = QByteArray("GeometricScaling"); - static const QVariant LCL_TRANSLATION = QByteArray("Lcl Translation"); - static const QVariant LCL_ROTATION = QByteArray("Lcl Rotation"); - static const QVariant LCL_SCALING = QByteArray("Lcl Scaling"); - static const QVariant ROTATION_MAX = QByteArray("RotationMax"); - static const QVariant ROTATION_MAX_X = QByteArray("RotationMaxX"); - static const QVariant ROTATION_MAX_Y = QByteArray("RotationMaxY"); - static const QVariant ROTATION_MAX_Z = QByteArray("RotationMaxZ"); - static const QVariant ROTATION_MIN = QByteArray("RotationMin"); - static const QVariant ROTATION_MIN_X = QByteArray("RotationMinX"); - static const QVariant ROTATION_MIN_Y = QByteArray("RotationMinY"); - static const QVariant ROTATION_MIN_Z = QByteArray("RotationMinZ"); - static const QVariant ROTATION_OFFSET = QByteArray("RotationOffset"); - static const QVariant ROTATION_PIVOT = QByteArray("RotationPivot"); - static const QVariant SCALING_OFFSET = QByteArray("ScalingOffset"); - static const QVariant SCALING_PIVOT = QByteArray("ScalingPivot"); - static const QVariant PRE_ROTATION = QByteArray("PreRotation"); - static const QVariant POST_ROTATION = QByteArray("PostRotation"); + static const QVariant ROTATION_ORDER = hifi::ByteArray("RotationOrder"); + static const QVariant GEOMETRIC_TRANSLATION = hifi::ByteArray("GeometricTranslation"); + static const QVariant GEOMETRIC_ROTATION = hifi::ByteArray("GeometricRotation"); + static const QVariant GEOMETRIC_SCALING = hifi::ByteArray("GeometricScaling"); + static const QVariant LCL_TRANSLATION = hifi::ByteArray("Lcl Translation"); + static const QVariant LCL_ROTATION = hifi::ByteArray("Lcl Rotation"); + static const QVariant LCL_SCALING = hifi::ByteArray("Lcl Scaling"); + static const QVariant ROTATION_MAX = hifi::ByteArray("RotationMax"); + static const QVariant ROTATION_MAX_X = hifi::ByteArray("RotationMaxX"); + static const QVariant ROTATION_MAX_Y = hifi::ByteArray("RotationMaxY"); + static const QVariant ROTATION_MAX_Z = hifi::ByteArray("RotationMaxZ"); + static const QVariant ROTATION_MIN = hifi::ByteArray("RotationMin"); + static const QVariant ROTATION_MIN_X = hifi::ByteArray("RotationMinX"); + static const QVariant ROTATION_MIN_Y = hifi::ByteArray("RotationMinY"); + static const QVariant ROTATION_MIN_Z = hifi::ByteArray("RotationMinZ"); + static const QVariant ROTATION_OFFSET = hifi::ByteArray("RotationOffset"); + static const QVariant ROTATION_PIVOT = hifi::ByteArray("RotationPivot"); + static const QVariant SCALING_OFFSET = hifi::ByteArray("ScalingOffset"); + static const QVariant SCALING_PIVOT = hifi::ByteArray("ScalingPivot"); + static const QVariant PRE_ROTATION = hifi::ByteArray("PreRotation"); + static const QVariant POST_ROTATION = hifi::ByteArray("PostRotation"); foreach(const FBXNode& property, subobject.children) { const auto& childProperty = property.properties.at(0); if (property.name == propertyName) { @@ -701,8 +701,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr const int MODEL_UV_SCALING_MIN_SIZE = 2; const int CROPPING_MIN_SIZE = 4; if (subobject.name == "RelativeFilename" && subobject.properties.length() >= RELATIVE_FILENAME_MIN_SIZE) { - QByteArray filename = subobject.properties.at(0).toByteArray(); - QByteArray filepath = filename.replace('\\', '/'); + hifi::ByteArray filename = subobject.properties.at(0).toByteArray(); + hifi::ByteArray filepath = filename.replace('\\', '/'); filename = fileOnUrl(filepath, url); _textureFilepaths.insert(getID(object.properties), filepath); _textureFilenames.insert(getID(object.properties), filename); @@ -731,17 +731,17 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr subobject.properties.at(2).value(), subobject.properties.at(3).value())); } else if (subobject.name == "Properties70") { - QByteArray propertyName; + hifi::ByteArray propertyName; int index; propertyName = "P"; index = 4; foreach (const FBXNode& property, subobject.children) { - static const QVariant UV_SET = QByteArray("UVSet"); - static const QVariant CURRENT_TEXTURE_BLEND_MODE = QByteArray("CurrentTextureBlendMode"); - static const QVariant USE_MATERIAL = QByteArray("UseMaterial"); - static const QVariant TRANSLATION = QByteArray("Translation"); - static const QVariant ROTATION = QByteArray("Rotation"); - static const QVariant SCALING = QByteArray("Scaling"); + static const QVariant UV_SET = hifi::ByteArray("UVSet"); + static const QVariant CURRENT_TEXTURE_BLEND_MODE = hifi::ByteArray("CurrentTextureBlendMode"); + static const QVariant USE_MATERIAL = hifi::ByteArray("UseMaterial"); + static const QVariant TRANSLATION = hifi::ByteArray("Translation"); + static const QVariant ROTATION = hifi::ByteArray("Rotation"); + static const QVariant SCALING = hifi::ByteArray("Scaling"); if (property.name == propertyName) { QString v = property.properties.at(0).toString(); if (property.properties.at(0) == UV_SET) { @@ -795,8 +795,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr _textureParams.insert(getID(object.properties), tex); } } else if (object.name == "Video") { - QByteArray filepath; - QByteArray content; + hifi::ByteArray filepath; + hifi::ByteArray content; foreach (const FBXNode& subobject, object.children) { if (subobject.name == "RelativeFilename") { filepath = subobject.properties.at(0).toByteArray(); @@ -816,7 +816,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr foreach (const FBXNode& subobject, object.children) { bool properties = false; - QByteArray propertyName; + hifi::ByteArray propertyName; int index; if (subobject.name == "Properties60") { properties = true; @@ -833,31 +833,31 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr if (properties) { std::vector unknowns; - static const QVariant DIFFUSE_COLOR = QByteArray("DiffuseColor"); - static const QVariant DIFFUSE_FACTOR = QByteArray("DiffuseFactor"); - static const QVariant DIFFUSE = QByteArray("Diffuse"); - static const QVariant SPECULAR_COLOR = QByteArray("SpecularColor"); - static const QVariant SPECULAR_FACTOR = QByteArray("SpecularFactor"); - static const QVariant SPECULAR = QByteArray("Specular"); - static const QVariant EMISSIVE_COLOR = QByteArray("EmissiveColor"); - static const QVariant EMISSIVE_FACTOR = QByteArray("EmissiveFactor"); - static const QVariant EMISSIVE = QByteArray("Emissive"); - static const QVariant AMBIENT_FACTOR = QByteArray("AmbientFactor"); - static const QVariant SHININESS = QByteArray("Shininess"); - static const QVariant OPACITY = QByteArray("Opacity"); - static const QVariant MAYA_USE_NORMAL_MAP = QByteArray("Maya|use_normal_map"); - static const QVariant MAYA_BASE_COLOR = QByteArray("Maya|base_color"); - static const QVariant MAYA_USE_COLOR_MAP = QByteArray("Maya|use_color_map"); - static const QVariant MAYA_ROUGHNESS = QByteArray("Maya|roughness"); - static const QVariant MAYA_USE_ROUGHNESS_MAP = QByteArray("Maya|use_roughness_map"); - static const QVariant MAYA_METALLIC = QByteArray("Maya|metallic"); - static const QVariant MAYA_USE_METALLIC_MAP = QByteArray("Maya|use_metallic_map"); - static const QVariant MAYA_EMISSIVE = QByteArray("Maya|emissive"); - static const QVariant MAYA_EMISSIVE_INTENSITY = QByteArray("Maya|emissive_intensity"); - static const QVariant MAYA_USE_EMISSIVE_MAP = QByteArray("Maya|use_emissive_map"); - static const QVariant MAYA_USE_AO_MAP = QByteArray("Maya|use_ao_map"); - static const QVariant MAYA_UV_SCALE = QByteArray("Maya|uv_scale"); - static const QVariant MAYA_UV_OFFSET = QByteArray("Maya|uv_offset"); + static const QVariant DIFFUSE_COLOR = hifi::ByteArray("DiffuseColor"); + static const QVariant DIFFUSE_FACTOR = hifi::ByteArray("DiffuseFactor"); + static const QVariant DIFFUSE = hifi::ByteArray("Diffuse"); + static const QVariant SPECULAR_COLOR = hifi::ByteArray("SpecularColor"); + static const QVariant SPECULAR_FACTOR = hifi::ByteArray("SpecularFactor"); + static const QVariant SPECULAR = hifi::ByteArray("Specular"); + static const QVariant EMISSIVE_COLOR = hifi::ByteArray("EmissiveColor"); + static const QVariant EMISSIVE_FACTOR = hifi::ByteArray("EmissiveFactor"); + static const QVariant EMISSIVE = hifi::ByteArray("Emissive"); + static const QVariant AMBIENT_FACTOR = hifi::ByteArray("AmbientFactor"); + static const QVariant SHININESS = hifi::ByteArray("Shininess"); + static const QVariant OPACITY = hifi::ByteArray("Opacity"); + static const QVariant MAYA_USE_NORMAL_MAP = hifi::ByteArray("Maya|use_normal_map"); + static const QVariant MAYA_BASE_COLOR = hifi::ByteArray("Maya|base_color"); + static const QVariant MAYA_USE_COLOR_MAP = hifi::ByteArray("Maya|use_color_map"); + static const QVariant MAYA_ROUGHNESS = hifi::ByteArray("Maya|roughness"); + static const QVariant MAYA_USE_ROUGHNESS_MAP = hifi::ByteArray("Maya|use_roughness_map"); + static const QVariant MAYA_METALLIC = hifi::ByteArray("Maya|metallic"); + static const QVariant MAYA_USE_METALLIC_MAP = hifi::ByteArray("Maya|use_metallic_map"); + static const QVariant MAYA_EMISSIVE = hifi::ByteArray("Maya|emissive"); + static const QVariant MAYA_EMISSIVE_INTENSITY = hifi::ByteArray("Maya|emissive_intensity"); + static const QVariant MAYA_USE_EMISSIVE_MAP = hifi::ByteArray("Maya|use_emissive_map"); + static const QVariant MAYA_USE_AO_MAP = hifi::ByteArray("Maya|use_ao_map"); + static const QVariant MAYA_UV_SCALE = hifi::ByteArray("Maya|uv_scale"); + static const QVariant MAYA_UV_OFFSET = hifi::ByteArray("Maya|uv_offset"); static const int MAYA_UV_OFFSET_PROPERTY_LENGTH = 6; static const int MAYA_UV_SCALE_PROPERTY_LENGTH = 6; @@ -1034,7 +1034,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr clusters.insert(getID(object.properties), cluster); } else if (object.properties.last() == "BlendShapeChannel") { - QByteArray name = object.properties.at(1).toByteArray(); + hifi::ByteArray name = object.properties.at(1).toByteArray(); name = name.left(name.indexOf('\0')); if (!blendshapeIndices.contains(name)) { @@ -1071,8 +1071,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr #endif } } else if (child.name == "Connections") { - static const QVariant OO = QByteArray("OO"); - static const QVariant OP = QByteArray("OP"); + static const QVariant OO = hifi::ByteArray("OO"); + static const QVariant OP = hifi::ByteArray("OP"); foreach (const FBXNode& connection, child.children) { if (connection.name == "C" || connection.name == "Connect") { if (connection.properties.at(0) == OO) { @@ -1091,7 +1091,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } else if (connection.properties.at(0) == OP) { int counter = 0; - QByteArray type = connection.properties.at(3).toByteArray().toLower(); + hifi::ByteArray type = connection.properties.at(3).toByteArray().toLower(); if (type.contains("DiffuseFactor")) { diffuseFactorTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if ((type.contains("diffuse") && !type.contains("tex_global_diffuse"))) { @@ -1678,8 +1678,8 @@ std::unique_ptr FBXSerializer::getFactory() const { return std::make_unique>(); } -HFMModel::Pointer FBXSerializer::read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url) { - QBuffer buffer(const_cast(&data)); +HFMModel::Pointer FBXSerializer::read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url) { + QBuffer buffer(const_cast(&data)); buffer.open(QIODevice::ReadOnly); _rootNode = parseFBX(&buffer); diff --git a/libraries/fbx/src/FBXSerializer.h b/libraries/fbx/src/FBXSerializer.h index 379b1ac743..481f2f4f63 100644 --- a/libraries/fbx/src/FBXSerializer.h +++ b/libraries/fbx/src/FBXSerializer.h @@ -15,9 +15,6 @@ #include #include #include -#include -#include -#include #include #include @@ -25,6 +22,7 @@ #include #include +#include #include "FBX.h" #include @@ -114,12 +112,12 @@ public: HFMModel* _hfmModel; /// Reads HFMModel from the supplied model and mapping data. /// \exception QString if an error occurs in parsing - HFMModel::Pointer read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url = QUrl()) override; + HFMModel::Pointer read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url = hifi::URL()) override; FBXNode _rootNode; static FBXNode parseFBX(QIODevice* device); - HFMModel* extractHFMModel(const QVariantHash& mapping, const QString& url); + HFMModel* extractHFMModel(const hifi::VariantHash& mapping, const QString& url); static ExtractedMesh extractMesh(const FBXNode& object, unsigned int& meshIndex, bool deduplicate = true); QHash meshes; @@ -128,11 +126,11 @@ public: QHash _textureNames; // Hashes the original RelativeFilename of textures - QHash _textureFilepaths; + QHash _textureFilepaths; // Hashes the place to look for textures, in case they are not inlined - QHash _textureFilenames; + QHash _textureFilenames; // Hashes texture content by filepath, in case they are inlined - QHash _textureContent; + QHash _textureContent; QHash _textureParams; diff --git a/libraries/fbx/src/FBXSerializer_Material.cpp b/libraries/fbx/src/FBXSerializer_Material.cpp index b47329e483..8b170eba1b 100644 --- a/libraries/fbx/src/FBXSerializer_Material.cpp +++ b/libraries/fbx/src/FBXSerializer_Material.cpp @@ -15,7 +15,6 @@ #include #include -#include #include #include #include @@ -29,7 +28,7 @@ HFMTexture FBXSerializer::getTexture(const QString& textureID, const QString& materialID) { HFMTexture texture; - const QByteArray& filepath = _textureFilepaths.value(textureID); + const hifi::ByteArray& filepath = _textureFilepaths.value(textureID); texture.content = _textureContent.value(filepath); if (texture.content.isEmpty()) { // the content is not inlined diff --git a/libraries/fbx/src/FBXSerializer_Mesh.cpp b/libraries/fbx/src/FBXSerializer_Mesh.cpp index fd1f80425b..f90c4bac6c 100644 --- a/libraries/fbx/src/FBXSerializer_Mesh.cpp +++ b/libraries/fbx/src/FBXSerializer_Mesh.cpp @@ -190,8 +190,8 @@ ExtractedMesh FBXSerializer::extractMesh(const FBXNode& object, unsigned int& me bool isMaterialPerPolygon = false; - static const QVariant BY_VERTICE = QByteArray("ByVertice"); - static const QVariant INDEX_TO_DIRECT = QByteArray("IndexToDirect"); + static const QVariant BY_VERTICE = hifi::ByteArray("ByVertice"); + static const QVariant INDEX_TO_DIRECT = hifi::ByteArray("IndexToDirect"); bool isDracoMesh = false; @@ -321,7 +321,7 @@ ExtractedMesh FBXSerializer::extractMesh(const FBXNode& object, unsigned int& me } } } else if (child.name == "LayerElementMaterial") { - static const QVariant BY_POLYGON = QByteArray("ByPolygon"); + static const QVariant BY_POLYGON = hifi::ByteArray("ByPolygon"); foreach (const FBXNode& subdata, child.children) { if (subdata.name == "Materials") { materials = getIntVector(subdata); @@ -348,7 +348,7 @@ ExtractedMesh FBXSerializer::extractMesh(const FBXNode& object, unsigned int& me // load the draco mesh from the FBX and create a draco::Mesh draco::Decoder decoder; draco::DecoderBuffer decodedBuffer; - QByteArray dracoArray = child.properties.at(0).value(); + hifi::ByteArray dracoArray = child.properties.at(0).value(); decodedBuffer.Init(dracoArray.data(), dracoArray.size()); std::unique_ptr dracoMesh(new draco::Mesh()); diff --git a/libraries/fbx/src/FBXSerializer_Node.cpp b/libraries/fbx/src/FBXSerializer_Node.cpp index c982dfc7cb..f9ef84c6f2 100644 --- a/libraries/fbx/src/FBXSerializer_Node.cpp +++ b/libraries/fbx/src/FBXSerializer_Node.cpp @@ -48,10 +48,10 @@ QVariant readBinaryArray(QDataStream& in, int& position) { QVector values; if ((int)QSysInfo::ByteOrder == (int)in.byteOrder()) { values.resize(arrayLength); - QByteArray arrayData; + hifi::ByteArray arrayData; if (encoding == FBX_PROPERTY_COMPRESSED_FLAG) { // preface encoded data with uncompressed length - QByteArray compressed(sizeof(quint32) + compressedLength, 0); + hifi::ByteArray compressed(sizeof(quint32) + compressedLength, 0); *((quint32*)compressed.data()) = qToBigEndian(arrayLength * sizeof(T)); in.readRawData(compressed.data() + sizeof(quint32), compressedLength); position += compressedLength; @@ -73,11 +73,11 @@ QVariant readBinaryArray(QDataStream& in, int& position) { values.reserve(arrayLength); if (encoding == FBX_PROPERTY_COMPRESSED_FLAG) { // preface encoded data with uncompressed length - QByteArray compressed(sizeof(quint32) + compressedLength, 0); + hifi::ByteArray compressed(sizeof(quint32) + compressedLength, 0); *((quint32*)compressed.data()) = qToBigEndian(arrayLength * sizeof(T)); in.readRawData(compressed.data() + sizeof(quint32), compressedLength); position += compressedLength; - QByteArray uncompressed = qUncompress(compressed); + hifi::ByteArray uncompressed = qUncompress(compressed); if (uncompressed.isEmpty()) { // answers empty byte array if corrupt throw QString("corrupt fbx file"); } @@ -234,7 +234,7 @@ public: }; int nextToken(); - const QByteArray& getDatum() const { return _datum; } + const hifi::ByteArray& getDatum() const { return _datum; } void pushBackToken(int token) { _pushedBackToken = token; } void ungetChar(char ch) { _device->ungetChar(ch); } @@ -242,7 +242,7 @@ public: private: QIODevice* _device; - QByteArray _datum; + hifi::ByteArray _datum; int _pushedBackToken; }; @@ -325,7 +325,7 @@ FBXNode parseTextFBXNode(Tokenizer& tokenizer) { expectingDatum = true; } else if (token == Tokenizer::DATUM_TOKEN && expectingDatum) { - QByteArray datum = tokenizer.getDatum(); + hifi::ByteArray datum = tokenizer.getDatum(); if ((token = tokenizer.nextToken()) == ':') { tokenizer.ungetChar(':'); tokenizer.pushBackToken(Tokenizer::DATUM_TOKEN); diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp index 736e7831c1..b5e87ad759 100755 --- a/libraries/fbx/src/GLTFSerializer.cpp +++ b/libraries/fbx/src/GLTFSerializer.cpp @@ -125,18 +125,18 @@ bool GLTFSerializer::getObjectArrayVal(const QJsonObject& object, const QString& return _defined; } -QByteArray GLTFSerializer::setGLBChunks(const QByteArray& data) { +hifi::ByteArray GLTFSerializer::setGLBChunks(const hifi::ByteArray& data) { int byte = 4; int jsonStart = data.indexOf("JSON", Qt::CaseSensitive); int binStart = data.indexOf("BIN", Qt::CaseSensitive); int jsonLength, binLength; - QByteArray jsonLengthChunk, binLengthChunk; + hifi::ByteArray jsonLengthChunk, binLengthChunk; jsonLengthChunk = data.mid(jsonStart - byte, byte); QDataStream tempJsonLen(jsonLengthChunk); tempJsonLen.setByteOrder(QDataStream::LittleEndian); tempJsonLen >> jsonLength; - QByteArray jsonChunk = data.mid(jsonStart + byte, jsonLength); + hifi::ByteArray jsonChunk = data.mid(jsonStart + byte, jsonLength); if (binStart != -1) { binLengthChunk = data.mid(binStart - byte, byte); @@ -567,10 +567,10 @@ bool GLTFSerializer::addTexture(const QJsonObject& object) { return true; } -bool GLTFSerializer::parseGLTF(const QByteArray& data) { +bool GLTFSerializer::parseGLTF(const hifi::ByteArray& data) { PROFILE_RANGE_EX(resource_parse, __FUNCTION__, 0xffff0000, nullptr); - QByteArray jsonChunk = data; + hifi::ByteArray jsonChunk = data; if (_url.toString().endsWith("glb") && data.indexOf("glTF") == 0 && data.contains("JSON")) { jsonChunk = setGLBChunks(data); @@ -734,7 +734,7 @@ glm::mat4 GLTFSerializer::getModelTransform(const GLTFNode& node) { return tmat; } -bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const QUrl& url) { +bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { //Build dependencies QVector> nodeDependencies(_file.nodes.size()); @@ -993,15 +993,15 @@ std::unique_ptr GLTFSerializer::getFactory() const { return std::make_unique>(); } -HFMModel::Pointer GLTFSerializer::read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url) { +HFMModel::Pointer GLTFSerializer::read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url) { _url = url; // Normalize url for local files - QUrl normalizeUrl = DependencyManager::get()->normalizeURL(_url); + hifi::URL normalizeUrl = DependencyManager::get()->normalizeURL(_url); if (normalizeUrl.scheme().isEmpty() || (normalizeUrl.scheme() == "file")) { QString localFileName = PathUtils::expandToLocalDataAbsolutePath(normalizeUrl).toLocalFile(); - _url = QUrl(QFileInfo(localFileName).absoluteFilePath()); + _url = hifi::URL(QFileInfo(localFileName).absoluteFilePath()); } if (parseGLTF(data)) { @@ -1019,15 +1019,15 @@ HFMModel::Pointer GLTFSerializer::read(const QByteArray& data, const QVariantHas return nullptr; } -bool GLTFSerializer::readBinary(const QString& url, QByteArray& outdata) { +bool GLTFSerializer::readBinary(const QString& url, hifi::ByteArray& outdata) { bool success; if (url.contains("data:application/octet-stream;base64,")) { outdata = requestEmbeddedData(url); success = !outdata.isEmpty(); } else { - QUrl binaryUrl = _url.resolved(url); - std::tie(success, outdata) = requestData(binaryUrl); + hifi::URL binaryUrl = _url.resolved(url); + std::tie(success, outdata) = requestData(binaryUrl); } return success; @@ -1037,16 +1037,16 @@ bool GLTFSerializer::doesResourceExist(const QString& url) { if (_url.isEmpty()) { return false; } - QUrl candidateUrl = _url.resolved(url); + hifi::URL candidateUrl = _url.resolved(url); return DependencyManager::get()->resourceExists(candidateUrl); } -std::tuple GLTFSerializer::requestData(QUrl& url) { +std::tuple GLTFSerializer::requestData(hifi::URL& url) { auto request = DependencyManager::get()->createResourceRequest( nullptr, url, true, -1, "GLTFSerializer::requestData"); if (!request) { - return std::make_tuple(false, QByteArray()); + return std::make_tuple(false, hifi::ByteArray()); } QEventLoop loop; @@ -1057,17 +1057,17 @@ std::tuple GLTFSerializer::requestData(QUrl& url) { if (request->getResult() == ResourceRequest::Success) { return std::make_tuple(true, request->getData()); } else { - return std::make_tuple(false, QByteArray()); + return std::make_tuple(false, hifi::ByteArray()); } } -QByteArray GLTFSerializer::requestEmbeddedData(const QString& url) { +hifi::ByteArray GLTFSerializer::requestEmbeddedData(const QString& url) { QString binaryUrl = url.split(",")[1]; - return binaryUrl.isEmpty() ? QByteArray() : QByteArray::fromBase64(binaryUrl.toUtf8()); + return binaryUrl.isEmpty() ? hifi::ByteArray() : QByteArray::fromBase64(binaryUrl.toUtf8()); } -QNetworkReply* GLTFSerializer::request(QUrl& url, bool isTest) { +QNetworkReply* GLTFSerializer::request(hifi::URL& url, bool isTest) { if (!qApp) { return nullptr; } @@ -1098,8 +1098,8 @@ HFMTexture GLTFSerializer::getHFMTexture(const GLTFTexture& texture) { if (texture.defined["source"]) { QString url = _file.images[texture.source].uri; - QString fname = QUrl(url).fileName(); - QUrl textureUrl = _url.resolved(url); + QString fname = hifi::URL(url).fileName(); + hifi::URL textureUrl = _url.resolved(url); qCDebug(modelformat) << "fname: " << fname; fbxtex.name = fname; fbxtex.filename = textureUrl.toEncoded(); @@ -1187,7 +1187,7 @@ void GLTFSerializer::setHFMMaterial(HFMMaterial& fbxmat, const GLTFMaterial& mat } template -bool GLTFSerializer::readArray(const QByteArray& bin, int byteOffset, int count, +bool GLTFSerializer::readArray(const hifi::ByteArray& bin, int byteOffset, int count, QVector& outarray, int accessorType) { QDataStream blobstream(bin); @@ -1244,7 +1244,7 @@ bool GLTFSerializer::readArray(const QByteArray& bin, int byteOffset, int count, return true; } template -bool GLTFSerializer::addArrayOfType(const QByteArray& bin, int byteOffset, int count, +bool GLTFSerializer::addArrayOfType(const hifi::ByteArray& bin, int byteOffset, int count, QVector& outarray, int accessorType, int componentType) { switch (componentType) { diff --git a/libraries/fbx/src/GLTFSerializer.h b/libraries/fbx/src/GLTFSerializer.h index a361e09fa6..05dc526f79 100755 --- a/libraries/fbx/src/GLTFSerializer.h +++ b/libraries/fbx/src/GLTFSerializer.h @@ -214,7 +214,7 @@ struct GLTFBufferView { struct GLTFBuffer { int byteLength; //required QString uri; - QByteArray blob; + hifi::ByteArray blob; QMap defined; void dump() { if (defined["byteLength"]) { @@ -705,16 +705,16 @@ public: MediaType getMediaType() const override; std::unique_ptr getFactory() const override; - HFMModel::Pointer read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url = QUrl()) override; + HFMModel::Pointer read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url = hifi::URL()) override; private: GLTFFile _file; - QUrl _url; - QByteArray _glbBinary; + hifi::URL _url; + hifi::ByteArray _glbBinary; glm::mat4 getModelTransform(const GLTFNode& node); - bool buildGeometry(HFMModel& hfmModel, const QUrl& url); - bool parseGLTF(const QByteArray& data); + bool buildGeometry(HFMModel& hfmModel, const hifi::URL& url); + bool parseGLTF(const hifi::ByteArray& data); bool getStringVal(const QJsonObject& object, const QString& fieldname, QString& value, QMap& defined); @@ -733,7 +733,7 @@ private: bool getObjectArrayVal(const QJsonObject& object, const QString& fieldname, QJsonArray& objects, QMap& defined); - QByteArray setGLBChunks(const QByteArray& data); + hifi::ByteArray setGLBChunks(const hifi::ByteArray& data); int getMaterialAlphaMode(const QString& type); int getAccessorType(const QString& type); @@ -760,24 +760,24 @@ private: bool addSkin(const QJsonObject& object); bool addTexture(const QJsonObject& object); - bool readBinary(const QString& url, QByteArray& outdata); + bool readBinary(const QString& url, hifi::ByteArray& outdata); template - bool readArray(const QByteArray& bin, int byteOffset, int count, + bool readArray(const hifi::ByteArray& bin, int byteOffset, int count, QVector& outarray, int accessorType); template - bool addArrayOfType(const QByteArray& bin, int byteOffset, int count, + bool addArrayOfType(const hifi::ByteArray& bin, int byteOffset, int count, QVector& outarray, int accessorType, int componentType); void retriangulate(const QVector& in_indices, const QVector& in_vertices, const QVector& in_normals, QVector& out_indices, QVector& out_vertices, QVector& out_normals); - std::tuple requestData(QUrl& url); - QByteArray requestEmbeddedData(const QString& url); + std::tuple requestData(hifi::URL& url); + hifi::ByteArray requestEmbeddedData(const QString& url); - QNetworkReply* request(QUrl& url, bool isTest); + QNetworkReply* request(hifi::URL& url, bool isTest); bool doesResourceExist(const QString& url); diff --git a/libraries/fbx/src/OBJSerializer.cpp b/libraries/fbx/src/OBJSerializer.cpp index 91d3fc7cc0..c2e9c08463 100644 --- a/libraries/fbx/src/OBJSerializer.cpp +++ b/libraries/fbx/src/OBJSerializer.cpp @@ -54,7 +54,7 @@ T& checked_at(QVector& vector, int i) { OBJTokenizer::OBJTokenizer(QIODevice* device) : _device(device), _pushedBackToken(-1) { } -const QByteArray OBJTokenizer::getLineAsDatum() { +const hifi::ByteArray OBJTokenizer::getLineAsDatum() { return _device->readLine().trimmed(); } @@ -117,7 +117,7 @@ bool OBJTokenizer::isNextTokenFloat() { if (nextToken() != OBJTokenizer::DATUM_TOKEN) { return false; } - QByteArray token = getDatum(); + hifi::ByteArray token = getDatum(); pushBackToken(OBJTokenizer::DATUM_TOKEN); bool ok; token.toFloat(&ok); @@ -182,7 +182,7 @@ void setMeshPartDefaults(HFMMeshPart& meshPart, QString materialID) { // OBJFace // NOTE (trent, 7/20/17): The vertexColors vector being passed-in isn't necessary here, but I'm just // pairing it with the vertices vector for consistency. -bool OBJFace::add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex, const QVector& vertices, const QVector& vertexColors) { +bool OBJFace::add(const hifi::ByteArray& vertexIndex, const hifi::ByteArray& textureIndex, const hifi::ByteArray& normalIndex, const QVector& vertices, const QVector& vertexColors) { bool ok; int index = vertexIndex.toInt(&ok); if (!ok) { @@ -238,11 +238,11 @@ void OBJFace::addFrom(const OBJFace* face, int index) { // add using data from f } } -bool OBJSerializer::isValidTexture(const QByteArray &filename) { +bool OBJSerializer::isValidTexture(const hifi::ByteArray &filename) { if (_url.isEmpty()) { return false; } - QUrl candidateUrl = _url.resolved(QUrl(filename)); + hifi::URL candidateUrl = _url.resolved(hifi::URL(filename)); return DependencyManager::get()->resourceExists(candidateUrl); } @@ -278,7 +278,7 @@ void OBJSerializer::parseMaterialLibrary(QIODevice* device) { #endif return; } - QByteArray token = tokenizer.getDatum(); + hifi::ByteArray token = tokenizer.getDatum(); if (token == "newmtl") { if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) { return; @@ -328,8 +328,8 @@ void OBJSerializer::parseMaterialLibrary(QIODevice* device) { } else if (token == "Ks") { currentMaterial.specularColor = tokenizer.getVec3(); } else if ((token == "map_Kd") || (token == "map_Ke") || (token == "map_Ks") || (token == "map_bump") || (token == "bump") || (token == "map_d")) { - const QByteArray textureLine = tokenizer.getLineAsDatum(); - QByteArray filename; + const hifi::ByteArray textureLine = tokenizer.getLineAsDatum(); + hifi::ByteArray filename; OBJMaterialTextureOptions textureOptions; parseTextureLine(textureLine, filename, textureOptions); if (filename.endsWith(".tga")) { @@ -354,7 +354,7 @@ void OBJSerializer::parseMaterialLibrary(QIODevice* device) { } } -void OBJSerializer::parseTextureLine(const QByteArray& textureLine, QByteArray& filename, OBJMaterialTextureOptions& textureOptions) { +void OBJSerializer::parseTextureLine(const hifi::ByteArray& textureLine, hifi::ByteArray& filename, OBJMaterialTextureOptions& textureOptions) { // Texture options reference http://paulbourke.net/dataformats/mtl/ // and https://wikivisually.com/wiki/Material_Template_Library @@ -442,12 +442,12 @@ void OBJSerializer::parseTextureLine(const QByteArray& textureLine, QByteArray& } } -std::tuple requestData(QUrl& url) { +std::tuple requestData(hifi::URL& url) { auto request = DependencyManager::get()->createResourceRequest( nullptr, url, true, -1, "(OBJSerializer) requestData"); if (!request) { - return std::make_tuple(false, QByteArray()); + return std::make_tuple(false, hifi::ByteArray()); } QEventLoop loop; @@ -458,12 +458,12 @@ std::tuple requestData(QUrl& url) { if (request->getResult() == ResourceRequest::Success) { return std::make_tuple(true, request->getData()); } else { - return std::make_tuple(false, QByteArray()); + return std::make_tuple(false, hifi::ByteArray()); } } -QNetworkReply* request(QUrl& url, bool isTest) { +QNetworkReply* request(hifi::URL& url, bool isTest) { if (!qApp) { return nullptr; } @@ -488,7 +488,7 @@ QNetworkReply* request(QUrl& url, bool isTest) { } -bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, HFMModel& hfmModel, +bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const hifi::VariantHash& mapping, HFMModel& hfmModel, float& scaleGuess, bool combineParts) { FaceGroup faces; HFMMesh& mesh = hfmModel.meshes[0]; @@ -522,7 +522,7 @@ bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& m result = false; break; } - QByteArray token = tokenizer.getDatum(); + hifi::ByteArray token = tokenizer.getDatum(); //qCDebug(modelformat) << token; // we don't support separate objects in the same file, so treat "o" the same as "g". if (token == "g" || token == "o") { @@ -535,7 +535,7 @@ bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& m if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) { break; } - QByteArray groupName = tokenizer.getDatum(); + hifi::ByteArray groupName = tokenizer.getDatum(); currentGroup = groupName; if (!combineParts) { currentMaterialName = QString("part-") + QString::number(_partCounter++); @@ -544,7 +544,7 @@ bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& m if (tokenizer.nextToken(true) != OBJTokenizer::DATUM_TOKEN) { break; } - QByteArray libraryName = tokenizer.getDatum(); + hifi::ByteArray libraryName = tokenizer.getDatum(); librariesSeen[libraryName] = true; // We'll read it later only if we actually need it. } else if (token == "usemtl") { @@ -598,14 +598,14 @@ bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& m // vertex-index // vertex-index/texture-index // vertex-index/texture-index/surface-normal-index - QByteArray token = tokenizer.getDatum(); + hifi::ByteArray token = tokenizer.getDatum(); auto firstChar = token[0]; // Tokenizer treats line endings as whitespace. Non-digit and non-negative sign indicates done; if (!isdigit(firstChar) && firstChar != '-') { tokenizer.pushBackToken(OBJTokenizer::DATUM_TOKEN); break; } - QList parts = token.split('/'); + QList parts = token.split('/'); assert(parts.count() >= 1); assert(parts.count() <= 3); // If indices are negative relative indices then adjust them to absolute indices based on current vector sizes @@ -626,7 +626,7 @@ bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& m } } } - const QByteArray noData {}; + const hifi::ByteArray noData {}; face.add(parts[0], (parts.count() > 1) ? parts[1] : noData, (parts.count() > 2) ? parts[2] : noData, vertices, vertexColors); face.groupName = currentGroup; @@ -661,9 +661,9 @@ std::unique_ptr OBJSerializer::getFactory() const { return std::make_unique>(); } -HFMModel::Pointer OBJSerializer::read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url) { +HFMModel::Pointer OBJSerializer::read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url) { PROFILE_RANGE_EX(resource_parse, __FUNCTION__, 0xffff0000, nullptr); - QBuffer buffer { const_cast(&data) }; + QBuffer buffer { const_cast(&data) }; buffer.open(QIODevice::ReadOnly); auto hfmModelPtr = std::make_shared(); @@ -849,11 +849,11 @@ HFMModel::Pointer OBJSerializer::read(const QByteArray& data, const QVariantHash int extIndex = filename.lastIndexOf('.'); // by construction, this does not fail QString basename = filename.remove(extIndex + 1, sizeof("obj")); preDefinedMaterial.diffuseColor = glm::vec3(1.0f); - QVector extensions = { "jpg", "jpeg", "png", "tga" }; - QByteArray base = basename.toUtf8(), textName = ""; + QVector extensions = { "jpg", "jpeg", "png", "tga" }; + hifi::ByteArray base = basename.toUtf8(), textName = ""; qCDebug(modelformat) << "OBJSerializer looking for default texture"; for (int i = 0; i < extensions.count(); i++) { - QByteArray candidateString = base + extensions[i]; + hifi::ByteArray candidateString = base + extensions[i]; if (isValidTexture(candidateString)) { textName = candidateString; break; @@ -871,11 +871,11 @@ HFMModel::Pointer OBJSerializer::read(const QByteArray& data, const QVariantHash if (needsMaterialLibrary) { foreach (QString libraryName, librariesSeen.keys()) { // Throw away any path part of libraryName, and merge against original url. - QUrl libraryUrl = _url.resolved(QUrl(libraryName).fileName()); + hifi::URL libraryUrl = _url.resolved(hifi::URL(libraryName).fileName()); qCDebug(modelformat) << "OBJSerializer material library" << libraryName; bool success; - QByteArray data; - std::tie(success, data) = requestData(libraryUrl); + hifi::ByteArray data; + std::tie(success, data) = requestData(libraryUrl); if (success) { QBuffer buffer { &data }; buffer.open(QIODevice::ReadOnly); diff --git a/libraries/fbx/src/OBJSerializer.h b/libraries/fbx/src/OBJSerializer.h index c4f8025e66..6fdd95e2c3 100644 --- a/libraries/fbx/src/OBJSerializer.h +++ b/libraries/fbx/src/OBJSerializer.h @@ -25,9 +25,9 @@ public: COMMENT_TOKEN = 0x101 }; int nextToken(bool allowSpaceChar = false); - const QByteArray& getDatum() const { return _datum; } + const hifi::ByteArray& getDatum() const { return _datum; } bool isNextTokenFloat(); - const QByteArray getLineAsDatum(); // some "filenames" have spaces in them + const hifi::ByteArray getLineAsDatum(); // some "filenames" have spaces in them void skipLine() { _device->readLine(); } void pushBackToken(int token) { _pushedBackToken = token; } void ungetChar(char ch) { _device->ungetChar(ch); } @@ -39,7 +39,7 @@ public: private: QIODevice* _device; - QByteArray _datum; + hifi::ByteArray _datum; int _pushedBackToken; QString _comment; }; @@ -52,7 +52,7 @@ public: QString groupName; // We don't make use of hierarchical structure, but it can be preserved for debugging and future use. QString materialName; // Add one more set of vertex data. Answers true if successful - bool add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex, + bool add(const hifi::ByteArray& vertexIndex, const hifi::ByteArray& textureIndex, const hifi::ByteArray& normalIndex, const QVector& vertices, const QVector& vertexColors); // Return a set of one or more OBJFaces from this one, in which each is just a triangle. // Even though HFMMeshPart can handle quads, it would be messy to try to keep track of mixed-size faces, so we treat everything as triangles. @@ -75,11 +75,11 @@ public: glm::vec3 diffuseColor; glm::vec3 specularColor; glm::vec3 emissiveColor; - QByteArray diffuseTextureFilename; - QByteArray specularTextureFilename; - QByteArray emissiveTextureFilename; - QByteArray bumpTextureFilename; - QByteArray opacityTextureFilename; + hifi::ByteArray diffuseTextureFilename; + hifi::ByteArray specularTextureFilename; + hifi::ByteArray emissiveTextureFilename; + hifi::ByteArray bumpTextureFilename; + hifi::ByteArray opacityTextureFilename; OBJMaterialTextureOptions bumpTextureOptions; int illuminationModel; @@ -103,17 +103,17 @@ public: QString currentMaterialName; QHash materials; - HFMModel::Pointer read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url = QUrl()) override; + HFMModel::Pointer read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url = hifi::URL()) override; private: - QUrl _url; + hifi::URL _url; - QHash librariesSeen; - bool parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, HFMModel& hfmModel, + QHash librariesSeen; + bool parseOBJGroup(OBJTokenizer& tokenizer, const hifi::VariantHash& mapping, HFMModel& hfmModel, float& scaleGuess, bool combineParts); void parseMaterialLibrary(QIODevice* device); - void parseTextureLine(const QByteArray& textureLine, QByteArray& filename, OBJMaterialTextureOptions& textureOptions); - bool isValidTexture(const QByteArray &filename); // true if the file exists. TODO?: check content-type header and that it is a supported format. + void parseTextureLine(const hifi::ByteArray& textureLine, hifi::ByteArray& filename, OBJMaterialTextureOptions& textureOptions); + bool isValidTexture(const hifi::ByteArray &filename); // true if the file exists. TODO?: check content-type header and that it is a supported format. int _partCounter { 0 }; }; From 612cf43c437a6328bc5832d58d768ee410432729 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Mon, 25 Feb 2019 13:03:23 -0800 Subject: [PATCH 041/446] Use HifiTypes.h in VHACDUtil.cpp --- tools/vhacd-util/src/VHACDUtil.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index 9401da4314..2b18c07c3a 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -42,7 +42,7 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, HFMModel& result) { return false; } try { - QByteArray fbxContents = fbx.readAll(); + hifi::ByteArray fbxContents = fbx.readAll(); HFMModel::Pointer hfmModel; if (filename.toLower().endsWith(".obj")) { hfmModel = OBJSerializer().read(fbxContents, QVariantHash(), filename); From 86c948f1165a1e2e886489b16b46b5be7dc2f0ce Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Thu, 21 Feb 2019 16:03:45 -0800 Subject: [PATCH 042/446] Convert hfmModel and materialMapping fields in model-baker Baker to getters --- libraries/model-baker/src/model-baker/Baker.cpp | 9 +++++++-- libraries/model-baker/src/model-baker/Baker.h | 4 ++-- .../model-networking/src/model-networking/ModelCache.cpp | 4 ++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp index 344af1ba8a..fc27756877 100644 --- a/libraries/model-baker/src/model-baker/Baker.cpp +++ b/libraries/model-baker/src/model-baker/Baker.cpp @@ -178,8 +178,13 @@ namespace baker { void Baker::run() { _engine->run(); - hfmModel = _engine->getOutput().get().get0(); - materialMapping = _engine->getOutput().get().get1(); } + hfm::Model::Pointer Baker::getHFMModel() const { + return _engine->getOutput().get().get0(); + } + + MaterialMapping Baker::getMaterialMapping() const { + return _engine->getOutput().get().get1(); + } }; diff --git a/libraries/model-baker/src/model-baker/Baker.h b/libraries/model-baker/src/model-baker/Baker.h index 542be0b559..1880aba618 100644 --- a/libraries/model-baker/src/model-baker/Baker.h +++ b/libraries/model-baker/src/model-baker/Baker.h @@ -28,8 +28,8 @@ namespace baker { void run(); // Outputs, available after run() is called - hfm::Model::Pointer hfmModel; - MaterialMapping materialMapping; + hfm::Model::Pointer getHFMModel() const; + MaterialMapping getMaterialMapping() const; protected: EnginePointer _engine; diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 581196b2cc..1d0032ee4c 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -341,8 +341,8 @@ void GeometryDefinitionResource::setGeometryDefinition(HFMModel::Pointer hfmMode modelBaker.run(); // Assume ownership of the processed HFMModel - _hfmModel = modelBaker.hfmModel; - _materialMapping = modelBaker.materialMapping; + _hfmModel = modelBaker.getHFMModel(); + _materialMapping = modelBaker.getMaterialMapping(); // Copy materials QHash materialIDAtlas; From 270b96aa8d3c18f453a623d5cf1508bf3f212cc3 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 20 Feb 2019 14:14:15 -0800 Subject: [PATCH 043/446] cleaning up oven --- libraries/baking/src/JSBaker.h | 3 + libraries/baking/src/ModelBaker.cpp | 5 +- libraries/baking/src/ModelBaker.h | 19 +- libraries/baking/src/baking/BakerLibrary.cpp | 50 +- libraries/baking/src/baking/BakerLibrary.h | 2 +- tools/oven/src/BakerCLI.cpp | 85 +-- tools/oven/src/DomainBaker.cpp | 518 ++++++++++++------- tools/oven/src/DomainBaker.h | 18 +- tools/oven/src/ui/ModelBakeWidget.cpp | 19 +- 9 files changed, 431 insertions(+), 288 deletions(-) diff --git a/libraries/baking/src/JSBaker.h b/libraries/baking/src/JSBaker.h index a7c3e62174..764681c71e 100644 --- a/libraries/baking/src/JSBaker.h +++ b/libraries/baking/src/JSBaker.h @@ -25,6 +25,9 @@ public: JSBaker(const QUrl& jsURL, const QString& bakedOutputDir); static bool bakeJS(const QByteArray& inputFile, QByteArray& outputFile); + QString getJSPath() const { return _jsURL.fileName(); } + QString getBakedJSFilePath() const { return _bakedJSFilePath; } + public slots: virtual void bake() override; diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index 6959a5c455..61eed9f655 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -32,11 +32,12 @@ #endif ModelBaker::ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, - const QString& bakedOutputDirectory, const QString& originalOutputDirectory) : + const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) : _modelURL(inputModelURL), _bakedOutputDir(bakedOutputDirectory), _originalOutputDir(originalOutputDirectory), - _textureThreadGetter(inputTextureThreadGetter) + _textureThreadGetter(inputTextureThreadGetter), + _hasBeenBaked(hasBeenBaked) { auto tempDir = PathUtils::generateTemporaryDir(); diff --git a/libraries/baking/src/ModelBaker.h b/libraries/baking/src/ModelBaker.h index dc9d43ad66..0f0cfbe07c 100644 --- a/libraries/baking/src/ModelBaker.h +++ b/libraries/baking/src/ModelBaker.h @@ -30,16 +30,19 @@ using TextureBakerThreadGetter = std::function; using GetMaterialIDCallback = std::function ; -static const QString BAKED_FBX_EXTENSION = ".baked.fbx"; -static const QString BAKEABLE_MODEL_FBX_EXTENSION { ".fbx" }; -static const QString BAKEABLE_MODEL_OBJ_EXTENSION { ".obj" }; +static const QString FST_EXTENSION { ".fst" }; +static const QString BAKED_FST_EXTENSION { ".baked.fst" }; +static const QString FBX_EXTENSION { ".fbx" }; +static const QString BAKED_FBX_EXTENSION { ".baked.fbx" }; +static const QString OBJ_EXTENSION { ".obj" }; +static const QString GLTF_EXTENSION { ".gltf" }; class ModelBaker : public Baker { Q_OBJECT public: ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, - const QString& bakedOutputDirectory, const QString& originalOutputDirectory = ""); + const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); virtual ~ModelBaker(); void initializeOutputDirs(); @@ -59,7 +62,7 @@ protected: void texturesFinished(); void embedTextureMetaData(); void exportScene(); - + FBXNode _rootNode; QHash _textureContentMap; QUrl _modelURL; @@ -79,12 +82,14 @@ private: void bakeTexture(const QUrl & textureURL, image::TextureUsage::Type textureType, const QDir & outputDir, const QString & bakedFilename, const QByteArray & textureContent); QString texturePathRelativeToModel(QUrl modelURL, QUrl textureURL); - + TextureBakerThreadGetter _textureThreadGetter; QMultiHash> _bakingTextures; QHash _textureNameMatchCount; QHash _remappedTexturePaths; - bool _pendingErrorEmission{ false }; + bool _pendingErrorEmission { false }; + + bool _hasBeenBaked { false }; }; #endif // hifi_ModelBaker_h diff --git a/libraries/baking/src/baking/BakerLibrary.cpp b/libraries/baking/src/baking/BakerLibrary.cpp index a587de97eb..af5e59ebbe 100644 --- a/libraries/baking/src/baking/BakerLibrary.cpp +++ b/libraries/baking/src/baking/BakerLibrary.cpp @@ -14,33 +14,26 @@ #include "../FBXBaker.h" #include "../OBJBaker.h" -QUrl getBakeableModelURL(const QUrl& url, bool shouldRebakeOriginals) { - // Check if the file pointed to by this URL is a bakeable model, by comparing extensions - auto modelFileName = url.fileName(); +// Check if the file pointed to by this URL is a bakeable model, by comparing extensions +QUrl getBakeableModelURL(const QUrl& url) { + static const std::vector extensionsToBake = { + FST_EXTENSION, + BAKED_FST_EXTENSION, + FBX_EXTENSION, + BAKED_FBX_EXTENSION, + OBJ_EXTENSION, + GLTF_EXTENSION + }; - bool isBakedModel = modelFileName.endsWith(BAKED_FBX_EXTENSION, Qt::CaseInsensitive); - bool isBakeableFBX = modelFileName.endsWith(BAKEABLE_MODEL_FBX_EXTENSION, Qt::CaseInsensitive); - bool isBakeableOBJ = modelFileName.endsWith(BAKEABLE_MODEL_OBJ_EXTENSION, Qt::CaseInsensitive); - bool isBakeable = isBakeableFBX || isBakeableOBJ; - - if (isBakeable || (shouldRebakeOriginals && isBakedModel)) { - if (isBakedModel) { - // Grab a URL to the original, that we assume is stored a directory up, in the "original" folder - // with just the fbx extension - qDebug() << "Inferring original URL for baked model URL" << url; - - auto originalFileName = modelFileName; - originalFileName.replace(".baked", ""); - qDebug() << "Original model URL must be present at" << url; - - return url.resolved("../original/" + originalFileName); - } else { - // Grab a clean version of the URL without a query or fragment - return url.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); + QUrl cleanURL = url.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); + QString cleanURLString = cleanURL.fileName(); + for (auto& extension : extensionsToBake) { + if (cleanURLString.endsWith(extension, Qt::CaseInsensitive)) { + return cleanURL; } } - qWarning() << "Unknown model type: " << modelFileName; + qWarning() << "Unknown model type: " << url.fileName(); return QUrl(); } @@ -59,11 +52,14 @@ std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, TextureB QString originalOutputDirectory = contentOutputPath + subDirName + "/original"; std::unique_ptr baker; - - if (filename.endsWith(BAKEABLE_MODEL_FBX_EXTENSION, Qt::CaseInsensitive)) { - baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory); - } else if (filename.endsWith(BAKEABLE_MODEL_OBJ_EXTENSION, Qt::CaseInsensitive)) { + if (filename.endsWith(FST_EXTENSION, Qt::CaseInsensitive)) { + //baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FST_EXTENSION, Qt::CaseInsensitive)); + } else if (filename.endsWith(FBX_EXTENSION, Qt::CaseInsensitive)) { + baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FBX_EXTENSION, Qt::CaseInsensitive)); + } else if (filename.endsWith(OBJ_EXTENSION, Qt::CaseInsensitive)) { baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory); + } else if (filename.endsWith(GLTF_EXTENSION, Qt::CaseInsensitive)) { + //baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory); } else { qDebug() << "Could not create ModelBaker for url" << bakeableModelURL; } diff --git a/libraries/baking/src/baking/BakerLibrary.h b/libraries/baking/src/baking/BakerLibrary.h index 8739b4e947..e77463b502 100644 --- a/libraries/baking/src/baking/BakerLibrary.h +++ b/libraries/baking/src/baking/BakerLibrary.h @@ -19,7 +19,7 @@ // Returns either the given model URL, or, if the model is baked and shouldRebakeOriginals is true, // the guessed location of the original model // Returns an empty URL if no bakeable URL found -QUrl getBakeableModelURL(const QUrl& url, bool shouldRebakeOriginals); +QUrl getBakeableModelURL(const QUrl& url); // Assuming the URL is valid, gets the appropriate baker for the given URL, and creates the base directory where the baker's output will later be stored // Returns an empty pointer if a baker could not be created diff --git a/tools/oven/src/BakerCLI.cpp b/tools/oven/src/BakerCLI.cpp index f4e64c3015..f5fffe6ea3 100644 --- a/tools/oven/src/BakerCLI.cpp +++ b/tools/oven/src/BakerCLI.cpp @@ -37,25 +37,16 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString& outputPath, const QString& qDebug() << "Baking file type: " << type; - static const QString MODEL_EXTENSION { "fbx" }; + static const QString MODEL_EXTENSION { "model" }; + static const QString FBX_EXTENSION { "fbx" }; // legacy + static const QString MATERIAL_EXTENSION { "material" }; static const QString SCRIPT_EXTENSION { "js" }; - // check what kind of baker we should be creating - bool isModel = type == MODEL_EXTENSION; - bool isScript = type == SCRIPT_EXTENSION; - - // If the type doesn't match the above, we assume we have a texture, and the type specified is the - // texture usage type (albedo, cubemap, normals, etc.) - auto url = inputUrl.toDisplayString(); - auto idx = url.lastIndexOf('.'); - auto extension = idx >= 0 ? url.mid(idx + 1).toLower() : ""; - bool isSupportedImage = QImageReader::supportedImageFormats().contains(extension.toLatin1()); - _outputPath = outputPath; // create our appropiate baker - if (isModel) { - QUrl bakeableModelURL = getBakeableModelURL(inputUrl, false); + if (type == MODEL_EXTENSION || type == FBX_EXTENSION) { + QUrl bakeableModelURL = getBakeableModelURL(inputUrl); if (!bakeableModelURL.isEmpty()) { auto getWorkerThreadCallback = []() -> QThread* { return Oven::instance().getNextWorkerThread(); @@ -65,35 +56,49 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString& outputPath, const QString& _baker->moveToThread(Oven::instance().getNextWorkerThread()); } } - } else if (isScript) { + } else if (type == SCRIPT_EXTENSION) { _baker = std::unique_ptr { new JSBaker(inputUrl, outputPath) }; _baker->moveToThread(Oven::instance().getNextWorkerThread()); - } else if (isSupportedImage) { - static const std::unordered_map STRING_TO_TEXTURE_USAGE_TYPE_MAP { - { "default", image::TextureUsage::DEFAULT_TEXTURE }, - { "strict", image::TextureUsage::STRICT_TEXTURE }, - { "albedo", image::TextureUsage::ALBEDO_TEXTURE }, - { "normal", image::TextureUsage::NORMAL_TEXTURE }, - { "bump", image::TextureUsage::BUMP_TEXTURE }, - { "specular", image::TextureUsage::SPECULAR_TEXTURE }, - { "metallic", image::TextureUsage::METALLIC_TEXTURE }, - { "roughness", image::TextureUsage::ROUGHNESS_TEXTURE }, - { "gloss", image::TextureUsage::GLOSS_TEXTURE }, - { "emissive", image::TextureUsage::EMISSIVE_TEXTURE }, - { "cube", image::TextureUsage::CUBE_TEXTURE }, - { "occlusion", image::TextureUsage::OCCLUSION_TEXTURE }, - { "scattering", image::TextureUsage::SCATTERING_TEXTURE }, - { "lightmap", image::TextureUsage::LIGHTMAP_TEXTURE }, - }; - - auto it = STRING_TO_TEXTURE_USAGE_TYPE_MAP.find(type); - if (it == STRING_TO_TEXTURE_USAGE_TYPE_MAP.end()) { - qCDebug(model_baking) << "Unknown texture usage type:" << type; - QCoreApplication::exit(OVEN_STATUS_CODE_FAIL); - } - _baker = std::unique_ptr { new TextureBaker(inputUrl, it->second, outputPath) }; - _baker->moveToThread(Oven::instance().getNextWorkerThread()); + } else if (type == MATERIAL_EXTENSION) { + //_baker = std::unique_ptr { new MaterialBaker(inputUrl, outputPath) }; + //_baker->moveToThread(Oven::instance().getNextWorkerThread()); } else { + // If the type doesn't match the above, we assume we have a texture, and the type specified is the + // texture usage type (albedo, cubemap, normals, etc.) + auto url = inputUrl.toDisplayString(); + auto idx = url.lastIndexOf('.'); + auto extension = idx >= 0 ? url.mid(idx + 1).toLower() : ""; + + if (QImageReader::supportedImageFormats().contains(extension.toLatin1())) { + static const std::unordered_map STRING_TO_TEXTURE_USAGE_TYPE_MAP { + { "default", image::TextureUsage::DEFAULT_TEXTURE }, + { "strict", image::TextureUsage::STRICT_TEXTURE }, + { "albedo", image::TextureUsage::ALBEDO_TEXTURE }, + { "normal", image::TextureUsage::NORMAL_TEXTURE }, + { "bump", image::TextureUsage::BUMP_TEXTURE }, + { "specular", image::TextureUsage::SPECULAR_TEXTURE }, + { "metallic", image::TextureUsage::METALLIC_TEXTURE }, + { "roughness", image::TextureUsage::ROUGHNESS_TEXTURE }, + { "gloss", image::TextureUsage::GLOSS_TEXTURE }, + { "emissive", image::TextureUsage::EMISSIVE_TEXTURE }, + { "cube", image::TextureUsage::CUBE_TEXTURE }, + { "skybox", image::TextureUsage::CUBE_TEXTURE }, + { "occlusion", image::TextureUsage::OCCLUSION_TEXTURE }, + { "scattering", image::TextureUsage::SCATTERING_TEXTURE }, + { "lightmap", image::TextureUsage::LIGHTMAP_TEXTURE }, + }; + + auto it = STRING_TO_TEXTURE_USAGE_TYPE_MAP.find(type); + if (it == STRING_TO_TEXTURE_USAGE_TYPE_MAP.end()) { + qCDebug(model_baking) << "Unknown texture usage type:" << type; + QCoreApplication::exit(OVEN_STATUS_CODE_FAIL); + } + _baker = std::unique_ptr { new TextureBaker(inputUrl, it->second, outputPath) }; + _baker->moveToThread(Oven::instance().getNextWorkerThread()); + } + } + + if (!_baker) { qCDebug(model_baking) << "Failed to determine baker type for file" << inputUrl; QCoreApplication::exit(OVEN_STATUS_CODE_FAIL); return; diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 11a38f2f24..3c2f1d77bb 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -27,8 +27,7 @@ DomainBaker::DomainBaker(const QUrl& localModelFileURL, const QString& domainNam bool shouldRebakeOriginals) : _localEntitiesFileURL(localModelFileURL), _domainName(domainName), - _baseOutputPath(baseOutputPath), - _shouldRebakeOriginals(shouldRebakeOriginals) + _baseOutputPath(baseOutputPath) { // make sure the destination path has a trailing slash if (!destinationPath.toString().endsWith('/')) { @@ -145,11 +144,139 @@ void DomainBaker::loadLocalFile() { } } -const QString ENTITY_MODEL_URL_KEY = "modelURL"; -const QString ENTITY_SKYBOX_KEY = "skybox"; -const QString ENTITY_SKYBOX_URL_KEY = "url"; -const QString ENTITY_KEYLIGHT_KEY = "keyLight"; -const QString ENTITY_KEYLIGHT_AMBIENT_URL_KEY = "ambientURL"; +void DomainBaker::addModelBaker(const QString& property, const QString& url, QJsonValueRef& jsonRef) { + // grab a QUrl for the model URL + QUrl bakeableModelURL = getBakeableModelURL(url); + if (!bakeableModelURL.isEmpty()) { + // setup a ModelBaker for this URL, as long as we don't already have one + if (!_modelBakers.contains(bakeableModelURL)) { + auto getWorkerThreadCallback = []() -> QThread* { + return Oven::instance().getNextWorkerThread(); + }; + QSharedPointer baker = QSharedPointer(getModelBaker(bakeableModelURL, getWorkerThreadCallback, _contentOutputPath).release(), &ModelBaker::deleteLater); + if (baker) { + // make sure our handler is called when the baker is done + connect(baker.data(), &Baker::finished, this, &DomainBaker::handleFinishedModelBaker); + + // insert it into our bakers hash so we hold a strong pointer to it + _modelBakers.insert(bakeableModelURL, baker); + + // move the baker to the baker thread + // and kickoff the bake + baker->moveToThread(Oven::instance().getNextWorkerThread()); + QMetaObject::invokeMethod(baker.data(), "bake"); + + // keep track of the total number of baking entities + ++_totalNumberOfSubBakes; + } + } + + // add this QJsonValueRef to our multi hash so that we can easily re-write + // the model URL to the baked version once the baker is complete + _entitiesNeedingRewrite.insert(bakeableModelURL, { property, jsonRef }); + } +} + +void DomainBaker::addTextureBaker(const QString& property, const QString& url, image::TextureUsage::Type type, QJsonValueRef& jsonRef) { + auto idx = url.lastIndexOf('.'); + auto extension = idx >= 0 ? url.mid(idx + 1).toLower() : ""; + + if (QImageReader::supportedImageFormats().contains(extension.toLatin1())) { + // grab a clean version of the URL without a query or fragment + QUrl textureURL = QUrl(url).adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); + + // setup a texture baker for this URL, as long as we aren't baking a texture already + if (!_textureBakers.contains(textureURL)) { + // setup a baker for this texture + + QSharedPointer textureBaker { + new TextureBaker(textureURL, type, _contentOutputPath), + &TextureBaker::deleteLater + }; + + // make sure our handler is called when the texture baker is done + connect(textureBaker.data(), &TextureBaker::finished, this, &DomainBaker::handleFinishedTextureBaker); + + // insert it into our bakers hash so we hold a strong pointer to it + _textureBakers.insert(textureURL, textureBaker); + + // move the baker to a worker thread and kickoff the bake + textureBaker->moveToThread(Oven::instance().getNextWorkerThread()); + QMetaObject::invokeMethod(textureBaker.data(), "bake"); + + // keep track of the total number of baking entities + ++_totalNumberOfSubBakes; + } + + // add this QJsonValueRef to our multi hash so that it can re-write the texture URL + // to the baked version once the baker is complete + _entitiesNeedingRewrite.insert(textureURL, { property, jsonRef }); + } +} + +void DomainBaker::addScriptBaker(const QString& property, const QString& url, QJsonValueRef& jsonRef) { + // grab a clean version of the URL without a query or fragment + QUrl scriptURL = QUrl(url).adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); + + // setup a texture baker for this URL, as long as we aren't baking a texture already + if (!_scriptBakers.contains(scriptURL)) { + // setup a baker for this texture + + QSharedPointer scriptBaker { + new JSBaker(scriptURL, _contentOutputPath), + &JSBaker::deleteLater + }; + + // make sure our handler is called when the texture baker is done + connect(scriptBaker.data(), &JSBaker::finished, this, &DomainBaker::handleFinishedScriptBaker); + + // insert it into our bakers hash so we hold a strong pointer to it + _scriptBakers.insert(scriptURL, scriptBaker); + + // move the baker to a worker thread and kickoff the bake + scriptBaker->moveToThread(Oven::instance().getNextWorkerThread()); + QMetaObject::invokeMethod(scriptBaker.data(), "bake"); + + // keep track of the total number of baking entities + ++_totalNumberOfSubBakes; + } + + // add this QJsonValueRef to our multi hash so that it can re-write the texture URL + // to the baked version once the baker is complete + _entitiesNeedingRewrite.insert(scriptURL, { property, jsonRef }); +} + +// All the Entity Properties that can be baked +// *************************************************************************************** + +// Models +const QString MODEL_URL_KEY = "modelURL"; +const QString COMPOUND_SHAPE_URL_KEY = "compoundShapeURL"; +const QString GRAP_KEY = "grab"; +const QString EQUIPPABLE_INDICATOR_URL_KEY = "equippableIndicatorURL"; +const QString ANIMATION_KEY = "animation"; +const QString ANIMATION_URL_KEY = "url"; + +// Textures +const QString TEXTURES_KEY = "textures"; +const QString IMAGE_URL_KEY = "imageURL"; +const QString X_TEXTURE_URL_KEY = "xTextureURL"; +const QString Y_TEXTURE_URL_KEY = "yTextureURL"; +const QString Z_TEXTURE_URL_KEY = "zTextureURL"; +const QString AMBIENT_LIGHT_KEY = "ambientLight"; +const QString AMBIENT_URL_KEY = "ambientURL"; +const QString SKYBOX_KEY = "skybox"; +const QString SKYBOX_URL_KEY = "url"; + +// Scripts +const QString SCRIPT_KEY = "script"; +const QString SERVER_SCRIPTS_KEY = "serverScripts"; + +// Materials +const QString MATERIAL_URL_KEY = "materialURL"; +const QString MATERIAL_DATA_KEY = "materialData"; + +// *************************************************************************************** void DomainBaker::enumerateEntities() { qDebug() << "Enumerating" << _entities.size() << "entities from domain"; @@ -159,65 +286,65 @@ void DomainBaker::enumerateEntities() { if (it->isObject()) { auto entity = it->toObject(); - // check if this is an entity with a model URL or is a skybox texture - if (entity.contains(ENTITY_MODEL_URL_KEY)) { - // grab a QUrl for the model URL - QUrl bakeableModelURL = getBakeableModelURL(entity[ENTITY_MODEL_URL_KEY].toString(), _shouldRebakeOriginals); - - if (!bakeableModelURL.isEmpty()) { - - // setup a ModelBaker for this URL, as long as we don't already have one - if (!_modelBakers.contains(bakeableModelURL)) { - auto getWorkerThreadCallback = []() -> QThread* { - return Oven::instance().getNextWorkerThread(); - }; - QSharedPointer baker = QSharedPointer(getModelBaker(bakeableModelURL, getWorkerThreadCallback, _contentOutputPath).release(), &ModelBaker::deleteLater); - if (baker) { - // make sure our handler is called when the baker is done - connect(baker.data(), &Baker::finished, this, &DomainBaker::handleFinishedModelBaker); - - // insert it into our bakers hash so we hold a strong pointer to it - _modelBakers.insert(bakeableModelURL, baker); - - // move the baker to the baker thread - // and kickoff the bake - baker->moveToThread(Oven::instance().getNextWorkerThread()); - QMetaObject::invokeMethod(baker.data(), "bake"); - - // keep track of the total number of baking entities - ++_totalNumberOfSubBakes; - - // add this QJsonValueRef to our multi hash so that we can easily re-write - // the model URL to the baked version once the baker is complete - _entitiesNeedingRewrite.insert(bakeableModelURL, *it); - } - - } - } - } else { -// // We check now to see if we have either a texture for a skybox or a keylight, or both. -// if (entity.contains(ENTITY_SKYBOX_KEY)) { -// auto skyboxObject = entity[ENTITY_SKYBOX_KEY].toObject(); -// if (skyboxObject.contains(ENTITY_SKYBOX_URL_KEY)) { -// // we have a URL to a skybox, grab it -// QUrl skyboxURL { skyboxObject[ENTITY_SKYBOX_URL_KEY].toString() }; -// -// // setup a bake of the skybox -// bakeSkybox(skyboxURL, *it); -// } -// } -// -// if (entity.contains(ENTITY_KEYLIGHT_KEY)) { -// auto keyLightObject = entity[ENTITY_KEYLIGHT_KEY].toObject(); -// if (keyLightObject.contains(ENTITY_KEYLIGHT_AMBIENT_URL_KEY)) { -// // we have a URL to a skybox, grab it -// QUrl skyboxURL { keyLightObject[ENTITY_KEYLIGHT_AMBIENT_URL_KEY].toString() }; -// -// // setup a bake of the skybox -// bakeSkybox(skyboxURL, *it); -// } -// } + // Models + if (entity.contains(MODEL_URL_KEY)) { + addModelBaker(MODEL_URL_KEY, entity[MODEL_URL_KEY].toString(), *it); } + if (entity.contains(COMPOUND_SHAPE_URL_KEY)) { + // TODO: handle compoundShapeURL + } + if (entity.contains(ANIMATION_KEY)) { + auto animationObject = entity[ANIMATION_KEY].toObject(); + if (animationObject.contains(ANIMATION_URL_KEY)) { + addModelBaker(ANIMATION_KEY + "." + ANIMATION_URL_KEY, animationObject[ANIMATION_URL_KEY].toString(), *it); + } + } + if (entity.contains(GRAP_KEY)) { + auto grabObject = entity[GRAP_KEY].toObject(); + if (grabObject.contains(EQUIPPABLE_INDICATOR_URL_KEY)) { + addModelBaker(GRAP_KEY + "." + EQUIPPABLE_INDICATOR_URL_KEY, grabObject[EQUIPPABLE_INDICATOR_URL_KEY].toString(), *it); + } + } + + // Textures + if (entity.contains(TEXTURES_KEY)) { + // TODO: the textures property is treated differently for different entity types + } + if (entity.contains(IMAGE_URL_KEY)) { + addTextureBaker(IMAGE_URL_KEY, entity[IMAGE_URL_KEY].toString(), image::TextureUsage::DEFAULT_TEXTURE, *it); + } + if (entity.contains(X_TEXTURE_URL_KEY)) { + addTextureBaker(X_TEXTURE_URL_KEY, entity[X_TEXTURE_URL_KEY].toString(), image::TextureUsage::DEFAULT_TEXTURE, *it); + } + if (entity.contains(Y_TEXTURE_URL_KEY)) { + addTextureBaker(Y_TEXTURE_URL_KEY, entity[Y_TEXTURE_URL_KEY].toString(), image::TextureUsage::DEFAULT_TEXTURE, *it); + } + if (entity.contains(Z_TEXTURE_URL_KEY)) { + addTextureBaker(Z_TEXTURE_URL_KEY, entity[Z_TEXTURE_URL_KEY].toString(), image::TextureUsage::DEFAULT_TEXTURE, *it); + } + if (entity.contains(AMBIENT_LIGHT_KEY)) { + auto ambientLight = entity[AMBIENT_LIGHT_KEY].toObject(); + if (ambientLight.contains(AMBIENT_URL_KEY)) { + addTextureBaker(AMBIENT_LIGHT_KEY + "." + AMBIENT_URL_KEY, ambientLight[AMBIENT_URL_KEY].toString(), image::TextureUsage::CUBE_TEXTURE, *it); + } + } + if (entity.contains(SKYBOX_KEY)) { + auto skybox = entity[SKYBOX_KEY].toObject(); + if (skybox.contains(SKYBOX_URL_KEY)) { + addTextureBaker(SKYBOX_KEY + "." + SKYBOX_URL_KEY, skybox[SKYBOX_URL_KEY].toString(), image::TextureUsage::CUBE_TEXTURE, *it); + } + } + + // Scripts + if (entity.contains(SCRIPT_KEY)) { + addScriptBaker(SCRIPT_KEY, entity[SCRIPT_KEY].toString(), *it); + } + if (entity.contains(SERVER_SCRIPTS_KEY)) { + // TODO: serverScripts can be multiple scripts, need to handle that + } + + // Materials + // TODO } } @@ -225,48 +352,6 @@ void DomainBaker::enumerateEntities() { emit bakeProgress(0, _totalNumberOfSubBakes); } -void DomainBaker::bakeSkybox(QUrl skyboxURL, QJsonValueRef entity) { - - auto skyboxFileName = skyboxURL.fileName(); - - static const QStringList BAKEABLE_SKYBOX_EXTENSIONS { - ".jpg", ".png", ".gif", ".bmp", ".pbm", ".pgm", ".ppm", ".xbm", ".xpm", ".svg" - }; - auto completeLowerExtension = skyboxFileName.mid(skyboxFileName.indexOf('.')).toLower(); - - if (BAKEABLE_SKYBOX_EXTENSIONS.contains(completeLowerExtension)) { - // grab a clean version of the URL without a query or fragment - skyboxURL = skyboxURL.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); - - // setup a texture baker for this URL, as long as we aren't baking a skybox already - if (!_skyboxBakers.contains(skyboxURL)) { - // setup a baker for this skybox - - QSharedPointer skyboxBaker { - new TextureBaker(skyboxURL, image::TextureUsage::CUBE_TEXTURE, _contentOutputPath), - &TextureBaker::deleteLater - }; - - // make sure our handler is called when the skybox baker is done - connect(skyboxBaker.data(), &TextureBaker::finished, this, &DomainBaker::handleFinishedSkyboxBaker); - - // insert it into our bakers hash so we hold a strong pointer to it - _skyboxBakers.insert(skyboxURL, skyboxBaker); - - // move the baker to a worker thread and kickoff the bake - skyboxBaker->moveToThread(Oven::instance().getNextWorkerThread()); - QMetaObject::invokeMethod(skyboxBaker.data(), "bake"); - - // keep track of the total number of baking entities - ++_totalNumberOfSubBakes; - } - - // add this QJsonValueRef to our multi hash so that it can re-write the skybox URL - // to the baked version once the baker is complete - _entitiesNeedingRewrite.insert(skyboxURL, entity); - } -} - void DomainBaker::handleFinishedModelBaker() { auto baker = qobject_cast(sender()); @@ -275,62 +360,51 @@ void DomainBaker::handleFinishedModelBaker() { // this FBXBaker is done and everything went according to plan qDebug() << "Re-writing entity references to" << baker->getModelURL(); - // enumerate the QJsonRef values for the URL of this FBX from our multi hash of + // setup a new URL using the prefix we were passed + auto relativeFBXFilePath = baker->getBakedModelFilePath().remove(_contentOutputPath); + if (relativeFBXFilePath.startsWith("/")) { + relativeFBXFilePath = relativeFBXFilePath.right(relativeFBXFilePath.length() - 1); + } + QUrl newURL = _destinationPath.resolved(relativeFBXFilePath); + + // enumerate the QJsonRef values for the URL of this model from our multi hash of // entity objects needing a URL re-write - for (QJsonValueRef entityValue : _entitiesNeedingRewrite.values(baker->getModelURL())) { - + for (auto propertyEntityPair : _entitiesNeedingRewrite.values(baker->getModelURL())) { + QString property = propertyEntityPair.first; // convert the entity QJsonValueRef to a QJsonObject so we can modify its URL - auto entity = entityValue.toObject(); + auto entity = propertyEntityPair.second.toObject(); - // grab the old URL - QUrl oldModelURL { entity[ENTITY_MODEL_URL_KEY].toString() }; + if (!property.contains(".")) { + // grab the old URL + QUrl oldURL = entity[property].toString(); - // setup a new URL using the prefix we were passed - auto relativeFBXFilePath = baker->getBakedModelFilePath().remove(_contentOutputPath); - if (relativeFBXFilePath.startsWith("/")) { - relativeFBXFilePath = relativeFBXFilePath.right(relativeFBXFilePath.length() - 1); + // copy the fragment and query, and user info from the old model URL + newURL.setQuery(oldURL.query()); + newURL.setFragment(oldURL.fragment()); + newURL.setUserInfo(oldURL.userInfo()); + + // set the new URL as the value in our temp QJsonObject + entity[property] = newURL.toString(); + } else { + // Group property + QStringList propertySplit = property.split("."); + assert(propertySplit.length() == 2); + // grab the old URL + auto oldObject = entity[propertySplit[0]].toObject(); + QUrl oldURL = oldObject[propertySplit[1]].toString(); + + // copy the fragment and query, and user info from the old model URL + newURL.setQuery(oldURL.query()); + newURL.setFragment(oldURL.fragment()); + newURL.setUserInfo(oldURL.userInfo()); + + // set the new URL as the value in our temp QJsonObject + oldObject[propertySplit[1]] = newURL.toString(); + entity[propertySplit[0]] = oldObject; } - QUrl newModelURL = _destinationPath.resolved(relativeFBXFilePath); - // copy the fragment and query, and user info from the old model URL - newModelURL.setQuery(oldModelURL.query()); - newModelURL.setFragment(oldModelURL.fragment()); - newModelURL.setUserInfo(oldModelURL.userInfo()); - - // set the new model URL as the value in our temp QJsonObject - entity[ENTITY_MODEL_URL_KEY] = newModelURL.toString(); - - // check if the entity also had an animation at the same URL - // in which case it should be replaced with our baked model URL too - const QString ENTITY_ANIMATION_KEY = "animation"; - const QString ENTITIY_ANIMATION_URL_KEY = "url"; - - if (entity.contains(ENTITY_ANIMATION_KEY)) { - auto animationObject = entity[ENTITY_ANIMATION_KEY].toObject(); - - if (animationObject.contains(ENTITIY_ANIMATION_URL_KEY)) { - // grab the old animation URL - QUrl oldAnimationURL { animationObject[ENTITIY_ANIMATION_URL_KEY].toString() }; - - // check if its stripped down version matches our stripped down model URL - if (oldAnimationURL.matches(oldModelURL, QUrl::RemoveQuery | QUrl::RemoveFragment)) { - // the animation URL matched the old model URL, so make the animation URL point to the baked FBX - // with its original query and fragment - auto newAnimationURL = _destinationPath.resolved(relativeFBXFilePath); - newAnimationURL.setQuery(oldAnimationURL.query()); - newAnimationURL.setFragment(oldAnimationURL.fragment()); - newAnimationURL.setUserInfo(oldAnimationURL.userInfo()); - - animationObject[ENTITIY_ANIMATION_URL_KEY] = newAnimationURL.toString(); - - // replace the animation object in the entity object - entity[ENTITY_ANIMATION_KEY] = animationObject; - } - } - } - // replace our temp object with the value referenced by our QJsonValueRef - entityValue = entity; + propertyEntityPair.second = entity; } } else { // this model failed to bake - this doesn't fail the entire bake but we need to add @@ -352,7 +426,7 @@ void DomainBaker::handleFinishedModelBaker() { } } -void DomainBaker::handleFinishedSkyboxBaker() { +void DomainBaker::handleFinishedTextureBaker() { auto baker = qobject_cast(sender()); if (baker) { @@ -360,36 +434,46 @@ void DomainBaker::handleFinishedSkyboxBaker() { // this FBXBaker is done and everything went according to plan qDebug() << "Re-writing entity references to" << baker->getTextureURL(); - // enumerate the QJsonRef values for the URL of this FBX from our multi hash of + auto newURL = _destinationPath.resolved(baker->getMetaTextureFileName()); + + // enumerate the QJsonRef values for the URL of this texture from our multi hash of // entity objects needing a URL re-write - for (QJsonValueRef entityValue : _entitiesNeedingRewrite.values(baker->getTextureURL())) { + for (auto propertyEntityPair : _entitiesNeedingRewrite.values(baker->getTextureURL())) { + QString property = propertyEntityPair.first; // convert the entity QJsonValueRef to a QJsonObject so we can modify its URL - auto entity = entityValue.toObject(); + auto entity = propertyEntityPair.second.toObject(); - if (entity.contains(ENTITY_SKYBOX_KEY)) { - auto skyboxObject = entity[ENTITY_SKYBOX_KEY].toObject(); + if (!property.contains(".")) { + // grab the old URL + QUrl oldURL = entity[property].toString(); - if (skyboxObject.contains(ENTITY_SKYBOX_URL_KEY)) { - if (rewriteSkyboxURL(skyboxObject[ENTITY_SKYBOX_URL_KEY], baker)) { - // we re-wrote the URL, replace the skybox object referenced by the entity object - entity[ENTITY_SKYBOX_KEY] = skyboxObject; - } - } - } + // copy the fragment and query, and user info from the old model URL + newURL.setQuery(oldURL.query()); + newURL.setFragment(oldURL.fragment()); + newURL.setUserInfo(oldURL.userInfo()); - if (entity.contains(ENTITY_KEYLIGHT_KEY)) { - auto ambientObject = entity[ENTITY_KEYLIGHT_KEY].toObject(); + // set the new URL as the value in our temp QJsonObject + entity[property] = newURL.toString(); + } else { + // Group property + QStringList propertySplit = property.split("."); + assert(propertySplit.length() == 2); + // grab the old URL + auto oldObject = entity[propertySplit[0]].toObject(); + QUrl oldURL = oldObject[propertySplit[1]].toString(); - if (ambientObject.contains(ENTITY_KEYLIGHT_AMBIENT_URL_KEY)) { - if (rewriteSkyboxURL(ambientObject[ENTITY_KEYLIGHT_AMBIENT_URL_KEY], baker)) { - // we re-wrote the URL, replace the ambient object referenced by the entity object - entity[ENTITY_KEYLIGHT_KEY] = ambientObject; - } - } + // copy the fragment and query, and user info from the old model URL + newURL.setQuery(oldURL.query()); + newURL.setFragment(oldURL.fragment()); + newURL.setUserInfo(oldURL.userInfo()); + + // set the new URL as the value in our temp QJsonObject + oldObject[propertySplit[1]] = newURL.toString(); + entity[propertySplit[0]] = oldObject; } // replace our temp object with the value referenced by our QJsonValueRef - entityValue = entity; + propertyEntityPair.second = entity; } } else { // this skybox failed to bake - this doesn't fail the entire bake but we need to add the errors from @@ -401,7 +485,7 @@ void DomainBaker::handleFinishedSkyboxBaker() { _entitiesNeedingRewrite.remove(baker->getTextureURL()); // drop our shared pointer to this baker so that it gets cleaned up - _skyboxBakers.remove(baker->getTextureURL()); + _textureBakers.remove(baker->getTextureURL()); // emit progress to tell listeners how many models we have baked emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes); @@ -411,23 +495,72 @@ void DomainBaker::handleFinishedSkyboxBaker() { } } -bool DomainBaker::rewriteSkyboxURL(QJsonValueRef urlValue, TextureBaker* baker) { - // grab the old skybox URL - QUrl oldSkyboxURL { urlValue.toString() }; +void DomainBaker::handleFinishedScriptBaker() { + auto baker = qobject_cast(sender()); - if (oldSkyboxURL.matches(baker->getTextureURL(), QUrl::RemoveQuery | QUrl::RemoveFragment)) { - // change the URL to point to the baked texture with its original query and fragment + if (baker) { + if (!baker->hasErrors()) { + // this FBXBaker is done and everything went according to plan + qDebug() << "Re-writing entity references to" << baker->getJSPath(); - auto newSkyboxURL = _destinationPath.resolved(baker->getMetaTextureFileName()); - newSkyboxURL.setQuery(oldSkyboxURL.query()); - newSkyboxURL.setFragment(oldSkyboxURL.fragment()); - newSkyboxURL.setUserInfo(oldSkyboxURL.userInfo()); + auto newURL = _destinationPath.resolved(baker->getBakedJSFilePath()); - urlValue = newSkyboxURL.toString(); + // enumerate the QJsonRef values for the URL of this script from our multi hash of + // entity objects needing a URL re-write + for (auto propertyEntityPair : _entitiesNeedingRewrite.values(baker->getJSPath())) { + QString property = propertyEntityPair.first; + // convert the entity QJsonValueRef to a QJsonObject so we can modify its URL + auto entity = propertyEntityPair.second.toObject(); - return true; - } else { - return false; + if (!property.contains(".")) { + // grab the old URL + QUrl oldURL = entity[property].toString(); + + // copy the fragment and query, and user info from the old model URL + newURL.setQuery(oldURL.query()); + newURL.setFragment(oldURL.fragment()); + newURL.setUserInfo(oldURL.userInfo()); + + // set the new URL as the value in our temp QJsonObject + entity[property] = newURL.toString(); + } else { + // Group property + QStringList propertySplit = property.split("."); + assert(propertySplit.length() == 2); + // grab the old URL + auto oldObject = entity[propertySplit[0]].toObject(); + QUrl oldURL = oldObject[propertySplit[1]].toString(); + + // copy the fragment and query, and user info from the old model URL + newURL.setQuery(oldURL.query()); + newURL.setFragment(oldURL.fragment()); + newURL.setUserInfo(oldURL.userInfo()); + + // set the new URL as the value in our temp QJsonObject + oldObject[propertySplit[1]] = newURL.toString(); + entity[propertySplit[0]] = oldObject; + } + + // replace our temp object with the value referenced by our QJsonValueRef + propertyEntityPair.second = entity; + } + } else { + // this model failed to bake - this doesn't fail the entire bake but we need to add + // the errors from the model to our warnings + _warningList << baker->getErrors(); + } + + // remove the baked URL from the multi hash of entities needing a re-write + _entitiesNeedingRewrite.remove(baker->getJSPath()); + + // drop our shared pointer to this baker so that it gets cleaned up + _scriptBakers.remove(baker->getJSPath()); + + // emit progress to tell listeners how many models we have baked + emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes); + + // check if this was the last model we needed to re-write and if we are done now + checkIfRewritingComplete(); } } @@ -480,4 +613,3 @@ void DomainBaker::writeNewEntitiesFile() { qDebug() << "Exported entities file with baked model URLs to" << bakedEntitiesFilePath; } - diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h index e0286a51ff..2a5abb4ca6 100644 --- a/tools/oven/src/DomainBaker.h +++ b/tools/oven/src/DomainBaker.h @@ -18,8 +18,9 @@ #include #include "Baker.h" -#include "FBXBaker.h" +#include "ModelBaker.h" #include "TextureBaker.h" +#include "JSBaker.h" class DomainBaker : public Baker { Q_OBJECT @@ -38,7 +39,8 @@ signals: private slots: virtual void bake() override; void handleFinishedModelBaker(); - void handleFinishedSkyboxBaker(); + void handleFinishedTextureBaker(); + void handleFinishedScriptBaker(); private: void setupOutputFolder(); @@ -47,9 +49,6 @@ private: void checkIfRewritingComplete(); void writeNewEntitiesFile(); - void bakeSkybox(QUrl skyboxURL, QJsonValueRef entity); - bool rewriteSkyboxURL(QJsonValueRef urlValue, TextureBaker* baker); - QUrl _localEntitiesFileURL; QString _domainName; QString _baseOutputPath; @@ -62,14 +61,17 @@ private: QJsonArray _entities; QHash> _modelBakers; - QHash> _skyboxBakers; + QHash> _textureBakers; + QHash> _scriptBakers; - QMultiHash _entitiesNeedingRewrite; + QMultiHash> _entitiesNeedingRewrite; int _totalNumberOfSubBakes { 0 }; int _completedSubBakes { 0 }; - bool _shouldRebakeOriginals { false }; + void addModelBaker(const QString& property, const QString& url, QJsonValueRef& jsonRef); + void addTextureBaker(const QString& property, const QString& url, image::TextureUsage::Type type, QJsonValueRef& jsonRef); + void addScriptBaker(const QString& property, const QString& url, QJsonValueRef& jsonRef); }; #endif // hifi_DomainBaker_h diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp index 5ac9b43348..8f8e068b50 100644 --- a/tools/oven/src/ui/ModelBakeWidget.cpp +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -116,7 +116,7 @@ void ModelBakeWidget::chooseFileButtonClicked() { startDir = QDir::homePath(); } - auto selectedFiles = QFileDialog::getOpenFileNames(this, "Choose Model", startDir, "Models (*.fbx *.obj)"); + auto selectedFiles = QFileDialog::getOpenFileNames(this, "Choose Model", startDir, "Models (*.fbx *.obj *.gltf *.fst)"); if (!selectedFiles.isEmpty()) { // set the contents of the model file text box to be the path to the selected file @@ -165,21 +165,20 @@ void ModelBakeWidget::bakeButtonClicked() { return; } + // make sure we have a valid output directory + QDir outputDirectory(_outputDirLineEdit->text()); + if (!outputDirectory.exists()) { + QMessageBox::warning(this, "Unable to create directory", "Unable to create output directory. Please create it manually or choose a different directory."); + return; + } + // split the list from the model line edit to see how many models we need to bake auto fileURLStrings = _modelLineEdit->text().split(','); foreach (QString fileURLString, fileURLStrings) { // construct a URL from the path in the model file text box QUrl modelToBakeURL(fileURLString); - // make sure we have a valid output directory - QDir outputDirectory(_outputDirLineEdit->text()); - if (!outputDirectory.exists()) { - QMessageBox::warning(this, "Unable to create directory", "Unable to create output directory. Please create it manually or choose a different directory."); - return; - } - - QUrl bakeableModelURL = getBakeableModelURL(QUrl(modelToBakeURL), false); - + QUrl bakeableModelURL = getBakeableModelURL(QUrl(modelToBakeURL)); if (!bakeableModelURL.isEmpty()) { auto getWorkerThreadCallback = []() -> QThread* { return Oven::instance().getNextWorkerThread(); From 162573bc634c722650c58827269ed0f4d800d638 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 20 Feb 2019 16:50:26 -0800 Subject: [PATCH 044/446] enable js baking from non-local file --- libraries/baking/src/JSBaker.cpp | 78 ++++++++++++++++++++++---- libraries/baking/src/JSBaker.h | 12 +++- tools/oven/src/DomainBaker.cpp | 11 ++-- tools/oven/src/DomainBaker.h | 3 +- tools/oven/src/ui/DomainBakeWidget.cpp | 7 +-- tools/oven/src/ui/DomainBakeWidget.h | 1 - 6 files changed, 87 insertions(+), 25 deletions(-) diff --git a/libraries/baking/src/JSBaker.cpp b/libraries/baking/src/JSBaker.cpp index b19336f4ca..82d482967b 100644 --- a/libraries/baking/src/JSBaker.cpp +++ b/libraries/baking/src/JSBaker.cpp @@ -11,9 +11,11 @@ #include "JSBaker.h" -#include +#include -#include "Baker.h" +#include +#include +#include const int ASCII_CHARACTERS_UPPER_LIMIT = 126; @@ -21,25 +23,79 @@ JSBaker::JSBaker(const QUrl& jsURL, const QString& bakedOutputDir) : _jsURL(jsURL), _bakedOutputDir(bakedOutputDir) { - } void JSBaker::bake() { qCDebug(js_baking) << "JS Baker " << _jsURL << "bake starting"; - // Import file to start baking - QFile jsFile(_jsURL.toLocalFile()); - if (!jsFile.open(QIODevice::ReadOnly | QIODevice::Text)) { - handleError("Error opening " + _jsURL.fileName() + " for reading"); - return; - } + // once our texture is loaded, kick off a the processing + connect(this, &JSBaker::originalScriptLoaded, this, &JSBaker::processScript); + if (_jsURL.isEmpty()) { + // first load the texture (either locally or remotely) + loadScript(); + } else { + // we already have a texture passed to us, use that + emit originalScriptLoaded(); + } +} + +void JSBaker::loadScript() { + // check if the texture is local or first needs to be downloaded + if (_jsURL.isLocalFile()) { + // load up the local file + QFile localScript(_jsURL.toLocalFile()); + if (!localScript.open(QIODevice::ReadOnly | QIODevice::Text)) { + handleError("Error opening " + _jsURL.fileName() + " for reading"); + return; + } + + _originalScript = localScript.readAll(); + + emit originalScriptLoaded(); + } else { + // remote file, kick off a download + auto& networkAccessManager = NetworkAccessManager::getInstance(); + + QNetworkRequest networkRequest; + + // setup the request to follow re-directs and always hit the network + networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); + networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + + networkRequest.setUrl(_jsURL); + + qCDebug(js_baking) << "Downloading" << _jsURL; + + // kickoff the download, wait for slot to tell us it is done + auto networkReply = networkAccessManager.get(networkRequest); + connect(networkReply, &QNetworkReply::finished, this, &JSBaker::handleScriptNetworkReply); + } +} + +void JSBaker::handleScriptNetworkReply() { + auto requestReply = qobject_cast(sender()); + + if (requestReply->error() == QNetworkReply::NoError) { + qCDebug(js_baking) << "Downloaded texture" << _jsURL; + + // store the original texture so it can be passed along for the bake + _originalScript = requestReply->readAll(); + + emit originalScriptLoaded(); + } else { + // add an error to our list stating that this texture could not be downloaded + handleError("Error downloading " + _jsURL.toString() + " - " + requestReply->errorString()); + } +} + +void JSBaker::processScript() { // Read file into an array - QByteArray inputJS = jsFile.readAll(); QByteArray outputJS; // Call baking on inputJS and store result in outputJS - bool success = bakeJS(inputJS, outputJS); + bool success = bakeJS(_originalScript, outputJS); if (!success) { qCDebug(js_baking) << "Bake Failed"; handleError("Unterminated multi-line comment"); diff --git a/libraries/baking/src/JSBaker.h b/libraries/baking/src/JSBaker.h index 764681c71e..7eda85fa6d 100644 --- a/libraries/baking/src/JSBaker.h +++ b/libraries/baking/src/JSBaker.h @@ -25,14 +25,24 @@ public: JSBaker(const QUrl& jsURL, const QString& bakedOutputDir); static bool bakeJS(const QByteArray& inputFile, QByteArray& outputFile); - QString getJSPath() const { return _jsURL.fileName(); } + QString getJSPath() const { return _jsURL.toDisplayString(); } QString getBakedJSFilePath() const { return _bakedJSFilePath; } public slots: virtual void bake() override; +signals: + void originalScriptLoaded(); + +private slots: + void processScript(); + private: + void loadScript(); + void handleScriptNetworkReply(); + QUrl _jsURL; + QByteArray _originalScript; QString _bakedOutputDir; QString _bakedJSFilePath; diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 3c2f1d77bb..6f94b455d9 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -23,8 +23,7 @@ #include "baking/BakerLibrary.h" DomainBaker::DomainBaker(const QUrl& localModelFileURL, const QString& domainName, - const QString& baseOutputPath, const QUrl& destinationPath, - bool shouldRebakeOriginals) : + const QString& baseOutputPath, const QUrl& destinationPath) : _localEntitiesFileURL(localModelFileURL), _domainName(domainName), _baseOutputPath(baseOutputPath) @@ -178,7 +177,8 @@ void DomainBaker::addModelBaker(const QString& property, const QString& url, QJs } void DomainBaker::addTextureBaker(const QString& property, const QString& url, image::TextureUsage::Type type, QJsonValueRef& jsonRef) { - auto idx = url.lastIndexOf('.'); + QString cleanURL = QUrl(url).adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment).toDisplayString(); + auto idx = cleanURL.lastIndexOf('.'); auto extension = idx >= 0 ? url.mid(idx + 1).toLower() : ""; if (QImageReader::supportedImageFormats().contains(extension.toLatin1())) { @@ -211,6 +211,8 @@ void DomainBaker::addTextureBaker(const QString& property, const QString& url, i // add this QJsonValueRef to our multi hash so that it can re-write the texture URL // to the baked version once the baker is complete _entitiesNeedingRewrite.insert(textureURL, { property, jsonRef }); + } else { + qDebug() << "Texture extension not supported: " << extension; } } @@ -551,6 +553,7 @@ void DomainBaker::handleFinishedScriptBaker() { } // remove the baked URL from the multi hash of entities needing a re-write + _entitiesNeedingRewrite.remove(baker->getJSPath()); // drop our shared pointer to this baker so that it gets cleaned up @@ -611,5 +614,5 @@ void DomainBaker::writeNewEntitiesFile() { return; } - qDebug() << "Exported entities file with baked model URLs to" << bakedEntitiesFilePath; + qDebug() << "Exported baked entities file to" << bakedEntitiesFilePath; } diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h index 2a5abb4ca6..2a9522143e 100644 --- a/tools/oven/src/DomainBaker.h +++ b/tools/oven/src/DomainBaker.h @@ -29,8 +29,7 @@ public: // This means that we need to put all of the FBX importing/exporting from the same process on the same thread. // That means you must pass a usable running QThread when constructing a domain baker. DomainBaker(const QUrl& localEntitiesFileURL, const QString& domainName, - const QString& baseOutputPath, const QUrl& destinationPath, - bool shouldRebakeOriginals = false); + const QString& baseOutputPath, const QUrl& destinationPath); signals: void allModelsFinished(); diff --git a/tools/oven/src/ui/DomainBakeWidget.cpp b/tools/oven/src/ui/DomainBakeWidget.cpp index 1121041e39..23074e775e 100644 --- a/tools/oven/src/ui/DomainBakeWidget.cpp +++ b/tools/oven/src/ui/DomainBakeWidget.cpp @@ -126,10 +126,6 @@ void DomainBakeWidget::setupUI() { // start a new row for the next component ++rowIndex; - // setup a checkbox to allow re-baking of original assets - _rebakeOriginalsCheckBox = new QCheckBox("Re-bake originals"); - gridLayout->addWidget(_rebakeOriginalsCheckBox, rowIndex, 0); - // add a button that will kickoff the bake QPushButton* bakeButton = new QPushButton("Bake"); connect(bakeButton, &QPushButton::clicked, this, &DomainBakeWidget::bakeButtonClicked); @@ -211,8 +207,7 @@ void DomainBakeWidget::bakeButtonClicked() { auto fileToBakeURL = QUrl::fromLocalFile(_entitiesFileLineEdit->text()); auto domainBaker = std::unique_ptr { new DomainBaker(fileToBakeURL, _domainNameLineEdit->text(), - outputDirectory.absolutePath(), _destinationPathLineEdit->text(), - _rebakeOriginalsCheckBox->isChecked()) + outputDirectory.absolutePath(), _destinationPathLineEdit->text()) }; // make sure we hear from the baker when it is done diff --git a/tools/oven/src/ui/DomainBakeWidget.h b/tools/oven/src/ui/DomainBakeWidget.h index a6f26b3731..0a1d613912 100644 --- a/tools/oven/src/ui/DomainBakeWidget.h +++ b/tools/oven/src/ui/DomainBakeWidget.h @@ -45,7 +45,6 @@ private: QLineEdit* _entitiesFileLineEdit; QLineEdit* _outputDirLineEdit; QLineEdit* _destinationPathLineEdit; - QCheckBox* _rebakeOriginalsCheckBox; Setting::Handle _domainNameSetting; Setting::Handle _exportDirectory; From 7fc9a3fdb65848527c661ec1ec6aac13fe4f0469 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 21 Feb 2019 12:12:09 -0800 Subject: [PATCH 045/446] wip --- libraries/baking/src/JSBaker.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/libraries/baking/src/JSBaker.cpp b/libraries/baking/src/JSBaker.cpp index 82d482967b..c43c5ad00a 100644 --- a/libraries/baking/src/JSBaker.cpp +++ b/libraries/baking/src/JSBaker.cpp @@ -28,20 +28,20 @@ JSBaker::JSBaker(const QUrl& jsURL, const QString& bakedOutputDir) : void JSBaker::bake() { qCDebug(js_baking) << "JS Baker " << _jsURL << "bake starting"; - // once our texture is loaded, kick off a the processing + // once our script is loaded, kick off a the processing connect(this, &JSBaker::originalScriptLoaded, this, &JSBaker::processScript); if (_jsURL.isEmpty()) { - // first load the texture (either locally or remotely) + // first load the script (either locally or remotely) loadScript(); } else { - // we already have a texture passed to us, use that + // we already have a script passed to us, use that emit originalScriptLoaded(); } } void JSBaker::loadScript() { - // check if the texture is local or first needs to be downloaded + // check if the script is local or first needs to be downloaded if (_jsURL.isLocalFile()) { // load up the local file QFile localScript(_jsURL.toLocalFile()); @@ -78,14 +78,14 @@ void JSBaker::handleScriptNetworkReply() { auto requestReply = qobject_cast(sender()); if (requestReply->error() == QNetworkReply::NoError) { - qCDebug(js_baking) << "Downloaded texture" << _jsURL; + qCDebug(js_baking) << "Downloaded script" << _jsURL; - // store the original texture so it can be passed along for the bake + // store the original script so it can be passed along for the bake _originalScript = requestReply->readAll(); emit originalScriptLoaded(); } else { - // add an error to our list stating that this texture could not be downloaded + // add an error to our list stating that this script could not be downloaded handleError("Error downloading " + _jsURL.toString() + " - " + requestReply->errorString()); } } @@ -139,7 +139,10 @@ bool JSBaker::bakeJS(const QByteArray& inputFile, QByteArray& outputFile) { in >> currentCharacter; + qDebug() << "boop" << inputFile; + while (!in.atEnd()) { + qDebug() << "boop2" << currentCharacter << nextCharacter << previousCharacter; in >> nextCharacter; if (currentCharacter == '\r') { @@ -228,6 +231,8 @@ bool JSBaker::bakeJS(const QByteArray& inputFile, QByteArray& outputFile) { out << currentCharacter; } + qDebug() << "boop3" << outputFile; + // Successful bake. Return true return true; } From 4965adbc2f2671466d72f46adf56e04bac4ae6e4 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 25 Feb 2019 18:12:11 -0800 Subject: [PATCH 046/446] bake js and collision hull --- libraries/baking/src/JSBaker.cpp | 7 +------ tools/oven/src/DomainBaker.cpp | 35 ++++++++++++++++---------------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/libraries/baking/src/JSBaker.cpp b/libraries/baking/src/JSBaker.cpp index c43c5ad00a..e5682cde20 100644 --- a/libraries/baking/src/JSBaker.cpp +++ b/libraries/baking/src/JSBaker.cpp @@ -31,7 +31,7 @@ void JSBaker::bake() { // once our script is loaded, kick off a the processing connect(this, &JSBaker::originalScriptLoaded, this, &JSBaker::processScript); - if (_jsURL.isEmpty()) { + if (_originalScript.isEmpty()) { // first load the script (either locally or remotely) loadScript(); } else { @@ -139,10 +139,7 @@ bool JSBaker::bakeJS(const QByteArray& inputFile, QByteArray& outputFile) { in >> currentCharacter; - qDebug() << "boop" << inputFile; - while (!in.atEnd()) { - qDebug() << "boop2" << currentCharacter << nextCharacter << previousCharacter; in >> nextCharacter; if (currentCharacter == '\r') { @@ -231,8 +228,6 @@ bool JSBaker::bakeJS(const QByteArray& inputFile, QByteArray& outputFile) { out << currentCharacter; } - qDebug() << "boop3" << outputFile; - // Successful bake. Return true return true; } diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 6f94b455d9..2eb2c8a36b 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -293,7 +293,9 @@ void DomainBaker::enumerateEntities() { addModelBaker(MODEL_URL_KEY, entity[MODEL_URL_KEY].toString(), *it); } if (entity.contains(COMPOUND_SHAPE_URL_KEY)) { - // TODO: handle compoundShapeURL + // TODO: this could be optimized so that we don't do the full baking pass for collision shapes, + // but we have to handle the case where it's also used as a modelURL somewhere + addModelBaker(COMPOUND_SHAPE_URL_KEY, entity[COMPOUND_SHAPE_URL_KEY].toString(), *it); } if (entity.contains(ANIMATION_KEY)) { auto animationObject = entity[ANIMATION_KEY].toObject(); @@ -359,7 +361,7 @@ void DomainBaker::handleFinishedModelBaker() { if (baker) { if (!baker->hasErrors()) { - // this FBXBaker is done and everything went according to plan + // this ModelBaker is done and everything went according to plan qDebug() << "Re-writing entity references to" << baker->getModelURL(); // setup a new URL using the prefix we were passed @@ -433,7 +435,7 @@ void DomainBaker::handleFinishedTextureBaker() { if (baker) { if (!baker->hasErrors()) { - // this FBXBaker is done and everything went according to plan + // this TextureBaker is done and everything went according to plan qDebug() << "Re-writing entity references to" << baker->getTextureURL(); auto newURL = _destinationPath.resolved(baker->getMetaTextureFileName()); @@ -449,7 +451,7 @@ void DomainBaker::handleFinishedTextureBaker() { // grab the old URL QUrl oldURL = entity[property].toString(); - // copy the fragment and query, and user info from the old model URL + // copy the fragment and query, and user info from the old texture URL newURL.setQuery(oldURL.query()); newURL.setFragment(oldURL.fragment()); newURL.setUserInfo(oldURL.userInfo()); @@ -464,7 +466,7 @@ void DomainBaker::handleFinishedTextureBaker() { auto oldObject = entity[propertySplit[0]].toObject(); QUrl oldURL = oldObject[propertySplit[1]].toString(); - // copy the fragment and query, and user info from the old model URL + // copy the fragment and query, and user info from the old texture URL newURL.setQuery(oldURL.query()); newURL.setFragment(oldURL.fragment()); newURL.setUserInfo(oldURL.userInfo()); @@ -478,8 +480,8 @@ void DomainBaker::handleFinishedTextureBaker() { propertyEntityPair.second = entity; } } else { - // this skybox failed to bake - this doesn't fail the entire bake but we need to add the errors from - // the model to our warnings + // this texture failed to bake - this doesn't fail the entire bake but we need to add the errors from + // the texture to our warnings _warningList << baker->getWarnings(); } @@ -489,10 +491,10 @@ void DomainBaker::handleFinishedTextureBaker() { // drop our shared pointer to this baker so that it gets cleaned up _textureBakers.remove(baker->getTextureURL()); - // emit progress to tell listeners how many models we have baked + // emit progress to tell listeners how many textures we have baked emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes); - // check if this was the last model we needed to re-write and if we are done now + // check if this was the last texture we needed to re-write and if we are done now checkIfRewritingComplete(); } } @@ -502,7 +504,7 @@ void DomainBaker::handleFinishedScriptBaker() { if (baker) { if (!baker->hasErrors()) { - // this FBXBaker is done and everything went according to plan + // this JSBaker is done and everything went according to plan qDebug() << "Re-writing entity references to" << baker->getJSPath(); auto newURL = _destinationPath.resolved(baker->getBakedJSFilePath()); @@ -518,7 +520,7 @@ void DomainBaker::handleFinishedScriptBaker() { // grab the old URL QUrl oldURL = entity[property].toString(); - // copy the fragment and query, and user info from the old model URL + // copy the fragment and query, and user info from the old script URL newURL.setQuery(oldURL.query()); newURL.setFragment(oldURL.fragment()); newURL.setUserInfo(oldURL.userInfo()); @@ -533,7 +535,7 @@ void DomainBaker::handleFinishedScriptBaker() { auto oldObject = entity[propertySplit[0]].toObject(); QUrl oldURL = oldObject[propertySplit[1]].toString(); - // copy the fragment and query, and user info from the old model URL + // copy the fragment and query, and user info from the old script URL newURL.setQuery(oldURL.query()); newURL.setFragment(oldURL.fragment()); newURL.setUserInfo(oldURL.userInfo()); @@ -547,22 +549,21 @@ void DomainBaker::handleFinishedScriptBaker() { propertyEntityPair.second = entity; } } else { - // this model failed to bake - this doesn't fail the entire bake but we need to add - // the errors from the model to our warnings + // this script failed to bake - this doesn't fail the entire bake but we need to add + // the errors from the script to our warnings _warningList << baker->getErrors(); } // remove the baked URL from the multi hash of entities needing a re-write - _entitiesNeedingRewrite.remove(baker->getJSPath()); // drop our shared pointer to this baker so that it gets cleaned up _scriptBakers.remove(baker->getJSPath()); - // emit progress to tell listeners how many models we have baked + // emit progress to tell listeners how many scripts we have baked emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes); - // check if this was the last model we needed to re-write and if we are done now + // check if this was the last script we needed to re-write and if we are done now checkIfRewritingComplete(); } } From 94de0c12bc8a8ddbd1e771abfdf0772790d9c00d Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 26 Feb 2019 15:02:13 -0800 Subject: [PATCH 047/446] working on material baker --- libraries/baking/CMakeLists.txt | 2 +- libraries/baking/src/MaterialBaker.cpp | 118 +++++++++++++++ libraries/baking/src/MaterialBaker.h | 58 ++++++++ .../src/MaterialBakingLoggingCategory.cpp | 14 ++ .../src/MaterialBakingLoggingCategory.h | 19 +++ .../src/graphics-scripting/Forward.h | 2 + .../GraphicsScriptingInterface.cpp | 87 +++++++++-- .../graphics-scripting/ScriptableModel.cpp | 115 ++++++++------- tools/oven/CMakeLists.txt | 2 +- tools/oven/src/BakerCLI.cpp | 5 +- tools/oven/src/DomainBaker.cpp | 139 +++++++++++++++++- tools/oven/src/DomainBaker.h | 4 + tools/oven/src/Oven.cpp | 4 + 13 files changed, 491 insertions(+), 78 deletions(-) create mode 100644 libraries/baking/src/MaterialBaker.cpp create mode 100644 libraries/baking/src/MaterialBaker.h create mode 100644 libraries/baking/src/MaterialBakingLoggingCategory.cpp create mode 100644 libraries/baking/src/MaterialBakingLoggingCategory.h diff --git a/libraries/baking/CMakeLists.txt b/libraries/baking/CMakeLists.txt index cce76f152f..2fa4c86691 100644 --- a/libraries/baking/CMakeLists.txt +++ b/libraries/baking/CMakeLists.txt @@ -1,7 +1,7 @@ set(TARGET_NAME baking) setup_hifi_library(Concurrent) -link_hifi_libraries(shared graphics networking ktx image fbx) +link_hifi_libraries(shared shaders graphics networking material-networking ktx image fbx) include_hifi_library_headers(gpu) include_hifi_library_headers(hfm) diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp new file mode 100644 index 0000000000..3427663b09 --- /dev/null +++ b/libraries/baking/src/MaterialBaker.cpp @@ -0,0 +1,118 @@ +// +// MaterialBaker.cpp +// libraries/baking/src +// +// Created by Sam Gondelman on 2/26/2019 +// Copyright 2019 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 +// + +#include "MaterialBaker.h" + +#include "QJsonObject" +#include "QJsonDocument" + +#include "MaterialBakingLoggingCategory.h" + +#include +#include + +MaterialBaker::MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir) : + _materialData(materialData), + _isURL(isURL), + _bakedOutputDir(bakedOutputDir) +{ +} + +void MaterialBaker::bake() { + qDebug(material_baking) << "Material Baker" << _materialData << "bake starting"; + + // once our script is loaded, kick off a the processing + connect(this, &MaterialBaker::originalMaterialLoaded, this, &MaterialBaker::processMaterial); + + if (!_materialResource) { + // first load the material (either locally or remotely) + loadMaterial(); + } else { + // we already have a material passed to us, use that + if (_materialResource->isLoaded()) { + emit originalMaterialLoaded(); + } else { + connect(_materialResource.data(), &Resource::finished, this, &MaterialBaker::originalMaterialLoaded); + } + } +} + +void MaterialBaker::loadMaterial() { + if (!_isURL) { + qCDebug(material_baking) << "Loading local material" << _materialData; + + _materialResource = NetworkMaterialResourcePointer(new NetworkMaterialResource()); + // TODO: add baseURL to allow these to reference relative files next to them + _materialResource->parsedMaterials = NetworkMaterialResource::parseJSONMaterials(QJsonDocument::fromVariant(_materialData), QUrl()); + } else { + qCDebug(material_baking) << "Downloading material" << _materialData; + _materialResource = MaterialCache::instance().getMaterial(_materialData); + } + + if (_materialResource) { + if (_materialResource->isLoaded()) { + emit originalMaterialLoaded(); + } else { + connect(_materialResource.data(), &Resource::finished, this, &MaterialBaker::originalMaterialLoaded); + } + } else { + handleError("Error loading " + _materialData); + } +} + +void MaterialBaker::processMaterial() { + if (!_materialResource || _materialResource->parsedMaterials.networkMaterials.size() == 0) { + handleError("Error processing " + _materialData); + } + + _numTexturesToLoad = _materialResource->parsedMaterials.networkMaterials.size(); + _numTexturesLoaded = 0; + + for (auto networkMaterial : _materialResource->parsedMaterials.networkMaterials) { + if (networkMaterial.second) { + auto textureMaps = networkMaterial.second->getTextureMaps(); + for (auto textureMap : textureMaps) { + if (textureMap.second && textureMap.second->getTextureSource()) { + auto texture = textureMap.second->getTextureSource(); + graphics::Material::MapChannel mapChannel = textureMap.first; + + qDebug() << "boop" << mapChannel << texture->getUrl(); + } + } + } + } +} + +void MaterialBaker::outputMaterial() { + //if (_isURL) { + // auto fileName = _materialData; + // auto baseName = fileName.left(fileName.lastIndexOf('.')); + // auto bakedFilename = baseName + BAKED_MATERIAL_EXTENSION; + + // _bakedMaterialData = _bakedOutputDir + "/" + bakedFilename; + + // QFile bakedFile; + // bakedFile.setFileName(_bakedMaterialData); + // if (!bakedFile.open(QIODevice::WriteOnly)) { + // handleError("Error opening " + _bakedMaterialData + " for writing"); + // return; + // } + + // bakedFile.write(outputMaterial); + + // // Export successful + // _outputFiles.push_back(_bakedMaterialData); + // qCDebug(material_baking) << "Exported" << _materialData << "to" << _bakedMaterialData; + //} + + // emit signal to indicate the material baking is finished + emit finished(); +} diff --git a/libraries/baking/src/MaterialBaker.h b/libraries/baking/src/MaterialBaker.h new file mode 100644 index 0000000000..6113515b81 --- /dev/null +++ b/libraries/baking/src/MaterialBaker.h @@ -0,0 +1,58 @@ +// +// MaterialBaker.h +// libraries/baking/src +// +// Created by Sam Gondelman on 2/26/2019 +// Copyright 2019 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 +// + +#ifndef hifi_MaterialBaker_h +#define hifi_MaterialBaker_h + +#include "Baker.h" + +#include "TextureBaker.h" + +#include + +static const QString BAKED_MATERIAL_EXTENSION = ".baked.json"; + +class MaterialBaker : public Baker { + Q_OBJECT +public: + MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir); + + QString getMaterialData() const { return _materialData; } + bool isURL() const { return _isURL; } + QString getBakedMaterialData() const { return _bakedMaterialData; } + +public slots: + virtual void bake() override; + +signals: + void originalMaterialLoaded(); + +private slots: + void processMaterial(); + void outputMaterial(); + +private: + void loadMaterial(); + + QString _materialData; + bool _isURL; + + NetworkMaterialResourcePointer _materialResource; + size_t _numTexturesToLoad { 0 }; + size_t _numTexturesLoaded { 0 }; + + QHash> _textureBakers; + + QString _bakedOutputDir; + QString _bakedMaterialData; +}; + +#endif // !hifi_MaterialBaker_h diff --git a/libraries/baking/src/MaterialBakingLoggingCategory.cpp b/libraries/baking/src/MaterialBakingLoggingCategory.cpp new file mode 100644 index 0000000000..75c0e6319c --- /dev/null +++ b/libraries/baking/src/MaterialBakingLoggingCategory.cpp @@ -0,0 +1,14 @@ +// +// MaterialBakingLoggingCategory.cpp +// libraries/baking/src +// +// Created by Sam Gondelman on 2/26/2019 +// Copyright 2019 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 +// + +#include "MaterialBakingLoggingCategory.h" + +Q_LOGGING_CATEGORY(material_baking, "hifi.material-baking"); diff --git a/libraries/baking/src/MaterialBakingLoggingCategory.h b/libraries/baking/src/MaterialBakingLoggingCategory.h new file mode 100644 index 0000000000..768bd9d769 --- /dev/null +++ b/libraries/baking/src/MaterialBakingLoggingCategory.h @@ -0,0 +1,19 @@ +// +// MaterialBakingLoggingCategory.h +// libraries/baking/src +// +// Created by Sam Gondelman on 2/26/2019 +// Copyright 2019 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 +// + +#ifndef hifi_MaterialBakingLoggingCategory_h +#define hifi_MaterialBakingLoggingCategory_h + +#include + +Q_DECLARE_LOGGING_CATEGORY(material_baking) + +#endif // hifi_MaterialBakingLoggingCategory_h diff --git a/libraries/graphics-scripting/src/graphics-scripting/Forward.h b/libraries/graphics-scripting/src/graphics-scripting/Forward.h index 747788aef8..d2d330167d 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/Forward.h +++ b/libraries/graphics-scripting/src/graphics-scripting/Forward.h @@ -96,6 +96,8 @@ namespace scriptable { bool defaultFallthrough; std::unordered_map propertyFallthroughs; // not actually exposed to script + + graphics::MaterialKey key { 0 }; }; /**jsdoc diff --git a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp index 848f9d42ac..1fd7ad9df5 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp +++ b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp @@ -364,20 +364,81 @@ namespace scriptable { obj.setProperty("model", material.model); const QScriptValue FALLTHROUGH("fallthrough"); - obj.setProperty("opacity", material.propertyFallthroughs.at(graphics::MaterialKey::OPACITY_VAL_BIT) ? FALLTHROUGH : material.opacity); - obj.setProperty("roughness", material.propertyFallthroughs.at(graphics::MaterialKey::GLOSSY_VAL_BIT) ? FALLTHROUGH : material.roughness); - obj.setProperty("metallic", material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_VAL_BIT) ? FALLTHROUGH : material.metallic); - obj.setProperty("scattering", material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_VAL_BIT) ? FALLTHROUGH : material.scattering); - obj.setProperty("unlit", material.propertyFallthroughs.at(graphics::MaterialKey::UNLIT_VAL_BIT) ? FALLTHROUGH : material.unlit); - obj.setProperty("emissive", material.propertyFallthroughs.at(graphics::MaterialKey::EMISSIVE_VAL_BIT) ? FALLTHROUGH : vec3ColorToScriptValue(engine, material.emissive)); - obj.setProperty("albedo", material.propertyFallthroughs.at(graphics::MaterialKey::ALBEDO_VAL_BIT) ? FALLTHROUGH : vec3ColorToScriptValue(engine, material.albedo)); + if (material.propertyFallthroughs.at(graphics::MaterialKey::OPACITY_VAL_BIT)) { + obj.setProperty("opacity", FALLTHROUGH); + } else if (material.key.isTranslucentFactor()) { + obj.setProperty("opacity", material.opacity); + } - obj.setProperty("emissiveMap", material.propertyFallthroughs.at(graphics::MaterialKey::EMISSIVE_MAP_BIT) ? FALLTHROUGH : material.emissiveMap); - obj.setProperty("albedoMap", material.propertyFallthroughs.at(graphics::MaterialKey::ALBEDO_MAP_BIT) ? FALLTHROUGH : material.albedoMap); - obj.setProperty("opacityMap", material.opacityMap); - obj.setProperty("occlusionMap", material.propertyFallthroughs.at(graphics::MaterialKey::OCCLUSION_MAP_BIT) ? FALLTHROUGH : material.occlusionMap); - obj.setProperty("lightmapMap", material.propertyFallthroughs.at(graphics::MaterialKey::LIGHTMAP_MAP_BIT) ? FALLTHROUGH : material.lightmapMap); - obj.setProperty("scatteringMap", material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_MAP_BIT) ? FALLTHROUGH : material.scatteringMap); + if (material.propertyFallthroughs.at(graphics::MaterialKey::GLOSSY_VAL_BIT)) { + obj.setProperty("roughness", FALLTHROUGH); + } else if (material.key.isGlossy()) { + obj.setProperty("roughness", material.roughness); + } + + if (material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_VAL_BIT)) { + obj.setProperty("metallic", FALLTHROUGH); + } else if (material.key.isMetallic()) { + obj.setProperty("metallic", material.metallic); + } + + if (material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_VAL_BIT)) { + obj.setProperty("scattering", FALLTHROUGH); + } else if (material.key.isScattering()) { + obj.setProperty("scattering", material.scattering); + } + + if (material.propertyFallthroughs.at(graphics::MaterialKey::UNLIT_VAL_BIT)) { + obj.setProperty("unlit", FALLTHROUGH); + } else if (material.key.isUnlit()) { + obj.setProperty("unlit", material.unlit); + } + + if (material.propertyFallthroughs.at(graphics::MaterialKey::EMISSIVE_VAL_BIT)) { + obj.setProperty("emissive", FALLTHROUGH); + } else if (material.key.isEmissive()) { + obj.setProperty("emissive", vec3ColorToScriptValue(engine, material.emissive)); + } + + if (material.propertyFallthroughs.at(graphics::MaterialKey::ALBEDO_VAL_BIT)) { + obj.setProperty("albedo", FALLTHROUGH); + } else if (material.key.isAlbedo()) { + obj.setProperty("albedo", vec3ColorToScriptValue(engine, material.albedo)); + } + + if (material.propertyFallthroughs.at(graphics::MaterialKey::EMISSIVE_MAP_BIT)) { + obj.setProperty("emissiveMap", FALLTHROUGH); + } else if (!material.emissiveMap.isEmpty()) { + obj.setProperty("emissiveMap", material.emissiveMap); + } + + if (material.propertyFallthroughs.at(graphics::MaterialKey::ALBEDO_MAP_BIT)) { + obj.setProperty("albedoMap", FALLTHROUGH); + } else if (!material.albedoMap.isEmpty()) { + obj.setProperty("albedoMap", material.albedoMap); + } + + if (!material.opacityMap.isEmpty()) { + obj.setProperty("opacityMap", material.opacityMap); + } + + if (material.propertyFallthroughs.at(graphics::MaterialKey::OCCLUSION_MAP_BIT)) { + obj.setProperty("occlusionMap", FALLTHROUGH); + } else if (!material.occlusionMap.isEmpty()) { + obj.setProperty("occlusionMap", material.occlusionMap); + } + + if (material.propertyFallthroughs.at(graphics::MaterialKey::LIGHTMAP_MAP_BIT)) { + obj.setProperty("lightmapMap", FALLTHROUGH); + } else if (!material.lightmapMap.isEmpty()) { + obj.setProperty("lightmapMap", material.lightmapMap); + } + + if (material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_MAP_BIT)) { + obj.setProperty("scatteringMap", FALLTHROUGH); + } else if (!material.scatteringMap.isEmpty()) { + obj.setProperty("scatteringMap", material.scatteringMap); + } // Only set one of each of these if (material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_MAP_BIT)) { diff --git a/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp b/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp index 4ff751782c..fdd06ffa64 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp +++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp @@ -45,75 +45,80 @@ scriptable::ScriptableMaterial& scriptable::ScriptableMaterial::operator=(const defaultFallthrough = material.defaultFallthrough; propertyFallthroughs = material.propertyFallthroughs; + key = material.key; + return *this; } -scriptable::ScriptableMaterial::ScriptableMaterial(const graphics::MaterialPointer& material) : - name(material->getName().c_str()), - model(material->getModel().c_str()), - opacity(material->getOpacity()), - roughness(material->getRoughness()), - metallic(material->getMetallic()), - scattering(material->getScattering()), - unlit(material->isUnlit()), - emissive(material->getEmissive()), - albedo(material->getAlbedo()), - defaultFallthrough(material->getDefaultFallthrough()), - propertyFallthroughs(material->getPropertyFallthroughs()) -{ - auto map = material->getTextureMap(graphics::Material::MapChannel::EMISSIVE_MAP); - if (map && map->getTextureSource()) { - emissiveMap = map->getTextureSource()->getUrl().toString(); - } +scriptable::ScriptableMaterial::ScriptableMaterial(const graphics::MaterialPointer& material) { + if (material) { + name = material->getName().c_str(); + model = material->getModel().c_str(); + opacity = material->getOpacity(); + roughness = material->getRoughness(); + metallic = material->getMetallic(); + scattering = material->getScattering(); + unlit = material->isUnlit(); + emissive = material->getEmissive(); + albedo = material->getAlbedo(); + defaultFallthrough = material->getDefaultFallthrough(); + propertyFallthroughs = material->getPropertyFallthroughs(); + key = material->getKey(); - map = material->getTextureMap(graphics::Material::MapChannel::ALBEDO_MAP); - if (map && map->getTextureSource()) { - albedoMap = map->getTextureSource()->getUrl().toString(); - if (map->useAlphaChannel()) { - opacityMap = albedoMap; + auto map = material->getTextureMap(graphics::Material::MapChannel::EMISSIVE_MAP); + if (map && map->getTextureSource()) { + emissiveMap = map->getTextureSource()->getUrl().toString(); } - } - map = material->getTextureMap(graphics::Material::MapChannel::METALLIC_MAP); - if (map && map->getTextureSource()) { - if (map->getTextureSource()->getType() == image::TextureUsage::Type::METALLIC_TEXTURE) { - metallicMap = map->getTextureSource()->getUrl().toString(); - } else if (map->getTextureSource()->getType() == image::TextureUsage::Type::SPECULAR_TEXTURE) { - specularMap = map->getTextureSource()->getUrl().toString(); + map = material->getTextureMap(graphics::Material::MapChannel::ALBEDO_MAP); + if (map && map->getTextureSource()) { + albedoMap = map->getTextureSource()->getUrl().toString(); + if (map->useAlphaChannel()) { + opacityMap = albedoMap; + } } - } - map = material->getTextureMap(graphics::Material::MapChannel::ROUGHNESS_MAP); - if (map && map->getTextureSource()) { - if (map->getTextureSource()->getType() == image::TextureUsage::Type::ROUGHNESS_TEXTURE) { - roughnessMap = map->getTextureSource()->getUrl().toString(); - } else if (map->getTextureSource()->getType() == image::TextureUsage::Type::GLOSS_TEXTURE) { - glossMap = map->getTextureSource()->getUrl().toString(); + map = material->getTextureMap(graphics::Material::MapChannel::METALLIC_MAP); + if (map && map->getTextureSource()) { + if (map->getTextureSource()->getType() == image::TextureUsage::Type::METALLIC_TEXTURE) { + metallicMap = map->getTextureSource()->getUrl().toString(); + } else if (map->getTextureSource()->getType() == image::TextureUsage::Type::SPECULAR_TEXTURE) { + specularMap = map->getTextureSource()->getUrl().toString(); + } } - } - map = material->getTextureMap(graphics::Material::MapChannel::NORMAL_MAP); - if (map && map->getTextureSource()) { - if (map->getTextureSource()->getType() == image::TextureUsage::Type::NORMAL_TEXTURE) { - normalMap = map->getTextureSource()->getUrl().toString(); - } else if (map->getTextureSource()->getType() == image::TextureUsage::Type::BUMP_TEXTURE) { - bumpMap = map->getTextureSource()->getUrl().toString(); + map = material->getTextureMap(graphics::Material::MapChannel::ROUGHNESS_MAP); + if (map && map->getTextureSource()) { + if (map->getTextureSource()->getType() == image::TextureUsage::Type::ROUGHNESS_TEXTURE) { + roughnessMap = map->getTextureSource()->getUrl().toString(); + } else if (map->getTextureSource()->getType() == image::TextureUsage::Type::GLOSS_TEXTURE) { + glossMap = map->getTextureSource()->getUrl().toString(); + } } - } - map = material->getTextureMap(graphics::Material::MapChannel::OCCLUSION_MAP); - if (map && map->getTextureSource()) { - occlusionMap = map->getTextureSource()->getUrl().toString(); - } + map = material->getTextureMap(graphics::Material::MapChannel::NORMAL_MAP); + if (map && map->getTextureSource()) { + if (map->getTextureSource()->getType() == image::TextureUsage::Type::NORMAL_TEXTURE) { + normalMap = map->getTextureSource()->getUrl().toString(); + } else if (map->getTextureSource()->getType() == image::TextureUsage::Type::BUMP_TEXTURE) { + bumpMap = map->getTextureSource()->getUrl().toString(); + } + } - map = material->getTextureMap(graphics::Material::MapChannel::LIGHTMAP_MAP); - if (map && map->getTextureSource()) { - lightmapMap = map->getTextureSource()->getUrl().toString(); - } + map = material->getTextureMap(graphics::Material::MapChannel::OCCLUSION_MAP); + if (map && map->getTextureSource()) { + occlusionMap = map->getTextureSource()->getUrl().toString(); + } - map = material->getTextureMap(graphics::Material::MapChannel::SCATTERING_MAP); - if (map && map->getTextureSource()) { - scatteringMap = map->getTextureSource()->getUrl().toString(); + map = material->getTextureMap(graphics::Material::MapChannel::LIGHTMAP_MAP); + if (map && map->getTextureSource()) { + lightmapMap = map->getTextureSource()->getUrl().toString(); + } + + map = material->getTextureMap(graphics::Material::MapChannel::SCATTERING_MAP); + if (map && map->getTextureSource()) { + scatteringMap = map->getTextureSource()->getUrl().toString(); + } } } diff --git a/tools/oven/CMakeLists.txt b/tools/oven/CMakeLists.txt index 022c9769fe..18ad37d7b9 100644 --- a/tools/oven/CMakeLists.txt +++ b/tools/oven/CMakeLists.txt @@ -2,7 +2,7 @@ set(TARGET_NAME oven) setup_hifi_project(Widgets Gui Concurrent) -link_hifi_libraries(networking shared image gpu ktx fbx hfm baking graphics) +link_hifi_libraries(shared shaders image gpu ktx fbx hfm baking graphics networking material-networking) setup_memory_debugger() diff --git a/tools/oven/src/BakerCLI.cpp b/tools/oven/src/BakerCLI.cpp index f5fffe6ea3..1aae6ccb72 100644 --- a/tools/oven/src/BakerCLI.cpp +++ b/tools/oven/src/BakerCLI.cpp @@ -23,6 +23,7 @@ #include "baking/BakerLibrary.h" #include "JSBaker.h" #include "TextureBaker.h" +#include "MaterialBaker.h" BakerCLI::BakerCLI(OvenCLIApplication* parent) : QObject(parent) { @@ -60,8 +61,8 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString& outputPath, const QString& _baker = std::unique_ptr { new JSBaker(inputUrl, outputPath) }; _baker->moveToThread(Oven::instance().getNextWorkerThread()); } else if (type == MATERIAL_EXTENSION) { - //_baker = std::unique_ptr { new MaterialBaker(inputUrl, outputPath) }; - //_baker->moveToThread(Oven::instance().getNextWorkerThread()); + _baker = std::unique_ptr { new MaterialBaker(inputUrl.toDisplayString(), true, outputPath) }; + _baker->moveToThread(Oven::instance().getNextWorkerThread()); } else { // If the type doesn't match the above, we assume we have a texture, and the type specified is the // texture usage type (albedo, cubemap, normals, etc.) diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 2eb2c8a36b..ca5c9b85fe 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -187,8 +187,8 @@ void DomainBaker::addTextureBaker(const QString& property, const QString& url, i // setup a texture baker for this URL, as long as we aren't baking a texture already if (!_textureBakers.contains(textureURL)) { - // setup a baker for this texture + // setup a baker for this texture QSharedPointer textureBaker { new TextureBaker(textureURL, type, _contentOutputPath), &TextureBaker::deleteLater @@ -220,16 +220,16 @@ void DomainBaker::addScriptBaker(const QString& property, const QString& url, QJ // grab a clean version of the URL without a query or fragment QUrl scriptURL = QUrl(url).adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); - // setup a texture baker for this URL, as long as we aren't baking a texture already + // setup a script baker for this URL, as long as we aren't baking a texture already if (!_scriptBakers.contains(scriptURL)) { - // setup a baker for this texture + // setup a baker for this script QSharedPointer scriptBaker { new JSBaker(scriptURL, _contentOutputPath), &JSBaker::deleteLater }; - // make sure our handler is called when the texture baker is done + // make sure our handler is called when the script baker is done connect(scriptBaker.data(), &JSBaker::finished, this, &DomainBaker::handleFinishedScriptBaker); // insert it into our bakers hash so we hold a strong pointer to it @@ -243,11 +243,48 @@ void DomainBaker::addScriptBaker(const QString& property, const QString& url, QJ ++_totalNumberOfSubBakes; } - // add this QJsonValueRef to our multi hash so that it can re-write the texture URL + // add this QJsonValueRef to our multi hash so that it can re-write the script URL // to the baked version once the baker is complete _entitiesNeedingRewrite.insert(scriptURL, { property, jsonRef }); } +void DomainBaker::addMaterialBaker(const QString& property, const QString& data, bool isURL, QJsonValueRef& jsonRef) { + // grab a clean version of the URL without a query or fragment + QString materialData; + if (isURL) { + materialData = QUrl(data).adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment).toDisplayString(); + } else { + materialData = data; + } + + // setup a material baker for this URL, as long as we aren't baking a material already + if (!_materialBakers.contains(materialData)) { + + // setup a baker for this material + QSharedPointer materialBaker { + new MaterialBaker(data, isURL, _contentOutputPath), + &MaterialBaker::deleteLater + }; + + // make sure our handler is called when the material baker is done + connect(materialBaker.data(), &MaterialBaker::finished, this, &DomainBaker::handleFinishedMaterialBaker); + + // insert it into our bakers hash so we hold a strong pointer to it + _materialBakers.insert(materialData, materialBaker); + + // move the baker to a worker thread and kickoff the bake + materialBaker->moveToThread(Oven::instance().getNextWorkerThread()); + QMetaObject::invokeMethod(materialBaker.data(), "bake"); + + // keep track of the total number of baking entities + ++_totalNumberOfSubBakes; + } + + // add this QJsonValueRef to our multi hash so that it can re-write the texture URL + // to the baked version once the baker is complete + _entitiesNeedingRewrite.insert(materialData, { property, jsonRef }); +} + // All the Entity Properties that can be baked // *************************************************************************************** @@ -348,7 +385,12 @@ void DomainBaker::enumerateEntities() { } // Materials - // TODO + if (entity.contains(MATERIAL_URL_KEY)) { + addMaterialBaker(MATERIAL_URL_KEY, entity[MATERIAL_URL_KEY].toString(), true, *it); + } + if (entity.contains(MATERIAL_DATA_KEY)) { + addMaterialBaker(MATERIAL_DATA_KEY, entity[MATERIAL_URL_KEY].toString(), false, *it); + } } } @@ -568,6 +610,91 @@ void DomainBaker::handleFinishedScriptBaker() { } } +void DomainBaker::handleFinishedMaterialBaker() { + auto baker = qobject_cast(sender()); + + if (baker) { + if (!baker->hasErrors()) { + // this MaterialBaker is done and everything went according to plan + qDebug() << "Re-writing entity references to" << baker->getMaterialData(); + + QString newDataOrURL; + if (baker->isURL()) { + newDataOrURL = _destinationPath.resolved(baker->getBakedMaterialData()).toDisplayString(); + } else { + newDataOrURL = baker->getBakedMaterialData(); + } + + // enumerate the QJsonRef values for the URL of this material from our multi hash of + // entity objects needing a URL re-write + for (auto propertyEntityPair : _entitiesNeedingRewrite.values(baker->getMaterialData())) { + QString property = propertyEntityPair.first; + // convert the entity QJsonValueRef to a QJsonObject so we can modify its URL + auto entity = propertyEntityPair.second.toObject(); + + if (!property.contains(".")) { + // grab the old URL + QUrl oldURL = entity[property].toString(); + + // copy the fragment and query, and user info from the old material data + if (baker->isURL()) { + QUrl newURL = newDataOrURL; + newURL.setQuery(oldURL.query()); + newURL.setFragment(oldURL.fragment()); + newURL.setUserInfo(oldURL.userInfo()); + + // set the new URL as the value in our temp QJsonObject + entity[property] = newURL.toString(); + } else { + entity[property] = newDataOrURL; + } + } else { + // Group property + QStringList propertySplit = property.split("."); + assert(propertySplit.length() == 2); + // grab the old URL + auto oldObject = entity[propertySplit[0]].toObject(); + QUrl oldURL = oldObject[propertySplit[1]].toString(); + + // copy the fragment and query, and user info from the old material data + if (baker->isURL()) { + QUrl newURL = newDataOrURL; + newURL.setQuery(oldURL.query()); + newURL.setFragment(oldURL.fragment()); + newURL.setUserInfo(oldURL.userInfo()); + + // set the new URL as the value in our temp QJsonObject + oldObject[propertySplit[1]] = newURL.toString(); + entity[propertySplit[0]] = oldObject; + } else { + oldObject[propertySplit[1]] = newDataOrURL; + entity[propertySplit[0]] = oldObject; + } + } + + // replace our temp object with the value referenced by our QJsonValueRef + propertyEntityPair.second = entity; + } + } else { + // this material failed to bake - this doesn't fail the entire bake but we need to add + // the errors from the material to our warnings + _warningList << baker->getErrors(); + } + + // remove the baked URL from the multi hash of entities needing a re-write + _entitiesNeedingRewrite.remove(baker->getMaterialData()); + + // drop our shared pointer to this baker so that it gets cleaned up + _materialBakers.remove(baker->getMaterialData()); + + // emit progress to tell listeners how many materials we have baked + emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes); + + // check if this was the last material we needed to re-write and if we are done now + checkIfRewritingComplete(); + } +} + void DomainBaker::checkIfRewritingComplete() { if (_entitiesNeedingRewrite.isEmpty()) { writeNewEntitiesFile(); diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h index 2a9522143e..4504d5b8fa 100644 --- a/tools/oven/src/DomainBaker.h +++ b/tools/oven/src/DomainBaker.h @@ -21,6 +21,7 @@ #include "ModelBaker.h" #include "TextureBaker.h" #include "JSBaker.h" +#include "MaterialBaker.h" class DomainBaker : public Baker { Q_OBJECT @@ -40,6 +41,7 @@ private slots: void handleFinishedModelBaker(); void handleFinishedTextureBaker(); void handleFinishedScriptBaker(); + void handleFinishedMaterialBaker(); private: void setupOutputFolder(); @@ -62,6 +64,7 @@ private: QHash> _modelBakers; QHash> _textureBakers; QHash> _scriptBakers; + QHash> _materialBakers; QMultiHash> _entitiesNeedingRewrite; @@ -71,6 +74,7 @@ private: void addModelBaker(const QString& property, const QString& url, QJsonValueRef& jsonRef); void addTextureBaker(const QString& property, const QString& url, image::TextureUsage::Type type, QJsonValueRef& jsonRef); void addScriptBaker(const QString& property, const QString& url, QJsonValueRef& jsonRef); + void addMaterialBaker(const QString& property, const QString& data, bool isURL, QJsonValueRef& jsonRef); }; #endif // hifi_DomainBaker_h diff --git a/tools/oven/src/Oven.cpp b/tools/oven/src/Oven.cpp index af98376034..6fdc45f6eb 100644 --- a/tools/oven/src/Oven.cpp +++ b/tools/oven/src/Oven.cpp @@ -63,6 +63,10 @@ void Oven::setupWorkerThreads(int numWorkerThreads) { } QThread* Oven::getNextWorkerThread() { + // FIXME: we assign these threads when we make the bakers, but if certain bakers finish quickly, we could end up + // in a situation where threads have finished and others have tons of work queued. Instead of assigning them at initialization, + // we should build a queue of bakers, and when threads finish, they can take the next available baker. + // Here we replicate some of the functionality of QThreadPool by giving callers an available worker thread to use. // We can't use QThreadPool because we want to put QObjects with signals/slots on these threads. // So instead we setup our own list of threads, up to one less than the ideal thread count From 1a1277e9e70fd82b7fc2b66206601b08d9776ce5 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 27 Feb 2019 18:00:37 -0800 Subject: [PATCH 048/446] it's working! --- libraries/baking/CMakeLists.txt | 2 +- libraries/baking/src/MaterialBaker.cpp | 155 +++++++++++++++--- libraries/baking/src/MaterialBaker.h | 12 +- libraries/baking/src/TextureBaker.cpp | 11 +- libraries/baking/src/TextureBaker.h | 8 + .../GraphicsScriptingInterface.cpp | 40 ++--- .../GraphicsScriptingInterface.h | 4 + tools/oven/src/DomainBaker.cpp | 16 +- tools/oven/src/Oven.cpp | 10 ++ 9 files changed, 203 insertions(+), 55 deletions(-) diff --git a/libraries/baking/CMakeLists.txt b/libraries/baking/CMakeLists.txt index 2fa4c86691..38b6268fb7 100644 --- a/libraries/baking/CMakeLists.txt +++ b/libraries/baking/CMakeLists.txt @@ -1,7 +1,7 @@ set(TARGET_NAME baking) setup_hifi_library(Concurrent) -link_hifi_libraries(shared shaders graphics networking material-networking ktx image fbx) +link_hifi_libraries(shared shaders graphics networking material-networking graphics-scripting ktx image fbx) include_hifi_library_headers(gpu) include_hifi_library_headers(hfm) diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp index 3427663b09..054d1ed0fd 100644 --- a/libraries/baking/src/MaterialBaker.cpp +++ b/libraries/baking/src/MaterialBaker.cpp @@ -19,10 +19,17 @@ #include #include +#include + +std::function MaterialBaker::_getNextOvenWorkerThreadOperator; + +static int materialNum = 0; + MaterialBaker::MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir) : _materialData(materialData), _isURL(isURL), - _bakedOutputDir(bakedOutputDir) + _bakedOutputDir(bakedOutputDir), + _textureOutputDir(bakedOutputDir + "/materialTextures/" + QString::number(materialNum++)) { } @@ -51,7 +58,7 @@ void MaterialBaker::loadMaterial() { _materialResource = NetworkMaterialResourcePointer(new NetworkMaterialResource()); // TODO: add baseURL to allow these to reference relative files next to them - _materialResource->parsedMaterials = NetworkMaterialResource::parseJSONMaterials(QJsonDocument::fromVariant(_materialData), QUrl()); + _materialResource->parsedMaterials = NetworkMaterialResource::parseJSONMaterials(QJsonDocument::fromJson(_materialData.toUtf8()), QUrl()); } else { qCDebug(material_baking) << "Downloading material" << _materialData; _materialResource = MaterialCache::instance().getMaterial(_materialData); @@ -71,47 +78,151 @@ void MaterialBaker::loadMaterial() { void MaterialBaker::processMaterial() { if (!_materialResource || _materialResource->parsedMaterials.networkMaterials.size() == 0) { handleError("Error processing " + _materialData); + return; } - _numTexturesToLoad = _materialResource->parsedMaterials.networkMaterials.size(); - _numTexturesLoaded = 0; + if (QDir(_textureOutputDir).exists()) { + qWarning() << "Output path" << _textureOutputDir << "already exists. Continuing."; + } else { + qCDebug(material_baking) << "Creating materialTextures output folder" << _textureOutputDir; + if (!QDir().mkpath(_textureOutputDir)) { + handleError("Failed to create materialTextures output folder " + _textureOutputDir); + } + } for (auto networkMaterial : _materialResource->parsedMaterials.networkMaterials) { if (networkMaterial.second) { auto textureMaps = networkMaterial.second->getTextureMaps(); for (auto textureMap : textureMaps) { if (textureMap.second && textureMap.second->getTextureSource()) { - auto texture = textureMap.second->getTextureSource(); graphics::Material::MapChannel mapChannel = textureMap.first; + auto texture = textureMap.second->getTextureSource(); - qDebug() << "boop" << mapChannel << texture->getUrl(); + QUrl url = texture->getUrl(); + QString cleanURL = url.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment).toDisplayString(); + auto idx = cleanURL.lastIndexOf('.'); + auto extension = idx >= 0 ? url.toDisplayString().mid(idx + 1).toLower() : ""; + + if (QImageReader::supportedImageFormats().contains(extension.toLatin1())) { + QUrl textureURL = url.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); + + // FIXME: this isn't properly handling bumpMaps or glossMaps + static const std::unordered_map MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP { + { graphics::Material::MapChannel::EMISSIVE_MAP, image::TextureUsage::EMISSIVE_TEXTURE }, + { graphics::Material::MapChannel::ALBEDO_MAP, image::TextureUsage::ALBEDO_TEXTURE }, + { graphics::Material::MapChannel::METALLIC_MAP, image::TextureUsage::METALLIC_TEXTURE }, + { graphics::Material::MapChannel::ROUGHNESS_MAP, image::TextureUsage::ROUGHNESS_TEXTURE }, + { graphics::Material::MapChannel::NORMAL_MAP, image::TextureUsage::NORMAL_TEXTURE }, + { graphics::Material::MapChannel::OCCLUSION_MAP, image::TextureUsage::OCCLUSION_TEXTURE }, + { graphics::Material::MapChannel::LIGHTMAP_MAP, image::TextureUsage::LIGHTMAP_TEXTURE }, + { graphics::Material::MapChannel::SCATTERING_MAP, image::TextureUsage::SCATTERING_TEXTURE } + }; + + auto it = MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP.find(mapChannel); + if (it == MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP.end()) { + handleError("Unknown map channel"); + return; + } + + QPair textureKey = { textureURL, it->second }; + if (!_textureBakers.contains(textureKey)) { + QSharedPointer textureBaker { + new TextureBaker(textureURL, it->second, _textureOutputDir), + &TextureBaker::deleteLater + }; + textureBaker->setMapChannel(mapChannel); + connect(textureBaker.data(), &TextureBaker::finished, this, &MaterialBaker::handleFinishedTextureBaker); + _textureBakers.insert(textureKey, textureBaker); + textureBaker->moveToThread(_getNextOvenWorkerThreadOperator ? _getNextOvenWorkerThreadOperator() : thread()); + QMetaObject::invokeMethod(textureBaker.data(), "bake"); + } + _materialsNeedingRewrite.insert(textureKey, networkMaterial.second); + } else { + qCDebug(material_baking) << "Texture extension not supported: " << extension; + } } } } } + + if (_textureBakers.empty()) { + outputMaterial(); + } +} + +void MaterialBaker::handleFinishedTextureBaker() { + auto baker = qobject_cast(sender()); + + if (baker) { + QPair textureKey = { baker->getTextureURL(), baker->getTextureType() }; + if (!baker->hasErrors()) { + // this TextureBaker is done and everything went according to plan + qCDebug(material_baking) << "Re-writing texture references to" << baker->getTextureURL(); + + auto newURL = QUrl(_textureOutputDir).resolved(baker->getMetaTextureFileName()); + + // Replace the old texture URLs + for (auto networkMaterial : _materialsNeedingRewrite.values(textureKey)) { + networkMaterial->getTextureMap(baker->getMapChannel())->getTextureSource()->setUrl(newURL); + } + } else { + // this texture failed to bake - this doesn't fail the entire bake but we need to add the errors from + // the texture to our warnings + _warningList << baker->getWarnings(); + } + + _materialsNeedingRewrite.remove(textureKey); + _textureBakers.remove(textureKey); + + if (_textureBakers.empty()) { + outputMaterial(); + } + } } void MaterialBaker::outputMaterial() { - //if (_isURL) { - // auto fileName = _materialData; - // auto baseName = fileName.left(fileName.lastIndexOf('.')); - // auto bakedFilename = baseName + BAKED_MATERIAL_EXTENSION; + if (_materialResource) { + QJsonDocument json; + if (_materialResource->parsedMaterials.networkMaterials.size() == 1) { + auto networkMaterial = _materialResource->parsedMaterials.networkMaterials.begin(); + auto scriptableMaterial = scriptable::ScriptableMaterial(networkMaterial->second); + QVariant materialVariant = scriptable::scriptableMaterialToScriptValue(&_scriptEngine, scriptableMaterial).toVariant(); + json = QJsonDocument::fromVariant(materialVariant); + } else { + QJsonArray materialArray; + for (auto networkMaterial : _materialResource->parsedMaterials.networkMaterials) { + auto scriptableMaterial = scriptable::ScriptableMaterial(networkMaterial.second); + QVariant materialVariant = scriptable::scriptableMaterialToScriptValue(&_scriptEngine, scriptableMaterial).toVariant(); + materialArray.append(QJsonDocument::fromVariant(materialVariant).object()); + } + json.setArray(materialArray); + } - // _bakedMaterialData = _bakedOutputDir + "/" + bakedFilename; + QByteArray outputMaterial = json.toJson(QJsonDocument::Compact); + if (_isURL) { + auto fileName = QUrl(_materialData).fileName(); + auto baseName = fileName.left(fileName.lastIndexOf('.')); + auto bakedFilename = baseName + BAKED_MATERIAL_EXTENSION; - // QFile bakedFile; - // bakedFile.setFileName(_bakedMaterialData); - // if (!bakedFile.open(QIODevice::WriteOnly)) { - // handleError("Error opening " + _bakedMaterialData + " for writing"); - // return; - // } + _bakedMaterialData = _bakedOutputDir + "/" + bakedFilename; - // bakedFile.write(outputMaterial); + QFile bakedFile; + bakedFile.setFileName(_bakedMaterialData); + if (!bakedFile.open(QIODevice::WriteOnly)) { + handleError("Error opening " + _bakedMaterialData + " for writing"); + return; + } - // // Export successful - // _outputFiles.push_back(_bakedMaterialData); - // qCDebug(material_baking) << "Exported" << _materialData << "to" << _bakedMaterialData; - //} + bakedFile.write(outputMaterial); + + // Export successful + _outputFiles.push_back(_bakedMaterialData); + qCDebug(material_baking) << "Exported" << _materialData << "to" << _bakedMaterialData; + } else { + _bakedMaterialData = QString(outputMaterial); + qCDebug(material_baking) << "Converted" << _materialData << "to" << _bakedMaterialData; + } + } // emit signal to indicate the material baking is finished emit finished(); diff --git a/libraries/baking/src/MaterialBaker.h b/libraries/baking/src/MaterialBaker.h index 6113515b81..b1678e5634 100644 --- a/libraries/baking/src/MaterialBaker.h +++ b/libraries/baking/src/MaterialBaker.h @@ -29,6 +29,8 @@ public: bool isURL() const { return _isURL; } QString getBakedMaterialData() const { return _bakedMaterialData; } + static void setNextOvenWorkerThreadOperator(std::function getNextOvenWorkerThreadOperator) { _getNextOvenWorkerThreadOperator = getNextOvenWorkerThreadOperator; } + public slots: virtual void bake() override; @@ -38,6 +40,7 @@ signals: private slots: void processMaterial(); void outputMaterial(); + void handleFinishedTextureBaker(); private: void loadMaterial(); @@ -46,13 +49,16 @@ private: bool _isURL; NetworkMaterialResourcePointer _materialResource; - size_t _numTexturesToLoad { 0 }; - size_t _numTexturesLoaded { 0 }; - QHash> _textureBakers; + QHash, QSharedPointer> _textureBakers; + QMultiHash, std::shared_ptr> _materialsNeedingRewrite; QString _bakedOutputDir; + QString _textureOutputDir; QString _bakedMaterialData; + + QScriptEngine _scriptEngine; + static std::function _getNextOvenWorkerThreadOperator; }; #endif // !hifi_MaterialBaker_h diff --git a/libraries/baking/src/TextureBaker.cpp b/libraries/baking/src/TextureBaker.cpp index 6407ce1846..db54cbdf98 100644 --- a/libraries/baking/src/TextureBaker.cpp +++ b/libraries/baking/src/TextureBaker.cpp @@ -128,7 +128,14 @@ void TextureBaker::processTexture() { TextureMeta meta; - auto originalCopyFilePath = _outputDirectory.absoluteFilePath(_textureURL.fileName()); + // If two textures have the same URL but are used differently, we need to process them separately + QString addMapChannel = QString::fromStdString("_" + std::to_string(_textureType)); + _baseFilename += addMapChannel; + + QString newFilename = _textureURL.fileName(); + newFilename.replace(QString("."), addMapChannel + "."); + QString originalCopyFilePath = _outputDirectory.absoluteFilePath(newFilename); + { QFile file { originalCopyFilePath }; if (!file.open(QIODevice::WriteOnly) || file.write(_originalTexture) == -1) { @@ -138,7 +145,7 @@ void TextureBaker::processTexture() { // IMPORTANT: _originalTexture is empty past this point _originalTexture.clear(); _outputFiles.push_back(originalCopyFilePath); - meta.original = _metaTexturePathPrefix + _textureURL.fileName(); + meta.original = _metaTexturePathPrefix + newFilename; } auto buffer = std::static_pointer_cast(std::make_shared(originalCopyFilePath)); diff --git a/libraries/baking/src/TextureBaker.h b/libraries/baking/src/TextureBaker.h index c8c4fb73b8..84e7c57aa1 100644 --- a/libraries/baking/src/TextureBaker.h +++ b/libraries/baking/src/TextureBaker.h @@ -22,6 +22,8 @@ #include "Baker.h" +#include + extern const QString BAKED_TEXTURE_KTX_EXT; extern const QString BAKED_META_TEXTURE_SUFFIX; @@ -43,6 +45,10 @@ public: static void setCompressionEnabled(bool enabled) { _compressionEnabled = enabled; } + void setMapChannel(graphics::Material::MapChannel mapChannel) { _mapChannel = mapChannel; } + graphics::Material::MapChannel getMapChannel() const { return _mapChannel; } + image::TextureUsage::Type getTextureType() const { return _textureType; } + public slots: virtual void bake() override; virtual void abort() override; @@ -60,6 +66,8 @@ private: QUrl _textureURL; QByteArray _originalTexture; image::TextureUsage::Type _textureType; + graphics::Material::MapChannel _mapChannel; + bool _mapChannelSet { false }; QString _baseFilename; QDir _outputDirectory; diff --git a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp index 1fd7ad9df5..3bd4af601c 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp +++ b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp @@ -363,56 +363,58 @@ namespace scriptable { obj.setProperty("name", material.name); obj.setProperty("model", material.model); + bool hasPropertyFallthroughs = !material.propertyFallthroughs.empty(); + const QScriptValue FALLTHROUGH("fallthrough"); - if (material.propertyFallthroughs.at(graphics::MaterialKey::OPACITY_VAL_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::OPACITY_VAL_BIT)) { obj.setProperty("opacity", FALLTHROUGH); } else if (material.key.isTranslucentFactor()) { obj.setProperty("opacity", material.opacity); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::GLOSSY_VAL_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::GLOSSY_VAL_BIT)) { obj.setProperty("roughness", FALLTHROUGH); } else if (material.key.isGlossy()) { obj.setProperty("roughness", material.roughness); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_VAL_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_VAL_BIT)) { obj.setProperty("metallic", FALLTHROUGH); } else if (material.key.isMetallic()) { obj.setProperty("metallic", material.metallic); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_VAL_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_VAL_BIT)) { obj.setProperty("scattering", FALLTHROUGH); } else if (material.key.isScattering()) { obj.setProperty("scattering", material.scattering); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::UNLIT_VAL_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::UNLIT_VAL_BIT)) { obj.setProperty("unlit", FALLTHROUGH); } else if (material.key.isUnlit()) { obj.setProperty("unlit", material.unlit); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::EMISSIVE_VAL_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::EMISSIVE_VAL_BIT)) { obj.setProperty("emissive", FALLTHROUGH); } else if (material.key.isEmissive()) { obj.setProperty("emissive", vec3ColorToScriptValue(engine, material.emissive)); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::ALBEDO_VAL_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::ALBEDO_VAL_BIT)) { obj.setProperty("albedo", FALLTHROUGH); } else if (material.key.isAlbedo()) { obj.setProperty("albedo", vec3ColorToScriptValue(engine, material.albedo)); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::EMISSIVE_MAP_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::EMISSIVE_MAP_BIT)) { obj.setProperty("emissiveMap", FALLTHROUGH); } else if (!material.emissiveMap.isEmpty()) { obj.setProperty("emissiveMap", material.emissiveMap); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::ALBEDO_MAP_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::ALBEDO_MAP_BIT)) { obj.setProperty("albedoMap", FALLTHROUGH); } else if (!material.albedoMap.isEmpty()) { obj.setProperty("albedoMap", material.albedoMap); @@ -422,26 +424,26 @@ namespace scriptable { obj.setProperty("opacityMap", material.opacityMap); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::OCCLUSION_MAP_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::OCCLUSION_MAP_BIT)) { obj.setProperty("occlusionMap", FALLTHROUGH); } else if (!material.occlusionMap.isEmpty()) { obj.setProperty("occlusionMap", material.occlusionMap); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::LIGHTMAP_MAP_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::LIGHTMAP_MAP_BIT)) { obj.setProperty("lightmapMap", FALLTHROUGH); } else if (!material.lightmapMap.isEmpty()) { obj.setProperty("lightmapMap", material.lightmapMap); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_MAP_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_MAP_BIT)) { obj.setProperty("scatteringMap", FALLTHROUGH); } else if (!material.scatteringMap.isEmpty()) { obj.setProperty("scatteringMap", material.scatteringMap); } // Only set one of each of these - if (material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_MAP_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_MAP_BIT)) { obj.setProperty("metallicMap", FALLTHROUGH); } else if (!material.metallicMap.isEmpty()) { obj.setProperty("metallicMap", material.metallicMap); @@ -449,7 +451,7 @@ namespace scriptable { obj.setProperty("specularMap", material.specularMap); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::ROUGHNESS_MAP_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::ROUGHNESS_MAP_BIT)) { obj.setProperty("roughnessMap", FALLTHROUGH); } else if (!material.roughnessMap.isEmpty()) { obj.setProperty("roughnessMap", material.roughnessMap); @@ -457,7 +459,7 @@ namespace scriptable { obj.setProperty("glossMap", material.glossMap); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::NORMAL_MAP_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::NORMAL_MAP_BIT)) { obj.setProperty("normalMap", FALLTHROUGH); } else if (!material.normalMap.isEmpty()) { obj.setProperty("normalMap", material.normalMap); @@ -466,16 +468,16 @@ namespace scriptable { } // These need to be implemented, but set the fallthrough for now - if (material.propertyFallthroughs.at(graphics::Material::TEXCOORDTRANSFORM0)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::TEXCOORDTRANSFORM0)) { obj.setProperty("texCoordTransform0", FALLTHROUGH); } - if (material.propertyFallthroughs.at(graphics::Material::TEXCOORDTRANSFORM1)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::TEXCOORDTRANSFORM1)) { obj.setProperty("texCoordTransform1", FALLTHROUGH); } - if (material.propertyFallthroughs.at(graphics::Material::LIGHTMAP_PARAMS)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::LIGHTMAP_PARAMS)) { obj.setProperty("lightmapParams", FALLTHROUGH); } - if (material.propertyFallthroughs.at(graphics::Material::MATERIAL_PARAMS)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::MATERIAL_PARAMS)) { obj.setProperty("materialParams", FALLTHROUGH); } diff --git a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.h b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.h index a72c3be14b..267ba01041 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.h +++ b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.h @@ -103,6 +103,10 @@ private: }; +namespace scriptable { + QScriptValue scriptableMaterialToScriptValue(QScriptEngine* engine, const scriptable::ScriptableMaterial &material); +}; + Q_DECLARE_METATYPE(glm::uint32) Q_DECLARE_METATYPE(QVector) Q_DECLARE_METATYPE(NestableType) diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index ca5c9b85fe..a74e402b63 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -220,7 +220,7 @@ void DomainBaker::addScriptBaker(const QString& property, const QString& url, QJ // grab a clean version of the URL without a query or fragment QUrl scriptURL = QUrl(url).adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); - // setup a script baker for this URL, as long as we aren't baking a texture already + // setup a script baker for this URL, as long as we aren't baking a script already if (!_scriptBakers.contains(scriptURL)) { // setup a baker for this script @@ -255,7 +255,7 @@ void DomainBaker::addMaterialBaker(const QString& property, const QString& data, materialData = QUrl(data).adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment).toDisplayString(); } else { materialData = data; - } + } // setup a material baker for this URL, as long as we aren't baking a material already if (!_materialBakers.contains(materialData)) { @@ -280,7 +280,7 @@ void DomainBaker::addMaterialBaker(const QString& property, const QString& data, ++_totalNumberOfSubBakes; } - // add this QJsonValueRef to our multi hash so that it can re-write the texture URL + // add this QJsonValueRef to our multi hash so that it can re-write the material URL // to the baked version once the baker is complete _entitiesNeedingRewrite.insert(materialData, { property, jsonRef }); } @@ -389,7 +389,7 @@ void DomainBaker::enumerateEntities() { addMaterialBaker(MATERIAL_URL_KEY, entity[MATERIAL_URL_KEY].toString(), true, *it); } if (entity.contains(MATERIAL_DATA_KEY)) { - addMaterialBaker(MATERIAL_DATA_KEY, entity[MATERIAL_URL_KEY].toString(), false, *it); + addMaterialBaker(MATERIAL_DATA_KEY, entity[MATERIAL_DATA_KEY].toString(), false, *it); } } } @@ -533,11 +533,11 @@ void DomainBaker::handleFinishedTextureBaker() { // drop our shared pointer to this baker so that it gets cleaned up _textureBakers.remove(baker->getTextureURL()); - // emit progress to tell listeners how many textures we have baked - emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes); + // emit progress to tell listeners how many textures we have baked + emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes); - // check if this was the last texture we needed to re-write and if we are done now - checkIfRewritingComplete(); + // check if this was the last texture we needed to re-write and if we are done now + checkIfRewritingComplete(); } } diff --git a/tools/oven/src/Oven.cpp b/tools/oven/src/Oven.cpp index 6fdc45f6eb..c70ca27d8b 100644 --- a/tools/oven/src/Oven.cpp +++ b/tools/oven/src/Oven.cpp @@ -20,6 +20,10 @@ #include #include #include +#include +#include + +#include "MaterialBaker.h" Oven* Oven::_staticInstance { nullptr }; @@ -33,6 +37,12 @@ Oven::Oven() { DependencyManager::set(); DependencyManager::set(false); DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + + MaterialBaker::setNextOvenWorkerThreadOperator([] { + return Oven::instance().getNextWorkerThread(); + }); } Oven::~Oven() { From 168e47aa62fb790c4ea87aeaa2d1f5fdf0a4e518 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 28 Feb 2019 09:59:56 -0800 Subject: [PATCH 049/446] bake particles and polylines --- libraries/baking/src/MaterialBaker.cpp | 8 ++++---- tools/oven/src/DomainBaker.cpp | 10 +++++++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp index 054d1ed0fd..558adedf68 100644 --- a/libraries/baking/src/MaterialBaker.cpp +++ b/libraries/baking/src/MaterialBaker.cpp @@ -182,12 +182,12 @@ void MaterialBaker::handleFinishedTextureBaker() { void MaterialBaker::outputMaterial() { if (_materialResource) { - QJsonDocument json; + QJsonObject json; if (_materialResource->parsedMaterials.networkMaterials.size() == 1) { auto networkMaterial = _materialResource->parsedMaterials.networkMaterials.begin(); auto scriptableMaterial = scriptable::ScriptableMaterial(networkMaterial->second); QVariant materialVariant = scriptable::scriptableMaterialToScriptValue(&_scriptEngine, scriptableMaterial).toVariant(); - json = QJsonDocument::fromVariant(materialVariant); + json.insert("materials", QJsonDocument::fromVariant(materialVariant).object()); } else { QJsonArray materialArray; for (auto networkMaterial : _materialResource->parsedMaterials.networkMaterials) { @@ -195,10 +195,10 @@ void MaterialBaker::outputMaterial() { QVariant materialVariant = scriptable::scriptableMaterialToScriptValue(&_scriptEngine, scriptableMaterial).toVariant(); materialArray.append(QJsonDocument::fromVariant(materialVariant).object()); } - json.setArray(materialArray); + json.insert("materials", materialArray); } - QByteArray outputMaterial = json.toJson(QJsonDocument::Compact); + QByteArray outputMaterial = QJsonDocument(json).toJson(QJsonDocument::Compact); if (_isURL) { auto fileName = QUrl(_materialData).fileName(); auto baseName = fileName.left(fileName.lastIndexOf('.')); diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index a74e402b63..42dfe59241 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -288,6 +288,8 @@ void DomainBaker::addMaterialBaker(const QString& property, const QString& data, // All the Entity Properties that can be baked // *************************************************************************************** +const QString TYPE_KEY = "type"; + // Models const QString MODEL_URL_KEY = "modelURL"; const QString COMPOUND_SHAPE_URL_KEY = "compoundShapeURL"; @@ -349,7 +351,13 @@ void DomainBaker::enumerateEntities() { // Textures if (entity.contains(TEXTURES_KEY)) { - // TODO: the textures property is treated differently for different entity types + if (entity.contains(TYPE_KEY)) { + QString type = entity[TYPE_KEY].toString(); + // TODO: handle textures for model entities + if (type == "ParticleEffect" || type == "PolyLine") { + addTextureBaker(TEXTURES_KEY, entity[TEXTURES_KEY].toString(), image::TextureUsage::DEFAULT_TEXTURE, *it); + } + } } if (entity.contains(IMAGE_URL_KEY)) { addTextureBaker(IMAGE_URL_KEY, entity[IMAGE_URL_KEY].toString(), image::TextureUsage::DEFAULT_TEXTURE, *it); From 82382fe9a1eac467495c5fd1e7e5b785ba41f425 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Thu, 21 Feb 2019 16:10:40 -0800 Subject: [PATCH 050/446] Use hifi types consistently in model-baker --- libraries/model-baker/src/model-baker/Baker.cpp | 6 ++---- libraries/model-baker/src/model-baker/Baker.h | 6 ++---- libraries/model-baker/src/model-baker/PrepareJointsTask.cpp | 4 ++-- libraries/model-baker/src/model-baker/PrepareJointsTask.h | 5 ++--- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp index fc27756877..4d740f4a94 100644 --- a/libraries/model-baker/src/model-baker/Baker.cpp +++ b/libraries/model-baker/src/model-baker/Baker.cpp @@ -11,8 +11,6 @@ #include "Baker.h" -#include - #include "BakerTypes.h" #include "ModelMath.h" #include "BuildGraphicsMeshTask.h" @@ -118,7 +116,7 @@ namespace baker { class BakerEngineBuilder { public: - using Input = VaryingSet2; + using Input = VaryingSet2; using Output = VaryingSet2; using JobModel = Task::ModelIO; void build(JobModel& model, const Varying& input, Varying& output) { @@ -170,7 +168,7 @@ namespace baker { } }; - Baker::Baker(const hfm::Model::Pointer& hfmModel, const QVariantHash& mapping) : + Baker::Baker(const hfm::Model::Pointer& hfmModel, const hifi::VariantHash& mapping) : _engine(std::make_shared(BakerEngineBuilder::JobModel::create("Baker"), std::make_shared())) { _engine->feedInput(0, hfmModel); _engine->feedInput(1, mapping); diff --git a/libraries/model-baker/src/model-baker/Baker.h b/libraries/model-baker/src/model-baker/Baker.h index 1880aba618..e8a97b863d 100644 --- a/libraries/model-baker/src/model-baker/Baker.h +++ b/libraries/model-baker/src/model-baker/Baker.h @@ -12,8 +12,7 @@ #ifndef hifi_baker_Baker_h #define hifi_baker_Baker_h -#include - +#include #include #include "Engine.h" @@ -23,7 +22,7 @@ namespace baker { class Baker { public: - Baker(const hfm::Model::Pointer& hfmModel, const QVariantHash& mapping); + Baker(const hfm::Model::Pointer& hfmModel, const hifi::VariantHash& mapping); void run(); @@ -34,7 +33,6 @@ namespace baker { protected: EnginePointer _engine; }; - }; #endif //hifi_baker_Baker_h diff --git a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp index 63d0408337..e5a2079d3f 100644 --- a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp +++ b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp @@ -13,7 +13,7 @@ #include "ModelBakerLogging.h" -QMap getJointNameMapping(const QVariantHash& mapping) { +QMap getJointNameMapping(const hifi::VariantHash& mapping) { static const QString JOINT_NAME_MAPPING_FIELD = "jointMap"; QMap hfmToHifiJointNameMap; if (!mapping.isEmpty() && mapping.contains(JOINT_NAME_MAPPING_FIELD) && mapping[JOINT_NAME_MAPPING_FIELD].type() == QVariant::Hash) { @@ -26,7 +26,7 @@ QMap getJointNameMapping(const QVariantHash& mapping) { return hfmToHifiJointNameMap; } -QMap getJointRotationOffsets(const QVariantHash& mapping) { +QMap getJointRotationOffsets(const hifi::VariantHash& mapping) { QMap jointRotationOffsets; static const QString JOINT_ROTATION_OFFSET_FIELD = "jointRotationOffset"; if (!mapping.isEmpty() && mapping.contains(JOINT_ROTATION_OFFSET_FIELD) && mapping[JOINT_ROTATION_OFFSET_FIELD].type() == QVariant::Hash) { diff --git a/libraries/model-baker/src/model-baker/PrepareJointsTask.h b/libraries/model-baker/src/model-baker/PrepareJointsTask.h index 6185d2fdad..0dbb9d584d 100644 --- a/libraries/model-baker/src/model-baker/PrepareJointsTask.h +++ b/libraries/model-baker/src/model-baker/PrepareJointsTask.h @@ -12,8 +12,7 @@ #ifndef hifi_PrepareJointsTask_h #define hifi_PrepareJointsTask_h -#include - +#include #include #include "Engine.h" @@ -29,7 +28,7 @@ public: class PrepareJointsTask { public: using Config = PrepareJointsTaskConfig; - using Input = baker::VaryingSet2, QVariantHash /*mapping*/>; + using Input = baker::VaryingSet2, hifi::VariantHash /*mapping*/>; using Output = baker::VaryingSet3, QMap /*jointRotationOffsets*/, QHash /*jointIndices*/>; using JobModel = baker::Job::ModelIO; From 1576125c4271839d6f88c025ee5cd8fd9fefb747 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Thu, 21 Feb 2019 15:36:31 -0800 Subject: [PATCH 051/446] Integrate HFM Asset Engine (aka model prep step) into Oven Add 'deduplicateIndices' parameter to FBXSerializer and make deduplicate a required parameter for extractMesh Add draco mesh and FBX draco node version Support generating/saving draco meshes from FBX Model nodes --- libraries/baking/CMakeLists.txt | 2 +- libraries/baking/src/FBXBaker.cpp | 272 +++++-------- libraries/baking/src/FBXBaker.h | 19 +- libraries/baking/src/ModelBaker.cpp | 371 ++++++++++-------- libraries/baking/src/ModelBaker.h | 12 +- libraries/baking/src/OBJBaker.cpp | 206 +++------- libraries/baking/src/OBJBaker.h | 16 +- libraries/baking/src/baking/BakerLibrary.cpp | 2 +- libraries/fbx/src/FBX.h | 3 + libraries/fbx/src/FBXSerializer.cpp | 14 +- libraries/fbx/src/FBXSerializer.h | 2 +- libraries/fbx/src/FBXSerializer_Mesh.cpp | 30 +- libraries/hfm/src/hfm/HFM.h | 3 + .../model-baker/src/model-baker/Baker.cpp | 25 +- libraries/model-baker/src/model-baker/Baker.h | 5 + .../src/model-baker/BuildDracoMeshTask.cpp | 233 +++++++++++ .../src/model-baker/BuildDracoMeshTask.h | 39 ++ .../src/model-baker/PrepareJointsTask.h | 4 +- .../src/model-networking/ModelCache.cpp | 1 + tools/oven/CMakeLists.txt | 2 +- tools/oven/src/Oven.cpp | 9 + tools/vhacd-util/src/VHACDUtil.cpp | 6 +- 22 files changed, 744 insertions(+), 532 deletions(-) create mode 100644 libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp create mode 100644 libraries/model-baker/src/model-baker/BuildDracoMeshTask.h diff --git a/libraries/baking/CMakeLists.txt b/libraries/baking/CMakeLists.txt index 38b6268fb7..aeb4346f93 100644 --- a/libraries/baking/CMakeLists.txt +++ b/libraries/baking/CMakeLists.txt @@ -1,7 +1,7 @@ set(TARGET_NAME baking) setup_hifi_library(Concurrent) -link_hifi_libraries(shared shaders graphics networking material-networking graphics-scripting ktx image fbx) +link_hifi_libraries(shared shaders graphics networking material-networking graphics-scripting ktx image fbx model-baker task) include_hifi_library_headers(gpu) include_hifi_library_headers(hfm) diff --git a/libraries/baking/src/FBXBaker.cpp b/libraries/baking/src/FBXBaker.cpp index 7c4354a2b6..e1bb86d051 100644 --- a/libraries/baking/src/FBXBaker.cpp +++ b/libraries/baking/src/FBXBaker.cpp @@ -37,24 +37,9 @@ #include "FBXToJSON.h" #endif -void FBXBaker::bake() { - qDebug() << "FBXBaker" << _modelURL << "bake starting"; - - // Setup the output folders for the results of this bake - initializeOutputDirs(); - - if (shouldStop()) { - return; - } - - connect(this, &FBXBaker::sourceCopyReadyToLoad, this, &FBXBaker::bakeSourceCopy); - - // make a local copy of the FBX file - loadSourceFBX(); -} - -void FBXBaker::bakeSourceCopy() { - // load the scene from the FBX file +void FBXBaker::bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) { + _hfmModel = hfmModel; + // Load the root node from the FBX file importScene(); if (shouldStop()) { @@ -68,94 +53,7 @@ void FBXBaker::bakeSourceCopy() { return; } - rewriteAndBakeSceneModels(); - - if (shouldStop()) { - return; - } - - // check if we're already done with textures (in case we had none to re-write) - checkIfTexturesFinished(); -} - -void FBXBaker::loadSourceFBX() { - // check if the FBX is local or first needs to be downloaded - if (_modelURL.isLocalFile()) { - // load up the local file - QFile localFBX { _modelURL.toLocalFile() }; - - qDebug() << "Local file url: " << _modelURL << _modelURL.toString() << _modelURL.toLocalFile() << ", copying to: " << _originalModelFilePath; - - if (!localFBX.exists()) { - //QMessageBox::warning(this, "Could not find " + _fbxURL.toString(), ""); - handleError("Could not find " + _modelURL.toString()); - return; - } - - // make a copy in the output folder - if (!_originalOutputDir.isEmpty()) { - qDebug() << "Copying to: " << _originalOutputDir << "/" << _modelURL.fileName(); - localFBX.copy(_originalOutputDir + "/" + _modelURL.fileName()); - } - - localFBX.copy(_originalModelFilePath); - - // emit our signal to start the import of the FBX source copy - emit sourceCopyReadyToLoad(); - } else { - // remote file, kick off a download - auto& networkAccessManager = NetworkAccessManager::getInstance(); - - QNetworkRequest networkRequest; - - // setup the request to follow re-directs and always hit the network - networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); - networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); - networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - - networkRequest.setUrl(_modelURL); - - qCDebug(model_baking) << "Downloading" << _modelURL; - auto networkReply = networkAccessManager.get(networkRequest); - - connect(networkReply, &QNetworkReply::finished, this, &FBXBaker::handleFBXNetworkReply); - } -} - -void FBXBaker::handleFBXNetworkReply() { - auto requestReply = qobject_cast(sender()); - - if (requestReply->error() == QNetworkReply::NoError) { - qCDebug(model_baking) << "Downloaded" << _modelURL; - - // grab the contents of the reply and make a copy in the output folder - QFile copyOfOriginal(_originalModelFilePath); - - qDebug(model_baking) << "Writing copy of original FBX to" << _originalModelFilePath << copyOfOriginal.fileName(); - - if (!copyOfOriginal.open(QIODevice::WriteOnly)) { - // add an error to the error list for this FBX stating that a duplicate of the original FBX could not be made - handleError("Could not create copy of " + _modelURL.toString() + " (Failed to open " + _originalModelFilePath + ")"); - return; - } - if (copyOfOriginal.write(requestReply->readAll()) == -1) { - handleError("Could not create copy of " + _modelURL.toString() + " (Failed to write)"); - return; - } - - // close that file now that we are done writing to it - copyOfOriginal.close(); - - if (!_originalOutputDir.isEmpty()) { - copyOfOriginal.copy(_originalOutputDir + "/" + _modelURL.fileName()); - } - - // emit our signal to start the import of the FBX source copy - emit sourceCopyReadyToLoad(); - } else { - // add an error to our list stating that the FBX could not be downloaded - handleError("Failed to download " + _modelURL.toString()); - } + rewriteAndBakeSceneModels(hfmModel->meshes, dracoMeshes, dracoMaterialLists); } void FBXBaker::importScene() { @@ -167,10 +65,8 @@ void FBXBaker::importScene() { return; } - FBXSerializer fbxSerializer; - qCDebug(model_baking) << "Parsing" << _modelURL; - _rootNode = fbxSerializer._rootNode = fbxSerializer.parseFBX(&fbxFile); + _rootNode = FBXSerializer().parseFBX(&fbxFile); #ifdef HIFI_DUMP_FBX { @@ -185,85 +81,113 @@ void FBXBaker::importScene() { } } #endif - - _hfmModel = fbxSerializer.extractHFMModel({}, _modelURL.toString()); - _textureContentMap = fbxSerializer._textureContent; } -void FBXBaker::rewriteAndBakeSceneModels() { - unsigned int meshIndex = 0; - bool hasDeformers { false }; - for (FBXNode& rootChild : _rootNode.children) { - if (rootChild.name == "Objects") { - for (FBXNode& objectChild : rootChild.children) { - if (objectChild.name == "Deformer") { - hasDeformers = true; - break; - } +void FBXBaker::replaceMeshNodeWithDraco(FBXNode& meshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList) { + // Compress mesh information and store in dracoMeshNode + FBXNode dracoMeshNode; + bool success = buildDracoMeshNode(dracoMeshNode, dracoMeshBytes, dracoMaterialList); + + if (!success) { + return; + } else { + meshNode.children.push_back(dracoMeshNode); + + static const std::vector nodeNamesToDelete { + // Node data that is packed into the draco mesh + "Vertices", + "PolygonVertexIndex", + "LayerElementNormal", + "LayerElementColor", + "LayerElementUV", + "LayerElementMaterial", + "LayerElementTexture", + + // Node data that we don't support + "Edges", + "LayerElementTangent", + "LayerElementBinormal", + "LayerElementSmoothing" + }; + auto& children = meshNode.children; + auto it = children.begin(); + while (it != children.end()) { + auto begin = nodeNamesToDelete.begin(); + auto end = nodeNamesToDelete.end(); + if (find(begin, end, it->name) != end) { + it = children.erase(it); + } else { + ++it; } } - if (hasDeformers) { - break; - } } +} + +void FBXBaker::rewriteAndBakeSceneModels(const QVector& meshes, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) { + std::vector meshIndexToRuntimeOrder; + auto meshCount = (int)meshes.size(); + meshIndexToRuntimeOrder.resize(meshCount); + for (int i = 0; i < meshCount; i++) { + meshIndexToRuntimeOrder[meshes[i].meshIndex] = i; + } + + // The meshIndex represents the order in which the meshes are loaded from the FBX file + // We replicate this order by iterating over the meshes in the same way that FBXSerializer does + int meshIndex = 0; for (FBXNode& rootChild : _rootNode.children) { if (rootChild.name == "Objects") { - for (FBXNode& objectChild : rootChild.children) { - if (objectChild.name == "Geometry") { + for (FBXNode& object : rootChild.children) { + if (object.name == "Geometry") { + if (object.properties.at(2) == "Mesh") { + int meshNum = meshIndexToRuntimeOrder[meshIndex]; + replaceMeshNodeWithDraco(object, dracoMeshes[meshNum], dracoMaterialLists[meshNum]); + meshIndex++; + } + } else if (object.name == "Model") { + for (FBXNode& modelChild : object.children) { + bool properties = false; + hifi::ByteArray propertyName; + int index; + if (modelChild.name == "Properties60") { + properties = true; + propertyName = "Property"; + index = 3; - // TODO Pull this out of _hfmModel instead so we don't have to reprocess it - auto extractedMesh = FBXSerializer::extractMesh(objectChild, meshIndex, false); - - // Callback to get MaterialID - GetMaterialIDCallback materialIDcallback = [&extractedMesh](int partIndex) { - return extractedMesh.partMaterialTextures[partIndex].first; - }; - - // Compress mesh information and store in dracoMeshNode - FBXNode dracoMeshNode; - bool success = compressMesh(extractedMesh.mesh, hasDeformers, dracoMeshNode, materialIDcallback); - - // if bake fails - return, if there were errors and continue, if there were warnings. - if (!success) { - if (hasErrors()) { - return; - } else if (hasWarnings()) { - continue; + } else if (modelChild.name == "Properties70") { + properties = true; + propertyName = "P"; + index = 4; } - } else { - objectChild.children.push_back(dracoMeshNode); - static const std::vector nodeNamesToDelete { - // Node data that is packed into the draco mesh - "Vertices", - "PolygonVertexIndex", - "LayerElementNormal", - "LayerElementColor", - "LayerElementUV", - "LayerElementMaterial", - "LayerElementTexture", - - // Node data that we don't support - "Edges", - "LayerElementTangent", - "LayerElementBinormal", - "LayerElementSmoothing" - }; - auto& children = objectChild.children; - auto it = children.begin(); - while (it != children.end()) { - auto begin = nodeNamesToDelete.begin(); - auto end = nodeNamesToDelete.end(); - if (find(begin, end, it->name) != end) { - it = children.erase(it); - } else { - ++it; + if (properties) { + // This is a properties node + // Remove the geometric transform because that has been applied directly to the vertices in FBXSerializer + static const QVariant GEOMETRIC_TRANSLATION = hifi::ByteArray("GeometricTranslation"); + static const QVariant GEOMETRIC_ROTATION = hifi::ByteArray("GeometricRotation"); + static const QVariant GEOMETRIC_SCALING = hifi::ByteArray("GeometricScaling"); + for (int i = 0; i < modelChild.children.size(); i++) { + const auto& prop = modelChild.children[i]; + const auto& propertyName = prop.properties.at(0); + if (propertyName == GEOMETRIC_TRANSLATION || + propertyName == GEOMETRIC_ROTATION || + propertyName == GEOMETRIC_SCALING) { + modelChild.children.removeAt(i); + --i; + } } + } else if (modelChild.name == "Vertices") { + // This model is also a mesh + int meshNum = meshIndexToRuntimeOrder[meshIndex]; + replaceMeshNodeWithDraco(object, dracoMeshes[meshNum], dracoMaterialLists[meshNum]); + meshIndex++; } } - } // Geometry Object + } - } // foreach root child + if (hasErrors()) { + return; + } + } } } } diff --git a/libraries/baking/src/FBXBaker.h b/libraries/baking/src/FBXBaker.h index 88443de1c0..7770e3014d 100644 --- a/libraries/baking/src/FBXBaker.h +++ b/libraries/baking/src/FBXBaker.h @@ -33,25 +33,16 @@ class FBXBaker : public ModelBaker { public: using ModelBaker::ModelBaker; -public slots: - virtual void bake() override; - -signals: - void sourceCopyReadyToLoad(); - -private slots: - void bakeSourceCopy(); - void handleFBXNetworkReply(); +protected: + virtual void bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) override; private: - void loadSourceFBX(); - void importScene(); - void embedTextureMetaData(); - void rewriteAndBakeSceneModels(); + void rewriteAndBakeSceneModels(const QVector& meshes, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists); void rewriteAndBakeSceneTextures(); + void replaceMeshNodeWithDraco(FBXNode& meshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList); - HFMModel* _hfmModel; + hfm::Model::Pointer _hfmModel; QHash _textureNameMatchCount; QHash _remappedTexturePaths; diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index 61eed9f655..6568850c1f 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -12,6 +12,13 @@ #include "ModelBaker.h" #include +#include + +#include +#include + +#include +#include #include @@ -31,6 +38,8 @@ #pragma warning( pop ) #endif +#include "baking/BakerLibrary.h" + ModelBaker::ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) : _modelURL(inputModelURL), @@ -65,6 +74,22 @@ ModelBaker::~ModelBaker() { } } +void ModelBaker::bake() { + qDebug() << "ModelBaker" << _modelURL << "bake starting"; + + // Setup the output folders for the results of this bake + initializeOutputDirs(); + + if (shouldStop()) { + return; + } + + connect(this, &ModelBaker::modelLoaded, this, &ModelBaker::bakeSourceCopy); + + // make a local copy of the model + saveSourceModel(); +} + void ModelBaker::initializeOutputDirs() { // Attempt to make the output folders // Warn if there is an output directory using the same name @@ -88,6 +113,166 @@ void ModelBaker::initializeOutputDirs() { } } +void ModelBaker::saveSourceModel() { + // check if the FBX is local or first needs to be downloaded + if (_modelURL.isLocalFile()) { + // load up the local file + QFile localModelURL { _modelURL.toLocalFile() }; + + qDebug() << "Local file url: " << _modelURL << _modelURL.toString() << _modelURL.toLocalFile() << ", copying to: " << _originalModelFilePath; + + if (!localModelURL.exists()) { + //QMessageBox::warning(this, "Could not find " + _fbxURL.toString(), ""); + handleError("Could not find " + _modelURL.toString()); + return; + } + + // make a copy in the output folder + if (!_originalOutputDir.isEmpty()) { + qDebug() << "Copying to: " << _originalOutputDir << "/" << _modelURL.fileName(); + localModelURL.copy(_originalOutputDir + "/" + _modelURL.fileName()); + } + + localModelURL.copy(_originalModelFilePath); + + // emit our signal to start the import of the FBX source copy + emit modelLoaded(); + } else { + // remote file, kick off a download + auto& networkAccessManager = NetworkAccessManager::getInstance(); + + QNetworkRequest networkRequest; + + // setup the request to follow re-directs and always hit the network + networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); + networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + + networkRequest.setUrl(_modelURL); + + qCDebug(model_baking) << "Downloading" << _modelURL; + auto networkReply = networkAccessManager.get(networkRequest); + + connect(networkReply, &QNetworkReply::finished, this, &ModelBaker::handleModelNetworkReply); + } +} + +void ModelBaker::handleModelNetworkReply() { + auto requestReply = qobject_cast(sender()); + + if (requestReply->error() == QNetworkReply::NoError) { + qCDebug(model_baking) << "Downloaded" << _modelURL; + + // grab the contents of the reply and make a copy in the output folder + QFile copyOfOriginal(_originalModelFilePath); + + qDebug(model_baking) << "Writing copy of original model file to" << _originalModelFilePath << copyOfOriginal.fileName(); + + if (!copyOfOriginal.open(QIODevice::WriteOnly)) { + // add an error to the error list for this model stating that a duplicate of the original model could not be made + handleError("Could not create copy of " + _modelURL.toString() + " (Failed to open " + _originalModelFilePath + ")"); + return; + } + if (copyOfOriginal.write(requestReply->readAll()) == -1) { + handleError("Could not create copy of " + _modelURL.toString() + " (Failed to write)"); + return; + } + + // close that file now that we are done writing to it + copyOfOriginal.close(); + + if (!_originalOutputDir.isEmpty()) { + copyOfOriginal.copy(_originalOutputDir + "/" + _modelURL.fileName()); + } + + // emit our signal to start the import of the model source copy + emit modelLoaded(); + } else { + // add an error to our list stating that the model could not be downloaded + handleError("Failed to download " + _modelURL.toString()); + } +} + +// TODO: Remove after testing +#include + +void ModelBaker::bakeSourceCopy() { + QFile modelFile(_originalModelFilePath); + if (!modelFile.open(QIODevice::ReadOnly)) { + handleError("Error opening " + _originalModelFilePath + " for reading"); + return; + } + hifi::ByteArray modelData = modelFile.readAll(); + + hfm::Model::Pointer bakedModel; + std::vector dracoMeshes; + std::vector> dracoMaterialLists; // Material order for per-mesh material lookup used by dracoMeshes + + { + auto serializer = DependencyManager::get()->getSerializerForMediaType(modelData, _modelURL, ""); + if (!serializer) { + handleError("Could not recognize file type of model file " + _originalModelFilePath); + return; + } + hifi::VariantHash mapping; + mapping["combineParts"] = true; // set true so that OBJSerializer reads material info from material library + hfm::Model::Pointer loadedModel = serializer->read(modelData, mapping, _modelURL); + + baker::Baker baker(loadedModel, mapping); + auto config = baker.getConfiguration(); + // Enable compressed draco mesh generation + config->getJobConfig("BuildDracoMesh")->setEnabled(true); + // Do not permit potentially lossy modification of joint data meant for runtime + ((PrepareJointsConfig*)config->getJobConfig("PrepareJoints"))->passthrough = true; + + // TODO: Remove after testing + { + auto* dracoConfig = ((BuildDracoMeshConfig*)config->getJobConfig("BuildDracoMesh")); + dracoConfig->encodeSpeed = 10; + dracoConfig->decodeSpeed = -1; + } + + // Begin hfm baking + baker.run(); + + bakedModel = baker.getHFMModel(); + dracoMeshes = baker.getDracoMeshes(); + dracoMaterialLists = baker.getDracoMaterialLists(); + } + + // Populate _textureContentMap with path to content mappings, for quick lookup by URL + for (auto materialIt = bakedModel->materials.cbegin(); materialIt != bakedModel->materials.cend(); materialIt++) { + static const auto addTexture = [](QHash& textureContentMap, const hfm::Texture& texture) { + if (!textureContentMap.contains(texture.filename)) { + // Content may be empty, unless the data is inlined + textureContentMap[texture.filename] = texture.content; + } + }; + const hfm::Material& material = *materialIt; + addTexture(_textureContentMap, material.normalTexture); + addTexture(_textureContentMap, material.albedoTexture); + addTexture(_textureContentMap, material.opacityTexture); + addTexture(_textureContentMap, material.glossTexture); + addTexture(_textureContentMap, material.roughnessTexture); + addTexture(_textureContentMap, material.specularTexture); + addTexture(_textureContentMap, material.metallicTexture); + addTexture(_textureContentMap, material.emissiveTexture); + addTexture(_textureContentMap, material.occlusionTexture); + addTexture(_textureContentMap, material.scatteringTexture); + addTexture(_textureContentMap, material.lightmapTexture); + } + + // Do format-specific baking + bakeProcessedSource(bakedModel, dracoMeshes, dracoMaterialLists); + + if (shouldStop()) { + return; + } + + // check if we're already done with textures (in case we had none to re-write) + checkIfTexturesFinished(); +} + void ModelBaker::abort() { Baker::abort(); @@ -98,176 +283,36 @@ void ModelBaker::abort() { } } -bool ModelBaker::compressMesh(HFMMesh& mesh, bool hasDeformers, FBXNode& dracoMeshNode, GetMaterialIDCallback materialIDCallback) { - if (mesh.wasCompressed) { - handleError("Cannot re-bake a file that contains compressed mesh"); +bool ModelBaker::buildDracoMeshNode(FBXNode& dracoMeshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList) { + if (dracoMeshBytes.isEmpty()) { + handleError("Failed to finalize the baking of a draco Geometry node"); return false; } - Q_ASSERT(mesh.normals.size() == 0 || mesh.normals.size() == mesh.vertices.size()); - Q_ASSERT(mesh.colors.size() == 0 || mesh.colors.size() == mesh.vertices.size()); - Q_ASSERT(mesh.texCoords.size() == 0 || mesh.texCoords.size() == mesh.vertices.size()); - - int64_t numTriangles{ 0 }; - for (auto& part : mesh.parts) { - if ((part.quadTrianglesIndices.size() % 3) != 0 || (part.triangleIndices.size() % 3) != 0) { - handleWarning("Found a mesh part with invalid index data, skipping"); - continue; - } - numTriangles += part.quadTrianglesIndices.size() / 3; - numTriangles += part.triangleIndices.size() / 3; - } - - if (numTriangles == 0) { - return false; - } - - draco::TriangleSoupMeshBuilder meshBuilder; - - meshBuilder.Start(numTriangles); - - bool hasNormals{ mesh.normals.size() > 0 }; - bool hasColors{ mesh.colors.size() > 0 }; - bool hasTexCoords{ mesh.texCoords.size() > 0 }; - bool hasTexCoords1{ mesh.texCoords1.size() > 0 }; - bool hasPerFaceMaterials = (materialIDCallback) ? (mesh.parts.size() > 1 || materialIDCallback(0) != 0 ) : true; - bool needsOriginalIndices{ hasDeformers }; - - int normalsAttributeID { -1 }; - int colorsAttributeID { -1 }; - int texCoordsAttributeID { -1 }; - int texCoords1AttributeID { -1 }; - int faceMaterialAttributeID { -1 }; - int originalIndexAttributeID { -1 }; - - const int positionAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::POSITION, - 3, draco::DT_FLOAT32); - if (needsOriginalIndices) { - originalIndexAttributeID = meshBuilder.AddAttribute( - (draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_ORIGINAL_INDEX, - 1, draco::DT_INT32); - } - - if (hasNormals) { - normalsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::NORMAL, - 3, draco::DT_FLOAT32); - } - if (hasColors) { - colorsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::COLOR, - 3, draco::DT_FLOAT32); - } - if (hasTexCoords) { - texCoordsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::TEX_COORD, - 2, draco::DT_FLOAT32); - } - if (hasTexCoords1) { - texCoords1AttributeID = meshBuilder.AddAttribute( - (draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_TEX_COORD_1, - 2, draco::DT_FLOAT32); - } - if (hasPerFaceMaterials) { - faceMaterialAttributeID = meshBuilder.AddAttribute( - (draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_MATERIAL_ID, - 1, draco::DT_UINT16); - } - - auto partIndex = 0; - draco::FaceIndex face; - uint16_t materialID; - - for (auto& part : mesh.parts) { - materialID = (materialIDCallback) ? materialIDCallback(partIndex) : partIndex; - - auto addFace = [&](QVector& indices, int index, draco::FaceIndex face) { - int32_t idx0 = indices[index]; - int32_t idx1 = indices[index + 1]; - int32_t idx2 = indices[index + 2]; - - if (hasPerFaceMaterials) { - meshBuilder.SetPerFaceAttributeValueForFace(faceMaterialAttributeID, face, &materialID); - } - - meshBuilder.SetAttributeValuesForFace(positionAttributeID, face, - &mesh.vertices[idx0], &mesh.vertices[idx1], - &mesh.vertices[idx2]); - - if (needsOriginalIndices) { - meshBuilder.SetAttributeValuesForFace(originalIndexAttributeID, face, - &mesh.originalIndices[idx0], - &mesh.originalIndices[idx1], - &mesh.originalIndices[idx2]); - } - if (hasNormals) { - meshBuilder.SetAttributeValuesForFace(normalsAttributeID, face, - &mesh.normals[idx0], &mesh.normals[idx1], - &mesh.normals[idx2]); - } - if (hasColors) { - meshBuilder.SetAttributeValuesForFace(colorsAttributeID, face, - &mesh.colors[idx0], &mesh.colors[idx1], - &mesh.colors[idx2]); - } - if (hasTexCoords) { - meshBuilder.SetAttributeValuesForFace(texCoordsAttributeID, face, - &mesh.texCoords[idx0], &mesh.texCoords[idx1], - &mesh.texCoords[idx2]); - } - if (hasTexCoords1) { - meshBuilder.SetAttributeValuesForFace(texCoords1AttributeID, face, - &mesh.texCoords1[idx0], &mesh.texCoords1[idx1], - &mesh.texCoords1[idx2]); - } - }; - - for (int i = 0; (i + 2) < part.quadTrianglesIndices.size(); i += 3) { - addFace(part.quadTrianglesIndices, i, face++); - } - - for (int i = 0; (i + 2) < part.triangleIndices.size(); i += 3) { - addFace(part.triangleIndices, i, face++); - } - - partIndex++; - } - - auto dracoMesh = meshBuilder.Finalize(); - - if (!dracoMesh) { - handleWarning("Failed to finalize the baking of a draco Geometry node"); - return false; - } - - // we need to modify unique attribute IDs for custom attributes - // so the attributes are easily retrievable on the other side - if (hasPerFaceMaterials) { - dracoMesh->attribute(faceMaterialAttributeID)->set_unique_id(DRACO_ATTRIBUTE_MATERIAL_ID); - } - - if (hasTexCoords1) { - dracoMesh->attribute(texCoords1AttributeID)->set_unique_id(DRACO_ATTRIBUTE_TEX_COORD_1); - } - - if (needsOriginalIndices) { - dracoMesh->attribute(originalIndexAttributeID)->set_unique_id(DRACO_ATTRIBUTE_ORIGINAL_INDEX); - } - - draco::Encoder encoder; - - encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 14); - encoder.SetAttributeQuantization(draco::GeometryAttribute::TEX_COORD, 12); - encoder.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 10); - encoder.SetSpeedOptions(0, 5); - - draco::EncoderBuffer buffer; - encoder.EncodeMeshToBuffer(*dracoMesh, &buffer); - FBXNode dracoNode; dracoNode.name = "DracoMesh"; - auto value = QVariant::fromValue(QByteArray(buffer.data(), (int)buffer.size())); - dracoNode.properties.append(value); + dracoNode.properties.append(QVariant::fromValue(dracoMeshBytes)); + // Additional draco mesh node information + { + FBXNode fbxVersionNode; + fbxVersionNode.name = "FBXDracoMeshVersion"; + fbxVersionNode.properties.append(FBX_DRACO_MESH_VERSION); + dracoNode.children.append(fbxVersionNode); + + FBXNode dracoVersionNode; + dracoVersionNode.name = "DracoMeshVersion"; + dracoVersionNode.properties.append(DRACO_MESH_VERSION); + dracoNode.children.append(dracoVersionNode); + + FBXNode materialListNode; + materialListNode.name = "MaterialList"; + for (const hifi::ByteArray& materialID : dracoMaterialList) { + materialListNode.properties.append(materialID); + } + dracoNode.children.append(materialListNode); + } dracoMeshNode = dracoNode; - // Mesh compression successful return true return true; } diff --git a/libraries/baking/src/ModelBaker.h b/libraries/baking/src/ModelBaker.h index 0f0cfbe07c..b0bd3798ff 100644 --- a/libraries/baking/src/ModelBaker.h +++ b/libraries/baking/src/ModelBaker.h @@ -47,17 +47,23 @@ public: void initializeOutputDirs(); - bool compressMesh(HFMMesh& mesh, bool hasDeformers, FBXNode& dracoMeshNode, GetMaterialIDCallback materialIDCallback = nullptr); + bool buildDracoMeshNode(FBXNode& dracoMeshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList); QString compressTexture(QString textureFileName, image::TextureUsage::Type = image::TextureUsage::Type::DEFAULT_TEXTURE); virtual void setWasAborted(bool wasAborted) override; QUrl getModelURL() const { return _modelURL; } QString getBakedModelFilePath() const { return _bakedModelFilePath; } +signals: + void modelLoaded(); + public slots: + virtual void bake() override; virtual void abort() override; protected: + void saveSourceModel(); + virtual void bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) = 0; void checkIfTexturesFinished(); void texturesFinished(); void embedTextureMetaData(); @@ -72,6 +78,10 @@ protected: QDir _modelTempDir; QString _originalModelFilePath; +protected slots: + void handleModelNetworkReply(); + virtual void bakeSourceCopy(); + private slots: void handleBakedTexture(); void handleAbortedTexture(); diff --git a/libraries/baking/src/OBJBaker.cpp b/libraries/baking/src/OBJBaker.cpp index 11cac0b4c2..ebc24201f4 100644 --- a/libraries/baking/src/OBJBaker.cpp +++ b/libraries/baking/src/OBJBaker.cpp @@ -35,150 +35,51 @@ const QByteArray CONNECTIONS_NODE_PROPERTY = "OO"; const QByteArray CONNECTIONS_NODE_PROPERTY_1 = "OP"; const QByteArray MESH = "Mesh"; -void OBJBaker::bake() { - qDebug() << "OBJBaker" << _modelURL << "bake starting"; - - // Setup the output folders for the results of this bake - initializeOutputDirs(); - - // trigger bakeOBJ once OBJ is loaded - connect(this, &OBJBaker::OBJLoaded, this, &OBJBaker::bakeOBJ); - - // make a local copy of the OBJ - loadOBJ(); -} - -void OBJBaker::loadOBJ() { - // check if the OBJ is local or it needs to be downloaded - if (_modelURL.isLocalFile()) { - // loading the local OBJ - QFile localOBJ { _modelURL.toLocalFile() }; - - qDebug() << "Local file url: " << _modelURL << _modelURL.toString() << _modelURL.toLocalFile() << ", copying to: " << _originalModelFilePath; - - if (!localOBJ.exists()) { - handleError("Could not find " + _modelURL.toString()); - return; - } - - // make a copy in the output folder - if (!_originalOutputDir.isEmpty()) { - qDebug() << "Copying to: " << _originalOutputDir << "/" << _modelURL.fileName(); - localOBJ.copy(_originalOutputDir + "/" + _modelURL.fileName()); - } - - localOBJ.copy(_originalModelFilePath); - - // local OBJ is loaded emit signal to trigger its baking - emit OBJLoaded(); - } else { - // OBJ is remote, start download - auto& networkAccessManager = NetworkAccessManager::getInstance(); - - QNetworkRequest networkRequest; - - // setup the request to follow re-directs and always hit the network - networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); - networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); - networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - networkRequest.setUrl(_modelURL); - - qCDebug(model_baking) << "Downloading" << _modelURL; - auto networkReply = networkAccessManager.get(networkRequest); - - connect(networkReply, &QNetworkReply::finished, this, &OBJBaker::handleOBJNetworkReply); - } -} - -void OBJBaker::handleOBJNetworkReply() { - auto requestReply = qobject_cast(sender()); - - if (requestReply->error() == QNetworkReply::NoError) { - qCDebug(model_baking) << "Downloaded" << _modelURL; - - // grab the contents of the reply and make a copy in the output folder - QFile copyOfOriginal(_originalModelFilePath); - - qDebug(model_baking) << "Writing copy of original obj to" << _originalModelFilePath << copyOfOriginal.fileName(); - - if (!copyOfOriginal.open(QIODevice::WriteOnly)) { - // add an error to the error list for this obj stating that a duplicate of the original obj could not be made - handleError("Could not create copy of " + _modelURL.toString() + " (Failed to open " + _originalModelFilePath + ")"); - return; - } - if (copyOfOriginal.write(requestReply->readAll()) == -1) { - handleError("Could not create copy of " + _modelURL.toString() + " (Failed to write)"); - return; - } - - // close that file now that we are done writing to it - copyOfOriginal.close(); - - if (!_originalOutputDir.isEmpty()) { - copyOfOriginal.copy(_originalOutputDir + "/" + _modelURL.fileName()); - } - - // remote OBJ is loaded emit signal to trigger its baking - emit OBJLoaded(); - } else { - // add an error to our list stating that the OBJ could not be downloaded - handleError("Failed to download " + _modelURL.toString()); - } -} - -void OBJBaker::bakeOBJ() { - // Read the OBJ file - QFile objFile(_originalModelFilePath); - if (!objFile.open(QIODevice::ReadOnly)) { - handleError("Error opening " + _originalModelFilePath + " for reading"); - return; - } - - QByteArray objData = objFile.readAll(); - - OBJSerializer serializer; - QVariantHash mapping; - mapping["combineParts"] = true; // set true so that OBJSerializer reads material info from material library - auto geometry = serializer.read(objData, mapping, _modelURL); - +void OBJBaker::bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) { // Write OBJ Data as FBX tree nodes - createFBXNodeTree(_rootNode, *geometry); - - checkIfTexturesFinished(); + createFBXNodeTree(_rootNode, hfmModel, dracoMeshes[0]); } -void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel) { +void OBJBaker::createFBXNodeTree(FBXNode& rootNode, const hfm::Model::Pointer& hfmModel, const hifi::ByteArray& dracoMesh) { + // Make all generated nodes children of rootNode + rootNode.children = { FBXNode(), FBXNode(), FBXNode() }; + FBXNode& globalSettingsNode = rootNode.children[0]; + FBXNode& objectNode = rootNode.children[1]; + FBXNode& connectionsNode = rootNode.children[2]; + // Generating FBX Header Node FBXNode headerNode; headerNode.name = FBX_HEADER_EXTENSION; // Generating global settings node // Required for Unit Scale Factor - FBXNode globalSettingsNode; globalSettingsNode.name = GLOBAL_SETTINGS_NODE_NAME; // Setting the tree hierarchy: GlobalSettings -> Properties70 -> P -> Properties - FBXNode properties70Node; - properties70Node.name = PROPERTIES70_NODE_NAME; - - FBXNode pNode; { - pNode.name = P_NODE_NAME; - pNode.properties.append({ - "UnitScaleFactor", "double", "Number", "", - UNIT_SCALE_FACTOR - }); + globalSettingsNode.children.push_back(FBXNode()); + FBXNode& properties70Node = globalSettingsNode.children.back(); + properties70Node.name = PROPERTIES70_NODE_NAME; + + FBXNode pNode; + { + pNode.name = P_NODE_NAME; + pNode.properties.append({ + "UnitScaleFactor", "double", "Number", "", + UNIT_SCALE_FACTOR + }); + } + properties70Node.children = { pNode }; + } - properties70Node.children = { pNode }; - globalSettingsNode.children = { properties70Node }; - // Generating Object node - FBXNode objectNode; objectNode.name = OBJECTS_NODE_NAME; + objectNode.children = { FBXNode(), FBXNode() }; + FBXNode& geometryNode = objectNode.children[0]; + FBXNode& modelNode = objectNode.children[1]; - // Generating Object node's child - Geometry node - FBXNode geometryNode; + // Generating Object node's child - Geometry node geometryNode.name = GEOMETRY_NODE_NAME; NodeID geometryID; { @@ -189,15 +90,8 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel) { MESH }; } - - // Compress the mesh information and store in dracoNode - bool hasDeformers = false; // No concept of deformers for an OBJ - FBXNode dracoNode; - compressMesh(hfmModel.meshes[0], hasDeformers, dracoNode); - geometryNode.children.append(dracoNode); - + // Generating Object node's child - Model node - FBXNode modelNode; modelNode.name = MODEL_NODE_NAME; NodeID modelID; { @@ -205,16 +99,14 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel) { modelNode.properties = { modelID, MODEL_NODE_NAME, MESH }; } - objectNode.children = { geometryNode, modelNode }; - // Generating Objects node's child - Material node - auto& meshParts = hfmModel.meshes[0].parts; + auto& meshParts = hfmModel->meshes[0].parts; for (auto& meshPart : meshParts) { FBXNode materialNode; materialNode.name = MATERIAL_NODE_NAME; - if (hfmModel.materials.size() == 1) { + if (hfmModel->materials.size() == 1) { // case when no material information is provided, OBJSerializer considers it as a single default material - for (auto& materialID : hfmModel.materials.keys()) { + for (auto& materialID : hfmModel->materials.keys()) { setMaterialNodeProperties(materialNode, materialID, hfmModel); } } else { @@ -224,12 +116,26 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel) { objectNode.children.append(materialNode); } + // Store the draco node containing the compressed mesh information, along with the per-meshPart material IDs the draco node references + // Because we redefine the material IDs when initializing the material nodes above, we pass that in for the material list + // The nth mesh part gets the nth material + { + std::vector newMaterialList; + newMaterialList.reserve(_materialIDs.size()); + for (auto materialID : _materialIDs) { + newMaterialList.push_back(hifi::ByteArray(std::to_string((int)materialID).c_str())); + } + FBXNode dracoNode; + buildDracoMeshNode(dracoNode, dracoMesh, newMaterialList); + geometryNode.children.append(dracoNode); + } + // Generating Texture Node // iterate through mesh parts and process the associated textures auto size = meshParts.size(); for (int i = 0; i < size; i++) { QString material = meshParts[i].materialID; - HFMMaterial currentMaterial = hfmModel.materials[material]; + HFMMaterial currentMaterial = hfmModel->materials[material]; if (!currentMaterial.albedoTexture.filename.isEmpty() || !currentMaterial.specularTexture.filename.isEmpty()) { auto textureID = nextNodeID(); _mapTextureMaterial.emplace_back(textureID, i); @@ -274,14 +180,15 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel) { } // Generating Connections node - FBXNode connectionsNode; connectionsNode.name = CONNECTIONS_NODE_NAME; - // connect Geometry to Model - FBXNode cNode; - cNode.name = C_NODE_NAME; - cNode.properties = { CONNECTIONS_NODE_PROPERTY, geometryID, modelID }; - connectionsNode.children = { cNode }; + // connect Geometry to Model + { + FBXNode cNode; + cNode.name = C_NODE_NAME; + cNode.properties = { CONNECTIONS_NODE_PROPERTY, geometryID, modelID }; + connectionsNode.children.push_back(cNode); + } // connect all materials to model for (auto& materialID : _materialIDs) { @@ -313,18 +220,15 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel) { }; connectionsNode.children.append(cDiffuseNode); } - - // Make all generated nodes children of rootNode - rootNode.children = { globalSettingsNode, objectNode, connectionsNode }; } // Set properties for material nodes -void OBJBaker::setMaterialNodeProperties(FBXNode& materialNode, QString material, HFMModel& hfmModel) { +void OBJBaker::setMaterialNodeProperties(FBXNode& materialNode, QString material, const hfm::Model::Pointer& hfmModel) { auto materialID = nextNodeID(); _materialIDs.push_back(materialID); materialNode.properties = { materialID, material, MESH }; - HFMMaterial currentMaterial = hfmModel.materials[material]; + HFMMaterial currentMaterial = hfmModel->materials[material]; // Setting the hierarchy: Material -> Properties70 -> P -> Properties FBXNode properties70Node; diff --git a/libraries/baking/src/OBJBaker.h b/libraries/baking/src/OBJBaker.h index 5aaae49d4a..d1eced5452 100644 --- a/libraries/baking/src/OBJBaker.h +++ b/libraries/baking/src/OBJBaker.h @@ -27,20 +27,12 @@ class OBJBaker : public ModelBaker { public: using ModelBaker::ModelBaker; -public slots: - virtual void bake() override; - -signals: - void OBJLoaded(); - -private slots: - void bakeOBJ(); - void handleOBJNetworkReply(); +protected: + virtual void bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) override; private: - void loadOBJ(); - void createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel); - void setMaterialNodeProperties(FBXNode& materialNode, QString material, HFMModel& hfmModel); + void createFBXNodeTree(FBXNode& rootNode, const hfm::Model::Pointer& hfmModel, const hifi::ByteArray& dracoMesh); + void setMaterialNodeProperties(FBXNode& materialNode, QString material, const hfm::Model::Pointer& hfmModel); NodeID nextNodeID() { return _nodeID++; } diff --git a/libraries/baking/src/baking/BakerLibrary.cpp b/libraries/baking/src/baking/BakerLibrary.cpp index af5e59ebbe..202fd4b3d8 100644 --- a/libraries/baking/src/baking/BakerLibrary.cpp +++ b/libraries/baking/src/baking/BakerLibrary.cpp @@ -69,4 +69,4 @@ std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, TextureB } return baker; -} \ No newline at end of file +} diff --git a/libraries/fbx/src/FBX.h b/libraries/fbx/src/FBX.h index 342d605337..362ae93e99 100644 --- a/libraries/fbx/src/FBX.h +++ b/libraries/fbx/src/FBX.h @@ -30,6 +30,9 @@ static const quint32 FBX_VERSION_2016 = 7500; static const int32_t FBX_PROPERTY_UNCOMPRESSED_FLAG = 0; static const int32_t FBX_PROPERTY_COMPRESSED_FLAG = 1; +// The version of the FBX node containing the draco mesh. See also: DRACO_MESH_VERSION in HFM.h +static const int FBX_DRACO_MESH_VERSION = 2; + class FBXNode; using FBXNodeList = QList; diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index d5a1f9a562..b4e95a8c2a 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -386,6 +386,8 @@ hifi::ByteArray fileOnUrl(const hifi::ByteArray& filepath, const QString& url) { HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const QString& url) { const FBXNode& node = _rootNode; + bool deduplicateIndices = mapping["deduplicateIndices"].toBool(); + QMap meshes; QHash modelIDsToNames; QHash meshIDsToMeshIndices; @@ -487,7 +489,7 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const foreach (const FBXNode& object, child.children) { if (object.name == "Geometry") { if (object.properties.at(2) == "Mesh") { - meshes.insert(getID(object.properties), extractMesh(object, meshIndex)); + meshes.insert(getID(object.properties), extractMesh(object, meshIndex, deduplicateIndices)); } else { // object.properties.at(2) == "Shape" ExtractedBlendshape extracted = { getID(object.properties), extractBlendshape(object) }; blendshapes.append(extracted); @@ -631,10 +633,10 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const } } } - } else if (subobject.name == "Vertices") { + } else if (subobject.name == "Vertices" || subobject.name == "DracoMesh") { // it's a mesh as well as a model mesh = &meshes[getID(object.properties)]; - *mesh = extractMesh(object, meshIndex); + *mesh = extractMesh(object, meshIndex, deduplicateIndices); } else if (subobject.name == "Shape") { ExtractedBlendshape blendshape = { subobject.properties.at(0).toString(), @@ -1386,9 +1388,9 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const // look for textures, material properties // allocate the Part material library + // NOTE: extracted.partMaterialTextures is empty for FBX_DRACO_MESH_VERSION >= 2. In that case, the mesh part's materialID string is already defined. int materialIndex = 0; int textureIndex = 0; - bool generateTangents = false; QList children = _connectionChildMap.values(modelID); for (int i = children.size() - 1; i >= 0; i--) { @@ -1401,12 +1403,10 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const if (extracted.partMaterialTextures.at(j).first == materialIndex) { HFMMeshPart& part = extracted.mesh.parts[j]; part.materialID = material.materialID; - generateTangents |= material.needTangentSpace(); } } materialIndex++; - } else if (_textureFilenames.contains(childID)) { // NOTE (Sabrina 2019/01/11): getTextures now takes in the materialID as a second parameter, because FBX material nodes can sometimes have uv transform information (ex: "Maya|uv_scale") // I'm leaving the second parameter blank right now as this code may never be used. @@ -1684,5 +1684,7 @@ HFMModel::Pointer FBXSerializer::read(const hifi::ByteArray& data, const hifi::V _rootNode = parseFBX(&buffer); + // FBXSerializer's mapping parameter supports the bool "deduplicateIndices," which is passed into FBXSerializer::extractMesh as "deduplicate" + return HFMModel::Pointer(extractHFMModel(mapping, url.toString())); } diff --git a/libraries/fbx/src/FBXSerializer.h b/libraries/fbx/src/FBXSerializer.h index 481f2f4f63..7d41f98444 100644 --- a/libraries/fbx/src/FBXSerializer.h +++ b/libraries/fbx/src/FBXSerializer.h @@ -119,7 +119,7 @@ public: HFMModel* extractHFMModel(const hifi::VariantHash& mapping, const QString& url); - static ExtractedMesh extractMesh(const FBXNode& object, unsigned int& meshIndex, bool deduplicate = true); + static ExtractedMesh extractMesh(const FBXNode& object, unsigned int& meshIndex, bool deduplicate); QHash meshes; HFMTexture getTexture(const QString& textureID, const QString& materialID); diff --git a/libraries/fbx/src/FBXSerializer_Mesh.cpp b/libraries/fbx/src/FBXSerializer_Mesh.cpp index f90c4bac6c..2f5286291c 100644 --- a/libraries/fbx/src/FBXSerializer_Mesh.cpp +++ b/libraries/fbx/src/FBXSerializer_Mesh.cpp @@ -345,6 +345,22 @@ ExtractedMesh FBXSerializer::extractMesh(const FBXNode& object, unsigned int& me isDracoMesh = true; data.extracted.mesh.wasCompressed = true; + // Check for additional metadata + unsigned int dracoMeshNodeVersion = 1; + std::vector dracoMaterialList; + for (const auto& dracoChild : child.children) { + if (dracoChild.name == "FBXDracoMeshVersion") { + if (!dracoChild.children.isEmpty()) { + dracoMeshNodeVersion = dracoChild.properties[0].toUInt(); + } + } else if (dracoChild.name == "MaterialList") { + dracoMaterialList.reserve(dracoChild.properties.size()); + for (const auto& materialID : dracoChild.properties) { + dracoMaterialList.push_back(materialID.toString()); + } + } + } + // load the draco mesh from the FBX and create a draco::Mesh draco::Decoder decoder; draco::DecoderBuffer decodedBuffer; @@ -462,8 +478,20 @@ ExtractedMesh FBXSerializer::extractMesh(const FBXNode& object, unsigned int& me // grab or setup the HFMMeshPart for the part this face belongs to int& partIndexPlusOne = materialTextureParts[materialTexture]; if (partIndexPlusOne == 0) { - data.extracted.partMaterialTextures.append(materialTexture); data.extracted.mesh.parts.resize(data.extracted.mesh.parts.size() + 1); + HFMMeshPart& part = data.extracted.mesh.parts.back(); + + // Figure out what material this part is + if (dracoMeshNodeVersion >= 2) { + // Define the materialID now + if (dracoMaterialList.size() - 1 <= materialID) { + part.materialID = dracoMaterialList[materialID]; + } + } else { + // Define the materialID later, based on the order of first appearance of the materials in the _connectionChildMap + data.extracted.partMaterialTextures.append(materialTexture); + } + partIndexPlusOne = data.extracted.mesh.parts.size(); } diff --git a/libraries/hfm/src/hfm/HFM.h b/libraries/hfm/src/hfm/HFM.h index 826c79e911..22c9005e98 100644 --- a/libraries/hfm/src/hfm/HFM.h +++ b/libraries/hfm/src/hfm/HFM.h @@ -53,6 +53,9 @@ using ColorType = glm::vec3; const int MAX_NUM_PIXELS_FOR_FBX_TEXTURE = 2048 * 2048; +// The version of the Draco mesh binary data itself. See also: FBX_DRACO_MESH_VERSION in FBX.h +static const int DRACO_MESH_VERSION = 2; + static const int DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES = 1000; static const int DRACO_ATTRIBUTE_MATERIAL_ID = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES; static const int DRACO_ATTRIBUTE_TEX_COORD_1 = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES + 1; diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp index 4d740f4a94..d7167fa577 100644 --- a/libraries/model-baker/src/model-baker/Baker.cpp +++ b/libraries/model-baker/src/model-baker/Baker.cpp @@ -19,6 +19,7 @@ #include "CalculateBlendshapeNormalsTask.h" #include "CalculateBlendshapeTangentsTask.h" #include "PrepareJointsTask.h" +#include "BuildDracoMeshTask.h" namespace baker { @@ -117,7 +118,7 @@ namespace baker { class BakerEngineBuilder { public: using Input = VaryingSet2; - using Output = VaryingSet2; + using Output = VaryingSet4, std::vector>>; using JobModel = Task::ModelIO; void build(JobModel& model, const Varying& input, Varying& output) { const auto& hfmModelIn = input.getN(0); @@ -156,6 +157,14 @@ namespace baker { // Parse material mapping const auto materialMapping = model.addJob("ParseMaterialMapping", mapping); + // Build Draco meshes + // NOTE: This task is disabled by default and must be enabled through configuration + // TODO: Tangent support (Needs changes to FBXSerializer_Mesh as well) + const auto buildDracoMeshInputs = BuildDracoMeshTask::Input(meshesIn, normalsPerMesh, tangentsPerMesh).asVarying(); + const auto buildDracoMeshOutputs = model.addJob("BuildDracoMesh", buildDracoMeshInputs); + const auto dracoMeshes = buildDracoMeshOutputs.getN(0); + const auto materialList = buildDracoMeshOutputs.getN(1); + // Combine the outputs into a new hfm::Model const auto buildBlendshapesInputs = BuildBlendshapesTask::Input(blendshapesPerMeshIn, normalsPerBlendshapePerMesh, tangentsPerBlendshapePerMesh).asVarying(); const auto blendshapesPerMeshOut = model.addJob("BuildBlendshapes", buildBlendshapesInputs); @@ -164,7 +173,7 @@ namespace baker { const auto buildModelInputs = BuildModelTask::Input(hfmModelIn, meshesOut, jointsOut, jointRotationOffsets, jointIndices).asVarying(); const auto hfmModelOut = model.addJob("BuildModel", buildModelInputs); - output = Output(hfmModelOut, materialMapping); + output = Output(hfmModelOut, materialMapping, dracoMeshes, materialList); } }; @@ -174,6 +183,10 @@ namespace baker { _engine->feedInput(1, mapping); } + std::shared_ptr Baker::getConfiguration() { + return _engine->getConfiguration(); + } + void Baker::run() { _engine->run(); } @@ -185,4 +198,12 @@ namespace baker { MaterialMapping Baker::getMaterialMapping() const { return _engine->getOutput().get().get1(); } + + const std::vector& Baker::getDracoMeshes() const { + return _engine->getOutput().get().get2(); + } + + std::vector> Baker::getDracoMaterialLists() const { + return _engine->getOutput().get().get3(); + } }; diff --git a/libraries/model-baker/src/model-baker/Baker.h b/libraries/model-baker/src/model-baker/Baker.h index e8a97b863d..de76c91fc8 100644 --- a/libraries/model-baker/src/model-baker/Baker.h +++ b/libraries/model-baker/src/model-baker/Baker.h @@ -24,11 +24,16 @@ namespace baker { public: Baker(const hfm::Model::Pointer& hfmModel, const hifi::VariantHash& mapping); + std::shared_ptr getConfiguration(); + void run(); // Outputs, available after run() is called hfm::Model::Pointer getHFMModel() const; MaterialMapping getMaterialMapping() const; + const std::vector& getDracoMeshes() const; + // This is a ByteArray and not a std::string because the character sequence can contain the null character (particularly for FBX materials) + std::vector> getDracoMaterialLists() const; protected: EnginePointer _engine; diff --git a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp new file mode 100644 index 0000000000..9bfd03e218 --- /dev/null +++ b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp @@ -0,0 +1,233 @@ +// +// BuildDracoMeshTask.cpp +// model-baker/src/model-baker +// +// Created by Sabrina Shanman on 2019/02/20. +// Copyright 2019 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 +// + +#include "BuildDracoMeshTask.h" + +// Fix build warnings due to draco headers not casting size_t +#ifdef _WIN32 +#pragma warning( push ) +#pragma warning( disable : 4267 ) +#endif + +#include +#include + +#ifdef _WIN32 +#pragma warning( pop ) +#endif + +#include "ModelBakerLogging.h" +#include "ModelMath.h" + +std::vector createMaterialList(const hfm::Mesh& mesh) { + std::vector materialList; + for (const auto& meshPart : mesh.parts) { + auto materialID = QVariant(meshPart.materialID).toByteArray(); + const auto materialIt = std::find(materialList.cbegin(), materialList.cend(), materialID); + if (materialIt == materialList.cend()) { + materialList.push_back(materialID); + } + } + return materialList; +} + +std::unique_ptr createDracoMesh(const hfm::Mesh& mesh, const std::vector& normals, const std::vector& tangents, const std::vector& materialList) { + Q_ASSERT(normals.size() == 0 || normals.size() == mesh.vertices.size()); + Q_ASSERT(mesh.colors.size() == 0 || mesh.colors.size() == mesh.vertices.size()); + Q_ASSERT(mesh.texCoords.size() == 0 || mesh.texCoords.size() == mesh.vertices.size()); + + int64_t numTriangles{ 0 }; + for (auto& part : mesh.parts) { + int extraQuadTriangleIndices = part.quadTrianglesIndices.size() % 3; + int extraTriangleIndices = part.triangleIndices.size() % 3; + if (extraQuadTriangleIndices != 0 || extraTriangleIndices != 0) { + qCWarning(model_baker) << "Found a mesh part with indices not divisible by three. Some indices will be discarded during Draco mesh creation."; + } + numTriangles += (part.quadTrianglesIndices.size() - extraQuadTriangleIndices) / 3; + numTriangles += (part.triangleIndices.size() - extraTriangleIndices) / 3; + } + + if (numTriangles == 0) { + return std::unique_ptr(); + } + + draco::TriangleSoupMeshBuilder meshBuilder; + + meshBuilder.Start(numTriangles); + + bool hasNormals{ normals.size() > 0 }; + bool hasColors{ mesh.colors.size() > 0 }; + bool hasTexCoords{ mesh.texCoords.size() > 0 }; + bool hasTexCoords1{ mesh.texCoords1.size() > 0 }; + bool hasPerFaceMaterials{ mesh.parts.size() > 1 }; + bool needsOriginalIndices{ (!mesh.clusterIndices.empty() || !mesh.blendshapes.empty()) && mesh.originalIndices.size() > 0 }; + + int normalsAttributeID { -1 }; + int colorsAttributeID { -1 }; + int texCoordsAttributeID { -1 }; + int texCoords1AttributeID { -1 }; + int faceMaterialAttributeID { -1 }; + int originalIndexAttributeID { -1 }; + + const int positionAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::POSITION, + 3, draco::DT_FLOAT32); + if (needsOriginalIndices) { + originalIndexAttributeID = meshBuilder.AddAttribute( + (draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_ORIGINAL_INDEX, + 1, draco::DT_INT32); + } + + if (hasNormals) { + normalsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::NORMAL, + 3, draco::DT_FLOAT32); + } + if (hasColors) { + colorsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::COLOR, + 3, draco::DT_FLOAT32); + } + if (hasTexCoords) { + texCoordsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::TEX_COORD, + 2, draco::DT_FLOAT32); + } + if (hasTexCoords1) { + texCoords1AttributeID = meshBuilder.AddAttribute( + (draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_TEX_COORD_1, + 2, draco::DT_FLOAT32); + } + if (hasPerFaceMaterials) { + faceMaterialAttributeID = meshBuilder.AddAttribute( + (draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_MATERIAL_ID, + 1, draco::DT_UINT16); + } + + auto partIndex = 0; + draco::FaceIndex face; + uint16_t materialID; + + for (auto& part : mesh.parts) { + auto materialIt = std::find(materialList.cbegin(), materialList.cend(), QVariant(part.materialID).toByteArray()); + materialID = (uint16_t)(materialIt - materialList.cbegin()); + + auto addFace = [&](const QVector& indices, int index, draco::FaceIndex face) { + int32_t idx0 = indices[index]; + int32_t idx1 = indices[index + 1]; + int32_t idx2 = indices[index + 2]; + + if (hasPerFaceMaterials) { + meshBuilder.SetPerFaceAttributeValueForFace(faceMaterialAttributeID, face, &materialID); + } + + meshBuilder.SetAttributeValuesForFace(positionAttributeID, face, + &mesh.vertices[idx0], &mesh.vertices[idx1], + &mesh.vertices[idx2]); + + if (needsOriginalIndices) { + meshBuilder.SetAttributeValuesForFace(originalIndexAttributeID, face, + &mesh.originalIndices[idx0], + &mesh.originalIndices[idx1], + &mesh.originalIndices[idx2]); + } + if (hasNormals) { + meshBuilder.SetAttributeValuesForFace(normalsAttributeID, face, + &normals[idx0], &normals[idx1], + &normals[idx2]); + } + if (hasColors) { + meshBuilder.SetAttributeValuesForFace(colorsAttributeID, face, + &mesh.colors[idx0], &mesh.colors[idx1], + &mesh.colors[idx2]); + } + if (hasTexCoords) { + meshBuilder.SetAttributeValuesForFace(texCoordsAttributeID, face, + &mesh.texCoords[idx0], &mesh.texCoords[idx1], + &mesh.texCoords[idx2]); + } + if (hasTexCoords1) { + meshBuilder.SetAttributeValuesForFace(texCoords1AttributeID, face, + &mesh.texCoords1[idx0], &mesh.texCoords1[idx1], + &mesh.texCoords1[idx2]); + } + }; + + for (int i = 0; (i + 2) < part.quadTrianglesIndices.size(); i += 3) { + addFace(part.quadTrianglesIndices, i, face++); + } + + for (int i = 0; (i + 2) < part.triangleIndices.size(); i += 3) { + addFace(part.triangleIndices, i, face++); + } + + partIndex++; + } + + auto dracoMesh = meshBuilder.Finalize(); + + if (!dracoMesh) { + qCWarning(model_baker) << "Failed to finalize the baking of a draco Geometry node"; + return std::unique_ptr(); + } + + // we need to modify unique attribute IDs for custom attributes + // so the attributes are easily retrievable on the other side + if (hasPerFaceMaterials) { + dracoMesh->attribute(faceMaterialAttributeID)->set_unique_id(DRACO_ATTRIBUTE_MATERIAL_ID); + } + + if (hasTexCoords1) { + dracoMesh->attribute(texCoords1AttributeID)->set_unique_id(DRACO_ATTRIBUTE_TEX_COORD_1); + } + + if (needsOriginalIndices) { + dracoMesh->attribute(originalIndexAttributeID)->set_unique_id(DRACO_ATTRIBUTE_ORIGINAL_INDEX); + } + + return dracoMesh; +} + +void BuildDracoMeshTask::configure(const Config& config) { + // Nothing to configure yet +} + +void BuildDracoMeshTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) { + const auto& meshes = input.get0(); + const auto& normalsPerMesh = input.get1(); + const auto& tangentsPerMesh = input.get2(); + auto& dracoBytesPerMesh = output.edit0(); + auto& materialLists = output.edit1(); + + dracoBytesPerMesh.reserve(meshes.size()); + materialLists.reserve(meshes.size()); + for (size_t i = 0; i < meshes.size(); i++) { + const auto& mesh = meshes[i]; + const auto& normals = baker::safeGet(normalsPerMesh, i); + const auto& tangents = baker::safeGet(tangentsPerMesh, i); + dracoBytesPerMesh.emplace_back(); + auto& dracoBytes = dracoBytesPerMesh.back(); + materialLists.push_back(createMaterialList(mesh)); + const auto& materialList = materialLists.back(); + + auto dracoMesh = createDracoMesh(mesh, normals, tangents, materialList); + + if (dracoMesh) { + draco::Encoder encoder; + + encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 14); + encoder.SetAttributeQuantization(draco::GeometryAttribute::TEX_COORD, 12); + encoder.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 10); + encoder.SetSpeedOptions(0, 5); + + draco::EncoderBuffer buffer; + encoder.EncodeMeshToBuffer(*dracoMesh, &buffer); + + dracoBytes = hifi::ByteArray(buffer.data(), (int)buffer.size()); + } + } +} diff --git a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h new file mode 100644 index 0000000000..ab1679959a --- /dev/null +++ b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h @@ -0,0 +1,39 @@ +// +// BuildDracoMeshTask.h +// model-baker/src/model-baker +// +// Created by Sabrina Shanman on 2019/02/20. +// Copyright 2019 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 +// + +#ifndef hifi_BuildDracoMeshTask_h +#define hifi_BuildDracoMeshTask_h + +#include +#include + +#include "Engine.h" +#include "BakerTypes.h" + +// BuildDracoMeshTask is disabled by default +class BuildDracoMeshConfig : public baker::JobConfig { + Q_OBJECT +public: + BuildDracoMeshConfig() : baker::JobConfig(false) {} +}; + +class BuildDracoMeshTask { +public: + using Config = BuildDracoMeshConfig; + using Input = baker::VaryingSet3, baker::NormalsPerMesh, baker::TangentsPerMesh>; + using Output = baker::VaryingSet2, std::vector>>; + using JobModel = baker::Job::ModelIO; + + void configure(const Config& config); + void run(const baker::BakeContextPointer& context, const Input& input, Output& output); +}; + +#endif // hifi_BuildDracoMeshTask_h diff --git a/libraries/model-baker/src/model-baker/PrepareJointsTask.h b/libraries/model-baker/src/model-baker/PrepareJointsTask.h index 0dbb9d584d..eecfea5752 100644 --- a/libraries/model-baker/src/model-baker/PrepareJointsTask.h +++ b/libraries/model-baker/src/model-baker/PrepareJointsTask.h @@ -18,7 +18,7 @@ #include "Engine.h" // The property "passthrough", when enabled, will let the input joints flow to the output unmodified, unlike the disabled property, which discards the data -class PrepareJointsTaskConfig : public baker::JobConfig { +class PrepareJointsConfig : public baker::JobConfig { Q_OBJECT Q_PROPERTY(bool passthrough MEMBER passthrough) public: @@ -27,7 +27,7 @@ public: class PrepareJointsTask { public: - using Config = PrepareJointsTaskConfig; + using Config = PrepareJointsConfig; using Input = baker::VaryingSet2, hifi::VariantHash /*mapping*/>; using Output = baker::VaryingSet3, QMap /*jointRotationOffsets*/, QHash /*jointIndices*/>; using JobModel = baker::Job::ModelIO; diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 1d0032ee4c..ebb53d8ef7 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -246,6 +246,7 @@ void GeometryReader::run() { HFMModel::Pointer hfmModel; QVariantHash serializerMapping = _mapping; serializerMapping["combineParts"] = _combineParts; + serializerMapping["deduplicateIndices"] = true; if (_url.path().toLower().endsWith(".gz")) { QByteArray uncompressedData; diff --git a/tools/oven/CMakeLists.txt b/tools/oven/CMakeLists.txt index 18ad37d7b9..c9b1aca1d4 100644 --- a/tools/oven/CMakeLists.txt +++ b/tools/oven/CMakeLists.txt @@ -2,7 +2,7 @@ set(TARGET_NAME oven) setup_hifi_project(Widgets Gui Concurrent) -link_hifi_libraries(shared shaders image gpu ktx fbx hfm baking graphics networking material-networking) +link_hifi_libraries(shared shaders image gpu ktx fbx hfm baking graphics networking material-networking model-baker task) setup_memory_debugger() diff --git a/tools/oven/src/Oven.cpp b/tools/oven/src/Oven.cpp index c70ca27d8b..0a5a989cbf 100644 --- a/tools/oven/src/Oven.cpp +++ b/tools/oven/src/Oven.cpp @@ -22,6 +22,9 @@ #include #include #include +#include +#include +#include #include "MaterialBaker.h" @@ -43,6 +46,12 @@ Oven::Oven() { MaterialBaker::setNextOvenWorkerThreadOperator([] { return Oven::instance().getNextWorkerThread(); }); + + { + auto modelFormatRegistry = DependencyManager::set(); + modelFormatRegistry->addFormat(FBXSerializer()); + modelFormatRegistry->addFormat(OBJSerializer()); + } } Oven::~Oven() { diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index 2b18c07c3a..a5ad5bc891 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -44,10 +44,12 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, HFMModel& result) { try { hifi::ByteArray fbxContents = fbx.readAll(); HFMModel::Pointer hfmModel; + hifi::VariantHash mapping; + mapping["deduplicateIndices"] = true; if (filename.toLower().endsWith(".obj")) { - hfmModel = OBJSerializer().read(fbxContents, QVariantHash(), filename); + hfmModel = OBJSerializer().read(fbxContents, mapping, filename); } else if (filename.toLower().endsWith(".fbx")) { - hfmModel = FBXSerializer().read(fbxContents, QVariantHash(), filename); + hfmModel = FBXSerializer().read(fbxContents, mapping, filename); } else { qWarning() << "file has unknown extension" << filename; return false; From e218e4bead36d28fa0e4d7de630f7acd72ec1851 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 6 Mar 2019 17:29:32 -0800 Subject: [PATCH 052/446] updating compile failure + icons/settings update --- .../icons/tablet-icons/mic-ptt-a.svg | 1 + .../icons/tablet-icons/mic-ptt-i.svg | 24 +++++++ interface/resources/qml/hifi/audio/Audio.qml | 64 ++++++++++++++++--- interface/resources/qml/hifi/audio/MicBar.qml | 8 ++- interface/src/scripting/Audio.cpp | 20 ------ scripts/system/audio.js | 14 ++-- 6 files changed, 93 insertions(+), 38 deletions(-) create mode 100644 interface/resources/icons/tablet-icons/mic-ptt-a.svg create mode 100644 interface/resources/icons/tablet-icons/mic-ptt-i.svg diff --git a/interface/resources/icons/tablet-icons/mic-ptt-a.svg b/interface/resources/icons/tablet-icons/mic-ptt-a.svg new file mode 100644 index 0000000000..e6df3c69d7 --- /dev/null +++ b/interface/resources/icons/tablet-icons/mic-ptt-a.svg @@ -0,0 +1 @@ +mic-ptt-a \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/mic-ptt-i.svg b/interface/resources/icons/tablet-icons/mic-ptt-i.svg new file mode 100644 index 0000000000..2141ea5229 --- /dev/null +++ b/interface/resources/icons/tablet-icons/mic-ptt-i.svg @@ -0,0 +1,24 @@ + + + + + + diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index 45358f59a2..d44a9c862e 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -120,6 +120,10 @@ Rectangle { isRedCheck: true; checked: AudioScriptingInterface.muted; onClicked: { + if (AudioScriptingInterface.pushToTalk && !checked) { + // disable push to talk if unmuting + AudioScriptingInterface.pushToTalk = false; + } AudioScriptingInterface.muted = checked; checked = Qt.binding(function() { return AudioScriptingInterface.muted; }); // restore binding } @@ -150,7 +154,23 @@ Rectangle { } AudioControls.CheckBox { spacing: muteMic.spacing - text: qsTr("Push To Talk"); + text: qsTr("Show audio level meter"); + checked: AvatarInputs.showAudioTools; + onClicked: { + AvatarInputs.showAudioTools = checked; + checked = Qt.binding(function() { return AvatarInputs.showAudioTools; }); // restore binding + } + onXChanged: rightMostInputLevelPos = x + width + } + } + + Separator {} + + ColumnLayout { + spacing: muteMic.spacing; + AudioControls.CheckBox { + spacing: muteMic.spacing + text: qsTr("Push To Talk (T)"); checked: isVR ? AudioScriptingInterface.pushToTalkHMD : AudioScriptingInterface.pushToTalkDesktop; onClicked: { if (isVR) { @@ -167,15 +187,41 @@ Rectangle { }); // restore binding } } - AudioControls.CheckBox { - spacing: muteMic.spacing - text: qsTr("Show audio level meter"); - checked: AvatarInputs.showAudioTools; - onClicked: { - AvatarInputs.showAudioTools = checked; - checked = Qt.binding(function() { return AvatarInputs.showAudioTools; }); // restore binding + Item { + id: pttTextContainer + x: margins.paddings; + width: rightMostInputLevelPos + height: pttTextMetrics.height + visible: true + TextMetrics { + id: pttTextMetrics + text: pttText.text + font: pttText.font + } + RalewayRegular { + id: pttText + wrapMode: Text.WordWrap + color: hifi.colors.white; + width: parent.width; + font.italic: true + size: 16; + text: isVR ? qsTr("Press and hold grip triggers on both of your controllers to unmute.") : + qsTr("Press and hold the button \"T\" to unmute."); + onTextChanged: { + if (pttTextMetrics.width > rightMostInputLevelPos) { + pttTextContainer.height = Math.ceil(pttTextMetrics.width / rightMostInputLevelPos) * pttTextMetrics.height; + } else { + pttTextContainer.height = pttTextMetrics.height; + } + } + } + Component.onCompleted: { + if (pttTextMetrics.width > rightMostInputLevelPos) { + pttTextContainer.height = Math.ceil(pttTextMetrics.width / rightMostInputLevelPos) * pttTextMetrics.height; + } else { + pttTextContainer.height = pttTextMetrics.height; + } } - onXChanged: rightMostInputLevelPos = x + width } } } diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index 9d1cbfbc6c..50477b82f8 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -67,6 +67,9 @@ Rectangle { hoverEnabled: true; scrollGestureEnabled: false; onClicked: { + if (AudioScriptingInterface.pushToTalk) { + return; + } AudioScriptingInterface.muted = !AudioScriptingInterface.muted; Tablet.playSound(TabletEnums.ButtonClick); } @@ -109,9 +112,10 @@ Rectangle { Image { readonly property string unmutedIcon: "../../../icons/tablet-icons/mic-unmute-i.svg"; readonly property string mutedIcon: "../../../icons/tablet-icons/mic-mute-i.svg"; + readonly property string pushToTalkIcon: "../../../icons/tablet-icons/mic-ptt-i.svg"; id: image; - source: AudioScriptingInterface.muted ? mutedIcon : unmutedIcon; + source: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) ? pushToTalkIcon : AudioScriptingInterface.muted ? mutedIcon : unmutedIcon; width: 30; height: 30; @@ -155,7 +159,7 @@ Rectangle { color: parent.color; - text: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) ? "MUTED-PTT (T)" : (AudioScriptingInterface.muted ? "MUTED" : "MUTE"); + text: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) ? "MUTED PTT-(T)" : (AudioScriptingInterface.muted ? "MUTED" : "MUTE"); font.pointSize: 12; } diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 45bb15f1a3..63ce9d2b2e 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -231,26 +231,6 @@ void Audio::loadData() { _pttHMD = _pttHMDSetting.get(); } -bool Audio::getPTTHMD() const { - return resultWithReadLock([&] { - return _pttHMD; - }); -} - -void Audio::saveData() { - _desktopMutedSetting.set(getMutedDesktop()); - _hmdMutedSetting.set(getMutedHMD()); - _pttDesktopSetting.set(getPTTDesktop()); - _pttHMDSetting.set(getPTTHMD()); -} - -void Audio::loadData() { - _desktopMuted = _desktopMutedSetting.get(); - _hmdMuted = _hmdMutedSetting.get(); - _pttDesktop = _pttDesktopSetting.get(); - _pttHMD = _pttHMDSetting.get(); -} - bool Audio::noiseReductionEnabled() const { return resultWithReadLock([&] { return _enableNoiseReduction; diff --git a/scripts/system/audio.js b/scripts/system/audio.js index bf44cfa7cc..19ed3faef2 100644 --- a/scripts/system/audio.js +++ b/scripts/system/audio.js @@ -27,12 +27,12 @@ var UNMUTE_ICONS = { activeIcon: "icons/tablet-icons/mic-unmute-a.svg" }; var PTT_ICONS = { - icon: "icons/tablet-icons/mic-unmute-i.svg", - activeIcon: "icons/tablet-icons/mic-unmute-a.svg" + icon: "icons/tablet-icons/mic-ptt-i.svg", + activeIcon: "icons/tablet-icons/mic-ptt-a.svg" }; function onMuteToggled() { - if (Audio.pushingToTalk) { + if (Audio.pushToTalk) { button.editProperties(PTT_ICONS); } else if (Audio.muted) { button.editProperties(MUTE_ICONS); @@ -63,8 +63,8 @@ function onScreenChanged(type, url) { var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var button = tablet.addButton({ - icon: Audio.muted ? MUTE_ICONS.icon : UNMUTE_ICONS.icon, - activeIcon: Audio.muted ? MUTE_ICONS.activeIcon : UNMUTE_ICONS.activeIcon, + icon: Audio.pushToTalk ? PTT_ICONS.icon : Audio.muted ? MUTE_ICONS.icon : UNMUTE_ICONS.icon, + activeIcon: Audio.pushToTalk ? PTT_ICONS.activeIcon : Audio.muted ? MUTE_ICONS.activeIcon : UNMUTE_ICONS.activeIcon, text: TABLET_BUTTON_NAME, sortOrder: 1 }); @@ -74,7 +74,7 @@ onMuteToggled(); button.clicked.connect(onClicked); tablet.screenChanged.connect(onScreenChanged); Audio.mutedChanged.connect(onMuteToggled); -Audio.pushingToTalkChanged.connect(onMuteToggled); +Audio.pushToTalkChanged.connect(onMuteToggled); Script.scriptEnding.connect(function () { if (onAudioScreen) { @@ -83,7 +83,7 @@ Script.scriptEnding.connect(function () { button.clicked.disconnect(onClicked); tablet.screenChanged.disconnect(onScreenChanged); Audio.mutedChanged.disconnect(onMuteToggled); - Audio.pushingToTalkChanged.disconnect(onMuteToggled); + Audio.pushToTalkChanged.disconnect(onMuteToggled); tablet.removeButton(button); }); From 36f62f05d1b8d96199419aacab89c204bee6a58d Mon Sep 17 00:00:00 2001 From: r3tk0n Date: Thu, 7 Mar 2019 11:31:28 -0800 Subject: [PATCH 053/446] Add pushToTalk.js controllerModule. --- .../controllerModules/pushToTalk.js | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 scripts/system/controllers/controllerModules/pushToTalk.js diff --git a/scripts/system/controllers/controllerModules/pushToTalk.js b/scripts/system/controllers/controllerModules/pushToTalk.js new file mode 100644 index 0000000000..e764b228c9 --- /dev/null +++ b/scripts/system/controllers/controllerModules/pushToTalk.js @@ -0,0 +1,75 @@ +"use strict"; + +// Created by Jason C. Najera on 3/7/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Handles Push-to-Talk functionality for HMD mode. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); + +(function() { // BEGIN LOCAL_SCOPE + function PushToTalkHandler() { + var _this = this; + this.active = false; + //var pttMapping, mappingName; + + this.setup = function() { + //mappingName = 'Hifi-PTT-Dev-' + Math.random(); + //pttMapping = Controller.newMapping(mappingName); + //pttMapping.enable(); + }; + + this.shouldTalk = function (controllerData) { + // Set up test against controllerData here... + var gripVal = controllerData.secondaryValues[this.hand]; + return (gripVal) ? true : false; + }; + + this.shouldStopTalking = function (controllerData) { + var gripVal = controllerData.secondaryValues[this.hand]; + return (gripVal) ? false : true; + }; + + this.isReady = function (controllerData, deltaTime) { + if (HMD.active() && Audio.pushToTalk && this.shouldTalk(controllerData)) { + Audio.pushingToTalk = true; + returnMakeRunningValues(true, [], []); + } + + return makeRunningValues(false, [], []); + }; + + this.run = function (controllerData, deltaTime) { + if (this.shouldStopTalking(controllerData) || !Audio.pushToTalk) { + Audio.pushingToTalk = false; + return makeRunningValues(false, [], []); + } + + return makeRunningValues(true, [], []); + }; + + this.cleanup = function () { + //pttMapping.disable(); + }; + + this.parameters = makeDispatcherModuleParameters( + 950, + ["head"], + [], + 100); + } + + var pushToTalk = new PushToTalkHandler(); + enableDispatcherModule("PushToTalk", pushToTalk); + + function cleanup () { + pushToTalk.cleanup(); + disableDispatcherModule("PushToTalk"); + }; + + Script.scriptEnding.connect(cleanup); +}()); // END LOCAL_SCOPE \ No newline at end of file From f444f383eba17e47a39032411ce08b33d6df2b57 Mon Sep 17 00:00:00 2001 From: r3tk0n Date: Thu, 7 Mar 2019 11:31:48 -0800 Subject: [PATCH 054/446] Enable pushToTalk.js controller module. --- scripts/system/controllers/controllerDispatcher.js | 1 + scripts/system/controllers/controllerScripts.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index 28c3e2a299..f4c0cbb0d6 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -58,6 +58,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); // not set to false (not in use), a module cannot start. When a module is using a slot, that module's name // is stored as the value, rather than false. this.activitySlots = { + head: false, leftHand: false, rightHand: false, rightHandTrigger: false, diff --git a/scripts/system/controllers/controllerScripts.js b/scripts/system/controllers/controllerScripts.js index 86ff7701c3..d236d6b12a 100644 --- a/scripts/system/controllers/controllerScripts.js +++ b/scripts/system/controllers/controllerScripts.js @@ -34,7 +34,8 @@ var CONTOLLER_SCRIPTS = [ "controllerModules/nearGrabHyperLinkEntity.js", "controllerModules/nearTabletHighlight.js", "controllerModules/nearGrabEntity.js", - "controllerModules/farGrabEntity.js" + "controllerModules/farGrabEntity.js", + "controllerModules/pushToTalk.js" ]; var DEBUG_MENU_ITEM = "Debug defaultScripts.js"; From d8478e0b17d56500ff1fd4debdd322fbeec77a4c Mon Sep 17 00:00:00 2001 From: r3tk0n Date: Tue, 5 Mar 2019 17:09:54 -0800 Subject: [PATCH 055/446] Initial implementation (deadlocks still occurring in Audio.cpp) --- interface/resources/qml/hifi/audio/Audio.qml | 19 ++ interface/resources/qml/hifi/audio/MicBar.qml | 9 +- interface/src/Application.cpp | 18 ++ interface/src/Application.h | 2 + interface/src/scripting/Audio.cpp | 174 +++++++++++++++++- interface/src/scripting/Audio.h | 89 ++++++++- scripts/system/audio.js | 3 + 7 files changed, 302 insertions(+), 12 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index c8dd83cd62..45358f59a2 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -148,6 +148,25 @@ Rectangle { checked = Qt.binding(function() { return AudioScriptingInterface.noiseReduction; }); // restore binding } } + AudioControls.CheckBox { + spacing: muteMic.spacing + text: qsTr("Push To Talk"); + checked: isVR ? AudioScriptingInterface.pushToTalkHMD : AudioScriptingInterface.pushToTalkDesktop; + onClicked: { + if (isVR) { + AudioScriptingInterface.pushToTalkHMD = checked; + } else { + AudioScriptingInterface.pushToTalkDesktop = checked; + } + checked = Qt.binding(function() { + if (isVR) { + return AudioScriptingInterface.pushToTalkHMD; + } else { + return AudioScriptingInterface.pushToTalkDesktop; + } + }); // restore binding + } + } AudioControls.CheckBox { spacing: muteMic.spacing text: qsTr("Show audio level meter"); diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index 39f75a9182..f91058bc3c 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -11,10 +11,13 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 +import stylesUit 1.0 import TabletScriptingInterface 1.0 Rectangle { + HifiConstants { id: hifi; } + readonly property var level: AudioScriptingInterface.inputLevel; property bool gated: false; @@ -131,9 +134,9 @@ Rectangle { Item { id: status; - readonly property string color: AudioScriptingInterface.muted ? colors.muted : colors.unmuted; + readonly property string color: (AudioScriptingInterface.pushingToTalk && AudioScriptingInterface.muted) ? hifi.colors.blueHighlight : AudioScriptingInterface.muted ? colors.muted : colors.unmuted; - visible: AudioScriptingInterface.muted; + visible: AudioScriptingInterface.pushingToTalk || AudioScriptingInterface.muted; anchors { left: parent.left; @@ -152,7 +155,7 @@ Rectangle { color: parent.color; - text: AudioScriptingInterface.muted ? "MUTED" : "MUTE"; + text: AudioScriptingInterface.pushToTalk ? (AudioScriptingInterface.pushingToTalk ? "SPEAKING" : "PUSH TO TALK") : (AudioScriptingInterface.muted ? "MUTED" : "MUTE"); font.pointSize: 12; } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index b8ab4d10db..fa63757560 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1435,6 +1435,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo }); connect(this, &Application::activeDisplayPluginChanged, reinterpret_cast(audioScriptingInterface.data()), &scripting::Audio::onContextChanged); + connect(this, &Application::pushedToTalk, + reinterpret_cast(audioScriptingInterface.data()), &scripting::Audio::handlePushedToTalk); } // Create the rendering engine. This can be slow on some machines due to lots of @@ -4199,6 +4201,10 @@ void Application::keyPressEvent(QKeyEvent* event) { } break; + case Qt::Key_T: + emit pushedToTalk(true); + break; + case Qt::Key_P: { if (!isShifted && !isMeta && !isOption && !event->isAutoRepeat()) { AudioInjectorOptions options; @@ -4304,6 +4310,12 @@ void Application::keyReleaseEvent(QKeyEvent* event) { if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->keyReleaseEvent(event); } + + switch (event->key()) { + case Qt::Key_T: + emit pushedToTalk(false); + break; + } } void Application::focusOutEvent(QFocusEvent* event) { @@ -5235,6 +5247,9 @@ void Application::loadSettings() { } } + auto audioScriptingInterface = reinterpret_cast(DependencyManager::get().data()); + audioScriptingInterface->loadData(); + getMyAvatar()->loadData(); _settingsLoaded = true; } @@ -5244,6 +5259,9 @@ void Application::saveSettings() const { DependencyManager::get()->saveSettings(); DependencyManager::get()->saveSettings(); + auto audioScriptingInterface = reinterpret_cast(DependencyManager::get().data()); + audioScriptingInterface->saveData(); + Menu::getInstance()->saveSettings(); getMyAvatar()->saveData(); PluginManager::getInstance()->saveSettings(); diff --git a/interface/src/Application.h b/interface/src/Application.h index a8cc9450c5..1c86326f90 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -358,6 +358,8 @@ signals: void miniTabletEnabledChanged(bool enabled); + void pushedToTalk(bool enabled); + public slots: QVector pasteEntities(float x, float y, float z); bool exportEntities(const QString& filename, const QVector& entityIDs, const glm::vec3* givenOffset = nullptr); diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 2c4c29ff65..fe04ce47ca 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -63,26 +63,163 @@ void Audio::stopRecording() { } bool Audio::isMuted() const { - return resultWithReadLock([&] { - return _isMuted; - }); + bool isHMD = qApp->isHMDMode(); + if (isHMD) { + return getMutedHMD(); + } + else { + return getMutedDesktop(); + } } void Audio::setMuted(bool isMuted) { + withWriteLock([&] { + bool isHMD = qApp->isHMDMode(); + if (isHMD) { + setMutedHMD(isMuted); + } + else { + setMutedDesktop(isMuted); + } + }); +} + +void Audio::setMutedDesktop(bool isMuted) { + bool changed = false; + if (_desktopMuted != isMuted) { + changed = true; + _desktopMuted = isMuted; + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); + } + if (changed) { + emit mutedChanged(isMuted); + emit desktopMutedChanged(isMuted); + } +} + +bool Audio::getMutedDesktop() const { + return resultWithReadLock([&] { + return _desktopMuted; + }); +} + +void Audio::setMutedHMD(bool isMuted) { + bool changed = false; + if (_hmdMuted != isMuted) { + changed = true; + _hmdMuted = isMuted; + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); + } + if (changed) { + emit mutedChanged(isMuted); + emit hmdMutedChanged(isMuted); + } +} + +bool Audio::getMutedHMD() const { + return resultWithReadLock([&] { + return _hmdMuted; + }); +} + +bool Audio::getPTT() { + bool isHMD = qApp->isHMDMode(); + if (isHMD) { + return getPTTHMD(); + } + else { + return getPTTDesktop(); + } +} + +bool Audio::getPushingToTalk() const { + return resultWithReadLock([&] { + return _pushingToTalk; + }); +} + +void Audio::setPTT(bool enabled) { + bool isHMD = qApp->isHMDMode(); + if (isHMD) { + setPTTHMD(enabled); + } + else { + setPTTDesktop(enabled); + } +} + +void Audio::setPTTDesktop(bool enabled) { bool changed = false; withWriteLock([&] { - if (_isMuted != isMuted) { - _isMuted = isMuted; - auto client = DependencyManager::get().data(); - QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); + if (_pttDesktop != enabled) { changed = true; + _pttDesktop = enabled; + if (!enabled) { + // Set to default behavior (unmuted for Desktop) on Push-To-Talk disable. + setMutedDesktop(true); + } + else { + // Should be muted when not pushing to talk while PTT is enabled. + setMutedDesktop(true); + } } }); if (changed) { - emit mutedChanged(isMuted); + emit pushToTalkChanged(enabled); + emit pushToTalkDesktopChanged(enabled); } } +bool Audio::getPTTDesktop() const { + return resultWithReadLock([&] { + return _pttDesktop; + }); +} + +void Audio::setPTTHMD(bool enabled) { + bool changed = false; + withWriteLock([&] { + if (_pttHMD != enabled) { + changed = true; + _pttHMD = enabled; + if (!enabled) { + // Set to default behavior (unmuted for HMD) on Push-To-Talk disable. + setMutedHMD(false); + } + else { + // Should be muted when not pushing to talk while PTT is enabled. + setMutedHMD(true); + } + } + }); + if (changed) { + emit pushToTalkChanged(enabled); + emit pushToTalkHMDChanged(enabled); + } +} + +bool Audio::getPTTHMD() const { + return resultWithReadLock([&] { + return _pttHMD; + }); +} + +void Audio::saveData() { + _desktopMutedSetting.set(getMutedDesktop()); + _hmdMutedSetting.set(getMutedHMD()); + _pttDesktopSetting.set(getPTTDesktop()); + _pttHMDSetting.set(getPTTHMD()); +} + +void Audio::loadData() { + _desktopMuted = _desktopMutedSetting.get(); + _hmdMuted = _hmdMutedSetting.get(); + _pttDesktop = _pttDesktopSetting.get(); + _pttHMD = _pttHMDSetting.get(); +} + bool Audio::noiseReductionEnabled() const { return resultWithReadLock([&] { return _enableNoiseReduction; @@ -179,11 +316,32 @@ void Audio::onContextChanged() { changed = true; } }); + if (isHMD) { + setMuted(getMutedHMD()); + } + else { + setMuted(getMutedDesktop()); + } if (changed) { emit contextChanged(isHMD ? Audio::HMD : Audio::DESKTOP); } } +void Audio::handlePushedToTalk(bool enabled) { + if (getPTT()) { + if (enabled) { + setMuted(false); + } + else { + setMuted(true); + } + if (_pushingToTalk != enabled) { + _pushingToTalk = enabled; + emit pushingToTalkChanged(enabled); + } + } +} + void Audio::setReverb(bool enable) { withWriteLock([&] { DependencyManager::get()->setReverb(enable); diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index e4dcba9130..6aa589e399 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -12,6 +12,7 @@ #ifndef hifi_scripting_Audio_h #define hifi_scripting_Audio_h +#include #include "AudioScriptingInterface.h" #include "AudioDevices.h" #include "AudioEffectOptions.h" @@ -19,6 +20,9 @@ #include "AudioFileWav.h" #include +using MutedGetter = std::function; +using MutedSetter = std::function; + namespace scripting { class Audio : public AudioScriptingInterface, protected ReadWriteLockable { @@ -63,6 +67,12 @@ class Audio : public AudioScriptingInterface, protected ReadWriteLockable { Q_PROPERTY(bool clipping READ isClipping NOTIFY clippingChanged) Q_PROPERTY(QString context READ getContext NOTIFY contextChanged) Q_PROPERTY(AudioDevices* devices READ getDevices NOTIFY nop) + Q_PROPERTY(bool desktopMuted READ getMutedDesktop WRITE setMutedDesktop NOTIFY desktopMutedChanged) + Q_PROPERTY(bool hmdMuted READ getMutedHMD WRITE setMutedHMD NOTIFY hmdMutedChanged) + Q_PROPERTY(bool pushToTalk READ getPTT WRITE setPTT NOTIFY pushToTalkChanged); + Q_PROPERTY(bool pushToTalkDesktop READ getPTTDesktop WRITE setPTTDesktop NOTIFY pushToTalkDesktopChanged) + Q_PROPERTY(bool pushToTalkHMD READ getPTTHMD WRITE setPTTHMD NOTIFY pushToTalkHMDChanged) + Q_PROPERTY(bool pushingToTalk READ getPushingToTalk NOTIFY pushingToTalkChanged) public: static QString AUDIO; @@ -82,6 +92,25 @@ public: void showMicMeter(bool show); + // Mute setting setters and getters + void setMutedDesktop(bool isMuted); + bool getMutedDesktop() const; + void setMutedHMD(bool isMuted); + bool getMutedHMD() const; + void setPTT(bool enabled); + bool getPTT(); + bool getPushingToTalk() const; + + // Push-To-Talk setters and getters + void setPTTDesktop(bool enabled); + bool getPTTDesktop() const; + void setPTTHMD(bool enabled); + bool getPTTHMD() const; + + // Settings handlers + void saveData(); + void loadData(); + /**jsdoc * @function Audio.setInputDevice * @param {object} device @@ -193,6 +222,46 @@ signals: */ void mutedChanged(bool isMuted); + /**jsdoc + * Triggered when desktop audio input is muted or unmuted. + * @function Audio.desktopMutedChanged + * @param {boolean} isMuted - true if the audio input is muted for desktop mode, otherwise false. + * @returns {Signal} + */ + void desktopMutedChanged(bool isMuted); + + /**jsdoc + * Triggered when HMD audio input is muted or unmuted. + * @function Audio.hmdMutedChanged + * @param {boolean} isMuted - true if the audio input is muted for HMD mode, otherwise false. + * @returns {Signal} + */ + void hmdMutedChanged(bool isMuted); + + /** + * Triggered when Push-to-Talk has been enabled or disabled. + * @function Audio.pushToTalkChanged + * @param {boolean} enabled - true if Push-to-Talk is enabled, otherwise false. + * @returns {Signal} + */ + void pushToTalkChanged(bool enabled); + + /** + * Triggered when Push-to-Talk has been enabled or disabled for desktop mode. + * @function Audio.pushToTalkDesktopChanged + * @param {boolean} enabled - true if Push-to-Talk is emabled for Desktop mode, otherwise false. + * @returns {Signal} + */ + void pushToTalkDesktopChanged(bool enabled); + + /** + * Triggered when Push-to-Talk has been enabled or disabled for HMD mode. + * @function Audio.pushToTalkHMDChanged + * @param {boolean} enabled - true if Push-to-Talk is emabled for HMD mode, otherwise false. + * @returns {Signal} + */ + void pushToTalkHMDChanged(bool enabled); + /**jsdoc * Triggered when the audio input noise reduction is enabled or disabled. * @function Audio.noiseReductionChanged @@ -237,6 +306,14 @@ signals: */ void contextChanged(const QString& context); + /**jsdoc + * Triggered when pushing to talk. + * @function Audio.pushingToTalkChanged + * @param {boolean} talking - true if broadcasting with PTT, false otherwise. + * @returns {Signal} + */ + void pushingToTalkChanged(bool talking); + public slots: /**jsdoc @@ -245,6 +322,8 @@ public slots: */ void onContextChanged(); + void handlePushedToTalk(bool enabled); + private slots: void setMuted(bool muted); void enableNoiseReduction(bool enable); @@ -260,11 +339,19 @@ private: float _inputVolume { 1.0f }; float _inputLevel { 0.0f }; bool _isClipping { false }; - bool _isMuted { false }; bool _enableNoiseReduction { true }; // Match default value of AudioClient::_isNoiseGateEnabled. bool _contextIsHMD { false }; AudioDevices* getDevices() { return &_devices; } AudioDevices _devices; + Setting::Handle _desktopMutedSetting{ QStringList { Audio::AUDIO, "desktopMuted" }, true }; + Setting::Handle _hmdMutedSetting{ QStringList { Audio::AUDIO, "hmdMuted" }, true }; + Setting::Handle _pttDesktopSetting{ QStringList { Audio::AUDIO, "pushToTalkDesktop" }, false }; + Setting::Handle _pttHMDSetting{ QStringList { Audio::AUDIO, "pushToTalkHMD" }, false }; + bool _desktopMuted{ true }; + bool _hmdMuted{ false }; + bool _pttDesktop{ false }; + bool _pttHMD{ false }; + bool _pushingToTalk{ false }; }; }; diff --git a/scripts/system/audio.js b/scripts/system/audio.js index ee82c0c6ea..51d070d8cd 100644 --- a/scripts/system/audio.js +++ b/scripts/system/audio.js @@ -28,6 +28,9 @@ var UNMUTE_ICONS = { }; function onMuteToggled() { + if (Audio.pushingToTalk) { + return; + } if (Audio.muted) { button.editProperties(MUTE_ICONS); } else { From eeb900b76165b0277ef17435d5e8c774ce85d9b5 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 6 Mar 2019 11:00:09 -0800 Subject: [PATCH 056/446] laying groundwork for audio app + fixing deadlocks --- interface/resources/qml/hifi/audio/MicBar.qml | 6 +- interface/src/scripting/Audio.cpp | 108 +++++++++--------- interface/src/scripting/Audio.h | 1 + scripts/system/audio.js | 11 +- 4 files changed, 69 insertions(+), 57 deletions(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index f91058bc3c..2ab1085408 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -134,7 +134,7 @@ Rectangle { Item { id: status; - readonly property string color: (AudioScriptingInterface.pushingToTalk && AudioScriptingInterface.muted) ? hifi.colors.blueHighlight : AudioScriptingInterface.muted ? colors.muted : colors.unmuted; + readonly property string color: AudioScriptingInterface.pushToTalk ? hifi.colors.blueHighlight : AudioScriptingInterface.muted ? colors.muted : colors.unmuted; visible: AudioScriptingInterface.pushingToTalk || AudioScriptingInterface.muted; @@ -165,7 +165,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - width: 50; + width: AudioScriptingInterface.pushToTalk ? (AudioScriptingInterface.pushingToTalk ? 45: 30) : 50; height: 4; color: parent.color; } @@ -176,7 +176,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - width: 50; + width: AudioScriptingInterface.pushToTalk ? (AudioScriptingInterface.pushingToTalk ? 45: 30) : 50; height: 4; color: parent.color; } diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index fe04ce47ca..63ce9d2b2e 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -66,32 +66,30 @@ bool Audio::isMuted() const { bool isHMD = qApp->isHMDMode(); if (isHMD) { return getMutedHMD(); - } - else { + } else { return getMutedDesktop(); } } void Audio::setMuted(bool isMuted) { - withWriteLock([&] { - bool isHMD = qApp->isHMDMode(); - if (isHMD) { - setMutedHMD(isMuted); - } - else { - setMutedDesktop(isMuted); - } - }); + bool isHMD = qApp->isHMDMode(); + if (isHMD) { + setMutedHMD(isMuted); + } else { + setMutedDesktop(isMuted); + } } void Audio::setMutedDesktop(bool isMuted) { bool changed = false; - if (_desktopMuted != isMuted) { - changed = true; - _desktopMuted = isMuted; - auto client = DependencyManager::get().data(); - QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); - } + withWriteLock([&] { + if (_desktopMuted != isMuted) { + changed = true; + _desktopMuted = isMuted; + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); + } + }); if (changed) { emit mutedChanged(isMuted); emit desktopMutedChanged(isMuted); @@ -106,12 +104,14 @@ bool Audio::getMutedDesktop() const { void Audio::setMutedHMD(bool isMuted) { bool changed = false; - if (_hmdMuted != isMuted) { - changed = true; - _hmdMuted = isMuted; - auto client = DependencyManager::get().data(); - QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); - } + withWriteLock([&] { + if (_hmdMuted != isMuted) { + changed = true; + _hmdMuted = isMuted; + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); + } + }); if (changed) { emit mutedChanged(isMuted); emit hmdMutedChanged(isMuted); @@ -128,12 +128,24 @@ bool Audio::getPTT() { bool isHMD = qApp->isHMDMode(); if (isHMD) { return getPTTHMD(); - } - else { + } else { return getPTTDesktop(); } } +void scripting::Audio::setPushingToTalk(bool pushingToTalk) { + bool changed = false; + withWriteLock([&] { + if (_pushingToTalk != pushingToTalk) { + changed = true; + _pushingToTalk = pushingToTalk; + } + }); + if (changed) { + emit pushingToTalkChanged(pushingToTalk); + } +} + bool Audio::getPushingToTalk() const { return resultWithReadLock([&] { return _pushingToTalk; @@ -144,8 +156,7 @@ void Audio::setPTT(bool enabled) { bool isHMD = qApp->isHMDMode(); if (isHMD) { setPTTHMD(enabled); - } - else { + } else { setPTTDesktop(enabled); } } @@ -156,16 +167,16 @@ void Audio::setPTTDesktop(bool enabled) { if (_pttDesktop != enabled) { changed = true; _pttDesktop = enabled; - if (!enabled) { - // Set to default behavior (unmuted for Desktop) on Push-To-Talk disable. - setMutedDesktop(true); - } - else { - // Should be muted when not pushing to talk while PTT is enabled. - setMutedDesktop(true); - } } }); + if (!enabled) { + // Set to default behavior (unmuted for Desktop) on Push-To-Talk disable. + setMutedDesktop(true); + } else { + // Should be muted when not pushing to talk while PTT is enabled. + setMutedDesktop(true); + } + if (changed) { emit pushToTalkChanged(enabled); emit pushToTalkDesktopChanged(enabled); @@ -184,16 +195,16 @@ void Audio::setPTTHMD(bool enabled) { if (_pttHMD != enabled) { changed = true; _pttHMD = enabled; - if (!enabled) { - // Set to default behavior (unmuted for HMD) on Push-To-Talk disable. - setMutedHMD(false); - } - else { - // Should be muted when not pushing to talk while PTT is enabled. - setMutedHMD(true); - } } }); + if (!enabled) { + // Set to default behavior (unmuted for HMD) on Push-To-Talk disable. + setMutedHMD(false); + } else { + // Should be muted when not pushing to talk while PTT is enabled. + setMutedHMD(true); + } + if (changed) { emit pushToTalkChanged(enabled); emit pushToTalkHMDChanged(enabled); @@ -318,8 +329,7 @@ void Audio::onContextChanged() { }); if (isHMD) { setMuted(getMutedHMD()); - } - else { + } else { setMuted(getMutedDesktop()); } if (changed) { @@ -331,14 +341,10 @@ void Audio::handlePushedToTalk(bool enabled) { if (getPTT()) { if (enabled) { setMuted(false); - } - else { + } else { setMuted(true); } - if (_pushingToTalk != enabled) { - _pushingToTalk = enabled; - emit pushingToTalkChanged(enabled); - } + setPushingToTalk(enabled); } } diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index 6aa589e399..94f8a7bf54 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -99,6 +99,7 @@ public: bool getMutedHMD() const; void setPTT(bool enabled); bool getPTT(); + void setPushingToTalk(bool pushingToTalk); bool getPushingToTalk() const; // Push-To-Talk setters and getters diff --git a/scripts/system/audio.js b/scripts/system/audio.js index 51d070d8cd..bf44cfa7cc 100644 --- a/scripts/system/audio.js +++ b/scripts/system/audio.js @@ -26,12 +26,15 @@ var UNMUTE_ICONS = { icon: "icons/tablet-icons/mic-unmute-i.svg", activeIcon: "icons/tablet-icons/mic-unmute-a.svg" }; +var PTT_ICONS = { + icon: "icons/tablet-icons/mic-unmute-i.svg", + activeIcon: "icons/tablet-icons/mic-unmute-a.svg" +}; function onMuteToggled() { if (Audio.pushingToTalk) { - return; - } - if (Audio.muted) { + button.editProperties(PTT_ICONS); + } else if (Audio.muted) { button.editProperties(MUTE_ICONS); } else { button.editProperties(UNMUTE_ICONS); @@ -71,6 +74,7 @@ onMuteToggled(); button.clicked.connect(onClicked); tablet.screenChanged.connect(onScreenChanged); Audio.mutedChanged.connect(onMuteToggled); +Audio.pushingToTalkChanged.connect(onMuteToggled); Script.scriptEnding.connect(function () { if (onAudioScreen) { @@ -79,6 +83,7 @@ Script.scriptEnding.connect(function () { button.clicked.disconnect(onClicked); tablet.screenChanged.disconnect(onScreenChanged); Audio.mutedChanged.disconnect(onMuteToggled); + Audio.pushingToTalkChanged.disconnect(onMuteToggled); tablet.removeButton(button); }); From 066d1797cf6f96c722274b18b33d4010ed60405a Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 6 Mar 2019 11:11:47 -0800 Subject: [PATCH 057/446] Push to talk changes + rebased with master (#7) Push to talk changes + rebased with master --- interface/src/scripting/Audio.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 63ce9d2b2e..45bb15f1a3 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -231,6 +231,26 @@ void Audio::loadData() { _pttHMD = _pttHMDSetting.get(); } +bool Audio::getPTTHMD() const { + return resultWithReadLock([&] { + return _pttHMD; + }); +} + +void Audio::saveData() { + _desktopMutedSetting.set(getMutedDesktop()); + _hmdMutedSetting.set(getMutedHMD()); + _pttDesktopSetting.set(getPTTDesktop()); + _pttHMDSetting.set(getPTTHMD()); +} + +void Audio::loadData() { + _desktopMuted = _desktopMutedSetting.get(); + _hmdMuted = _hmdMutedSetting.get(); + _pttDesktop = _pttDesktopSetting.get(); + _pttHMD = _pttHMDSetting.get(); +} + bool Audio::noiseReductionEnabled() const { return resultWithReadLock([&] { return _enableNoiseReduction; From 9613cfb6b9da73248c95151ddc15d002b85a87b2 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 6 Mar 2019 11:18:57 -0800 Subject: [PATCH 058/446] changing text display --- interface/resources/qml/hifi/audio/MicBar.qml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index 2ab1085408..9d1cbfbc6c 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -134,9 +134,9 @@ Rectangle { Item { id: status; - readonly property string color: AudioScriptingInterface.pushToTalk ? hifi.colors.blueHighlight : AudioScriptingInterface.muted ? colors.muted : colors.unmuted; + readonly property string color: AudioScriptingInterface.muted ? colors.muted : colors.unmuted; - visible: AudioScriptingInterface.pushingToTalk || AudioScriptingInterface.muted; + visible: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) || AudioScriptingInterface.muted; anchors { left: parent.left; @@ -155,7 +155,7 @@ Rectangle { color: parent.color; - text: AudioScriptingInterface.pushToTalk ? (AudioScriptingInterface.pushingToTalk ? "SPEAKING" : "PUSH TO TALK") : (AudioScriptingInterface.muted ? "MUTED" : "MUTE"); + text: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) ? "MUTED-PTT (T)" : (AudioScriptingInterface.muted ? "MUTED" : "MUTE"); font.pointSize: 12; } @@ -165,7 +165,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - width: AudioScriptingInterface.pushToTalk ? (AudioScriptingInterface.pushingToTalk ? 45: 30) : 50; + width: AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk ? 25 : 50; height: 4; color: parent.color; } @@ -176,7 +176,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - width: AudioScriptingInterface.pushToTalk ? (AudioScriptingInterface.pushingToTalk ? 45: 30) : 50; + width: AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk ? 25 : 50; height: 4; color: parent.color; } From 0cb9e7eb3609189d305df8cb700c5225e801b951 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 6 Mar 2019 11:45:43 -0800 Subject: [PATCH 059/446] exposing setting pushingToTalk --- interface/src/scripting/Audio.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index 94f8a7bf54..9ad4aac9c1 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -72,7 +72,7 @@ class Audio : public AudioScriptingInterface, protected ReadWriteLockable { Q_PROPERTY(bool pushToTalk READ getPTT WRITE setPTT NOTIFY pushToTalkChanged); Q_PROPERTY(bool pushToTalkDesktop READ getPTTDesktop WRITE setPTTDesktop NOTIFY pushToTalkDesktopChanged) Q_PROPERTY(bool pushToTalkHMD READ getPTTHMD WRITE setPTTHMD NOTIFY pushToTalkHMDChanged) - Q_PROPERTY(bool pushingToTalk READ getPushingToTalk NOTIFY pushingToTalkChanged) + Q_PROPERTY(bool pushingToTalk READ getPushingToTalk WRITE setPushingToTalk NOTIFY pushingToTalkChanged) public: static QString AUDIO; From 7b197c975c9b5926e48cb2d2847e490d8f9ad0d8 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 6 Mar 2019 17:29:32 -0800 Subject: [PATCH 060/446] updating compile failure + icons/settings update --- .../icons/tablet-icons/mic-ptt-a.svg | 1 + .../icons/tablet-icons/mic-ptt-i.svg | 24 +++++++ interface/resources/qml/hifi/audio/Audio.qml | 64 ++++++++++++++++--- interface/resources/qml/hifi/audio/MicBar.qml | 8 ++- interface/src/scripting/Audio.cpp | 20 ------ scripts/system/audio.js | 14 ++-- 6 files changed, 93 insertions(+), 38 deletions(-) create mode 100644 interface/resources/icons/tablet-icons/mic-ptt-a.svg create mode 100644 interface/resources/icons/tablet-icons/mic-ptt-i.svg diff --git a/interface/resources/icons/tablet-icons/mic-ptt-a.svg b/interface/resources/icons/tablet-icons/mic-ptt-a.svg new file mode 100644 index 0000000000..e6df3c69d7 --- /dev/null +++ b/interface/resources/icons/tablet-icons/mic-ptt-a.svg @@ -0,0 +1 @@ +mic-ptt-a \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/mic-ptt-i.svg b/interface/resources/icons/tablet-icons/mic-ptt-i.svg new file mode 100644 index 0000000000..2141ea5229 --- /dev/null +++ b/interface/resources/icons/tablet-icons/mic-ptt-i.svg @@ -0,0 +1,24 @@ + + + + + + diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index 45358f59a2..d44a9c862e 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -120,6 +120,10 @@ Rectangle { isRedCheck: true; checked: AudioScriptingInterface.muted; onClicked: { + if (AudioScriptingInterface.pushToTalk && !checked) { + // disable push to talk if unmuting + AudioScriptingInterface.pushToTalk = false; + } AudioScriptingInterface.muted = checked; checked = Qt.binding(function() { return AudioScriptingInterface.muted; }); // restore binding } @@ -150,7 +154,23 @@ Rectangle { } AudioControls.CheckBox { spacing: muteMic.spacing - text: qsTr("Push To Talk"); + text: qsTr("Show audio level meter"); + checked: AvatarInputs.showAudioTools; + onClicked: { + AvatarInputs.showAudioTools = checked; + checked = Qt.binding(function() { return AvatarInputs.showAudioTools; }); // restore binding + } + onXChanged: rightMostInputLevelPos = x + width + } + } + + Separator {} + + ColumnLayout { + spacing: muteMic.spacing; + AudioControls.CheckBox { + spacing: muteMic.spacing + text: qsTr("Push To Talk (T)"); checked: isVR ? AudioScriptingInterface.pushToTalkHMD : AudioScriptingInterface.pushToTalkDesktop; onClicked: { if (isVR) { @@ -167,15 +187,41 @@ Rectangle { }); // restore binding } } - AudioControls.CheckBox { - spacing: muteMic.spacing - text: qsTr("Show audio level meter"); - checked: AvatarInputs.showAudioTools; - onClicked: { - AvatarInputs.showAudioTools = checked; - checked = Qt.binding(function() { return AvatarInputs.showAudioTools; }); // restore binding + Item { + id: pttTextContainer + x: margins.paddings; + width: rightMostInputLevelPos + height: pttTextMetrics.height + visible: true + TextMetrics { + id: pttTextMetrics + text: pttText.text + font: pttText.font + } + RalewayRegular { + id: pttText + wrapMode: Text.WordWrap + color: hifi.colors.white; + width: parent.width; + font.italic: true + size: 16; + text: isVR ? qsTr("Press and hold grip triggers on both of your controllers to unmute.") : + qsTr("Press and hold the button \"T\" to unmute."); + onTextChanged: { + if (pttTextMetrics.width > rightMostInputLevelPos) { + pttTextContainer.height = Math.ceil(pttTextMetrics.width / rightMostInputLevelPos) * pttTextMetrics.height; + } else { + pttTextContainer.height = pttTextMetrics.height; + } + } + } + Component.onCompleted: { + if (pttTextMetrics.width > rightMostInputLevelPos) { + pttTextContainer.height = Math.ceil(pttTextMetrics.width / rightMostInputLevelPos) * pttTextMetrics.height; + } else { + pttTextContainer.height = pttTextMetrics.height; + } } - onXChanged: rightMostInputLevelPos = x + width } } } diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index 9d1cbfbc6c..50477b82f8 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -67,6 +67,9 @@ Rectangle { hoverEnabled: true; scrollGestureEnabled: false; onClicked: { + if (AudioScriptingInterface.pushToTalk) { + return; + } AudioScriptingInterface.muted = !AudioScriptingInterface.muted; Tablet.playSound(TabletEnums.ButtonClick); } @@ -109,9 +112,10 @@ Rectangle { Image { readonly property string unmutedIcon: "../../../icons/tablet-icons/mic-unmute-i.svg"; readonly property string mutedIcon: "../../../icons/tablet-icons/mic-mute-i.svg"; + readonly property string pushToTalkIcon: "../../../icons/tablet-icons/mic-ptt-i.svg"; id: image; - source: AudioScriptingInterface.muted ? mutedIcon : unmutedIcon; + source: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) ? pushToTalkIcon : AudioScriptingInterface.muted ? mutedIcon : unmutedIcon; width: 30; height: 30; @@ -155,7 +159,7 @@ Rectangle { color: parent.color; - text: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) ? "MUTED-PTT (T)" : (AudioScriptingInterface.muted ? "MUTED" : "MUTE"); + text: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) ? "MUTED PTT-(T)" : (AudioScriptingInterface.muted ? "MUTED" : "MUTE"); font.pointSize: 12; } diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 45bb15f1a3..63ce9d2b2e 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -231,26 +231,6 @@ void Audio::loadData() { _pttHMD = _pttHMDSetting.get(); } -bool Audio::getPTTHMD() const { - return resultWithReadLock([&] { - return _pttHMD; - }); -} - -void Audio::saveData() { - _desktopMutedSetting.set(getMutedDesktop()); - _hmdMutedSetting.set(getMutedHMD()); - _pttDesktopSetting.set(getPTTDesktop()); - _pttHMDSetting.set(getPTTHMD()); -} - -void Audio::loadData() { - _desktopMuted = _desktopMutedSetting.get(); - _hmdMuted = _hmdMutedSetting.get(); - _pttDesktop = _pttDesktopSetting.get(); - _pttHMD = _pttHMDSetting.get(); -} - bool Audio::noiseReductionEnabled() const { return resultWithReadLock([&] { return _enableNoiseReduction; diff --git a/scripts/system/audio.js b/scripts/system/audio.js index bf44cfa7cc..19ed3faef2 100644 --- a/scripts/system/audio.js +++ b/scripts/system/audio.js @@ -27,12 +27,12 @@ var UNMUTE_ICONS = { activeIcon: "icons/tablet-icons/mic-unmute-a.svg" }; var PTT_ICONS = { - icon: "icons/tablet-icons/mic-unmute-i.svg", - activeIcon: "icons/tablet-icons/mic-unmute-a.svg" + icon: "icons/tablet-icons/mic-ptt-i.svg", + activeIcon: "icons/tablet-icons/mic-ptt-a.svg" }; function onMuteToggled() { - if (Audio.pushingToTalk) { + if (Audio.pushToTalk) { button.editProperties(PTT_ICONS); } else if (Audio.muted) { button.editProperties(MUTE_ICONS); @@ -63,8 +63,8 @@ function onScreenChanged(type, url) { var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var button = tablet.addButton({ - icon: Audio.muted ? MUTE_ICONS.icon : UNMUTE_ICONS.icon, - activeIcon: Audio.muted ? MUTE_ICONS.activeIcon : UNMUTE_ICONS.activeIcon, + icon: Audio.pushToTalk ? PTT_ICONS.icon : Audio.muted ? MUTE_ICONS.icon : UNMUTE_ICONS.icon, + activeIcon: Audio.pushToTalk ? PTT_ICONS.activeIcon : Audio.muted ? MUTE_ICONS.activeIcon : UNMUTE_ICONS.activeIcon, text: TABLET_BUTTON_NAME, sortOrder: 1 }); @@ -74,7 +74,7 @@ onMuteToggled(); button.clicked.connect(onClicked); tablet.screenChanged.connect(onScreenChanged); Audio.mutedChanged.connect(onMuteToggled); -Audio.pushingToTalkChanged.connect(onMuteToggled); +Audio.pushToTalkChanged.connect(onMuteToggled); Script.scriptEnding.connect(function () { if (onAudioScreen) { @@ -83,7 +83,7 @@ Script.scriptEnding.connect(function () { button.clicked.disconnect(onClicked); tablet.screenChanged.disconnect(onScreenChanged); Audio.mutedChanged.disconnect(onMuteToggled); - Audio.pushingToTalkChanged.disconnect(onMuteToggled); + Audio.pushToTalkChanged.disconnect(onMuteToggled); tablet.removeButton(button); }); From 2536fcaa1794c30813544e616f1ca4666c7a05a3 Mon Sep 17 00:00:00 2001 From: r3tk0n Date: Thu, 7 Mar 2019 11:31:28 -0800 Subject: [PATCH 061/446] Add pushToTalk.js controllerModule. --- .../controllerModules/pushToTalk.js | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 scripts/system/controllers/controllerModules/pushToTalk.js diff --git a/scripts/system/controllers/controllerModules/pushToTalk.js b/scripts/system/controllers/controllerModules/pushToTalk.js new file mode 100644 index 0000000000..e764b228c9 --- /dev/null +++ b/scripts/system/controllers/controllerModules/pushToTalk.js @@ -0,0 +1,75 @@ +"use strict"; + +// Created by Jason C. Najera on 3/7/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Handles Push-to-Talk functionality for HMD mode. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); + +(function() { // BEGIN LOCAL_SCOPE + function PushToTalkHandler() { + var _this = this; + this.active = false; + //var pttMapping, mappingName; + + this.setup = function() { + //mappingName = 'Hifi-PTT-Dev-' + Math.random(); + //pttMapping = Controller.newMapping(mappingName); + //pttMapping.enable(); + }; + + this.shouldTalk = function (controllerData) { + // Set up test against controllerData here... + var gripVal = controllerData.secondaryValues[this.hand]; + return (gripVal) ? true : false; + }; + + this.shouldStopTalking = function (controllerData) { + var gripVal = controllerData.secondaryValues[this.hand]; + return (gripVal) ? false : true; + }; + + this.isReady = function (controllerData, deltaTime) { + if (HMD.active() && Audio.pushToTalk && this.shouldTalk(controllerData)) { + Audio.pushingToTalk = true; + returnMakeRunningValues(true, [], []); + } + + return makeRunningValues(false, [], []); + }; + + this.run = function (controllerData, deltaTime) { + if (this.shouldStopTalking(controllerData) || !Audio.pushToTalk) { + Audio.pushingToTalk = false; + return makeRunningValues(false, [], []); + } + + return makeRunningValues(true, [], []); + }; + + this.cleanup = function () { + //pttMapping.disable(); + }; + + this.parameters = makeDispatcherModuleParameters( + 950, + ["head"], + [], + 100); + } + + var pushToTalk = new PushToTalkHandler(); + enableDispatcherModule("PushToTalk", pushToTalk); + + function cleanup () { + pushToTalk.cleanup(); + disableDispatcherModule("PushToTalk"); + }; + + Script.scriptEnding.connect(cleanup); +}()); // END LOCAL_SCOPE \ No newline at end of file From 9d59e68d45d53e53d6471993dd34bca6695ddfab Mon Sep 17 00:00:00 2001 From: r3tk0n Date: Thu, 7 Mar 2019 11:31:48 -0800 Subject: [PATCH 062/446] Enable pushToTalk.js controller module. --- scripts/system/controllers/controllerDispatcher.js | 1 + scripts/system/controllers/controllerScripts.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index 28c3e2a299..f4c0cbb0d6 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -58,6 +58,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); // not set to false (not in use), a module cannot start. When a module is using a slot, that module's name // is stored as the value, rather than false. this.activitySlots = { + head: false, leftHand: false, rightHand: false, rightHandTrigger: false, diff --git a/scripts/system/controllers/controllerScripts.js b/scripts/system/controllers/controllerScripts.js index 86ff7701c3..d236d6b12a 100644 --- a/scripts/system/controllers/controllerScripts.js +++ b/scripts/system/controllers/controllerScripts.js @@ -34,7 +34,8 @@ var CONTOLLER_SCRIPTS = [ "controllerModules/nearGrabHyperLinkEntity.js", "controllerModules/nearTabletHighlight.js", "controllerModules/nearGrabEntity.js", - "controllerModules/farGrabEntity.js" + "controllerModules/farGrabEntity.js", + "controllerModules/pushToTalk.js" ]; var DEBUG_MENU_ITEM = "Debug defaultScripts.js"; From 55efc315f10604c663b8df2fa6f65c7dce7d4f3f Mon Sep 17 00:00:00 2001 From: r3tk0n Date: Thu, 7 Mar 2019 11:45:59 -0800 Subject: [PATCH 063/446] Fix activation / deactivation criteria for PTT controller module. --- scripts/system/controllers/controllerModules/pushToTalk.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/system/controllers/controllerModules/pushToTalk.js b/scripts/system/controllers/controllerModules/pushToTalk.js index e764b228c9..557476ccd7 100644 --- a/scripts/system/controllers/controllerModules/pushToTalk.js +++ b/scripts/system/controllers/controllerModules/pushToTalk.js @@ -25,12 +25,12 @@ Script.include("/~/system/libraries/controllers.js"); this.shouldTalk = function (controllerData) { // Set up test against controllerData here... - var gripVal = controllerData.secondaryValues[this.hand]; + var gripVal = controllerData.secondaryValues[LEFT_HAND] && controllerData.secondaryValues[RIGHT_HAND]; return (gripVal) ? true : false; }; this.shouldStopTalking = function (controllerData) { - var gripVal = controllerData.secondaryValues[this.hand]; + var gripVal = controllerData.secondaryValues[LEFT_HAND] && controllerData.secondaryValues[RIGHT_HAND]; return (gripVal) ? false : true; }; From a62cadc54145dcbc54bf17d6f13d1277a1832247 Mon Sep 17 00:00:00 2001 From: r3tk0n Date: Thu, 7 Mar 2019 12:35:04 -0800 Subject: [PATCH 064/446] Fix typo. --- scripts/system/controllers/controllerModules/pushToTalk.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/controllerModules/pushToTalk.js b/scripts/system/controllers/controllerModules/pushToTalk.js index 557476ccd7..dd959ae6fb 100644 --- a/scripts/system/controllers/controllerModules/pushToTalk.js +++ b/scripts/system/controllers/controllerModules/pushToTalk.js @@ -35,7 +35,7 @@ Script.include("/~/system/libraries/controllers.js"); }; this.isReady = function (controllerData, deltaTime) { - if (HMD.active() && Audio.pushToTalk && this.shouldTalk(controllerData)) { + if (HMD.active && Audio.pushToTalk && this.shouldTalk(controllerData)) { Audio.pushingToTalk = true; returnMakeRunningValues(true, [], []); } From b83e9f70e64ff7717f1fda7a5ed7d7401cdf304b Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Thu, 7 Mar 2019 12:36:56 -0800 Subject: [PATCH 065/446] adding PushToTalk action --- interface/src/Application.cpp | 11 +++++++++++ libraries/controllers/src/controllers/Actions.cpp | 4 ++++ libraries/controllers/src/controllers/Actions.h | 1 + .../controllers/controllerModules/pushToTalk.js | 4 ++-- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index fa63757560..1582c69bc9 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1601,12 +1601,23 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(userInputMapper.data(), &UserInputMapper::actionEvent, [this](int action, float state) { using namespace controller; auto tabletScriptingInterface = DependencyManager::get(); + auto audioScriptingInterface = reinterpret_cast(DependencyManager::get().data()); { auto actionEnum = static_cast(action); int key = Qt::Key_unknown; static int lastKey = Qt::Key_unknown; bool navAxis = false; switch (actionEnum) { + case Action::TOGGLE_PUSHTOTALK: + if (audioScriptingInterface->getPTT()) { + qDebug() << "State is " << state; + if (state > 0.0f) { + audioScriptingInterface->setPushingToTalk(false); + } else if (state < 0.0f) { + audioScriptingInterface->setPushingToTalk(true); + } + } + case Action::UI_NAV_VERTICAL: navAxis = true; if (state > 0.0f) { diff --git a/libraries/controllers/src/controllers/Actions.cpp b/libraries/controllers/src/controllers/Actions.cpp index 5a396231b6..57be2f788b 100644 --- a/libraries/controllers/src/controllers/Actions.cpp +++ b/libraries/controllers/src/controllers/Actions.cpp @@ -180,6 +180,7 @@ namespace controller { * third person, to full screen mirror, then back to first person and repeat. * ContextMenunumbernumberShow / hide the tablet. * ToggleMutenumbernumberToggle the microphone mute. + * TogglePushToTalknumbernumberToggle push to talk. * ToggleOverlaynumbernumberToggle the display of overlays. * SprintnumbernumberSet avatar sprint mode. * ReticleClicknumbernumberSet mouse-pressed. @@ -245,6 +246,8 @@ namespace controller { * ContextMenu instead. * TOGGLE_MUTEnumbernumberDeprecated: Use * ToggleMute instead. + * TOGGLE_PUSHTOTALKnumbernumberDeprecated: Use + * TogglePushToTalk instead. * SPRINTnumbernumberDeprecated: Use * Sprint instead. * LONGITUDINAL_BACKWARDnumbernumberDeprecated: Use @@ -411,6 +414,7 @@ namespace controller { makeButtonPair(Action::ACTION2, "SecondaryAction"), makeButtonPair(Action::CONTEXT_MENU, "ContextMenu"), makeButtonPair(Action::TOGGLE_MUTE, "ToggleMute"), + makeButtonPair(Action::TOGGLE_PUSHTOTALK, "TogglePushToTalk"), makeButtonPair(Action::CYCLE_CAMERA, "CycleCamera"), makeButtonPair(Action::TOGGLE_OVERLAY, "ToggleOverlay"), makeButtonPair(Action::SPRINT, "Sprint"), diff --git a/libraries/controllers/src/controllers/Actions.h b/libraries/controllers/src/controllers/Actions.h index a12a3d60a9..3e99d8d147 100644 --- a/libraries/controllers/src/controllers/Actions.h +++ b/libraries/controllers/src/controllers/Actions.h @@ -60,6 +60,7 @@ enum class Action { CONTEXT_MENU, TOGGLE_MUTE, + TOGGLE_PUSHTOTALK, CYCLE_CAMERA, TOGGLE_OVERLAY, diff --git a/scripts/system/controllers/controllerModules/pushToTalk.js b/scripts/system/controllers/controllerModules/pushToTalk.js index e764b228c9..916769934d 100644 --- a/scripts/system/controllers/controllerModules/pushToTalk.js +++ b/scripts/system/controllers/controllerModules/pushToTalk.js @@ -35,7 +35,7 @@ Script.include("/~/system/libraries/controllers.js"); }; this.isReady = function (controllerData, deltaTime) { - if (HMD.active() && Audio.pushToTalk && this.shouldTalk(controllerData)) { + if (HMD.active && Audio.pushToTalk && this.shouldTalk(controllerData)) { Audio.pushingToTalk = true; returnMakeRunningValues(true, [], []); } @@ -72,4 +72,4 @@ Script.include("/~/system/libraries/controllers.js"); }; Script.scriptEnding.connect(cleanup); -}()); // END LOCAL_SCOPE \ No newline at end of file +}()); // END LOCAL_SCOPE From 472c7ffab443afbeaa31b090a07f1ddfd475e5ad Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Thu, 7 Mar 2019 12:54:32 -0800 Subject: [PATCH 066/446] removing debug statement --- interface/src/Application.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1582c69bc9..16f4a9094f 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1610,7 +1610,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo switch (actionEnum) { case Action::TOGGLE_PUSHTOTALK: if (audioScriptingInterface->getPTT()) { - qDebug() << "State is " << state; if (state > 0.0f) { audioScriptingInterface->setPushingToTalk(false); } else if (state < 0.0f) { From 0b7cddb886ffd24f7e150b9150fe7a582626be77 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 8 Mar 2019 13:49:10 +1300 Subject: [PATCH 067/446] Fill in and tidy MyAvatar JSDoc --- interface/src/avatar/MyAvatar.cpp | 14 + interface/src/avatar/MyAvatar.h | 533 +++++++++++++----- .../src/avatars-renderer/Avatar.h | 93 ++- libraries/shared/src/RegisteredMetaTypes.cpp | 9 +- 4 files changed, 483 insertions(+), 166 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 30e8733a42..f62896772d 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3531,6 +3531,12 @@ void MyAvatar::clearScaleRestriction() { _haveReceivedHeightLimitsFromDomain = false; } +/**jsdoc + * A teleport target. + * @typedef {object} MyAvatar.GoToProperties + * @property {Vec3} position - The new position for the avatar, in world coordinates. + * @property {Quat} [orientation] - The new orientation for the avatar. + */ void MyAvatar::goToLocation(const QVariant& propertiesVar) { qCDebug(interfaceapp, "MyAvatar QML goToLocation"); auto properties = propertiesVar.toMap(); @@ -3887,6 +3893,14 @@ void MyAvatar::setCollisionWithOtherAvatarsFlags() { _characterController.setPendingFlagsUpdateCollisionMask(); } +/**jsdoc + * A collision capsule is a cylinder with hemispherical ends. It is used, in particular, to approximate the extents of an + * avatar. + * @typedef {object} MyAvatar.CollisionCapsule + * @property {Vec3} start - The bottom end of the cylinder, excluding the bottom hemisphere. + * @property {Vec3} end - The top end of the cylinder, excluding the top hemisphere. + * @property {number} radius - The radius of the cylinder and the hemispheres. + */ void MyAvatar::updateCollisionCapsuleCache() { glm::vec3 start, end; float radius; diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index a0f1531e64..f05bdd5184 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -155,33 +155,45 @@ class MyAvatar : public Avatar { * @property {boolean} hasProceduralEyeFaceMovement=true - If true then procedural eye movement is turned on. * @property {boolean} hasAudioEnabledFaceMovement=true - If true then voice audio will move the mouth * blendshapes while MyAvatar.hasScriptedBlendshapes is enabled. - * @property {number} rotationRecenterFilterLength - * @property {number} rotationThreshold - * @property {boolean} enableStepResetRotation - * @property {boolean} enableDrawAverageFacing + * @property {number} rotationRecenterFilterLength - Configures how quickly the avatar root rotates to recenter its facing + * direction to match that of the user's torso based on head and hands orientation. A smaller value makes the + * recentering happen more quickly. The minimum value is 0.01. + * @property {number} rotationThreshold - The angle in radians that the user's torso facing direction (based on head and + * hands orientation) can differ from that of the avatar before the avatar's root is rotated to match the user's torso. + * @property {boolean} enableStepResetRotation - If true then after the user's avatar takes a step, the + * avatar's root immediately rotates to recenter its facing direction to match that of the user's torso based on head + * and hands orientation. + * @property {boolean} enableDrawAverageFacing - If true, debug graphics are drawn that show the average + * facing direction of the user's torso (based on head and hands orientation). This can be useful if you want to try + * out different filter lengths and thresholds. * * @property {Vec3} leftHandPosition - The position of the left hand in avatar coordinates if it's being positioned by * controllers, otherwise {@link Vec3(0)|Vec3.ZERO}. Read-only. * @property {Vec3} rightHandPosition - The position of the right hand in avatar coordinates if it's being positioned by * controllers, otherwise {@link Vec3(0)|Vec3.ZERO}. Read-only. - * @property {Vec3} leftHandTipPosition - The position 30cm offset from the left hand in avatar coordinates if it's being - * positioned by controllers, otherwise {@link Vec3(0)|Vec3.ZERO}. Read-only. - * @property {Vec3} rightHandTipPosition - The position 30cm offset from the right hand in avatar coordinates if it's being - * positioned by controllers, otherwise {@link Vec3(0)|Vec3.ZERO}. Read-only. + * @property {Vec3} leftHandTipPosition - The position 0.3m in front of the left hand's position, in the direction along the + * palm, in avatar coordinates. If the hand isn't being positioned by a controller, the value is + * {@link Vec3(0)|Vec3.ZERO}. Read-only. + * @property {Vec3} rightHandTipPosition - The position 0.3m in front of the right hand's position, in the direction along + * the palm, in avatar coordinates. If the hand isn't being positioned by a controller, the value is + * {@link Vec3(0)|Vec3.ZERO}. Read-only. * - * @property {Pose} leftHandPose - The pose of the left hand as determined by the hand controllers. Read-only. - * @property {Pose} rightHandPose - The pose right hand position as determined by the hand controllers. Read-only. - * @property {Pose} leftHandTipPose - The pose of the left hand as determined by the hand controllers, with the position - * by 30cm. Read-only. - * @property {Pose} rightHandTipPose - The pose of the right hand as determined by the hand controllers, with the position - * by 30cm. Read-only. + * @property {Pose} leftHandPose - The pose of the left hand as determined by the hand controllers, relative to the avatar. + * Read-only. + * @property {Pose} rightHandPose - The pose right hand position as determined by the hand controllers, relative to the + * avatar. Read-only. + * @property {Pose} leftHandTipPose - The pose of the left hand as determined by the hand controllers, relative to the + * avatar, with the position adjusted to be 0.3m along the direction of the palm. Read-only. + * @property {Pose} rightHandTipPose - The pose of the right hand as determined by the hand controllers, relative to the + * avatar, with the position adjusted by 0.3m along the direction of the palm. Read-only. * - * @property {number} energy - * @property {boolean} isAway + * @property {number} energy - Deprecated: This property will be removed from the API. + * @property {boolean} isAway - true if your avatar is away (i.e., inactive), false if it is + * active. * - * @property {boolean} centerOfGravityModelEnabled=true - If true then the avatar hips are placed according to the center of - * gravity model that balance the center of gravity over the base of support of the feet. Setting the value false - * will result in the default behaviour where the hips are placed under the head. + * @property {boolean} centerOfGravityModelEnabled=true - If true then the avatar hips are placed according to + * the center of gravity model that balances the center of gravity over the base of support of the feet. Setting the + * value false results in the default behavior where the hips are positioned under the head. * @property {boolean} hmdLeanRecenterEnabled=true - If true then the avatar is re-centered to be under the * head's position. In room-scale VR, this behavior is what causes your avatar to follow your HMD as you walk around * the room. Setting the value false is useful if you want to pin the avatar to a fixed position. @@ -198,8 +210,8 @@ class MyAvatar : public Avatar { * boundaries while teleporting.
* Note: Setting the value has no effect unless Interface is restarted. * - * @property {number} yawSpeed=75 - * @property {number} pitchSpeed=50 + * @property {number} yawSpeed=75 - The mouse X sensitivity value in Settings > General. Read-only. + * @property {number} pitchSpeed=50 - The mouse Y sensitivity value in Settings > General. Read-only. * * @property {boolean} hmdRollControlEnabled=true - If true, the roll angle of your HMD turns your avatar * while flying. @@ -215,13 +227,20 @@ class MyAvatar : public Avatar { * where MyAvatar.sessionUUID is not available (e.g., if not connected to a domain). Note: Likely to be deprecated. * Read-only. * - * @property {number} walkSpeed - * @property {number} walkBackwardSpeed - * @property {number} sprintSpeed - * @property {number} isInSittingState - * @property {MyAvatar.SitStandModelType} userRecenterModel - * @property {boolean} isSitStandStateLocked - * @property {boolean} allowTeleporting + * @property {number} walkSpeed - Adjusts the walk speed of your avatar. + * @property {number} walkBackwardSpeed - Adjusts the walk backward speed of your avatar. + * @property {number} sprintSpeed - Adjusts the sprint speed of your avatar. + * @property {MyAvatar.SitStandModelType} userRecenterModel - Controls avatar leaning and recentering behavior. + * @property {number} isInSittingState - true if your avatar is sitting (avatar leaning is disabled, + * recenntering is enabled), false if it is standing (avatar leaning is enabled, and avatar recenters if it + * leans too far). If userRecenterModel == 2 (i.e., auto) the property value automatically updates as the + * user sits or stands, unless isSitStandStateLocked == true. Setting the property value overrides the + * current siting / standing state, which is updated when the user next sits or stands unless + * isSitStandStateLocked == true. + * @property {boolean} isSitStandStateLocked - true locks the avatar sitting / standing state, i.e., disables + * automatically changing it based on the user sitting or standing. + * @property {boolean} allowTeleporting - true if teleporting is enabled in the Interface settings, + * false if it isn't. Read-only. * * @borrows Avatar.getDomainMinScale as getDomainMinScale * @borrows Avatar.getDomainMaxScale as getDomainMaxScale @@ -353,6 +372,40 @@ class MyAvatar : public Avatar { using TimePoint = Clock::time_point; public: + + /**jsdoc + *

Logical keys that drive your avatar and camera.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ValueNameDescription
0TRANSLATE_XMove the user's avatar in the direction of its x-axis, if the + * camera isn't in independent or mirror modes.
1TRANSLATE_YMove the user's avatar in the direction of its y-axis, if the + * camera isn't in independent or mirror modes.
2TRANSLATE_ZMove the user's avatar in the direction of its z-axis, if the + * camera isn't in independent or mirror modes
3YAWRotate the user's avatar about its y-axis at a rate proportional to the + * control value, if the camera isn't in independent or mirror modes.
4STEP_TRANSLATE_XNo action.
5STEP_TRANSLATE_YNo action.
6STEP_TRANSLATE_ZNo action.
7STEP_YAWRotate the user's avatar about its y-axis in a step increment, if + * the camera isn't in independent or mirror modes.
8PITCHRotate the user's avatar head and attached camera about its negative + * x-axis (i.e., positive values pitch down) at a rate proportional to the control value, if the camera isn't in HMD, + * independent, or mirror modes.
9ZOOMZooms the camera in or out.
10DELTA_YAWRotate the user's avatar about its y-axis by an amount proportional + * to the control value, if the camera isn't in independent or mirror modes.
11DELTA_PITCHRotate the user's avatar head and attached camera about its + * negative x-axis (i.e., positive values pitch down) by an amount proportional to the control value, if the camera + * isn't in HMD, independent, or mirror modes.
+ * @typedef {number} MyAvatar.DriveKeys + */ enum DriveKeys { TRANSLATE_X = 0, TRANSLATE_Y, @@ -371,6 +424,25 @@ public: Q_ENUM(DriveKeys) /**jsdoc + *

Specifies different avatar leaning and recentering behaviors.

+ * + * + * + * + * + * + * + * + * + * + *
ValueNameDescription
0ForceSitAssumes the user is seated in the real world. Disables avatar + * leaning regardless of what the avatar is doing in the virtual world (i.e., avatar always recenters).
1ForceStandAssumes the user is standing in the real world. Enables avatar + * leaning regardless of what the avatar is doing in the virtual world (i.e. avatar leans, then if leans too far it + * recenters).
2AutoInterface detects when the user is standing or seated in the real world. + * Avatar leaning is disabled when the user is sitting (i.e., avatar always recenters), and avatar leaning is enabled + * when the user is standing (i.e., avatar leans, then if leans too far it recenters).
3DisableHMDLeanBoth avatar leaning and recentering are disabled regardless of + * what the user is doing in the real world and no matter what their avatar is doing in the virtual world. Enables + * the avatar to sit on the floor when the user sits on the floor.
Note: Experimental.
* @typedef {number} MyAvatar.SitStandModelType */ enum SitStandModelType { @@ -399,6 +471,7 @@ public: void setCollisionWithOtherAvatarsFlags() override; /**jsdoc + * Resets the sensor positioning of your HMD (if used) and recenters your avatar body and head. * @function MyAvatar.resetSensorsAndBody */ Q_INVOKABLE void resetSensorsAndBody(); @@ -427,14 +500,16 @@ public: const glm::quat& getHMDSensorOrientation() const { return _hmdSensorOrientation; } /**jsdoc + * Gets the avatar orientation. Suitable for use in QML. * @function MyAvatar.setOrientationVar - * @param {object} newOrientationVar + * @param {object} newOrientationVar - The avatar orientation. */ Q_INVOKABLE void setOrientationVar(const QVariant& newOrientationVar); /**jsdoc + * Gets the avatar orientation. Suitable for use in QML. * @function MyAvatar.getOrientationVar - * @returns {object} + * @returns {object} The avatar orientation. */ Q_INVOKABLE QVariant getOrientationVar() const; @@ -598,74 +673,124 @@ public: // a handler must not remove properties from animStateDictionaryIn, nor change property values that it does not intend to change. // It is not specified in what order multiple handlers are called. /**jsdoc + * Adds an animation state handler function that is invoked just before each animation graph update. More than one + * animation state handler function may be added by calling addAnimationStateHandler multiple times. It is not + * specified in what order multiple handlers are called. + *

The animation state handler function is called with an {@link MyAvatar.AnimStateDictionary|AnimStateDictionary} + * "animStateDictionaryIn" parameter and is expected to return an + * {@link MyAvatar.AnimStateDictionary|AnimStateDictionary} "animStateDictionaryOut" object. The + * animStateDictionaryOut object can be the same object as animStateDictionaryIn, or it can be a + * different object. The animStateDictionaryIn may be shared among multiple handlers and thus may contain + * additional properties specified when adding the different handlers.

+ *

A handler may change a value from animStateDictionaryIn or add different values in the + * animStateDictionaryOut returned. Any property values set in animStateDictionaryOut will + * override those of the internal animation machinery.|null} propertiesList - The list of {@link MyAvatar.AnimStateDictionary|AnimStateDictionary} + * properties that should be included the in parameter that the handler function is called with. If null + * then all properties are included in the call parameter. + * @returns {number} The ID of the animation state handler function if successfully added, undefined if not + * successfully added. + * @example Log all the animation state dictionary parameters for a short while. + * function animStateHandler(dictionary) { + * print("Anim state dictionary: " + JSON.stringify(dictionary)); + * } + * + * var handler = MyAvatar.addAnimationStateHandler(animStateHandler, null); + * + * Script.setTimeout(function () { + * MyAvatar.removeAnimationStateHandler(handler); + * }, 100); */ Q_INVOKABLE QScriptValue addAnimationStateHandler(QScriptValue handler, QScriptValue propertiesList) { return _skeletonModel->getRig().addAnimationStateHandler(handler, propertiesList); } /**jsdoc + * Removes an animation state handler function. * @function MyAvatar.removeAnimationStateHandler - * @param {number} handler + * @param {number} handler - The ID of the animation state handler function to remove. */ // Removes a handler previously added by addAnimationStateHandler. Q_INVOKABLE void removeAnimationStateHandler(QScriptValue handler) { _skeletonModel->getRig().removeAnimationStateHandler(handler); } /**jsdoc + * Gets whether or not you do snap turns in HMD mode. * @function MyAvatar.getSnapTurn - * @returns {boolean} + * @returns {boolean} true if you do snap turns in HMD mode; false if you do smooth turns in HMD + * mode. */ Q_INVOKABLE bool getSnapTurn() const { return _useSnapTurn; } /**jsdoc + * Sets whether your should do snap turns or smooth turns in HMD mode. * @function MyAvatar.setSnapTurn - * @param {boolean} on + * @param {boolean} on - true to do snap turns in HMD mode; false to do smooth turns in HMD mode. */ Q_INVOKABLE void setSnapTurn(bool on) { _useSnapTurn = on; } /**jsdoc + * Sets the avatar's dominant hand. * @function MyAvatar.setDominantHand - * @param {string} hand + * @param {string} hand - The dominant hand: "left" for the left hand or "right" for the right + * hand. Any other value has no effect. */ Q_INVOKABLE void setDominantHand(const QString& hand); /**jsdoc + * Gets the avatar's dominant hand. * @function MyAvatar.getDominantHand - * @returns {string} + * @returns {string} "left" for the left hand, "right" for the right hand. */ Q_INVOKABLE QString getDominantHand() const; /**jsdoc * @function MyAvatar.setHmdAvatarAlignmentType - * @param {string} hand + * @param {string} type - "head" to align your head and your avatar's head, "eyes" to align your + * eyes and your avatar's eyes. + * */ - Q_INVOKABLE void setHmdAvatarAlignmentType(const QString& hand); + Q_INVOKABLE void setHmdAvatarAlignmentType(const QString& type); /**jsdoc + * Gets the HMD alignment for your avatar. * @function MyAvatar.getHmdAvatarAlignmentType - * @returns {string} + * @returns {string} "head" if aligning your head and your avatar's head, "eyes" if aligning your + * eyes and your avatar's eyes. */ Q_INVOKABLE QString getHmdAvatarAlignmentType() const; /**jsdoc + * Sets whether the avatar hips are balanced over the feet or positioned under the head. * @function MyAvatar.setCenterOfGravityModelEnabled - * @param {boolean} enabled + * @param {boolean} enabled - true to balance the hips over the feet, false to position the hips + * under the head. */ Q_INVOKABLE void setCenterOfGravityModelEnabled(bool value) { _centerOfGravityModelEnabled = value; } /**jsdoc + * Gets whether the avatar hips are being balanced over the feet or placed under the head. * @function MyAvatar.getCenterOfGravityModelEnabled - * @returns {boolean} + * @returns {boolean} true if the hips are being balanced over the feet, false if the hips are + * being positioned under the head. */ Q_INVOKABLE bool getCenterOfGravityModelEnabled() const { return _centerOfGravityModelEnabled; } + /**jsdoc + * Sets whether or not the avatar's position updates to recenter the avatar under the head. In room-scale VR, recentering + * causes your avatar to follow your HMD as you walk around the room. Disabling recentering is useful if you want to pin + * the avatar to a fixed position. * @function MyAvatar.setHMDLeanRecenterEnabled - * @param {boolean} enabled + * @param {boolean} enabled - true to recenter the avatar under the head as it moves, false to + * disable recentering. */ Q_INVOKABLE void setHMDLeanRecenterEnabled(bool value) { _hmdLeanRecenterEnabled = value; } /**jsdoc + * Gets whether or not the avatar's position updates to recenter the avatar under the head. In room-scale VR, recentering + * causes your avatar to follow your HMD as you walk around the room. * @function MyAvatar.getHMDLeanRecenterEnabled - * @returns {boolean} + * @returns {boolean} true if recentering is enabled, false if not. */ Q_INVOKABLE bool getHMDLeanRecenterEnabled() const { return _hmdLeanRecenterEnabled; } @@ -739,30 +864,42 @@ public: float getDriveKey(DriveKeys key) const; /**jsdoc + * Gets the value of a drive key, regardless of whether or not it is disabled. * @function MyAvatar.getRawDriveKey - * @param {DriveKeys} key - * @returns {number} + * @param {MyAvatar.DriveKeys} key - The drive key. + * @returns {number} The value of the drive key. */ Q_INVOKABLE float getRawDriveKey(DriveKeys key) const; void relayDriveKeysToCharacterController(); /**jsdoc + * Disables the action associated with a drive key. * @function MyAvatar.disableDriveKey - * @param {DriveKeys} key + * @param {MyAvatar.DriveKeys} key - The drive key to disable. + * @example Disable rotating your avatar using the keyboard for a couple of seconds. + * var YAW = 3; + * print("Disable"); + * MyAvatar.disableDriveKey(YAW); + * Script.setTimeout(function () { + * print("Enable"); + * MyAvatar.enableDriveKey(YAW); + * }, 5000); */ Q_INVOKABLE void disableDriveKey(DriveKeys key); /**jsdoc + * Enables the acction associated with a drive key. * @function MyAvatar.enableDriveKey - * @param {DriveKeys} key + * @param {MyAvatar.DriveKeys} key - The drive key to enable. */ Q_INVOKABLE void enableDriveKey(DriveKeys key); /**jsdoc + * Checks whether or not a drive key is disabled. * @function MyAvatar.isDriveKeyDisabled - * @param {DriveKeys} key - * @returns {boolean} + * @param {DriveKeys} key - The drive key to check. + * @returns {boolean} true if the drive key is disabled, false if it isn't. */ Q_INVOKABLE bool isDriveKeyDisabled(DriveKeys key) const; @@ -809,26 +946,32 @@ public: Q_INVOKABLE glm::vec3 getHeadPosition() const { return getHead()->getPosition(); } /**jsdoc + * Gets the yaw of the avatar's head relative to its body. * @function MyAvatar.getHeadFinalYaw - * @returns {number} + * @returns {number} The yaw of the avatar's head, in degrees. */ Q_INVOKABLE float getHeadFinalYaw() const { return getHead()->getFinalYaw(); } /**jsdoc + * Gets the roll of the avatar's head relative to its body. * @function MyAvatar.getHeadFinalRoll - * @returns {number} + * @returns {number} The roll of the avatar's head, in degrees. */ Q_INVOKABLE float getHeadFinalRoll() const { return getHead()->getFinalRoll(); } /**jsdoc + * Gets the pitch of the avatar's head relative to its body. * @function MyAvatar.getHeadFinalPitch - * @returns {number} + * @returns {number} The pitch of the avatar's head, in degrees. */ Q_INVOKABLE float getHeadFinalPitch() const { return getHead()->getFinalPitch(); } /**jsdoc + * If a face tracker is connected and being used, gets the estimated pitch of the user's head scaled such that the avatar + * looks at the edge of the view frustum when the user looks at the edge of their screen. * @function MyAvatar.getHeadDeltaPitch - * @returns {number} + * @returns {number} The pitch that the avatar's head should be if a face tracker is connected and being used, otherwise + * 0, in degrees. */ Q_INVOKABLE float getHeadDeltaPitch() const { return getHead()->getDeltaPitch(); } @@ -845,21 +988,27 @@ public: /**jsdoc * Gets the position of the avatar your avatar is currently looking at. * @function MyAvatar.getTargetAvatarPosition - * @returns {Vec3} The position of the avatar your avatar is currently looking at. + * @returns {Vec3} The position of the avatar beeing looked at. * @example Report the position of the avatar you're currently looking at. * print(JSON.stringify(MyAvatar.getTargetAvatarPosition())); */ + // FIXME: If not looking at an avatar, the most recently looked-at position is returned. This should be fixed to return + // undefined or {NaN, NaN, NaN} or similar. Q_INVOKABLE glm::vec3 getTargetAvatarPosition() const { return _targetAvatarPosition; } /**jsdoc + * Gets information on the avatar your avatar is currently looking at. * @function MyAvatar.getTargetAvatar - * @returns {AvatarData} + * @returns {AvatarData} Information on the avatar being looked at. */ + // FIXME: The return type doesn't have a conversion to a script value so the function always returns undefined in + // JavaScript. Note: When fixed, JSDoc is needed for the return type. Q_INVOKABLE ScriptAvatarData* getTargetAvatar() const; /**jsdoc - * Gets the position of the avatar's left hand as positioned by a hand controller (e.g., Oculus Touch or Vive).
+ * Gets the position of the avatar's left hand, relative to the avatar, as positioned by a hand controller (e.g., Oculus + * Touch or Vive). *

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints * for hand animation.)

* @function MyAvatar.getLeftHandPosition @@ -871,7 +1020,8 @@ public: Q_INVOKABLE glm::vec3 getLeftHandPosition() const; /**jsdoc - * Gets the position of the avatar's right hand as positioned by a hand controller (e.g., Oculus Touch or Vive).
+ * Gets the position of the avatar's right hand, relative to the avatar, as positioned by a hand controller (e.g., Oculus + * Touch or Vive). *

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints * for hand animation.)

* @function MyAvatar.getRightHandPosition @@ -883,26 +1033,32 @@ public: Q_INVOKABLE glm::vec3 getRightHandPosition() const; /**jsdoc + * Gets the position 0.3m in front of the left hand's position in the direction along the palm, in avatar coordinates, as + * positioned by a hand controller. * @function MyAvatar.getLeftHandTipPosition - * @returns {Vec3} + * @returns {Vec3} The position 0.3m in front of the left hand's position in the direction along the palm, in avatar + * coordinates. If the hand isn't being positioned by a controller, {@link Vec3(0)|Vec3.ZERO} is returned. */ Q_INVOKABLE glm::vec3 getLeftHandTipPosition() const; /**jsdoc + * Gets the position 0.3m in front of the right hand's position in the direction along the palm, in avatar coordinates, as + * positioned by a hand controller. * @function MyAvatar.getRightHandTipPosition - * @returns {Vec3} + * @returns {Vec3} The position 0.3m in front of the right hand's position in the direction along the palm, in avatar + * coordinates. If the hand isn't being positioned by a controller, {@link Vec3(0)|Vec3.ZERO} is returned. */ Q_INVOKABLE glm::vec3 getRightHandTipPosition() const; /**jsdoc * Gets the pose (position, rotation, velocity, and angular velocity) of the avatar's left hand as positioned by a - * hand controller (e.g., Oculus Touch or Vive).
+ * hand controller (e.g., Oculus Touch or Vive). *

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints * for hand animation.) If you are using the Leap Motion, the return value's valid property will be * false and any pose values returned will not be meaningful.

* @function MyAvatar.getLeftHandPose - * @returns {Pose} + * @returns {Pose} The pose of the avatar's left hand, relative to the avatar, as positioned by a hand controller. * @example Report the pose of your avatar's left hand. * print(JSON.stringify(MyAvatar.getLeftHandPose())); */ @@ -910,26 +1066,38 @@ public: /**jsdoc * Gets the pose (position, rotation, velocity, and angular velocity) of the avatar's left hand as positioned by a - * hand controller (e.g., Oculus Touch or Vive).
+ * hand controller (e.g., Oculus Touch or Vive). *

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints * for hand animation.) If you are using the Leap Motion, the return value's valid property will be * false and any pose values returned will not be meaningful.

* @function MyAvatar.getRightHandPose - * @returns {Pose} + * @returns {Pose} The pose of the avatar's right hand, relative to the avatar, as positioned by a hand controller. * @example Report the pose of your avatar's right hand. * print(JSON.stringify(MyAvatar.getRightHandPose())); */ Q_INVOKABLE controller::Pose getRightHandPose() const; /**jsdoc + * Gets the pose (position, rotation, velocity, and angular velocity) of the avatar's left hand, relative to the avatar, as + * positioned by a hand controller (e.g., Oculus Touch or Vive), and translated 0.3m along the palm. + *

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints + * for hand animation.) If you are using the Leap Motion, the return value's valid property will be + * false and any pose values returned will not be meaningful.

* @function MyAvatar.getLeftHandTipPose - * @returns {Pose} + * @returns {Pose} The pose of the avatar's left hand, relative to the avatar, as positioned by a hand controller, and + * translated 0.3m along the palm. */ Q_INVOKABLE controller::Pose getLeftHandTipPose() const; /**jsdoc + * Gets the pose (position, rotation, velocity, and angular velocity) of the avatar's right hand, relative to the avatar, as + * positioned by a hand controller (e.g., Oculus Touch or Vive), and translated 0.3m along the palm. + *

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints + * for hand animation.) If you are using the Leap Motion, the return value's valid property will be + * false and any pose values returned will not be meaningful.

* @function MyAvatar.getRightHandTipPose - * @returns {Pose} + * @returns {Pose} The pose of the avatar's right hand, relative to the avatar, as positioned by a hand controller, and + * translated 0.3m along the palm. */ Q_INVOKABLE controller::Pose getRightHandTipPose() const; @@ -952,33 +1120,39 @@ public: virtual void clearJointsData() override; /**jsdoc + * Sets and locks a joint's position and orientation. + *

Note: Only works on the hips joint.

* @function MyAvatar.pinJoint - * @param {number} index - * @param {Vec3} position - * @param {Quat} orientation - * @returns {boolean} + * @param {number} index - The index of the joint. + * @param {Vec3} position - The position of the joint in world coordinates. + * @param {Quat} orientation - The orientation of the joint in world coordinates. + * @returns {boolean} true if the joint was pinned, false if it wasn't. */ Q_INVOKABLE bool pinJoint(int index, const glm::vec3& position, const glm::quat& orientation); bool isJointPinned(int index); /**jsdoc + * Clears a lock on a joint's position and orientation, as set by {@link MyAvatar.pinJoint|pinJoint}. + *

Note: Only works on the hips joint.

* @function MyAvatar.clearPinOnJoint - * @param {number} index - * @returns {boolean} + * @param {number} index - The index of the joint. + * @returns {boolean} true if the joint was unpinned, false if it wasn't. */ Q_INVOKABLE bool clearPinOnJoint(int index); /**jsdoc + * Gets the maximum error distance from the most recent inverse kinematics (IK) solution. * @function MyAvatar.getIKErrorOnLastSolve - * @returns {number} + * @returns {number} The maximum IK error distance. */ Q_INVOKABLE float getIKErrorOnLastSolve() const; /**jsdoc + * Changes the user's avatar and associated descriptive name. * @function MyAvatar.useFullAvatarURL - * @param {string} fullAvatarURL - * @param {string} [modelName=""] + * @param {string} fullAvatarURL - The URL of the avatar's .fst file. + * @param {string} [modelName=""] - Descriptive name of the avatar. */ Q_INVOKABLE void useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelName = QString()); @@ -1065,7 +1239,7 @@ public: /**jsdoc * Gets the list of avatar entities and their properties. * @function MyAvatar.getAvatarEntitiesVariant - * @returns {MyAvatar.AvatarEntityData[]} + * @returns {MyAvatar.AvatarEntityData[]} The list of avatar entities and their properties. */ Q_INVOKABLE QVariantList getAvatarEntitiesVariant(); @@ -1141,60 +1315,76 @@ public: */ Q_INVOKABLE bool getFlyingHMDPref(); - /**jsdoc + * Gets the target scale of the avatar. The target scale is the desired scale of the avatar without any restrictions on + * permissible scale values imposed by the domain. * @function MyAvatar.getAvatarScale - * @returns {number} + * @returns {number} The target scale for the avatar, range 0.0051000.0. */ Q_INVOKABLE float getAvatarScale(); /**jsdoc + * Sets the target scale of the avatar. The target scale is the desired scale of the avatar without any restrictions on + * permissible scale values imposed by the domain. * @function MyAvatar.setAvatarScale - * @param {number} scale + * @param {number} scale - The target scale for the avatar, range 0.0051000.0. */ Q_INVOKABLE void setAvatarScale(float scale); - /**jsdoc + * Sets whether or not the avatar should collide with entities. + *

Note: A false value won't disable collisions if the avatar is in a zone that disallows + * collisionless avatars, however the false value will be set so that collisions are disabled as soon as the + * avatar moves to a position where collisionless avatars are allowed. * @function MyAvatar.setCollisionsEnabled - * @param {boolean} enabled + * @param {boolean} enabled - true to enable the avatar to collide with entities, false to + * disable. */ Q_INVOKABLE void setCollisionsEnabled(bool enabled); /**jsdoc + * Gets whether or not the avatar will currently collide with entities. + *

Note: The avatar will always collide with entities if in a zone that disallows collisionless avatars. * @function MyAvatar.getCollisionsEnabled - * @returns {boolean} + * @returns {boolean} true if the avatar will currently collide with entities, false if it won't. */ Q_INVOKABLE bool getCollisionsEnabled(); /**jsdoc + * Sets whether or not the avatar should collide with other avatars. * @function MyAvatar.setOtherAvatarsCollisionsEnabled - * @param {boolean} enabled + * @param {boolean} enabled - true to enable the avatar to collide with other avatars, false + * to disable. */ Q_INVOKABLE void setOtherAvatarsCollisionsEnabled(bool enabled); /**jsdoc + * Gets whether or not the avatar will collide with other avatars. * @function MyAvatar.getOtherAvatarsCollisionsEnabled - * @returns {boolean} + * @returns {boolean} true if the avatar will collide with other avatars, false if it won't. */ Q_INVOKABLE bool getOtherAvatarsCollisionsEnabled(); /**jsdoc + * Gets the avatar's collision capsule: a cylinder with hemispherical ends that approximates the extents or the avatar. + *

Warning: The values returned are in world coordinates but aren't necessarily up to date with the + * avatar's current position.

* @function MyAvatar.getCollisionCapsule - * @returns {object} + * @returns {MyAvatar.CollisionCapsule} The avatar's collision capsule. */ Q_INVOKABLE QVariantMap getCollisionCapsule() const; /**jsdoc * @function MyAvatar.setCharacterControllerEnabled - * @param {boolean} enabled + * @param {boolean} enabled - true to enable the avatar to collide with entities, false to + * disable. * @deprecated Use {@link MyAvatar.setCollisionsEnabled} instead. */ Q_INVOKABLE void setCharacterControllerEnabled(bool enabled); // deprecated /**jsdoc * @function MyAvatar.getCharacterControllerEnabled - * @returns {boolean} + * @returns {boolean} true if the avatar will currently collide with entities, false if it won't. * @deprecated Use {@link MyAvatar.getCollisionsEnabled} instead. */ Q_INVOKABLE bool getCharacterControllerEnabled(); // deprecated @@ -1248,16 +1438,22 @@ public: glm::mat4 deriveBodyUsingCgModel(); /**jsdoc + * Tests whether a vector is pointing in the general direction of the avatar's "up" direction (i.e., dot product of vectors + * is > 0). * @function MyAvatar.isUp - * @param {Vec3} direction - * @returns {boolean} + * @param {Vec3} direction - The vector to test. + * @returns {boolean} true if the direction vector is pointing generally in the direction of the avatar's "up" + * direction. */ Q_INVOKABLE bool isUp(const glm::vec3& direction) { return glm::dot(direction, _worldUpDirection) > 0.0f; }; // true iff direction points up wrt avatar's definition of up. /**jsdoc + * Tests whether a vector is pointing in the general direction of the avatar's "down" direction (i.e., dot product of + * vectors is < 0). * @function MyAvatar.isDown - * @param {Vec3} direction - * @returns {boolean} + * @param {Vec3} direction - The vector to test. + * @returns {boolean} true if the direction vector is pointing generally in the direction of the avatar's + * "down" direction. */ Q_INVOKABLE bool isDown(const glm::vec3& direction) { return glm::dot(direction, _worldUpDirection) < 0.0f; }; @@ -1326,14 +1522,14 @@ public: * Gets the avatar entities as binary data. * @function MyAvatar.getAvatarEntityData * @override - * @returns {AvatarEntityMap} + * @returns {AvatarEntityMap} The avatar entities as binary data. */ AvatarEntityMap getAvatarEntityData() const override; /**jsdoc * Sets the avatar entities from binary data. * @function MyAvatar.setAvatarEntityData - * @param {AvatarEntityMap} avatarEntityData + * @param {AvatarEntityMap} avatarEntityData - The avatar entities as binary data. */ void setAvatarEntityData(const AvatarEntityMap& avatarEntityData) override; @@ -1345,8 +1541,7 @@ public: void avatarEntityDataToJson(QJsonObject& root) const override; /**jsdoc - * @function MyAvatar.sendAvatarDataPacket - * @param {boolean} sendAll + * @comment Uses the base class's JSDoc. */ int sendAvatarDataPacket(bool sendAll = false) override; @@ -1386,18 +1581,22 @@ public slots: /**jsdoc * @function MyAvatar.animGraphLoaded + * @deprecated This function is deprecated and will be removed. */ void animGraphLoaded(); /**jsdoc + * Sets the amount of gravity applied to the avatar in the y-axis direction. (Negative values are downward.) * @function MyAvatar.setGravity - * @param {number} gravity + * @param {number} gravity - The amount of gravity to be applied to the avatar, in m/s2. */ void setGravity(float gravity); /**jsdoc + * Sets the amount of gravity applied to the avatar in the y-axis direction. (Negative values are downward.) The default + * value is -5 m/s2. * @function MyAvatar.getGravity - * @returns {number} + * @returns {number} The amount of gravity currently applied to the avatar, in m/s2. */ float getGravity(); @@ -1428,125 +1627,149 @@ public slots: bool hasOrientation = false, const glm::quat& newOrientation = glm::quat(), bool shouldFaceLocation = false, bool withSafeLanding = true); /**jsdoc + * Moves the avatar to a new position and (optional) orientation in the domain. * @function MyAvatar.goToLocation - * @param {object} properties + * @param {MyAvatar.GoToProperties} target - The goto target. */ void goToLocation(const QVariant& properties); /**jsdoc + * Moves the avatar to a new position and then enables collisions. * @function MyAvatar.goToLocationAndEnableCollisions - * @param {Vec3} position + * @param {Vec3} position - The new position for the avatar, in world coordinates. */ void goToLocationAndEnableCollisions(const glm::vec3& newPosition); /**jsdoc * @function MyAvatar.safeLanding - * @param {Vec3} position - * @returns {boolean} + * @param {Vec3} position -The new position for the avatar, in world coordinates. + * @returns {boolean} true if the avatar was moved, false if it wasn't. + * @deprecated This function is deprecated and will be removed. */ bool safeLanding(const glm::vec3& position); /**jsdoc * @function MyAvatar.restrictScaleFromDomainSettings - * @param {object} domainSettingsObject + * @param {object} domainSettings - Domain settings. + * @deprecated This function is deprecated and will be removed. */ void restrictScaleFromDomainSettings(const QJsonObject& domainSettingsObject); /**jsdoc * @function MyAvatar.clearScaleRestriction + * @deprecated This function is deprecated and will be removed from the API. */ void clearScaleRestriction(); /**jsdoc + * Adds a thrust to your avatar's current thrust, to be applied for a short while. * @function MyAvatar.addThrust - * @param {Vec3} thrust + * @param {Vec3} thrust - The thrust direction and magnitude. + * @deprecated Use {@link MyAvatar|MyAvatar.motorVelocity} and related properties instead. */ // Set/Get update the thrust that will move the avatar around void addThrust(glm::vec3 newThrust) { _thrust += newThrust; }; /**jsdoc + * Gets the thrust currently being applied to your avatar. * @function MyAvatar.getThrust - * @returns {vec3} + * @returns {Vec3} The thrust currently being applied to your avatar. + * @deprecated Use {@link MyAvatar|MyAvatar.motorVelocity} and related properties instead. */ glm::vec3 getThrust() { return _thrust; }; /**jsdoc + * Sets the thrust to be applied to your avatar for a short while. * @function MyAvatar.setThrust - * @param {Vec3} thrust + * @param {Vec3} thrust - The thrust direction and magnitude. + * @deprecated Use {@link MyAvatar|MyAvatar.motorVelocity} and related properties instead. */ void setThrust(glm::vec3 newThrust) { _thrust = newThrust; } /**jsdoc + * Updates avatar motion behavior from the Developer > Avatar > Enable Default Motor Control and Enable Scripted + * Motor Control menu items. * @function MyAvatar.updateMotionBehaviorFromMenu */ Q_INVOKABLE void updateMotionBehaviorFromMenu(); /**jsdoc * @function MyAvatar.setToggleHips - * @param {boolean} enabled + * @param {boolean} enabled - Enabled. + * @deprecated This function is deprecated and will be removed. */ void setToggleHips(bool followHead); /**jsdoc + * Displays base of support of feet debug graphics. * @function MyAvatar.setEnableDebugDrawBaseOfSupport - * @param {boolean} enabled + * @param {boolean} enabled - true to show the debug graphics, false to hide. */ void setEnableDebugDrawBaseOfSupport(bool isEnabled); /**jsdoc + * Displays default pose debug graphics. * @function MyAvatar.setEnableDebugDrawDefaultPose - * @param {boolean} enabled + * @param {boolean} enabled - true to show the debug graphics, false to hide. */ void setEnableDebugDrawDefaultPose(bool isEnabled); /**jsdoc + * Displays animation debug graphics. * @function MyAvatar.setEnableDebugDrawAnimPose - * @param {boolean} enabled + * @param {boolean} enabled - true to show the debug graphics, false to hide. */ void setEnableDebugDrawAnimPose(bool isEnabled); /**jsdoc + * Displays position debug graphics. * @function MyAvatar.setEnableDebugDrawPosition - * @param {boolean} enabled + * @param {boolean} enabled - true to show the debug graphics, false to hide. */ void setEnableDebugDrawPosition(bool isEnabled); /**jsdoc + * Displays controller hand target debug graphics. * @function MyAvatar.setEnableDebugDrawHandControllers - * @param {boolean} enabled + * @param {boolean} enabled - true to show the debug graphics, false to hide. */ void setEnableDebugDrawHandControllers(bool isEnabled); /**jsdoc + * Displays sensor-to-world matrix debug graphics. * @function MyAvatar.setEnableDebugDrawSensorToWorldMatrix - * @param {boolean} enabled + * @param {boolean} enable - true to show the debug graphics, false to hide. */ void setEnableDebugDrawSensorToWorldMatrix(bool isEnabled); /**jsdoc + * Displays inverse kinematics targets debug graphics. * @function MyAvatar.setEnableDebugDrawIKTargets - * @param {boolean} enabled + * @param {boolean} enabled - true to show the debug graphics, false to hide. */ void setEnableDebugDrawIKTargets(bool isEnabled); /**jsdoc + * Displays inverse kinematics constraints debug graphics. * @function MyAvatar.setEnableDebugDrawIKConstraints - * @param {boolean} enabled + * @param {boolean} enabled - true to show the debug graphics, false to hide. */ void setEnableDebugDrawIKConstraints(bool isEnabled); /**jsdoc + * Displays inverse kinematics chains debug graphics. * @function MyAvatar.setEnableDebugDrawIKChains - * @param {boolean} enabled + * @param {boolean} enabled - true to show the debug graphics, false to hide. */ void setEnableDebugDrawIKChains(bool isEnabled); /**jsdoc + * Displays detailed collision debug graphics. * @function MyAvatar.setEnableDebugDrawDetailedCollision - * @param {boolean} enabled + * @param {boolean} enabled - true to show the debug graphics, false to hide. */ void setEnableDebugDrawDetailedCollision(bool isEnabled); @@ -1570,13 +1793,15 @@ public slots: /**jsdoc * @function MyAvatar.sanitizeAvatarEntityProperties + * @param {EntityItemProperties} properties - Properties. + * @deprecated This function is deprecated and will be removed. */ void sanitizeAvatarEntityProperties(EntityItemProperties& properties) const; /**jsdoc - * Sets whether or not your avatar mesh is visible. + * Sets whether or not your avatar mesh is visible to you. * @function MyAvatar.setEnableMeshVisible - * @param {boolean} visible - true to set your avatar mesh visible; false to set it invisible. + * @param {boolean} enabled - true to show your avatar mesh, false to hide. * @example Make your avatar invisible for 10s. * MyAvatar.setEnableMeshVisible(false); * Script.setTimeout(function () { @@ -1586,57 +1811,76 @@ public slots: virtual void setEnableMeshVisible(bool isEnabled) override; /**jsdoc + * Sets whether or not inverse kinematics (IK) for your avatar. * @function MyAvatar.setEnableInverseKinematics - * @param {boolean} enabled + * @param {boolean} enabled - true to enable IK, false to disable. */ void setEnableInverseKinematics(bool isEnabled); /**jsdoc + * Gets the URL of the override animation graph. + *

See {@link https://docs.highfidelity.com/create/avatars/custom-animations.html|Custom Avatar Animations} for + * information on animation graphs.

* @function MyAvatar.getAnimGraphOverrideUrl - * @returns {string} + * @returns {string} The URL of the override animation graph. "" if there is no override animation graph. */ QUrl getAnimGraphOverrideUrl() const; // thread-safe /**jsdoc + * Sets the animation graph to use in preference to the default animation graph. + *

See {@link https://docs.highfidelity.com/create/avatars/custom-animations.html|Custom Avatar Animations} for + * information on animation graphs.

* @function MyAvatar.setAnimGraphOverrideUrl - * @param {string} url + * @param {string} url - The URL of the animation graph to use. Set to "" to clear an override. */ void setAnimGraphOverrideUrl(QUrl value); // thread-safe /**jsdoc + * Gets the URL of animation graph that's currently being used for avatar animations. + *

See {@link https://docs.highfidelity.com/create/avatars/custom-animations.html|Custom Avatar Animations} for + * information on animation graphs.

* @function MyAvatar.getAnimGraphUrl - * @returns {string} + * @returns {string} The URL of the current animation graph. */ QUrl getAnimGraphUrl() const; // thread-safe /**jsdoc + * Sets the current animation graph to use for avatar animations and makes it the default. + *

See {@link https://docs.highfidelity.com/create/avatars/custom-animations.html|Custom Avatar Animations} for + * information on animation graphs.

* @function MyAvatar.setAnimGraphUrl - * @param {string} url + * @param {string} url - The URL of the animation graph to use. */ void setAnimGraphUrl(const QUrl& url); // thread-safe /**jsdoc + * Gets your listening position for spatialized audio. The position depends on the value of the + * {@link Myavatar|audioListenerMode} property. * @function MyAvatar.getPositionForAudio - * @returns {Vec3} + * @returns {Vec3} Your listening position. */ glm::vec3 getPositionForAudio(); /**jsdoc + * Gets the orientation of your listening position for spatialized audio. The orientation depends on the value of the + * {@link Myavatar|audioListenerMode} property. * @function MyAvatar.getOrientationForAudio - * @returns {Quat} + * @returns {Quat} The orientation of your listening position. */ glm::quat getOrientationForAudio(); /**jsdoc * @function MyAvatar.setModelScale - * @param {number} scale + * @param {number} scale - The scale. + * @deprecated This function is deprecated and will be removed. */ virtual void setModelScale(float scale) override; signals: /**jsdoc + * Triggered when the {@link MyAvatar|audioListenerMode} property value changes. * @function MyAvatar.audioListenerModeChanged * @returns {Signal} */ @@ -1645,12 +1889,14 @@ signals: /**jsdoc * @function MyAvatar.transformChanged * @returns {Signal} + * @deprecated This signal is deprecated and will be removed. */ void transformChanged(); /**jsdoc + * Triggered when the {@link MyAvatar|collisionSoundURL} property value changes. * @function MyAvatar.newCollisionSoundURL - * @param {string} url + * @param {string} url - The URL of the new collision sound. * @returns {Signal} */ void newCollisionSoundURL(const QUrl& url); @@ -1658,7 +1904,7 @@ signals: /**jsdoc * Triggered when the avatar collides with an entity. * @function MyAvatar.collisionWithEntity - * @param {Collision} collision + * @param {Collision} collision - Details of the collision. * @returns {Signal} * @example Report each time your avatar collides with an entity. * MyAvatar.collisionWithEntity.connect(function (collision) { @@ -1686,21 +1932,23 @@ signals: void otherAvatarsCollisionsEnabledChanged(bool enabled); /**jsdoc - * Triggered when the avatar's animation changes. + * Triggered when the avatar's animation graph URL changes. * @function MyAvatar.animGraphUrlChanged - * @param {url} url - The URL of the new animation. + * @param {string} url - The URL of the new animation graph. * @returns {Signal} */ void animGraphUrlChanged(const QUrl& url); /**jsdoc * @function MyAvatar.energyChanged - * @param {number} energy + * @param {number} energy - Avatar energy. * @returns {Signal} + * @deprecated This signal is deprecated and will be removed. */ void energyChanged(float newEnergy); /**jsdoc + * Triggered when the avatar has been moved to a new position by one of the MyAvatar "goTo" functions. * @function MyAvatar.positionGoneTo * @returns {Signal} */ @@ -1708,57 +1956,78 @@ signals: void positionGoneTo(); /**jsdoc + * Triggered when the avatar's model finishes loading. * @function MyAvatar.onLoadComplete * @returns {Signal} */ void onLoadComplete(); /**jsdoc + * Triggered when your avatar changes from being active to being away. * @function MyAvatar.wentAway * @returns {Signal} + * @example Report when your avatar goes away. + * MyAvatar.wentAway.connect(function () { + * print("My avatar went away"); + * }); + * // In desktop mode, pres the Esc key to go away. */ void wentAway(); /**jsdoc + * Triggered when your avatar changes from being away to being active. * @function MyAvatar.wentActive * @returns {Signal} */ void wentActive(); /**jsdoc + * Triggered when the avatar's model (i.e., {@link MyAvatar|skeletonModelURL} property value) is changed. + *

Synonym of {@link MyAvatar.skeletonModelURLChanged|skeletonModelURLChanged}.

* @function MyAvatar.skeletonChanged * @returns {Signal} */ void skeletonChanged(); /**jsdoc + * Triggered when the avatar's dominant hand changes. * @function MyAvatar.dominantHandChanged - * @param {string} hand + * @param {string} hand - The dominant hand: "left" for the left hand, "right" for the right hand. * @returns {Signal} */ void dominantHandChanged(const QString& hand); /**jsdoc + * Triggered when the HMD alignment for your avatar changes. * @function MyAvatar.hmdAvatarAlignmentTypeChanged - * @param {string} type + * @param {string} type - "head" if aligning your head and your avatar's head, "eyes" if aligning + * your eyes and your avatar's eyes. * @returns {Signal} */ void hmdAvatarAlignmentTypeChanged(const QString& type); /**jsdoc + * Triggered when the avatar's sensorToWorldScale property value changes. * @function MyAvatar.sensorToWorldScaleChanged - * @param {number} scale + * @param {number} scale - The scale that transforms dimensions in the user's real world to the avatar's size in the virtual + * world. * @returns {Signal} */ void sensorToWorldScaleChanged(float sensorToWorldScale); /**jsdoc + * Triggered when the a model is attached to or detached from one of the avatar's joints using one of + * {@link MyAvatar.attach|attach}, {@link MyAvatar.detachOne|detachOne}, {@link MyAvatar.detachAll|detachAll}, or + * {@link MyAvatar.setAttachmentData|setAttachmentData}. * @function MyAvatar.attachmentsChanged * @returns {Signal} + * @deprecated Use avatar entities instead. */ void attachmentsChanged(); /**jsdoc + * Triggered when the avatar's size changes. This can be due to the user changing the size of their avatar or the domain + * limiting the size of their avatar. * @function MyAvatar.scaleChanged * @returns {Signal} */ diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 11940ad76a..aedfcedf89 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -201,16 +201,24 @@ public: virtual QStringList getJointNames() const override; /**jsdoc + * Gets the default rotation of a joint (in the current avatar) relative to its parent. + *

For information on the joint hierarchy used, see + * Avatar Standards.

* @function MyAvatar.getDefaultJointRotation - * @param {number} index - * @returns {Quat} + * @param {number} index - The joint index. + * @returns {Quat} The default rotation of the joint if the joint index is valid, otherwise {@link Quat(0)|Quat.IDENTITY}. */ Q_INVOKABLE virtual glm::quat getDefaultJointRotation(int index) const; /**jsdoc + * Gets the default translation of a joint (in the current avatar) relative to its parent, in model coordinates. + *

Warning: These coordinates are not necessarily in meters.

+ *

For information on the joint hierarchy used, see + * Avatar Standards.

* @function MyAvatar.getDefaultJointTranslation - * @param {number} index - * @returns {Vec3} + * @param {number} index - The joint index. + * @returns {Vec3} The default translation of the joint (in model coordinates) if the joint index is valid, otherwise + * {@link Vec3(0)|Vec3.ZERO}. */ Q_INVOKABLE virtual glm::vec3 getDefaultJointTranslation(int index) const; @@ -261,50 +269,62 @@ public: // world-space to avatar-space rigconversion functions /**jsdoc + * Transforms a position in world coordinates to a position in a joint's coordinates, or avatar coordinates if no joint is + * specified. * @function MyAvatar.worldToJointPoint - * @param {Vec3} position - * @param {number} [jointIndex=-1] - * @returns {Vec3} + * @param {Vec3} position - The position in world coordinates. + * @param {number} [jointIndex=-1] - The index of the joint. + * @returns {Vec3} The position in the joint's coordinate system, or avatar coordinate system if no joint is specified. */ Q_INVOKABLE glm::vec3 worldToJointPoint(const glm::vec3& position, const int jointIndex = -1) const; /**jsdoc + * Transforms a direction in world coordinates to a direction in a joint's coordinates, or avatar coordinates if no joint + * is specified. * @function MyAvatar.worldToJointDirection - * @param {Vec3} direction - * @param {number} [jointIndex=-1] - * @returns {Vec3} + * @param {Vec3} direction - The direction in world coordinates. + * @param {number} [jointIndex=-1] - The index of the joint. + * @returns {Vec3} The direction in the joint's coordinate system, or avatar coordinate system if no joint is specified. */ Q_INVOKABLE glm::vec3 worldToJointDirection(const glm::vec3& direction, const int jointIndex = -1) const; /**jsdoc + * Transforms a rotation in world coordinates to a rotation in a joint's coordinates, or avatar coordinates if no joint is + * specified. * @function MyAvatar.worldToJointRotation - * @param {Quat} rotation - * @param {number} [jointIndex=-1] - * @returns {Quat} + * @param {Quat} rotation - The rotation in world coordinates. + * @param {number} [jointIndex=-1] - The index of the joint. + * @returns {Quat} The rotation in the joint's coordinate system, or avatar coordinate system if no joint is specified. */ Q_INVOKABLE glm::quat worldToJointRotation(const glm::quat& rotation, const int jointIndex = -1) const; /**jsdoc + * Transforms a position in a joint's coordinates, or avatar coordinates if no joint is specified, to a position in world + * coordinates. * @function MyAvatar.jointToWorldPoint - * @param {vec3} position - * @param {number} [jointIndex=-1] - * @returns {Vec3} + * @param {Vec3} position - The position in joint coordinates, or avatar coordinates if no joint is specified. + * @param {number} [jointIndex=-1] - The index of the joint. + * @returns {Vec3} The position in world coordinates. */ Q_INVOKABLE glm::vec3 jointToWorldPoint(const glm::vec3& position, const int jointIndex = -1) const; /**jsdoc + * Transforms a direction in a joint's coordinates, or avatar coordinates if no joint is specified, to a direction in world + * coordinates. * @function MyAvatar.jointToWorldDirection - * @param {Vec3} direction - * @param {number} [jointIndex=-1] - * @returns {Vec3} + * @param {Vec3} direction - The direction in joint coordinates, or avatar coordinates if no joint is specified. + * @param {number} [jointIndex=-1] - The index of the joint. + * @returns {Vec3} The direction in world coordinates. */ Q_INVOKABLE glm::vec3 jointToWorldDirection(const glm::vec3& direction, const int jointIndex = -1) const; /**jsdoc + * Transforms a rotation in a joint's coordinates, or avatar coordinates if no joint is specified, to a rotation in world + * coordinates. * @function MyAvatar.jointToWorldRotation - * @param {Quat} rotation - * @param {number} [jointIndex=-1] - * @returns {Quat} + * @param {Quat} rotation - The rotation in joint coordinates, or avatar coordinates if no joint is specified. + * @param {number} [jointIndex=-1] - The index of the joint. + * @returns {Quat} The rotation in world coordinates. */ Q_INVOKABLE glm::quat jointToWorldRotation(const glm::quat& rotation, const int jointIndex = -1) const; @@ -375,8 +395,9 @@ public: Q_INVOKABLE glm::vec3 getNeckPosition() const; /**jsdoc + * Gets the current acceleration of the avatar. * @function MyAvatar.getAcceleration - * @returns {Vec3} + * @returns {Vec3} The current acceleration of the avatar. */ Q_INVOKABLE glm::vec3 getAcceleration() const { return _acceleration; } @@ -410,29 +431,37 @@ public: void setOrientationViaScript(const glm::quat& orientation) override; /**jsdoc + * Gets the ID of the entity of avatar that the avatar is parented to. * @function MyAvatar.getParentID - * @returns {Uuid} + * @returns {Uuid} The ID of the entity or avatar that the avatar is parented to. {@link Uuid|Uuid.NULL} if not parented. */ // This calls through to the SpatiallyNestable versions, but is here to expose these to JavaScript. Q_INVOKABLE virtual const QUuid getParentID() const override { return SpatiallyNestable::getParentID(); } /**jsdoc + * Sets the ID of the entity of avatar that the avatar is parented to. * @function MyAvatar.setParentID - * @param {Uuid} parentID + * @param {Uuid} parentID - The ID of the entity or avatar that the avatar should be parented to. Set to + * {@link Uuid|Uuid.NULL} to unparent. */ // This calls through to the SpatiallyNestable versions, but is here to expose these to JavaScript. Q_INVOKABLE virtual void setParentID(const QUuid& parentID) override; /**jsdoc + * Gets the joint of the entity or avatar that the avatar is parented to. * @function MyAvatar.getParentJointIndex - * @returns {number} + * @returns {number} The joint of the entity or avatar that the avatar is parented to. 65535 or + * -1 if parented to the entity or avatar's position and orientation rather than a joint. */ // This calls through to the SpatiallyNestable versions, but is here to expose these to JavaScript. Q_INVOKABLE virtual quint16 getParentJointIndex() const override { return SpatiallyNestable::getParentJointIndex(); } /**jsdoc + * sets the joint of the entity or avatar that the avatar is parented to. * @function MyAvatar.setParentJointIndex - * @param {number} parentJointIndex + * @param {number} parentJointIndex - he joint of the entity or avatar that the avatar should be parented to. Use + * 65535 or -1 to parent to the entity or avatar's position and orientation rather than a + * joint. */ // This calls through to the SpatiallyNestable versions, but is here to expose these to JavaScript. Q_INVOKABLE virtual void setParentJointIndex(quint16 parentJointIndex) override; @@ -466,8 +495,9 @@ public: /**jsdoc * @function MyAvatar.getSimulationRate - * @param {string} [rateName=""] - * @returns {number} + * @param {string} [rateName=""] - Rate name. + * @returns {number} Simulation rate. + * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE float getSimulationRate(const QString& rateName = QString("")) const; @@ -522,8 +552,10 @@ public: signals: /**jsdoc + * Triggered when the avatar's target scale is changed. The target scale is the desired scale of the avatar without any + * restrictions on permissible scale values imposed by the domain. * @function MyAvatar.targetScaleChanged - * @param {number} targetScale + * @param {number} targetScale - The avatar's target scale. * @returns Signal */ void targetScaleChanged(float targetScale); @@ -572,6 +604,7 @@ public slots: /**jsdoc * @function MyAvatar.setModelURLFinished * @param {boolean} success + * @deprecated This function is deprecated and will be removed. */ // hooked up to Model::setURLFinished signal void setModelURLFinished(bool success); diff --git a/libraries/shared/src/RegisteredMetaTypes.cpp b/libraries/shared/src/RegisteredMetaTypes.cpp index 614858a77d..79085e05ba 100644 --- a/libraries/shared/src/RegisteredMetaTypes.cpp +++ b/libraries/shared/src/RegisteredMetaTypes.cpp @@ -1069,13 +1069,14 @@ void pickRayFromScriptValue(const QScriptValue& object, PickRay& pickRay) { } /**jsdoc + * Details of a collision between avatars and entities. * @typedef {object} Collision * @property {ContactEventType} type - The contact type of the collision event. - * @property {Uuid} idA - The ID of one of the entities in the collision. - * @property {Uuid} idB - The ID of the other of the entities in the collision. - * @property {Vec3} penetration - The amount of penetration between the two entities. + * @property {Uuid} idA - The ID of one of the avatars or entities in the collision. + * @property {Uuid} idB - The ID of the other of the avatars or entities in the collision. + * @property {Vec3} penetration - The amount of penetration between the two items. * @property {Vec3} contactPoint - The point of contact. - * @property {Vec3} velocityChange - The change in relative velocity of the two entities, in m/s. + * @property {Vec3} velocityChange - The change in relative velocity of the two items, in m/s. */ QScriptValue collisionToScriptValue(QScriptEngine* engine, const Collision& collision) { QScriptValue obj = engine->newObject(); From eba89e8a80ff0d49a3ef3350b079ee486ec42f69 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 8 Mar 2019 13:50:03 +1300 Subject: [PATCH 068/446] Revise Avatar JSDoc as a result of MyAvatar JSDoc work --- .../src/avatars/ScriptableAvatar.h | 4 +- libraries/avatars/src/AvatarData.h | 84 +++++++++++-------- 2 files changed, 51 insertions(+), 37 deletions(-) diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h index 2d8dce23de..37e82947ae 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.h +++ b/assignment-client/src/avatars/ScriptableAvatar.h @@ -149,7 +149,7 @@ public: * Gets the avatar entities as binary data. *

Warning: Potentially a very expensive call. Do not use if possible.

* @function Avatar.getAvatarEntityData - * @returns {AvatarEntityMap} + * @returns {AvatarEntityMap} The avatar entities as binary data. */ Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const override; @@ -157,7 +157,7 @@ public: * Sets the avatar entities from binary data. *

Warning: Potentially an expensive call. Do not use if possible.

* @function Avatar.setAvatarEntityData - * @param {AvatarEntityMap} avatarEntityData + * @param {AvatarEntityMap} avatarEntityData - The avatar entities as binary data. */ Q_INVOKABLE void setAvatarEntityData(const AvatarEntityMap& avatarEntityData) override; diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index a20518076f..858af7ba3c 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -131,7 +131,7 @@ const int COLLIDE_WITH_OTHER_AVATARS = 11; // 12th bit * * *

The values for the hand states are added together to give the HandState value. For example, if the left - * hand's finger is pointing, the value is 1 + 4 == 5. + * hand's finger is pointing, the value is 1 + 4 == 5. * @typedef {number} HandState */ const char HAND_STATE_NULL = 0; @@ -705,8 +705,9 @@ public: Q_INVOKABLE void setRawJointData(QVector data); /**jsdoc - * Sets a specific joint's rotation and position relative to its parent. - *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse + * Sets a specific joint's rotation and position relative to its parent, in model coordinates. + *

Warning: These coordinates are not necessarily in meters.

+ *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set @@ -714,7 +715,7 @@ public: * @function Avatar.setJointData * @param {number} index - The index of the joint. * @param {Quat} rotation - The rotation of the joint relative to its parent. - * @param {Vec3} translation - The translation of the joint relative to its parent. + * @param {Vec3} translation - The translation of the joint relative to its parent, in model coordinates. * @example Set your avatar to it's default T-pose for a while.
* Avatar in T-pose * // Set all joint translations and rotations to defaults. @@ -748,15 +749,16 @@ public: Q_INVOKABLE virtual void setJointRotation(int index, const glm::quat& rotation); /**jsdoc - * Sets a specific joint's translation relative to its parent. - *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse + * Sets a specific joint's translation relative to its parent, in model coordinates. + *

Warning: These coordinates are not necessarily in meters.

+ *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

* @function Avatar.setJointTranslation * @param {number} index - The index of the joint. - * @param {Vec3} translation - The translation of the joint relative to its parent. + * @param {Vec3} translation - The translation of the joint relative to its parent, in model coordinates. */ Q_INVOKABLE virtual void setJointTranslation(int index, const glm::vec3& translation); @@ -773,7 +775,7 @@ public: * Checks that the data for a joint are valid. * @function Avatar.isJointDataValid * @param {number} index - The index of the joint. - * @returns {boolean} true if the joint data is valid, false if not. + * @returns {boolean} true if the joint data are valid, false if not. */ Q_INVOKABLE bool isJointDataValid(int index) const; @@ -787,17 +789,20 @@ public: Q_INVOKABLE virtual glm::quat getJointRotation(int index) const; /**jsdoc - * Gets the translation of a joint relative to its parent. For information on the joint hierarchy used, see - * Avatar Standards. + * Gets the translation of a joint relative to its parent, in model coordinates. + *

Warning: These coordinates are not necessarily in meters.

+ *

For information on the joint hierarchy used, see + * Avatar Standards.

* @function Avatar.getJointTranslation * @param {number} index - The index of the joint. - * @returns {Vec3} The translation of the joint relative to its parent. + * @returns {Vec3} The translation of the joint relative to its parent, in model coordinates. */ Q_INVOKABLE virtual glm::vec3 getJointTranslation(int index) const; /**jsdoc - * Sets a specific joint's rotation and position relative to its parent. - *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse + * Sets a specific joint's rotation and position relative to its parent, in model coordinates. + *

Warning: These coordinates are not necessarily in meters.

+ *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set @@ -805,7 +810,7 @@ public: * @function Avatar.setJointData * @param {string} name - The name of the joint. * @param {Quat} rotation - The rotation of the joint relative to its parent. - * @param {Vec3} translation - The translation of the joint relative to its parent. + * @param {Vec3} translation - The translation of the joint relative to its parent, in model coordinates. */ Q_INVOKABLE virtual void setJointData(const QString& name, const glm::quat& rotation, const glm::vec3& translation); @@ -843,20 +848,21 @@ public: Q_INVOKABLE virtual void setJointRotation(const QString& name, const glm::quat& rotation); /**jsdoc - * Sets a specific joint's translation relative to its parent. - *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse + * Sets a specific joint's translation relative to its parent, in model coordinates. + *

Warning: These coordinates are not necessarily in meters.

+ *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

* @function Avatar.setJointTranslation * @param {string} name - The name of the joint. - * @param {Vec3} translation - The translation of the joint relative to its parent. + * @param {Vec3} translation - The translation of the joint relative to its parent, in model coordinates. * @example Stretch your avatar's neck. Depending on the avatar you are using, you will either see a gap between * the head and body or you will see the neck stretched.
* Avatar with neck stretched * // Stretch your avatar's neck. - * MyAvatar.setJointTranslation("Neck", { x: 0, y: 25, z: 0 }); + * MyAvatar.setJointTranslation("Neck", Vec3.multiply(2, MyAvatar.getJointTranslation("Neck"))); * * // Restore your avatar's neck after 5s. * Script.setTimeout(function () { @@ -874,10 +880,10 @@ public: * @function Avatar.clearJointData * @param {string} name - The name of the joint. * @example Offset and restore the position of your avatar's head. - * // Move your avatar's head up by 25cm from where it should be. - * MyAvatar.setJointTranslation("Neck", { x: 0, y: 0.25, z: 0 }); + * // Stretch your avatar's neck. + * MyAvatar.setJointTranslation("Neck", Vec3.multiply(2, MyAvatar.getJointTranslation("Neck"))); * - * // Restore your avatar's head to its default position after 5s. + * // Restore your avatar's neck after 5s. * Script.setTimeout(function () { * MyAvatar.clearJointData("Neck"); * }, 5000); @@ -890,7 +896,7 @@ public: * Checks that the data for a joint are valid. * @function Avatar.isJointDataValid * @param {string} name - The name of the joint. - * @returns {boolean} true if the joint data is valid, false if not. + * @returns {boolean} true if the joint data are valid, false if not. */ Q_INVOKABLE virtual bool isJointDataValid(const QString& name) const; @@ -908,11 +914,13 @@ public: Q_INVOKABLE virtual glm::quat getJointRotation(const QString& name) const; /**jsdoc - * Gets the translation of a joint relative to its parent. For information on the joint hierarchy used, see - * Avatar Standards. + * Gets the translation of a joint relative to its parent, in model coordinates. + *

Warning: These coordinates are not necessarily in meters.

+ *

For information on the joint hierarchy used, see + * Avatar Standards.

* @function Avatar.getJointTranslation * @param {number} name - The name of the joint. - * @returns {Vec3} The translation of the joint relative to its parent. + * @returns {Vec3} The translation of the joint relative to its parent, in model coordinates. * @example Report the translation of your avatar's hips joint. * print(JSON.stringify(MyAvatar.getJointRotation("Hips"))); * @@ -933,10 +941,13 @@ public: Q_INVOKABLE virtual QVector getJointRotations() const; /**jsdoc - * Gets the translations of all joints in the current avatar. Each joint's rotation is relative to its parent joint. + * Gets the translations of all joints in the current avatar. Each joint's translation is relative to its parent joint, in + * model coordinates. + *

Warning: These coordinates are not necessarily in meters.

* @function Avatar.getJointTranslations - * @returns {Vec3[]} The translations of all joints relative to each's parent. The values are in the same order as the array - * returned by {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the Avatar API. + * @returns {Vec3[]} The translations of all joints relative to each's parent, in model coordinates. The values are in the + * same order as the array returned by {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the + * Avatar API. */ Q_INVOKABLE virtual QVector getJointTranslations() const; @@ -979,15 +990,18 @@ public: Q_INVOKABLE virtual void setJointRotations(const QVector& jointRotations); /**jsdoc - * Sets the translations of all joints in the current avatar. Each joint's translation is relative to its parent joint. + * Sets the translations of all joints in the current avatar. Each joint's translation is relative to its parent joint, in + * model coordinates. + *

Warning: These coordinates are not necessarily in meters.

*

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

* @function Avatar.setJointTranslations - * @param {Vec3[]} translations - The translations for all joints in the avatar. The values are in the same order as the - * array returned by {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the Avatar API. + * @param {Vec3[]} translations - The translations for all joints in the avatar, in model coordinates. The values are in + * the same order as the array returned by {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the + * Avatar API. */ Q_INVOKABLE virtual void setJointTranslations(const QVector& jointTranslations); @@ -1271,12 +1285,12 @@ public: AABox getDefaultBubbleBox() const; /**jsdoc - * @comment Documented in derived classes' JSDoc. + * @comment Documented in derived classes' JSDoc because implementations are different. */ Q_INVOKABLE virtual AvatarEntityMap getAvatarEntityData() const; /**jsdoc - * @comment Documented in derived classes' JSDoc. + * @comment Documented in derived classes' JSDoc because implementations are different. */ Q_INVOKABLE virtual void setAvatarEntityData(const AvatarEntityMap& avatarEntityData); @@ -1383,14 +1397,14 @@ signals: void displayNameChanged(); /**jsdoc - * Triggered when the avattr's sessionDisplayName property value changes. + * Triggered when the avatar's sessionDisplayName property value changes. * @function Avatar.sessionDisplayNameChanged * @returns {Signal} */ void sessionDisplayNameChanged(); /**jsdoc - * Triggered when the avatar's skeletonModelURL property value changes. + * Triggered when the avatar's model (i.e., skeletonModelURL property value) is changed. * @function Avatar.skeletonModelURLChanged * @returns {Signal} */ From c8648c70161d00e5bb0e2e150bfd467295834046 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 8 Mar 2019 08:53:48 -0800 Subject: [PATCH 069/446] Added worst tile value to test. --- tools/nitpick/src/ImageComparer.cpp | 11 +++++++++++ tools/nitpick/src/ImageComparer.h | 2 ++ tools/nitpick/src/MismatchWindow.cpp | 2 +- tools/nitpick/src/Nitpick.cpp | 2 +- tools/nitpick/src/TestCreator.cpp | 14 +++++++++----- tools/nitpick/src/TestCreator.h | 3 ++- tools/nitpick/src/common.h | 10 +++++++--- tools/nitpick/ui/MismatchWindow.ui | 6 +++--- 8 files changed, 36 insertions(+), 14 deletions(-) diff --git a/tools/nitpick/src/ImageComparer.cpp b/tools/nitpick/src/ImageComparer.cpp index 7e3e6eaf63..b35c5d639d 100644 --- a/tools/nitpick/src/ImageComparer.cpp +++ b/tools/nitpick/src/ImageComparer.cpp @@ -43,6 +43,8 @@ void ImageComparer::compareImages(const QImage& resultImage, const QImage& expec int windowCounter{ 0 }; double ssim{ 0.0 }; + double worstTileValue{ 1.0 }; + double min { 1.0 }; double max { -1.0 }; @@ -108,6 +110,10 @@ void ImageComparer::compareImages(const QImage& resultImage, const QImage& expec if (value < min) min = value; if (value > max) max = value; + if (value < worstTileValue) { + worstTileValue = value; + } + ++windowCounter; y += WIN_SIZE; @@ -122,12 +128,17 @@ void ImageComparer::compareImages(const QImage& resultImage, const QImage& expec _ssimResults.min = min; _ssimResults.max = max; _ssimResults.ssim = ssim / windowCounter; + _ssimResults.worstTileValue = worstTileValue; }; double ImageComparer::getSSIMValue() { return _ssimResults.ssim; } +double ImageComparer::getWorstTileValue() { + return _ssimResults.worstTileValue; +} + SSIMResults ImageComparer::getSSIMResults() { return _ssimResults; } diff --git a/tools/nitpick/src/ImageComparer.h b/tools/nitpick/src/ImageComparer.h index fc14dab94d..a18e432a01 100644 --- a/tools/nitpick/src/ImageComparer.h +++ b/tools/nitpick/src/ImageComparer.h @@ -18,7 +18,9 @@ class ImageComparer { public: void compareImages(const QImage& resultImage, const QImage& expectedImage); + double getSSIMValue(); + double getWorstTileValue(); SSIMResults getSSIMResults(); diff --git a/tools/nitpick/src/MismatchWindow.cpp b/tools/nitpick/src/MismatchWindow.cpp index fd5df0dd4e..2a7aca9f2e 100644 --- a/tools/nitpick/src/MismatchWindow.cpp +++ b/tools/nitpick/src/MismatchWindow.cpp @@ -61,7 +61,7 @@ QPixmap MismatchWindow::computeDiffPixmap(const QImage& expectedImage, const QIm } void MismatchWindow::setTestResult(const TestResult& testResult) { - errorLabel->setText("Similarity: " + QString::number(testResult._error)); + errorLabel->setText("Similarity: " + QString::number(testResult._errorGlobal) + " (worst tile: " + QString::number(testResult._errorLocal) + ")"); imagePath->setText("Path to test: " + testResult._pathname); diff --git a/tools/nitpick/src/Nitpick.cpp b/tools/nitpick/src/Nitpick.cpp index cf50774617..e72de9d1ad 100644 --- a/tools/nitpick/src/Nitpick.cpp +++ b/tools/nitpick/src/Nitpick.cpp @@ -38,7 +38,7 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { _ui.plainTextEdit->setReadOnly(true); - setWindowTitle("Nitpick - v3.1.2"); + setWindowTitle("Nitpick - v3.1.3"); clientProfiles << "VR-High" << "Desktop-High" << "Desktop-Low" << "Mobile-Touch" << "VR-Standalone"; _ui.clientProfileComboBox->insertItems(0, clientProfiles); diff --git a/tools/nitpick/src/TestCreator.cpp b/tools/nitpick/src/TestCreator.cpp index 089e84904a..a79a2b3b09 100644 --- a/tools/nitpick/src/TestCreator.cpp +++ b/tools/nitpick/src/TestCreator.cpp @@ -83,6 +83,7 @@ int TestCreator::compareImageLists() { QImage expectedImage(_expectedImagesFullFilenames[i]); double similarityIndex; // in [-1.0 .. 1.0], where 1.0 means images are identical + double worstTileValue; // in [-1.0 .. 1.0], where 1.0 means images are identical bool isInteractiveMode = (!_isRunningFromCommandLine && _checkBoxInteractiveMode->isChecked() && !_isRunningInAutomaticTestRun); @@ -93,10 +94,12 @@ int TestCreator::compareImageLists() { } else { _imageComparer.compareImages(resultImage, expectedImage); similarityIndex = _imageComparer.getSSIMValue(); + worstTileValue = _imageComparer.getWorstTileValue(); } TestResult testResult = TestResult{ - (float)similarityIndex, + similarityIndex, + worstTileValue, _expectedImagesFullFilenames[i].left(_expectedImagesFullFilenames[i].lastIndexOf("/") + 1), // path to the test (including trailing /) QFileInfo(_expectedImagesFullFilenames[i].toStdString().c_str()).fileName(), // filename of expected image QFileInfo(_resultImagesFullFilenames[i].toStdString().c_str()).fileName(), // filename of result image @@ -105,10 +108,9 @@ int TestCreator::compareImageLists() { _mismatchWindow.setTestResult(testResult); - if (similarityIndex < THRESHOLD) { - ++numberOfFailures; - + if (similarityIndex < THRESHOLD_GLOBAL || worstTileValue < THRESHOLD_LOCAL) { if (!isInteractiveMode) { + ++numberOfFailures; appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), _mismatchWindow.getSSIMResultsImage(testResult._ssimResults), true); } else { _mismatchWindow.exec(); @@ -117,6 +119,7 @@ int TestCreator::compareImageLists() { case USER_RESPONSE_PASS: break; case USE_RESPONSE_FAIL: + ++numberOfFailures; appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), _mismatchWindow.getSSIMResultsImage(testResult._ssimResults), true); break; case USER_RESPONSE_ABORT: @@ -198,7 +201,8 @@ void TestCreator::appendTestResultsToFile(const TestResult& testResult, const QP stream << "TestCreator in folder " << testResult._pathname.left(testResult._pathname.length() - 1) << endl; // remove trailing '/' stream << "Expected image was " << testResult._expectedImageFilename << endl; stream << "Actual image was " << testResult._actualImageFilename << endl; - stream << "Similarity index was " << testResult._error << endl; + stream << "Similarity index was " << testResult._errorGlobal << endl; + stream << "Worst tile was " << testResult._errorLocal << endl; descriptionFile.close(); diff --git a/tools/nitpick/src/TestCreator.h b/tools/nitpick/src/TestCreator.h index 7cd38b42d4..6491d6fe6c 100644 --- a/tools/nitpick/src/TestCreator.h +++ b/tools/nitpick/src/TestCreator.h @@ -121,7 +121,8 @@ private: const QString TEST_RESULTS_FOLDER { "TestResults" }; const QString TEST_RESULTS_FILENAME { "TestResults.txt" }; - const double THRESHOLD{ 0.9999 }; + const double THRESHOLD_GLOBAL{ 0.9999 }; + const double THRESHOLD_LOCAL { 0.7770 }; QDir _imageDirectory; diff --git a/tools/nitpick/src/common.h b/tools/nitpick/src/common.h index eb228ff2b3..17090c46db 100644 --- a/tools/nitpick/src/common.h +++ b/tools/nitpick/src/common.h @@ -18,7 +18,9 @@ public: int width; int height; std::vector results; + double ssim; + double worstTileValue; // Used for scaling double min; @@ -27,15 +29,17 @@ public: class TestResult { public: - TestResult(float error, const QString& pathname, const QString& expectedImageFilename, const QString& actualImageFilename, const SSIMResults& ssimResults) : - _error(error), + TestResult(double errorGlobal, double errorLocal, const QString& pathname, const QString& expectedImageFilename, const QString& actualImageFilename, const SSIMResults& ssimResults) : + _errorGlobal(errorGlobal), + _errorLocal(errorLocal), _pathname(pathname), _expectedImageFilename(expectedImageFilename), _actualImageFilename(actualImageFilename), _ssimResults(ssimResults) {} - double _error; + double _errorGlobal; + double _errorLocal; QString _pathname; QString _expectedImageFilename; diff --git a/tools/nitpick/ui/MismatchWindow.ui b/tools/nitpick/ui/MismatchWindow.ui index 8a174989d4..fa3e21957f 100644 --- a/tools/nitpick/ui/MismatchWindow.ui +++ b/tools/nitpick/ui/MismatchWindow.ui @@ -45,7 +45,7 @@ - 540 + 900 480 800 450 @@ -78,7 +78,7 @@ 60 630 - 480 + 540 28 @@ -145,7 +145,7 @@ - Abort current test + Abort evaluation From 20e1753605afc1d6dcafd8e1aa306f4bf663c869 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 8 Mar 2019 09:22:52 -0800 Subject: [PATCH 070/446] Reduced threshold a wee bit after testing on laptop. --- tools/nitpick/src/TestCreator.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/nitpick/src/TestCreator.h b/tools/nitpick/src/TestCreator.h index 6491d6fe6c..c2e7ba14f2 100644 --- a/tools/nitpick/src/TestCreator.h +++ b/tools/nitpick/src/TestCreator.h @@ -121,7 +121,7 @@ private: const QString TEST_RESULTS_FOLDER { "TestResults" }; const QString TEST_RESULTS_FILENAME { "TestResults.txt" }; - const double THRESHOLD_GLOBAL{ 0.9999 }; + const double THRESHOLD_GLOBAL{ 0.9998 }; const double THRESHOLD_LOCAL { 0.7770 }; QDir _imageDirectory; From 19c7c26c6369b5df000f68e794c2e5f5834427b6 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 8 Mar 2019 10:04:23 -0800 Subject: [PATCH 071/446] gcc / Mac compilation error. --- tools/nitpick/src/TestCreator.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/nitpick/src/TestCreator.cpp b/tools/nitpick/src/TestCreator.cpp index a79a2b3b09..f87134ce5b 100644 --- a/tools/nitpick/src/TestCreator.cpp +++ b/tools/nitpick/src/TestCreator.cpp @@ -91,6 +91,7 @@ int TestCreator::compareImageLists() { if (isInteractiveMode && (resultImage.width() != expectedImage.width() || resultImage.height() != expectedImage.height())) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Images are not the same size"); similarityIndex = -100.0; + worstTileValue = 0.0; } else { _imageComparer.compareImages(resultImage, expectedImage); similarityIndex = _imageComparer.getSSIMValue(); From 5068075645c8c1511d2cc99c94c78dec68f0f325 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 9 Mar 2019 07:42:55 +1300 Subject: [PATCH 072/446] Fill in MyAvatar animation JSDoc --- .../animation/src/AnimInverseKinematics.h | 41 ++++ libraries/animation/src/AnimOverlay.h | 27 +++ libraries/animation/src/IKTarget.h | 21 ++ libraries/animation/src/Rig.cpp | 211 ++++++++++++++++++ libraries/script-engine/src/ScriptEngine.h | 11 +- 5 files changed, 306 insertions(+), 5 deletions(-) diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h index 0136b7d125..d5a110ea76 100644 --- a/libraries/animation/src/AnimInverseKinematics.h +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -59,6 +59,47 @@ public: float getMaxErrorOnLastSolve() { return _maxErrorOnLastSolve; } + /**jsdoc + *

Specifies the initial conditions of the IK solver.

+ * + * + * + * + * + * + * + * + * + * + * + *
ValueName

Description
0RelaxToUnderPosesThis is a blend between PreviousSolution and + * UnderPoses: it is 15/16 PreviousSolution and 1/16 UnderPoses. This + * provides some of the benefits of using UnderPoses so that the underlying animation is still visible, + * while at the same time converging faster then using the UnderPoses only initial solution.
1RelaxToLimitCenterPosesThis is a blend between + * Previous Solution and LimitCenterPoses: it is 15/16 PreviousSolution and + * 1/16 LimitCenterPoses. This should converge quickly because it is close to the previous solution, but + * still provides the benefits of avoiding limb locking.
2PreviousSolutionThe IK system will begin to solve from the same position and + * orientations for each joint that was the result from the previous frame.
+ * Pros: because the end effectors typically do not move much from frame to frame, this is likely to converge quickly + * to a valid solution.
+ * Cons: If the previous solution resulted in an awkward or uncomfortable posture, the next frame will also be + * awkward and uncomfortable. It can also result in locked elbows and knees.
3UnderPosesThe IK occurs at one of the top-most layers, it has access to the + * full posture that was computed via canned animations and blends. We call this animated set of poses the "under + * pose". The under poses are what would be visible if IK was completely disabled. Using the under poses as the + * initial conditions of the CCD solve will cause some of the animated motion to be blended in to the result of the + * IK. This can result in very natural results, especially if there are only a few IK targets enabled. On the other + * hand, because the under poses might be quite far from the desired end effector, it can converge slowly in some + * cases, causing it to never reach the IK target in the allotted number of iterations. Also, in situations where all + * of the IK targets are being controlled by external sensors, sometimes starting from the under poses can cause + * awkward motions from the underlying animations to leak into the IK result.
4LimitCenterPosesThis pose is taken to be the center of all the joint + * constraints. This can prevent the IK solution from getting locked or stuck at a particular constraint. For + * example, if the arm is pointing straight outward from the body, as the end effector moves towards the body, at + * some point the elbow should bend to accommodate. However, because the CCD solver is stuck at a local maximum, it + * will not rotate the elbow, unless the initial conditions already has the elbow bent, which is the case for + * LimitCenterPoses. When all the IK targets are enabled, this result will provide a consistent starting + * point for each IK solve, hopefully resulting in a consistent, natural result.
+ * @typedef {number} MyAvatar.AnimIKSolutionSource + */ enum class SolutionSource { RelaxToUnderPoses = 0, RelaxToLimitCenterPoses, diff --git a/libraries/animation/src/AnimOverlay.h b/libraries/animation/src/AnimOverlay.h index 70929bd4e4..623672143e 100644 --- a/libraries/animation/src/AnimOverlay.h +++ b/libraries/animation/src/AnimOverlay.h @@ -24,6 +24,33 @@ class AnimOverlay : public AnimNode { public: friend class AnimTests; + /**jsdoc + *

Specifies sets of joints.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ValueName

Description
0FullBodyBoneSetAll joints.
1UpperBodyBoneSetOnly the "Spine" joint and its children.
2LowerBodyBoneSetOnly the leg joints and their children.
3LeftArmBoneSetJoints that are children of the "LeftShoulder" joint.
4RightArmBoneSetJoints that are children of the "RightShoulder" + * joint.
5AboveTheHeadBoneSetJoints that are children of the "Head" joint.
6BelowTheHeadBoneSetJoints that are NOT children of the "head" + * joint.
7HeadOnlyBoneSetThe "Head" joint.
8SpineOnlyBoneSetThe "Spine" joint.
9EmptyBoneSetNo joints.
10LeftHandBoneSetjoints that are children of the "LeftHand" joint.
11RightHandBoneSetJoints that are children of the "RightHand" joint.
12HipsOnlyBoneSetThe "Hips" joint.
13BothFeetBoneSetThe "LeftFoot" and "RightFoot" joints.
+ * @typedef {number} MyAvatar.AnimOverlayBoneSet + */ enum BoneSet { FullBodyBoneSet = 0, UpperBodyBoneSet, diff --git a/libraries/animation/src/IKTarget.h b/libraries/animation/src/IKTarget.h index 325a1b40b6..7e53e6a7ea 100644 --- a/libraries/animation/src/IKTarget.h +++ b/libraries/animation/src/IKTarget.h @@ -16,6 +16,27 @@ const float HACK_HMD_TARGET_WEIGHT = 8.0f; class IKTarget { public: + /**jsdoc + *

An IK target type.

+ * + * + * + * + * + * + * + * + * + * + * + * + *
ValueName

Description
0RotationAndPositionAttempt to reach the rotation and position end + * effector.
1RotationOnlyAttempt to reach the end effector rotation only.
2HmdHeadDeprecated: A special mode of IK that would attempt + * to prevent unnecessary bending of the spine.
3HipsRelativeRotationAndPositionAttempt to reach a rotation and position end + * effector that is not in absolute rig coordinates but is offset by the avatar hips translation.
4SplineUse a cubic Hermite spline to model the human spine. This prevents + * kinks in the spine and allows for a small amount of stretch and squash.
5UnknownIK is disabled.
+ * @typedef {number} MyAvatar.IKTargetType + */ enum class Type { RotationAndPosition, RotationOnly, diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index a9c57a4a15..1ab680fba2 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -75,6 +75,217 @@ static const QString MAIN_STATE_MACHINE_RIGHT_FOOT_ROTATION("mainStateMachineRig static const QString MAIN_STATE_MACHINE_RIGHT_FOOT_POSITION("mainStateMachineRightFootPosition"); +/**jsdoc + *

An AnimStateDictionary object may have the following properties. It may also have other properties, set by + * scripts.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + + * + * + * + * + * + * + * + * + * + + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
NameType

Description
userAnimNonebooleantrue when no user overrideAnimation is + * playing.
userAnimAbooleantrue when a user overrideAnimation is + * playing.
userAnimBbooleantrue when a user overrideAnimation is + * playing.
sinenumberOscillating sine wave.
moveForwardSpeednumberControls the blend between the various forward walking + * & running animations.
moveBackwardSpeednumberControls the blend between the various backward walking + * & running animations.
moveLateralSpeednumberControls the blend between the various sidestep walking + * & running animations.
isMovingForwardbooleantrue if the avatar is moving + * forward.
isMovingBackwardbooleantrue if the avatar is moving + * backward.
isMovingRightbooleantrue if the avatar is moving to the + * right.
isMovingLeftbooleantrue if the avatar is moving to the + * left.
isMovingRightHmdbooleantrue if the avatar is moving to the right + * while the user is in HMD mode.
isMovingLeftHmdbooleantrue if the avatar is moving to the left while + * the user is in HMD mode.
isNotMovingbooleantrue if the avatar is stationary.
isTurningRightbooleantrue if the avatar is turning + * clockwise.
isTurningLeftbooleantrue if the avatar is turning + * counter-clockwise.
isNotTurningbooleantrue if the avatar is not turning.
isFlyingbooleantrue if the avatar is flying.
isNotFlyingbooleantrue if the avatar is not flying.
isTakeoffStandbooleantrue if the avatar is about to execute a + * standing jump.
isTakeoffRunbooleantrue if the avatar is about to execute a running + * jump.
isNotTakeoffbooleantrue if the avatar is not jumping.
isInAirStandbooleantrue if the avatar is in the air after a standing + * jump.
isInAirRunbooleantrue if the avatar is in the air after a running + * jump.
isNotInAirbooleantrue if the avatar on the ground.
inAirAlphanumberUsed to interpolate between the up, apex, and down in-air + * animations.
ikOverlayAlphanumberThe blend between upper body and spline IK versus the + * underlying animation
headPosition{@link Vec3}The desired position of the Head joint in + * rig coordinates.
headRotation{@link Quat}The desired orientation of the Head joint in + * rig coordinates.
headType{@link MyAvatar.IKTargetType|IKTargetType}The type of IK used for the + * head.
headWeightnumberHow strongly the head chain blends with the other IK + * chains.
leftHandPosition{@link Vec3}The desired position of the LeftHand + * joint in rig coordinates.
leftHandRotation{@link Quat}The desired orientation of the LeftHand + * joint in rig coordinates.
leftHandType{@link MyAvatar.IKTargetType|IKTargetType}The type of IK used for the + * left arm.
leftHandPoleVectorEnabledbooleanWhen true, the elbow angle is + * controlled by the rightHandPoleVector property value. Otherwise the elbow direction comes from the + * underlying animation.
leftHandPoleReferenceVector{@link Vec3}The direction of the elbow in the local + * coordinate system of the elbow.
leftHandPoleVector{@link Vec3}The direction the elbow should point in rig + * coordinates.
rightHandPosition{@link Vec3}The desired position of the RightHand + * joint in rig coordinates.
rightHandRotation{@link Quat}The desired orientation of the + * RightHand joint in rig coordinates.
rightHandType{@link MyAvatar.IKTargetType|IKTargetType}The type of IK used for + * the right arm.
rightHandPoleVectorEnabledbooleanWhen true, the elbow angle is + * controlled by the rightHandPoleVector property value. Otherwise the elbow direction comes from the + * underlying animation.
rightHandPoleReferenceVector{@link Vec3}The direction of the elbow in the local + * coordinate system of the elbow.
rightHandPoleVector{@link Vec3}The direction the elbow should point in rig + * coordinates.
leftFootIKEnabledbooleantrue if IK is enabled for the left + * foot.
rightFootIKEnabledbooleantrue if IK is enabled for the right + * foot.
leftFootIKPositionVarstringThe name of the source for the desired position + * of the LeftFoot joint. If not set, the foot rotation of the underlying animation will be used.
leftFootIKRotationVarstringThe name of the source for the desired rotation + * of the LeftFoot joint. If not set, the foot rotation of the underlying animation will be used.
leftFootPoleVectorEnabledbooleanWhen true, the knee angle is + * controlled by the leftFootPoleVector property value. Otherwise the knee direction comes from the + * underlying animation.
leftFootPoleVector{@link Vec3}The direction the knee should face in rig + * coordinates.
rightFootIKPositionVarstringThe name of the source for the desired position + * of the RightFoot joint. If not set, the foot rotation of the underlying animation will be used.
rightFootIKRotationVarstringThe name of the source for the desired rotation + * of the RightFoot joint. If not set, the foot rotation of the underlying animation will be used.
rightFootPoleVectorEnabledbooleanWhen true, the knee angle is + * controlled by the rightFootPoleVector property value. Otherwise the knee direction comes from the + * underlying animation.
rightFootPoleVector{@link Vec3}The direction the knee should face in rig + * coordinates.
isTalkingbooleantrue if the avatar is talking.
notIsTalkingbooleantrue if the avatar is not talking.
solutionSource{@link MyAvatar.AnimIKSolutionSource|AnimIKSolutionSource}Determines the initial conditions of the IK solver.
defaultPoseOverlayAlphanumberControls the blend between the main animation state + * machine and the default pose. Mostly used during full body tracking so that walking & jumping animations do not + * affect the IK of the figure.
defaultPoseOverlayBoneSet{@link MyAvatar.AnimOverlayBoneSet|AnimOverlayBoneSet}Specifies which bones will be replace by the source overlay.
hipsType{@link MyAvatar.IKTargetType|IKTargetType}The type of IK used for the + * hips.
hipsPosition{@link Vec3}The desired position of Hips joint in rig + * coordinates.
hipsRotation{@link Quat}the desired orientation of the Hips joint in + * rig coordinates.
spine2Type{@link MyAvatar.IKTargetType|IKTargetType}The type of IK used for the + * Spine2 joint.
spine2Position{@link Vec3}The desired position of the Spine2 joint + * in rig coordinates.
spine2Rotation{@link Quat}The desired orientation of the Spine2 + * joint in rig coordinates.
leftFootIKAlphanumberBlends between full IK for the leg and the underlying + * animation.
rightFootIKAlphanumberBlends between full IK for the leg and the underlying + * animation.
hipsWeightnumberHow strongly the hips target blends with the IK solution for + * other IK chains.
leftHandWeightnumberHow strongly the left hand blends with IK solution of other + * IK chains.
rightHandWeightnumberHow strongly the right hand blends with IK solution of other + * IK chains.
spine2WeightnumberHow strongly the spine2 chain blends with the rest of the IK + * solution.
leftHandOverlayAlphanumberUsed to blend in the animated hand gesture poses, such + * as point and thumbs up.
leftHandGraspAlphanumberUsed to blend between an open hand and a closed hand. + * Usually changed as you squeeze the trigger of the hand controller.
rightHandOverlayAlphanumberUsed to blend in the animated hand gesture poses, + * such as point and thumbs up.
rightHandGraspAlphanumberUsed to blend between an open hand and a closed hand. + * Usually changed as you squeeze the trigger of the hand controller.
isLeftIndexPointbooleantrue if the left hand should be + * pointing.
isLeftThumbRaisebooleantrue if the left hand should be + * thumbs-up.
isLeftIndexPointAndThumbRaisebooleantrue if the left hand should be + * pointing and thumbs-up.
isLeftHandGraspbooleantrue if the left hand should be at rest, + * grasping the controller.
isRightIndexPointbooleantrue if the right hand should be + * pointing.
isRightThumbRaisebooleantrue if the right hand should be + * thumbs-up.
isRightIndexPointAndThumbRaisebooleantrue if the right hand should + * be pointing and thumbs-up.
isRightHandGraspbooleantrue if the right hand should be at rest, + * grasping the controller.
+ *

Note: Rig coordinates are +z forward and +y up.

+ * @typedef {object} MyAvatar.AnimStateDictionary + */ +// Note: The following animVars are intentionally not documented: +// - leftFootPosition +// - leftFootRotation +// - rightFooKPosition +// - rightFooKRotation +// Note: The following items aren't set in the code below but are still intentionally documented: +// - leftFootIKAlpha +// - rightFootIKAlpha +// - hipsWeight +// - leftHandWeight +// - rightHandWeight +// - spine2Weight +// - rightHandOverlayAlpha +// - rightHandGraspAlpha +// - leftHandOverlayAlpha +// - leftHandGraspAlpha +// - isRightIndexPoint +// - isRightThumbRaise +// - isRightIndexPointAndThumbRaise +// - isRightHandGrasp +// - isLeftIndexPoint +// - isLeftThumbRaise +// - isLeftIndexPointAndThumbRaise +// - isLeftHandGrasp Rig::Rig() { // Ensure thread-safe access to the rigRegistry. std::lock_guard guard(rigRegistryMutex); diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 48fb4f0b83..3549578ed5 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -573,11 +573,12 @@ public slots: /**jsdoc * @function Script.callAnimationStateHandler - * @param {function} callback - * @param {object} parameters - * @param {string[]} names - * @param {boolean} useNames - * @param {object} resultHandler + * @param {function} callback - Callback. + * @param {object} parameters - Parameters. + * @param {string[]} names - Names. + * @param {boolean} useNames - Use names. + * @param {function} resultHandler - Result handler. + * @deprecated This function is deprecated and will be removed. */ void callAnimationStateHandler(QScriptValue callback, AnimVariantMap parameters, QStringList names, bool useNames, AnimVariantResultHandler resultHandler); From 1e5837f25f5a5a1bffdf36da761040627ce2ceec Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 8 Mar 2019 11:10:54 -0800 Subject: [PATCH 073/446] Enable Android buttons as needed. --- tools/nitpick/src/TestRunnerMobile.cpp | 3 +-- tools/nitpick/ui/Nitpick.ui | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 4d0d18ef3d..d7800f35b4 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -60,6 +60,7 @@ void TestRunnerMobile::setWorkingFolderAndEnableControls() { setWorkingFolder(_workingFolderLabel); _connectDeviceButton->setEnabled(true); + _downloadAPKPushbutton->setEnabled(true); } void TestRunnerMobile::connectDevice() { @@ -154,8 +155,6 @@ void TestRunnerMobile::downloadComplete() { } else { _statusLabel->setText("Installer download complete"); } - - _installAPKPushbutton->setEnabled(true); } void TestRunnerMobile::installAPK() { diff --git a/tools/nitpick/ui/Nitpick.ui b/tools/nitpick/ui/Nitpick.ui index a0f368863d..c85311d86b 100644 --- a/tools/nitpick/ui/Nitpick.ui +++ b/tools/nitpick/ui/Nitpick.ui @@ -46,7 +46,7 @@ - 5 + 0 From dbdf5fdd1f73e1799a9ecd3351ac6dc774ff3ceb Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 8 Mar 2019 13:53:07 -0800 Subject: [PATCH 074/446] Decreased threshold after additional testing. --- tools/nitpick/src/TestCreator.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/nitpick/src/TestCreator.h b/tools/nitpick/src/TestCreator.h index c2e7ba14f2..f2bd520574 100644 --- a/tools/nitpick/src/TestCreator.h +++ b/tools/nitpick/src/TestCreator.h @@ -122,7 +122,7 @@ private: const QString TEST_RESULTS_FILENAME { "TestResults.txt" }; const double THRESHOLD_GLOBAL{ 0.9998 }; - const double THRESHOLD_LOCAL { 0.7770 }; + const double THRESHOLD_LOCAL { 0.7500 }; QDir _imageDirectory; From 24c7c8be190cd605f2531a6f2353e44aefb747e6 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 9 Mar 2019 12:03:59 +1300 Subject: [PATCH 075/446] Update JSDoc per merge from master --- interface/src/avatar/MyAvatar.cpp | 18 ++++++++++++++++++ interface/src/avatar/MyAvatar.h | 21 ++++++++++++--------- libraries/animation/src/Rig.cpp | 15 +++++++++++---- 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 3db1228796..710a0550c5 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -5358,6 +5358,24 @@ void MyAvatar::addAvatarHandsToFlow(const std::shared_ptr& otherAvatar) } } +/**jsdoc + * Physics options to use in the flow simulation of a joint. + * @typedef {object} MyAvatar.FlowPhysicsOptions + * @property {boolean} [active=true] - true to enable flow on the joint, false if it isn't., + * @property {number} [radius=0.01] - The thickness of segments and knots. (Needed for collisions.) + * @property {number} [gravity=-0.0096] - Y-value of the gravity vector. + * @property {number} [inertia=0.8] - Rotational inertia multiplier. + * @property {number} [damping=0.85] - The amount of damping on joint oscillation. + * @property {number} [stiffness=0.0] - How stiff each thread is. + * @property {number} [delta=0.55] - Delta time for every integration step. + */ +/**jsdoc + * Collision options to use in the flow simulation of a joint. + * @typedef {object} MyAvatar.FlowCollisionsOptions + * @property {string} [type="sphere"] - Currently, only "sphere" is supported. + * @property {number} [radius=0.05] - Collision sphere radius. + * @property {number} [offset=Vec3.ZERO] - Offset of the collision sphere from the joint. + */ void MyAvatar::useFlow(bool isActive, bool isCollidable, const QVariantMap& physicsConfig, const QVariantMap& collisionsConfig) { if (_skeletonModel->isLoaded()) { _skeletonModel->getRig().initFlow(isActive); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 5e049c7a02..1c44db703f 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1548,15 +1548,18 @@ public: void addAvatarHandsToFlow(const std::shared_ptr& otherAvatar); /**jsdoc - * Init flow simulation on avatar. - * @function MyAvatar.useFlow - * @param {boolean} - Set to true to activate flow simulation. - * @param {boolean} - Set to true to activate collisions. - * @param {Object} physicsConfig - object with the customized physic parameters - * i.e. {"hair": {"active": true, "stiffness": 0.0, "radius": 0.04, "gravity": -0.035, "damping": 0.8, "inertia": 0.8, "delta": 0.35}} - * @param {Object} collisionsConfig - object with the customized collision parameters - * i.e. {"Spine2": {"type": "sphere", "radius": 0.14, "offset": {"x": 0.0, "y": 0.2, "z": 0.0}}} - */ + * Enables and disables flow simulation of physics on the avatar's hair, clothes, and body parts. See + * {@link https://docs.highfidelity.com/create/avatars/create-avatars/add-flow.html|Add Flow to Your Avatar} for more + * information. + * @function MyAvatar.useFlow + * @param {boolean} isActive - true if flow simulation is enabled on the joint, false if it isn't. + * @param {boolean} isCollidable - true to enable collisions in the flow simulation, false to + * disable. + * @param {Object} [physicsConfig>] - Physic configurations for particular entity + * and avatar joints. + * @param {Object} [collisionsConfig] - Collision configurations for particular + * entity and avatar joints. + */ Q_INVOKABLE void useFlow(bool isActive, bool isCollidable, const QVariantMap& physicsConfig = QVariantMap(), const QVariantMap& collisionsConfig = QVariantMap()); public slots: diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 2a4c2326db..e1ee134530 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -170,8 +170,13 @@ static const QString MAIN_STATE_MACHINE_RIGHT_HAND_POSITION("mainStateMachineRig * coordinate system of the elbow. * leftHandPoleVector{@link Vec3}The direction the elbow should point in rig * coordinates. - - * rightHandPosition{@link Vec3}The desired position of the RightHand + * + * leftHandIKEnabledbooleantrue if IK is enabled for the left + * hand. + * rightHandIKEnabledbooleantrue if IK is enabled for the right + * hand. + * + * rightHandPosition{@link Vec3}The desired position of the RightHand * joint in rig coordinates. * rightHandRotation{@link Quat}The desired orientation of the * RightHand joint in rig coordinates. @@ -189,7 +194,7 @@ static const QString MAIN_STATE_MACHINE_RIGHT_HAND_POSITION("mainStateMachineRig * foot. * rightFootIKEnabledbooleantrue if IK is enabled for the right * foot. - + * * leftFootIKPositionVarstringThe name of the source for the desired position * of the LeftFoot joint. If not set, the foot rotation of the underlying animation will be used. * leftFootIKRotationVarstringThe name of the source for the desired rotation @@ -199,7 +204,7 @@ static const QString MAIN_STATE_MACHINE_RIGHT_HAND_POSITION("mainStateMachineRig * underlying animation. * leftFootPoleVector{@link Vec3}The direction the knee should face in rig * coordinates. - * rightFootIKPositionVarstringThe name of the source for the desired position + * rightFootIKPositionVarstringThe name of the source for the desired position * of the RightFoot joint. If not set, the foot rotation of the underlying animation will be used. * rightFootIKRotationVarstringThe name of the source for the desired rotation * of the RightFoot joint. If not set, the foot rotation of the underlying animation will be used. @@ -209,6 +214,8 @@ static const QString MAIN_STATE_MACHINE_RIGHT_HAND_POSITION("mainStateMachineRig * rightFootPoleVector{@link Vec3}The direction the knee should face in rig * coordinates. * + * splineIKEnabledbooleantrue if IK is enabled for the spline. + * * isTalkingbooleantrue if the avatar is talking. * notIsTalkingbooleantrue if the avatar is not talking. * From 15d49fd9a923ff3ddde21b15d2f1b9fe2e2ece65 Mon Sep 17 00:00:00 2001 From: r3tk0n Date: Tue, 5 Mar 2019 17:09:54 -0800 Subject: [PATCH 076/446] Initial implementation (deadlocks still occurring in Audio.cpp) --- interface/resources/qml/hifi/audio/Audio.qml | 19 ++ interface/resources/qml/hifi/audio/MicBar.qml | 9 +- interface/src/Application.cpp | 18 ++ interface/src/Application.h | 2 + interface/src/scripting/Audio.cpp | 174 +++++++++++++++++- interface/src/scripting/Audio.h | 89 ++++++++- scripts/system/audio.js | 3 + 7 files changed, 302 insertions(+), 12 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index c8dd83cd62..45358f59a2 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -148,6 +148,25 @@ Rectangle { checked = Qt.binding(function() { return AudioScriptingInterface.noiseReduction; }); // restore binding } } + AudioControls.CheckBox { + spacing: muteMic.spacing + text: qsTr("Push To Talk"); + checked: isVR ? AudioScriptingInterface.pushToTalkHMD : AudioScriptingInterface.pushToTalkDesktop; + onClicked: { + if (isVR) { + AudioScriptingInterface.pushToTalkHMD = checked; + } else { + AudioScriptingInterface.pushToTalkDesktop = checked; + } + checked = Qt.binding(function() { + if (isVR) { + return AudioScriptingInterface.pushToTalkHMD; + } else { + return AudioScriptingInterface.pushToTalkDesktop; + } + }); // restore binding + } + } AudioControls.CheckBox { spacing: muteMic.spacing text: qsTr("Show audio level meter"); diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index 39f75a9182..f91058bc3c 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -11,10 +11,13 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 +import stylesUit 1.0 import TabletScriptingInterface 1.0 Rectangle { + HifiConstants { id: hifi; } + readonly property var level: AudioScriptingInterface.inputLevel; property bool gated: false; @@ -131,9 +134,9 @@ Rectangle { Item { id: status; - readonly property string color: AudioScriptingInterface.muted ? colors.muted : colors.unmuted; + readonly property string color: (AudioScriptingInterface.pushingToTalk && AudioScriptingInterface.muted) ? hifi.colors.blueHighlight : AudioScriptingInterface.muted ? colors.muted : colors.unmuted; - visible: AudioScriptingInterface.muted; + visible: AudioScriptingInterface.pushingToTalk || AudioScriptingInterface.muted; anchors { left: parent.left; @@ -152,7 +155,7 @@ Rectangle { color: parent.color; - text: AudioScriptingInterface.muted ? "MUTED" : "MUTE"; + text: AudioScriptingInterface.pushToTalk ? (AudioScriptingInterface.pushingToTalk ? "SPEAKING" : "PUSH TO TALK") : (AudioScriptingInterface.muted ? "MUTED" : "MUTE"); font.pointSize: 12; } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 6d9a1823a1..dad86e748e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1435,6 +1435,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo }); connect(this, &Application::activeDisplayPluginChanged, reinterpret_cast(audioScriptingInterface.data()), &scripting::Audio::onContextChanged); + connect(this, &Application::pushedToTalk, + reinterpret_cast(audioScriptingInterface.data()), &scripting::Audio::handlePushedToTalk); } // Create the rendering engine. This can be slow on some machines due to lots of @@ -4205,6 +4207,10 @@ void Application::keyPressEvent(QKeyEvent* event) { } break; + case Qt::Key_T: + emit pushedToTalk(true); + break; + case Qt::Key_P: { if (!isShifted && !isMeta && !isOption && !event->isAutoRepeat()) { AudioInjectorOptions options; @@ -4310,6 +4316,12 @@ void Application::keyReleaseEvent(QKeyEvent* event) { if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->keyReleaseEvent(event); } + + switch (event->key()) { + case Qt::Key_T: + emit pushedToTalk(false); + break; + } } void Application::focusOutEvent(QFocusEvent* event) { @@ -5241,6 +5253,9 @@ void Application::loadSettings() { } } + auto audioScriptingInterface = reinterpret_cast(DependencyManager::get().data()); + audioScriptingInterface->loadData(); + getMyAvatar()->loadData(); _settingsLoaded = true; } @@ -5250,6 +5265,9 @@ void Application::saveSettings() const { DependencyManager::get()->saveSettings(); DependencyManager::get()->saveSettings(); + auto audioScriptingInterface = reinterpret_cast(DependencyManager::get().data()); + audioScriptingInterface->saveData(); + Menu::getInstance()->saveSettings(); getMyAvatar()->saveData(); PluginManager::getInstance()->saveSettings(); diff --git a/interface/src/Application.h b/interface/src/Application.h index a8cc9450c5..1c86326f90 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -358,6 +358,8 @@ signals: void miniTabletEnabledChanged(bool enabled); + void pushedToTalk(bool enabled); + public slots: QVector pasteEntities(float x, float y, float z); bool exportEntities(const QString& filename, const QVector& entityIDs, const glm::vec3* givenOffset = nullptr); diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 2c4c29ff65..fe04ce47ca 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -63,26 +63,163 @@ void Audio::stopRecording() { } bool Audio::isMuted() const { - return resultWithReadLock([&] { - return _isMuted; - }); + bool isHMD = qApp->isHMDMode(); + if (isHMD) { + return getMutedHMD(); + } + else { + return getMutedDesktop(); + } } void Audio::setMuted(bool isMuted) { + withWriteLock([&] { + bool isHMD = qApp->isHMDMode(); + if (isHMD) { + setMutedHMD(isMuted); + } + else { + setMutedDesktop(isMuted); + } + }); +} + +void Audio::setMutedDesktop(bool isMuted) { + bool changed = false; + if (_desktopMuted != isMuted) { + changed = true; + _desktopMuted = isMuted; + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); + } + if (changed) { + emit mutedChanged(isMuted); + emit desktopMutedChanged(isMuted); + } +} + +bool Audio::getMutedDesktop() const { + return resultWithReadLock([&] { + return _desktopMuted; + }); +} + +void Audio::setMutedHMD(bool isMuted) { + bool changed = false; + if (_hmdMuted != isMuted) { + changed = true; + _hmdMuted = isMuted; + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); + } + if (changed) { + emit mutedChanged(isMuted); + emit hmdMutedChanged(isMuted); + } +} + +bool Audio::getMutedHMD() const { + return resultWithReadLock([&] { + return _hmdMuted; + }); +} + +bool Audio::getPTT() { + bool isHMD = qApp->isHMDMode(); + if (isHMD) { + return getPTTHMD(); + } + else { + return getPTTDesktop(); + } +} + +bool Audio::getPushingToTalk() const { + return resultWithReadLock([&] { + return _pushingToTalk; + }); +} + +void Audio::setPTT(bool enabled) { + bool isHMD = qApp->isHMDMode(); + if (isHMD) { + setPTTHMD(enabled); + } + else { + setPTTDesktop(enabled); + } +} + +void Audio::setPTTDesktop(bool enabled) { bool changed = false; withWriteLock([&] { - if (_isMuted != isMuted) { - _isMuted = isMuted; - auto client = DependencyManager::get().data(); - QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); + if (_pttDesktop != enabled) { changed = true; + _pttDesktop = enabled; + if (!enabled) { + // Set to default behavior (unmuted for Desktop) on Push-To-Talk disable. + setMutedDesktop(true); + } + else { + // Should be muted when not pushing to talk while PTT is enabled. + setMutedDesktop(true); + } } }); if (changed) { - emit mutedChanged(isMuted); + emit pushToTalkChanged(enabled); + emit pushToTalkDesktopChanged(enabled); } } +bool Audio::getPTTDesktop() const { + return resultWithReadLock([&] { + return _pttDesktop; + }); +} + +void Audio::setPTTHMD(bool enabled) { + bool changed = false; + withWriteLock([&] { + if (_pttHMD != enabled) { + changed = true; + _pttHMD = enabled; + if (!enabled) { + // Set to default behavior (unmuted for HMD) on Push-To-Talk disable. + setMutedHMD(false); + } + else { + // Should be muted when not pushing to talk while PTT is enabled. + setMutedHMD(true); + } + } + }); + if (changed) { + emit pushToTalkChanged(enabled); + emit pushToTalkHMDChanged(enabled); + } +} + +bool Audio::getPTTHMD() const { + return resultWithReadLock([&] { + return _pttHMD; + }); +} + +void Audio::saveData() { + _desktopMutedSetting.set(getMutedDesktop()); + _hmdMutedSetting.set(getMutedHMD()); + _pttDesktopSetting.set(getPTTDesktop()); + _pttHMDSetting.set(getPTTHMD()); +} + +void Audio::loadData() { + _desktopMuted = _desktopMutedSetting.get(); + _hmdMuted = _hmdMutedSetting.get(); + _pttDesktop = _pttDesktopSetting.get(); + _pttHMD = _pttHMDSetting.get(); +} + bool Audio::noiseReductionEnabled() const { return resultWithReadLock([&] { return _enableNoiseReduction; @@ -179,11 +316,32 @@ void Audio::onContextChanged() { changed = true; } }); + if (isHMD) { + setMuted(getMutedHMD()); + } + else { + setMuted(getMutedDesktop()); + } if (changed) { emit contextChanged(isHMD ? Audio::HMD : Audio::DESKTOP); } } +void Audio::handlePushedToTalk(bool enabled) { + if (getPTT()) { + if (enabled) { + setMuted(false); + } + else { + setMuted(true); + } + if (_pushingToTalk != enabled) { + _pushingToTalk = enabled; + emit pushingToTalkChanged(enabled); + } + } +} + void Audio::setReverb(bool enable) { withWriteLock([&] { DependencyManager::get()->setReverb(enable); diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index e4dcba9130..6aa589e399 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -12,6 +12,7 @@ #ifndef hifi_scripting_Audio_h #define hifi_scripting_Audio_h +#include #include "AudioScriptingInterface.h" #include "AudioDevices.h" #include "AudioEffectOptions.h" @@ -19,6 +20,9 @@ #include "AudioFileWav.h" #include +using MutedGetter = std::function; +using MutedSetter = std::function; + namespace scripting { class Audio : public AudioScriptingInterface, protected ReadWriteLockable { @@ -63,6 +67,12 @@ class Audio : public AudioScriptingInterface, protected ReadWriteLockable { Q_PROPERTY(bool clipping READ isClipping NOTIFY clippingChanged) Q_PROPERTY(QString context READ getContext NOTIFY contextChanged) Q_PROPERTY(AudioDevices* devices READ getDevices NOTIFY nop) + Q_PROPERTY(bool desktopMuted READ getMutedDesktop WRITE setMutedDesktop NOTIFY desktopMutedChanged) + Q_PROPERTY(bool hmdMuted READ getMutedHMD WRITE setMutedHMD NOTIFY hmdMutedChanged) + Q_PROPERTY(bool pushToTalk READ getPTT WRITE setPTT NOTIFY pushToTalkChanged); + Q_PROPERTY(bool pushToTalkDesktop READ getPTTDesktop WRITE setPTTDesktop NOTIFY pushToTalkDesktopChanged) + Q_PROPERTY(bool pushToTalkHMD READ getPTTHMD WRITE setPTTHMD NOTIFY pushToTalkHMDChanged) + Q_PROPERTY(bool pushingToTalk READ getPushingToTalk NOTIFY pushingToTalkChanged) public: static QString AUDIO; @@ -82,6 +92,25 @@ public: void showMicMeter(bool show); + // Mute setting setters and getters + void setMutedDesktop(bool isMuted); + bool getMutedDesktop() const; + void setMutedHMD(bool isMuted); + bool getMutedHMD() const; + void setPTT(bool enabled); + bool getPTT(); + bool getPushingToTalk() const; + + // Push-To-Talk setters and getters + void setPTTDesktop(bool enabled); + bool getPTTDesktop() const; + void setPTTHMD(bool enabled); + bool getPTTHMD() const; + + // Settings handlers + void saveData(); + void loadData(); + /**jsdoc * @function Audio.setInputDevice * @param {object} device @@ -193,6 +222,46 @@ signals: */ void mutedChanged(bool isMuted); + /**jsdoc + * Triggered when desktop audio input is muted or unmuted. + * @function Audio.desktopMutedChanged + * @param {boolean} isMuted - true if the audio input is muted for desktop mode, otherwise false. + * @returns {Signal} + */ + void desktopMutedChanged(bool isMuted); + + /**jsdoc + * Triggered when HMD audio input is muted or unmuted. + * @function Audio.hmdMutedChanged + * @param {boolean} isMuted - true if the audio input is muted for HMD mode, otherwise false. + * @returns {Signal} + */ + void hmdMutedChanged(bool isMuted); + + /** + * Triggered when Push-to-Talk has been enabled or disabled. + * @function Audio.pushToTalkChanged + * @param {boolean} enabled - true if Push-to-Talk is enabled, otherwise false. + * @returns {Signal} + */ + void pushToTalkChanged(bool enabled); + + /** + * Triggered when Push-to-Talk has been enabled or disabled for desktop mode. + * @function Audio.pushToTalkDesktopChanged + * @param {boolean} enabled - true if Push-to-Talk is emabled for Desktop mode, otherwise false. + * @returns {Signal} + */ + void pushToTalkDesktopChanged(bool enabled); + + /** + * Triggered when Push-to-Talk has been enabled or disabled for HMD mode. + * @function Audio.pushToTalkHMDChanged + * @param {boolean} enabled - true if Push-to-Talk is emabled for HMD mode, otherwise false. + * @returns {Signal} + */ + void pushToTalkHMDChanged(bool enabled); + /**jsdoc * Triggered when the audio input noise reduction is enabled or disabled. * @function Audio.noiseReductionChanged @@ -237,6 +306,14 @@ signals: */ void contextChanged(const QString& context); + /**jsdoc + * Triggered when pushing to talk. + * @function Audio.pushingToTalkChanged + * @param {boolean} talking - true if broadcasting with PTT, false otherwise. + * @returns {Signal} + */ + void pushingToTalkChanged(bool talking); + public slots: /**jsdoc @@ -245,6 +322,8 @@ public slots: */ void onContextChanged(); + void handlePushedToTalk(bool enabled); + private slots: void setMuted(bool muted); void enableNoiseReduction(bool enable); @@ -260,11 +339,19 @@ private: float _inputVolume { 1.0f }; float _inputLevel { 0.0f }; bool _isClipping { false }; - bool _isMuted { false }; bool _enableNoiseReduction { true }; // Match default value of AudioClient::_isNoiseGateEnabled. bool _contextIsHMD { false }; AudioDevices* getDevices() { return &_devices; } AudioDevices _devices; + Setting::Handle _desktopMutedSetting{ QStringList { Audio::AUDIO, "desktopMuted" }, true }; + Setting::Handle _hmdMutedSetting{ QStringList { Audio::AUDIO, "hmdMuted" }, true }; + Setting::Handle _pttDesktopSetting{ QStringList { Audio::AUDIO, "pushToTalkDesktop" }, false }; + Setting::Handle _pttHMDSetting{ QStringList { Audio::AUDIO, "pushToTalkHMD" }, false }; + bool _desktopMuted{ true }; + bool _hmdMuted{ false }; + bool _pttDesktop{ false }; + bool _pttHMD{ false }; + bool _pushingToTalk{ false }; }; }; diff --git a/scripts/system/audio.js b/scripts/system/audio.js index ee82c0c6ea..51d070d8cd 100644 --- a/scripts/system/audio.js +++ b/scripts/system/audio.js @@ -28,6 +28,9 @@ var UNMUTE_ICONS = { }; function onMuteToggled() { + if (Audio.pushingToTalk) { + return; + } if (Audio.muted) { button.editProperties(MUTE_ICONS); } else { From 7cb17b2d6ea31da2aefb0f775569ca1abf43e637 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 6 Mar 2019 11:00:09 -0800 Subject: [PATCH 077/446] laying groundwork for audio app + fixing deadlocks --- interface/resources/qml/hifi/audio/MicBar.qml | 6 +- interface/src/scripting/Audio.cpp | 108 +++++++++--------- interface/src/scripting/Audio.h | 1 + scripts/system/audio.js | 11 +- 4 files changed, 69 insertions(+), 57 deletions(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index f91058bc3c..2ab1085408 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -134,7 +134,7 @@ Rectangle { Item { id: status; - readonly property string color: (AudioScriptingInterface.pushingToTalk && AudioScriptingInterface.muted) ? hifi.colors.blueHighlight : AudioScriptingInterface.muted ? colors.muted : colors.unmuted; + readonly property string color: AudioScriptingInterface.pushToTalk ? hifi.colors.blueHighlight : AudioScriptingInterface.muted ? colors.muted : colors.unmuted; visible: AudioScriptingInterface.pushingToTalk || AudioScriptingInterface.muted; @@ -165,7 +165,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - width: 50; + width: AudioScriptingInterface.pushToTalk ? (AudioScriptingInterface.pushingToTalk ? 45: 30) : 50; height: 4; color: parent.color; } @@ -176,7 +176,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - width: 50; + width: AudioScriptingInterface.pushToTalk ? (AudioScriptingInterface.pushingToTalk ? 45: 30) : 50; height: 4; color: parent.color; } diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index fe04ce47ca..63ce9d2b2e 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -66,32 +66,30 @@ bool Audio::isMuted() const { bool isHMD = qApp->isHMDMode(); if (isHMD) { return getMutedHMD(); - } - else { + } else { return getMutedDesktop(); } } void Audio::setMuted(bool isMuted) { - withWriteLock([&] { - bool isHMD = qApp->isHMDMode(); - if (isHMD) { - setMutedHMD(isMuted); - } - else { - setMutedDesktop(isMuted); - } - }); + bool isHMD = qApp->isHMDMode(); + if (isHMD) { + setMutedHMD(isMuted); + } else { + setMutedDesktop(isMuted); + } } void Audio::setMutedDesktop(bool isMuted) { bool changed = false; - if (_desktopMuted != isMuted) { - changed = true; - _desktopMuted = isMuted; - auto client = DependencyManager::get().data(); - QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); - } + withWriteLock([&] { + if (_desktopMuted != isMuted) { + changed = true; + _desktopMuted = isMuted; + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); + } + }); if (changed) { emit mutedChanged(isMuted); emit desktopMutedChanged(isMuted); @@ -106,12 +104,14 @@ bool Audio::getMutedDesktop() const { void Audio::setMutedHMD(bool isMuted) { bool changed = false; - if (_hmdMuted != isMuted) { - changed = true; - _hmdMuted = isMuted; - auto client = DependencyManager::get().data(); - QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); - } + withWriteLock([&] { + if (_hmdMuted != isMuted) { + changed = true; + _hmdMuted = isMuted; + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); + } + }); if (changed) { emit mutedChanged(isMuted); emit hmdMutedChanged(isMuted); @@ -128,12 +128,24 @@ bool Audio::getPTT() { bool isHMD = qApp->isHMDMode(); if (isHMD) { return getPTTHMD(); - } - else { + } else { return getPTTDesktop(); } } +void scripting::Audio::setPushingToTalk(bool pushingToTalk) { + bool changed = false; + withWriteLock([&] { + if (_pushingToTalk != pushingToTalk) { + changed = true; + _pushingToTalk = pushingToTalk; + } + }); + if (changed) { + emit pushingToTalkChanged(pushingToTalk); + } +} + bool Audio::getPushingToTalk() const { return resultWithReadLock([&] { return _pushingToTalk; @@ -144,8 +156,7 @@ void Audio::setPTT(bool enabled) { bool isHMD = qApp->isHMDMode(); if (isHMD) { setPTTHMD(enabled); - } - else { + } else { setPTTDesktop(enabled); } } @@ -156,16 +167,16 @@ void Audio::setPTTDesktop(bool enabled) { if (_pttDesktop != enabled) { changed = true; _pttDesktop = enabled; - if (!enabled) { - // Set to default behavior (unmuted for Desktop) on Push-To-Talk disable. - setMutedDesktop(true); - } - else { - // Should be muted when not pushing to talk while PTT is enabled. - setMutedDesktop(true); - } } }); + if (!enabled) { + // Set to default behavior (unmuted for Desktop) on Push-To-Talk disable. + setMutedDesktop(true); + } else { + // Should be muted when not pushing to talk while PTT is enabled. + setMutedDesktop(true); + } + if (changed) { emit pushToTalkChanged(enabled); emit pushToTalkDesktopChanged(enabled); @@ -184,16 +195,16 @@ void Audio::setPTTHMD(bool enabled) { if (_pttHMD != enabled) { changed = true; _pttHMD = enabled; - if (!enabled) { - // Set to default behavior (unmuted for HMD) on Push-To-Talk disable. - setMutedHMD(false); - } - else { - // Should be muted when not pushing to talk while PTT is enabled. - setMutedHMD(true); - } } }); + if (!enabled) { + // Set to default behavior (unmuted for HMD) on Push-To-Talk disable. + setMutedHMD(false); + } else { + // Should be muted when not pushing to talk while PTT is enabled. + setMutedHMD(true); + } + if (changed) { emit pushToTalkChanged(enabled); emit pushToTalkHMDChanged(enabled); @@ -318,8 +329,7 @@ void Audio::onContextChanged() { }); if (isHMD) { setMuted(getMutedHMD()); - } - else { + } else { setMuted(getMutedDesktop()); } if (changed) { @@ -331,14 +341,10 @@ void Audio::handlePushedToTalk(bool enabled) { if (getPTT()) { if (enabled) { setMuted(false); - } - else { + } else { setMuted(true); } - if (_pushingToTalk != enabled) { - _pushingToTalk = enabled; - emit pushingToTalkChanged(enabled); - } + setPushingToTalk(enabled); } } diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index 6aa589e399..94f8a7bf54 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -99,6 +99,7 @@ public: bool getMutedHMD() const; void setPTT(bool enabled); bool getPTT(); + void setPushingToTalk(bool pushingToTalk); bool getPushingToTalk() const; // Push-To-Talk setters and getters diff --git a/scripts/system/audio.js b/scripts/system/audio.js index 51d070d8cd..bf44cfa7cc 100644 --- a/scripts/system/audio.js +++ b/scripts/system/audio.js @@ -26,12 +26,15 @@ var UNMUTE_ICONS = { icon: "icons/tablet-icons/mic-unmute-i.svg", activeIcon: "icons/tablet-icons/mic-unmute-a.svg" }; +var PTT_ICONS = { + icon: "icons/tablet-icons/mic-unmute-i.svg", + activeIcon: "icons/tablet-icons/mic-unmute-a.svg" +}; function onMuteToggled() { if (Audio.pushingToTalk) { - return; - } - if (Audio.muted) { + button.editProperties(PTT_ICONS); + } else if (Audio.muted) { button.editProperties(MUTE_ICONS); } else { button.editProperties(UNMUTE_ICONS); @@ -71,6 +74,7 @@ onMuteToggled(); button.clicked.connect(onClicked); tablet.screenChanged.connect(onScreenChanged); Audio.mutedChanged.connect(onMuteToggled); +Audio.pushingToTalkChanged.connect(onMuteToggled); Script.scriptEnding.connect(function () { if (onAudioScreen) { @@ -79,6 +83,7 @@ Script.scriptEnding.connect(function () { button.clicked.disconnect(onClicked); tablet.screenChanged.disconnect(onScreenChanged); Audio.mutedChanged.disconnect(onMuteToggled); + Audio.pushingToTalkChanged.disconnect(onMuteToggled); tablet.removeButton(button); }); From 60b98d2c70b2b9366d9d1d6abd18e9136d566a69 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 6 Mar 2019 11:11:47 -0800 Subject: [PATCH 078/446] Push to talk changes + rebased with master (#7) Push to talk changes + rebased with master --- interface/src/scripting/Audio.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 63ce9d2b2e..45bb15f1a3 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -231,6 +231,26 @@ void Audio::loadData() { _pttHMD = _pttHMDSetting.get(); } +bool Audio::getPTTHMD() const { + return resultWithReadLock([&] { + return _pttHMD; + }); +} + +void Audio::saveData() { + _desktopMutedSetting.set(getMutedDesktop()); + _hmdMutedSetting.set(getMutedHMD()); + _pttDesktopSetting.set(getPTTDesktop()); + _pttHMDSetting.set(getPTTHMD()); +} + +void Audio::loadData() { + _desktopMuted = _desktopMutedSetting.get(); + _hmdMuted = _hmdMutedSetting.get(); + _pttDesktop = _pttDesktopSetting.get(); + _pttHMD = _pttHMDSetting.get(); +} + bool Audio::noiseReductionEnabled() const { return resultWithReadLock([&] { return _enableNoiseReduction; From 8a5924077a24d0892ab402dceb0c1a258ce07860 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 6 Mar 2019 11:18:57 -0800 Subject: [PATCH 079/446] changing text display --- interface/resources/qml/hifi/audio/MicBar.qml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index 2ab1085408..9d1cbfbc6c 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -134,9 +134,9 @@ Rectangle { Item { id: status; - readonly property string color: AudioScriptingInterface.pushToTalk ? hifi.colors.blueHighlight : AudioScriptingInterface.muted ? colors.muted : colors.unmuted; + readonly property string color: AudioScriptingInterface.muted ? colors.muted : colors.unmuted; - visible: AudioScriptingInterface.pushingToTalk || AudioScriptingInterface.muted; + visible: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) || AudioScriptingInterface.muted; anchors { left: parent.left; @@ -155,7 +155,7 @@ Rectangle { color: parent.color; - text: AudioScriptingInterface.pushToTalk ? (AudioScriptingInterface.pushingToTalk ? "SPEAKING" : "PUSH TO TALK") : (AudioScriptingInterface.muted ? "MUTED" : "MUTE"); + text: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) ? "MUTED-PTT (T)" : (AudioScriptingInterface.muted ? "MUTED" : "MUTE"); font.pointSize: 12; } @@ -165,7 +165,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - width: AudioScriptingInterface.pushToTalk ? (AudioScriptingInterface.pushingToTalk ? 45: 30) : 50; + width: AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk ? 25 : 50; height: 4; color: parent.color; } @@ -176,7 +176,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - width: AudioScriptingInterface.pushToTalk ? (AudioScriptingInterface.pushingToTalk ? 45: 30) : 50; + width: AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk ? 25 : 50; height: 4; color: parent.color; } From a688c0a92b56062b46442f9e25d6d8b57f77294d Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 6 Mar 2019 11:45:43 -0800 Subject: [PATCH 080/446] exposing setting pushingToTalk --- interface/src/scripting/Audio.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index 94f8a7bf54..9ad4aac9c1 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -72,7 +72,7 @@ class Audio : public AudioScriptingInterface, protected ReadWriteLockable { Q_PROPERTY(bool pushToTalk READ getPTT WRITE setPTT NOTIFY pushToTalkChanged); Q_PROPERTY(bool pushToTalkDesktop READ getPTTDesktop WRITE setPTTDesktop NOTIFY pushToTalkDesktopChanged) Q_PROPERTY(bool pushToTalkHMD READ getPTTHMD WRITE setPTTHMD NOTIFY pushToTalkHMDChanged) - Q_PROPERTY(bool pushingToTalk READ getPushingToTalk NOTIFY pushingToTalkChanged) + Q_PROPERTY(bool pushingToTalk READ getPushingToTalk WRITE setPushingToTalk NOTIFY pushingToTalkChanged) public: static QString AUDIO; From b7d44403e16e76e8d7b1c672c457946e1933a378 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 6 Mar 2019 17:29:32 -0800 Subject: [PATCH 081/446] updating compile failure + icons/settings update --- .../icons/tablet-icons/mic-ptt-a.svg | 1 + .../icons/tablet-icons/mic-ptt-i.svg | 24 +++++++ interface/resources/qml/hifi/audio/Audio.qml | 64 ++++++++++++++++--- interface/resources/qml/hifi/audio/MicBar.qml | 8 ++- interface/src/scripting/Audio.cpp | 20 ------ scripts/system/audio.js | 14 ++-- 6 files changed, 93 insertions(+), 38 deletions(-) create mode 100644 interface/resources/icons/tablet-icons/mic-ptt-a.svg create mode 100644 interface/resources/icons/tablet-icons/mic-ptt-i.svg diff --git a/interface/resources/icons/tablet-icons/mic-ptt-a.svg b/interface/resources/icons/tablet-icons/mic-ptt-a.svg new file mode 100644 index 0000000000..e6df3c69d7 --- /dev/null +++ b/interface/resources/icons/tablet-icons/mic-ptt-a.svg @@ -0,0 +1 @@ +mic-ptt-a \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/mic-ptt-i.svg b/interface/resources/icons/tablet-icons/mic-ptt-i.svg new file mode 100644 index 0000000000..2141ea5229 --- /dev/null +++ b/interface/resources/icons/tablet-icons/mic-ptt-i.svg @@ -0,0 +1,24 @@ + + + + + + diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index 45358f59a2..d44a9c862e 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -120,6 +120,10 @@ Rectangle { isRedCheck: true; checked: AudioScriptingInterface.muted; onClicked: { + if (AudioScriptingInterface.pushToTalk && !checked) { + // disable push to talk if unmuting + AudioScriptingInterface.pushToTalk = false; + } AudioScriptingInterface.muted = checked; checked = Qt.binding(function() { return AudioScriptingInterface.muted; }); // restore binding } @@ -150,7 +154,23 @@ Rectangle { } AudioControls.CheckBox { spacing: muteMic.spacing - text: qsTr("Push To Talk"); + text: qsTr("Show audio level meter"); + checked: AvatarInputs.showAudioTools; + onClicked: { + AvatarInputs.showAudioTools = checked; + checked = Qt.binding(function() { return AvatarInputs.showAudioTools; }); // restore binding + } + onXChanged: rightMostInputLevelPos = x + width + } + } + + Separator {} + + ColumnLayout { + spacing: muteMic.spacing; + AudioControls.CheckBox { + spacing: muteMic.spacing + text: qsTr("Push To Talk (T)"); checked: isVR ? AudioScriptingInterface.pushToTalkHMD : AudioScriptingInterface.pushToTalkDesktop; onClicked: { if (isVR) { @@ -167,15 +187,41 @@ Rectangle { }); // restore binding } } - AudioControls.CheckBox { - spacing: muteMic.spacing - text: qsTr("Show audio level meter"); - checked: AvatarInputs.showAudioTools; - onClicked: { - AvatarInputs.showAudioTools = checked; - checked = Qt.binding(function() { return AvatarInputs.showAudioTools; }); // restore binding + Item { + id: pttTextContainer + x: margins.paddings; + width: rightMostInputLevelPos + height: pttTextMetrics.height + visible: true + TextMetrics { + id: pttTextMetrics + text: pttText.text + font: pttText.font + } + RalewayRegular { + id: pttText + wrapMode: Text.WordWrap + color: hifi.colors.white; + width: parent.width; + font.italic: true + size: 16; + text: isVR ? qsTr("Press and hold grip triggers on both of your controllers to unmute.") : + qsTr("Press and hold the button \"T\" to unmute."); + onTextChanged: { + if (pttTextMetrics.width > rightMostInputLevelPos) { + pttTextContainer.height = Math.ceil(pttTextMetrics.width / rightMostInputLevelPos) * pttTextMetrics.height; + } else { + pttTextContainer.height = pttTextMetrics.height; + } + } + } + Component.onCompleted: { + if (pttTextMetrics.width > rightMostInputLevelPos) { + pttTextContainer.height = Math.ceil(pttTextMetrics.width / rightMostInputLevelPos) * pttTextMetrics.height; + } else { + pttTextContainer.height = pttTextMetrics.height; + } } - onXChanged: rightMostInputLevelPos = x + width } } } diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index 9d1cbfbc6c..50477b82f8 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -67,6 +67,9 @@ Rectangle { hoverEnabled: true; scrollGestureEnabled: false; onClicked: { + if (AudioScriptingInterface.pushToTalk) { + return; + } AudioScriptingInterface.muted = !AudioScriptingInterface.muted; Tablet.playSound(TabletEnums.ButtonClick); } @@ -109,9 +112,10 @@ Rectangle { Image { readonly property string unmutedIcon: "../../../icons/tablet-icons/mic-unmute-i.svg"; readonly property string mutedIcon: "../../../icons/tablet-icons/mic-mute-i.svg"; + readonly property string pushToTalkIcon: "../../../icons/tablet-icons/mic-ptt-i.svg"; id: image; - source: AudioScriptingInterface.muted ? mutedIcon : unmutedIcon; + source: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) ? pushToTalkIcon : AudioScriptingInterface.muted ? mutedIcon : unmutedIcon; width: 30; height: 30; @@ -155,7 +159,7 @@ Rectangle { color: parent.color; - text: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) ? "MUTED-PTT (T)" : (AudioScriptingInterface.muted ? "MUTED" : "MUTE"); + text: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) ? "MUTED PTT-(T)" : (AudioScriptingInterface.muted ? "MUTED" : "MUTE"); font.pointSize: 12; } diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 45bb15f1a3..63ce9d2b2e 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -231,26 +231,6 @@ void Audio::loadData() { _pttHMD = _pttHMDSetting.get(); } -bool Audio::getPTTHMD() const { - return resultWithReadLock([&] { - return _pttHMD; - }); -} - -void Audio::saveData() { - _desktopMutedSetting.set(getMutedDesktop()); - _hmdMutedSetting.set(getMutedHMD()); - _pttDesktopSetting.set(getPTTDesktop()); - _pttHMDSetting.set(getPTTHMD()); -} - -void Audio::loadData() { - _desktopMuted = _desktopMutedSetting.get(); - _hmdMuted = _hmdMutedSetting.get(); - _pttDesktop = _pttDesktopSetting.get(); - _pttHMD = _pttHMDSetting.get(); -} - bool Audio::noiseReductionEnabled() const { return resultWithReadLock([&] { return _enableNoiseReduction; diff --git a/scripts/system/audio.js b/scripts/system/audio.js index bf44cfa7cc..19ed3faef2 100644 --- a/scripts/system/audio.js +++ b/scripts/system/audio.js @@ -27,12 +27,12 @@ var UNMUTE_ICONS = { activeIcon: "icons/tablet-icons/mic-unmute-a.svg" }; var PTT_ICONS = { - icon: "icons/tablet-icons/mic-unmute-i.svg", - activeIcon: "icons/tablet-icons/mic-unmute-a.svg" + icon: "icons/tablet-icons/mic-ptt-i.svg", + activeIcon: "icons/tablet-icons/mic-ptt-a.svg" }; function onMuteToggled() { - if (Audio.pushingToTalk) { + if (Audio.pushToTalk) { button.editProperties(PTT_ICONS); } else if (Audio.muted) { button.editProperties(MUTE_ICONS); @@ -63,8 +63,8 @@ function onScreenChanged(type, url) { var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var button = tablet.addButton({ - icon: Audio.muted ? MUTE_ICONS.icon : UNMUTE_ICONS.icon, - activeIcon: Audio.muted ? MUTE_ICONS.activeIcon : UNMUTE_ICONS.activeIcon, + icon: Audio.pushToTalk ? PTT_ICONS.icon : Audio.muted ? MUTE_ICONS.icon : UNMUTE_ICONS.icon, + activeIcon: Audio.pushToTalk ? PTT_ICONS.activeIcon : Audio.muted ? MUTE_ICONS.activeIcon : UNMUTE_ICONS.activeIcon, text: TABLET_BUTTON_NAME, sortOrder: 1 }); @@ -74,7 +74,7 @@ onMuteToggled(); button.clicked.connect(onClicked); tablet.screenChanged.connect(onScreenChanged); Audio.mutedChanged.connect(onMuteToggled); -Audio.pushingToTalkChanged.connect(onMuteToggled); +Audio.pushToTalkChanged.connect(onMuteToggled); Script.scriptEnding.connect(function () { if (onAudioScreen) { @@ -83,7 +83,7 @@ Script.scriptEnding.connect(function () { button.clicked.disconnect(onClicked); tablet.screenChanged.disconnect(onScreenChanged); Audio.mutedChanged.disconnect(onMuteToggled); - Audio.pushingToTalkChanged.disconnect(onMuteToggled); + Audio.pushToTalkChanged.disconnect(onMuteToggled); tablet.removeButton(button); }); From f6add9cafdd87c6a253c344f905c295cbaa617fc Mon Sep 17 00:00:00 2001 From: r3tk0n Date: Thu, 7 Mar 2019 11:31:28 -0800 Subject: [PATCH 082/446] Add pushToTalk.js controllerModule. --- .../controllerModules/pushToTalk.js | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 scripts/system/controllers/controllerModules/pushToTalk.js diff --git a/scripts/system/controllers/controllerModules/pushToTalk.js b/scripts/system/controllers/controllerModules/pushToTalk.js new file mode 100644 index 0000000000..e764b228c9 --- /dev/null +++ b/scripts/system/controllers/controllerModules/pushToTalk.js @@ -0,0 +1,75 @@ +"use strict"; + +// Created by Jason C. Najera on 3/7/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Handles Push-to-Talk functionality for HMD mode. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); + +(function() { // BEGIN LOCAL_SCOPE + function PushToTalkHandler() { + var _this = this; + this.active = false; + //var pttMapping, mappingName; + + this.setup = function() { + //mappingName = 'Hifi-PTT-Dev-' + Math.random(); + //pttMapping = Controller.newMapping(mappingName); + //pttMapping.enable(); + }; + + this.shouldTalk = function (controllerData) { + // Set up test against controllerData here... + var gripVal = controllerData.secondaryValues[this.hand]; + return (gripVal) ? true : false; + }; + + this.shouldStopTalking = function (controllerData) { + var gripVal = controllerData.secondaryValues[this.hand]; + return (gripVal) ? false : true; + }; + + this.isReady = function (controllerData, deltaTime) { + if (HMD.active() && Audio.pushToTalk && this.shouldTalk(controllerData)) { + Audio.pushingToTalk = true; + returnMakeRunningValues(true, [], []); + } + + return makeRunningValues(false, [], []); + }; + + this.run = function (controllerData, deltaTime) { + if (this.shouldStopTalking(controllerData) || !Audio.pushToTalk) { + Audio.pushingToTalk = false; + return makeRunningValues(false, [], []); + } + + return makeRunningValues(true, [], []); + }; + + this.cleanup = function () { + //pttMapping.disable(); + }; + + this.parameters = makeDispatcherModuleParameters( + 950, + ["head"], + [], + 100); + } + + var pushToTalk = new PushToTalkHandler(); + enableDispatcherModule("PushToTalk", pushToTalk); + + function cleanup () { + pushToTalk.cleanup(); + disableDispatcherModule("PushToTalk"); + }; + + Script.scriptEnding.connect(cleanup); +}()); // END LOCAL_SCOPE \ No newline at end of file From 3b274c2b6e7dbe77a9e89b255ae197c36e9184b8 Mon Sep 17 00:00:00 2001 From: r3tk0n Date: Thu, 7 Mar 2019 11:31:48 -0800 Subject: [PATCH 083/446] Enable pushToTalk.js controller module. --- scripts/system/controllers/controllerDispatcher.js | 1 + scripts/system/controllers/controllerScripts.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index 28c3e2a299..f4c0cbb0d6 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -58,6 +58,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); // not set to false (not in use), a module cannot start. When a module is using a slot, that module's name // is stored as the value, rather than false. this.activitySlots = { + head: false, leftHand: false, rightHand: false, rightHandTrigger: false, diff --git a/scripts/system/controllers/controllerScripts.js b/scripts/system/controllers/controllerScripts.js index 726e075fcc..ca7d041792 100644 --- a/scripts/system/controllers/controllerScripts.js +++ b/scripts/system/controllers/controllerScripts.js @@ -33,7 +33,8 @@ var CONTOLLER_SCRIPTS = [ "controllerModules/nearGrabHyperLinkEntity.js", "controllerModules/nearTabletHighlight.js", "controllerModules/nearGrabEntity.js", - "controllerModules/farGrabEntity.js" + "controllerModules/farGrabEntity.js", + "controllerModules/pushToTalk.js" ]; var DEBUG_MENU_ITEM = "Debug defaultScripts.js"; From 88a125aff0a5dd3de6d523e5325ddedc24dff30e Mon Sep 17 00:00:00 2001 From: r3tk0n Date: Tue, 5 Mar 2019 17:09:54 -0800 Subject: [PATCH 084/446] Initial implementation (deadlocks still occurring in Audio.cpp) --- interface/resources/qml/hifi/audio/Audio.qml | 19 ++++++++ interface/resources/qml/hifi/audio/MicBar.qml | 18 ++++---- interface/src/scripting/Audio.cpp | 20 ++++++++ interface/src/scripting/Audio.h | 46 +++++++++---------- 4 files changed, 71 insertions(+), 32 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index d44a9c862e..9e9a8c0022 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -152,6 +152,25 @@ Rectangle { checked = Qt.binding(function() { return AudioScriptingInterface.noiseReduction; }); // restore binding } } + AudioControls.CheckBox { + spacing: muteMic.spacing + text: qsTr("Push To Talk"); + checked: isVR ? AudioScriptingInterface.pushToTalkHMD : AudioScriptingInterface.pushToTalkDesktop; + onClicked: { + if (isVR) { + AudioScriptingInterface.pushToTalkHMD = checked; + } else { + AudioScriptingInterface.pushToTalkDesktop = checked; + } + checked = Qt.binding(function() { + if (isVR) { + return AudioScriptingInterface.pushToTalkHMD; + } else { + return AudioScriptingInterface.pushToTalkDesktop; + } + }); // restore binding + } + } AudioControls.CheckBox { spacing: muteMic.spacing text: qsTr("Show audio level meter"); diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index 50477b82f8..6cb45eaecb 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -19,13 +19,13 @@ Rectangle { HifiConstants { id: hifi; } readonly property var level: AudioScriptingInterface.inputLevel; - + property bool gated: false; Component.onCompleted: { AudioScriptingInterface.noiseGateOpened.connect(function() { gated = false; }); AudioScriptingInterface.noiseGateClosed.connect(function() { gated = true; }); } - + property bool standalone: false; property var dragTarget: null; @@ -138,7 +138,7 @@ Rectangle { Item { id: status; - readonly property string color: AudioScriptingInterface.muted ? colors.muted : colors.unmuted; + readonly property string color: (AudioScriptingInterface.pushingToTalk && AudioScriptingInterface.muted) ? hifi.colors.blueHighlight : AudioScriptingInterface.muted ? colors.muted : colors.unmuted; visible: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) || AudioScriptingInterface.muted; @@ -235,12 +235,12 @@ Rectangle { } } } - + Rectangle { id: gatedIndicator; visible: gated && !AudioScriptingInterface.clipping - - radius: 4; + + radius: 4; width: 2 * radius; height: 2 * radius; color: "#0080FF"; @@ -249,12 +249,12 @@ Rectangle { verticalCenter: parent.verticalCenter; } } - + Rectangle { id: clippingIndicator; visible: AudioScriptingInterface.clipping - - radius: 4; + + radius: 4; width: 2 * radius; height: 2 * radius; color: colors.red; diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 63ce9d2b2e..45bb15f1a3 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -231,6 +231,26 @@ void Audio::loadData() { _pttHMD = _pttHMDSetting.get(); } +bool Audio::getPTTHMD() const { + return resultWithReadLock([&] { + return _pttHMD; + }); +} + +void Audio::saveData() { + _desktopMutedSetting.set(getMutedDesktop()); + _hmdMutedSetting.set(getMutedHMD()); + _pttDesktopSetting.set(getPTTDesktop()); + _pttHMDSetting.set(getPTTHMD()); +} + +void Audio::loadData() { + _desktopMuted = _desktopMutedSetting.get(); + _hmdMuted = _hmdMutedSetting.get(); + _pttDesktop = _pttDesktopSetting.get(); + _pttHMD = _pttHMDSetting.get(); +} + bool Audio::noiseReductionEnabled() const { return resultWithReadLock([&] { return _enableNoiseReduction; diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index 9ad4aac9c1..10aceb02fb 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -41,16 +41,16 @@ class Audio : public AudioScriptingInterface, protected ReadWriteLockable { * @hifi-assignment-client * * @property {boolean} muted - true if the audio input is muted, otherwise false. - * @property {boolean} noiseReduction - true if noise reduction is enabled, otherwise false. When - * enabled, the input audio signal is blocked (fully attenuated) when it falls below an adaptive threshold set just + * @property {boolean} noiseReduction - true if noise reduction is enabled, otherwise false. When + * enabled, the input audio signal is blocked (fully attenuated) when it falls below an adaptive threshold set just * above the noise floor. - * @property {number} inputLevel - The loudness of the audio input, range 0.0 (no sound) – + * @property {number} inputLevel - The loudness of the audio input, range 0.0 (no sound) – * 1.0 (the onset of clipping). Read-only. * @property {boolean} clipping - true if the audio input is clipping, otherwise false. - * @property {number} inputVolume - Adjusts the volume of the input audio; range 0.01.0. - * If set to a value, the resulting value depends on the input device: for example, the volume can't be changed on some + * @property {number} inputVolume - Adjusts the volume of the input audio; range 0.01.0. + * If set to a value, the resulting value depends on the input device: for example, the volume can't be changed on some * devices, and others might only support values of 0.0 and 1.0. - * @property {boolean} isStereoInput - true if the input audio is being used in stereo, otherwise + * @property {boolean} isStereoInput - true if the input audio is being used in stereo, otherwise * false. Some devices do not support stereo, in which case the value is always false. * @property {string} context - The current context of the audio: either "Desktop" or "HMD". * Read-only. @@ -115,7 +115,7 @@ public: /**jsdoc * @function Audio.setInputDevice * @param {object} device - * @param {boolean} isHMD + * @param {boolean} isHMD * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE void setInputDevice(const QAudioDeviceInfo& device, bool isHMD); @@ -129,8 +129,8 @@ public: Q_INVOKABLE void setOutputDevice(const QAudioDeviceInfo& device, bool isHMD); /**jsdoc - * Enable or disable reverberation. Reverberation is done by the client, on the post-mix audio. The reverberation options - * come from either the domain's audio zone if used — configured on the server — or as scripted by + * Enable or disable reverberation. Reverberation is done by the client, on the post-mix audio. The reverberation options + * come from either the domain's audio zone if used — configured on the server — or as scripted by * {@link Audio.setReverbOptions|setReverbOptions}. * @function Audio.setReverb * @param {boolean} enable - true to enable reverberation, false to disable. @@ -140,13 +140,13 @@ public: * var injectorOptions = { * position: MyAvatar.position * }; - * + * * Script.setTimeout(function () { * print("Reverb OFF"); * Audio.setReverb(false); * injector = Audio.playSound(sound, injectorOptions); * }, 1000); - * + * * Script.setTimeout(function () { * var reverbOptions = new AudioEffectOptions(); * reverbOptions.roomSize = 100; @@ -154,26 +154,26 @@ public: * print("Reverb ON"); * Audio.setReverb(true); * }, 4000); - * + * * Script.setTimeout(function () { * print("Reverb OFF"); * Audio.setReverb(false); * }, 8000); */ Q_INVOKABLE void setReverb(bool enable); - + /**jsdoc * Configure reverberation options. Use {@link Audio.setReverb|setReverb} to enable or disable reverberation. * @function Audio.setReverbOptions * @param {AudioEffectOptions} options - The reverberation options. */ Q_INVOKABLE void setReverbOptions(const AudioEffectOptions* options); - + /**jsdoc * Starts making an audio recording of the audio being played in-world (i.e., not local-only audio) to a file in WAV format. * @function Audio.startRecording - * @param {string} filename - The path and name of the file to make the recording in. Should have a .wav + * @param {string} filename - The path and name of the file to make the recording in. Should have a .wav * extension. The file is overwritten if it already exists. - * @returns {boolean} true if the specified file could be opened and audio recording has started, otherwise + * @returns {boolean} true if the specified file could be opened and audio recording has started, otherwise * false. * @example Make a 10 second audio recording. * var filename = File.getTempDir() + "/audio.wav"; @@ -182,13 +182,13 @@ public: * Audio.stopRecording(); * print("Audio recording made in: " + filename); * }, 10000); - * + * * } else { * print("Could not make an audio recording in: " + filename); * } */ Q_INVOKABLE bool startRecording(const QString& filename); - + /**jsdoc * Finish making an audio recording started with {@link Audio.startRecording|startRecording}. * @function Audio.stopRecording @@ -222,7 +222,7 @@ signals: * }); */ void mutedChanged(bool isMuted); - + /**jsdoc * Triggered when desktop audio input is muted or unmuted. * @function Audio.desktopMutedChanged @@ -274,9 +274,9 @@ signals: /**jsdoc * Triggered when the input audio volume changes. * @function Audio.inputVolumeChanged - * @param {number} volume - The requested volume to be applied to the audio input, range 0.0 – - * 1.0. The resulting value of Audio.inputVolume depends on the capabilities of the device: - * for example, the volume can't be changed on some devices, and others might only support values of 0.0 + * @param {number} volume - The requested volume to be applied to the audio input, range 0.0 – + * 1.0. The resulting value of Audio.inputVolume depends on the capabilities of the device: + * for example, the volume can't be changed on some devices, and others might only support values of 0.0 * and 1.0. * @returns {Signal} */ @@ -285,7 +285,7 @@ signals: /**jsdoc * Triggered when the input audio level changes. * @function Audio.inputLevelChanged - * @param {number} level - The loudness of the input audio, range 0.0 (no sound) – 1.0 (the + * @param {number} level - The loudness of the input audio, range 0.0 (no sound) – 1.0 (the * onset of clipping). * @returns {Signal} */ From cb0fdd2ef2ff91854b0c4962bb2bd12bac4aa551 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 6 Mar 2019 11:00:09 -0800 Subject: [PATCH 085/446] laying groundwork for audio app + fixing deadlocks --- interface/resources/qml/hifi/audio/MicBar.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index 6cb45eaecb..41ab0b6e91 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -138,7 +138,7 @@ Rectangle { Item { id: status; - readonly property string color: (AudioScriptingInterface.pushingToTalk && AudioScriptingInterface.muted) ? hifi.colors.blueHighlight : AudioScriptingInterface.muted ? colors.muted : colors.unmuted; + readonly property string color: AudioScriptingInterface.pushToTalk ? hifi.colors.blueHighlight : AudioScriptingInterface.muted ? colors.muted : colors.unmuted; visible: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) || AudioScriptingInterface.muted; From f4db9fefd0d81e539491492981b32a8a75a51964 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 6 Mar 2019 11:11:47 -0800 Subject: [PATCH 086/446] Push to talk changes + rebased with master (#7) Push to talk changes + rebased with master --- interface/src/scripting/Audio.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 45bb15f1a3..0a859c4dcc 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -251,6 +251,26 @@ void Audio::loadData() { _pttHMD = _pttHMDSetting.get(); } +bool Audio::getPTTHMD() const { + return resultWithReadLock([&] { + return _pttHMD; + }); +} + +void Audio::saveData() { + _desktopMutedSetting.set(getMutedDesktop()); + _hmdMutedSetting.set(getMutedHMD()); + _pttDesktopSetting.set(getPTTDesktop()); + _pttHMDSetting.set(getPTTHMD()); +} + +void Audio::loadData() { + _desktopMuted = _desktopMutedSetting.get(); + _hmdMuted = _hmdMutedSetting.get(); + _pttDesktop = _pttDesktopSetting.get(); + _pttHMD = _pttHMDSetting.get(); +} + bool Audio::noiseReductionEnabled() const { return resultWithReadLock([&] { return _enableNoiseReduction; From 39db3979b9eb8e46b71623752ff4785fcb6f93dc Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 6 Mar 2019 11:18:57 -0800 Subject: [PATCH 087/446] changing text display --- interface/resources/qml/hifi/audio/MicBar.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index 41ab0b6e91..491b9f9554 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -138,7 +138,7 @@ Rectangle { Item { id: status; - readonly property string color: AudioScriptingInterface.pushToTalk ? hifi.colors.blueHighlight : AudioScriptingInterface.muted ? colors.muted : colors.unmuted; + readonly property string color: AudioScriptingInterface.muted ? colors.muted : colors.unmuted; visible: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) || AudioScriptingInterface.muted; From 88d8163b04e8b740c1433530a8821f94dae7ae3b Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 6 Mar 2019 17:29:32 -0800 Subject: [PATCH 088/446] updating compile failure + icons/settings update --- interface/resources/qml/hifi/audio/Audio.qml | 60 +++++++++++++++++--- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index 9e9a8c0022..569cd23176 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -154,7 +154,23 @@ Rectangle { } AudioControls.CheckBox { spacing: muteMic.spacing - text: qsTr("Push To Talk"); + text: qsTr("Show audio level meter"); + checked: AvatarInputs.showAudioTools; + onClicked: { + AvatarInputs.showAudioTools = checked; + checked = Qt.binding(function() { return AvatarInputs.showAudioTools; }); // restore binding + } + onXChanged: rightMostInputLevelPos = x + width + } + } + + Separator {} + + ColumnLayout { + spacing: muteMic.spacing; + AudioControls.CheckBox { + spacing: muteMic.spacing + text: qsTr("Push To Talk (T)"); checked: isVR ? AudioScriptingInterface.pushToTalkHMD : AudioScriptingInterface.pushToTalkDesktop; onClicked: { if (isVR) { @@ -171,15 +187,41 @@ Rectangle { }); // restore binding } } - AudioControls.CheckBox { - spacing: muteMic.spacing - text: qsTr("Show audio level meter"); - checked: AvatarInputs.showAudioTools; - onClicked: { - AvatarInputs.showAudioTools = checked; - checked = Qt.binding(function() { return AvatarInputs.showAudioTools; }); // restore binding + Item { + id: pttTextContainer + x: margins.paddings; + width: rightMostInputLevelPos + height: pttTextMetrics.height + visible: true + TextMetrics { + id: pttTextMetrics + text: pttText.text + font: pttText.font + } + RalewayRegular { + id: pttText + wrapMode: Text.WordWrap + color: hifi.colors.white; + width: parent.width; + font.italic: true + size: 16; + text: isVR ? qsTr("Press and hold grip triggers on both of your controllers to unmute.") : + qsTr("Press and hold the button \"T\" to unmute."); + onTextChanged: { + if (pttTextMetrics.width > rightMostInputLevelPos) { + pttTextContainer.height = Math.ceil(pttTextMetrics.width / rightMostInputLevelPos) * pttTextMetrics.height; + } else { + pttTextContainer.height = pttTextMetrics.height; + } + } + } + Component.onCompleted: { + if (pttTextMetrics.width > rightMostInputLevelPos) { + pttTextContainer.height = Math.ceil(pttTextMetrics.width / rightMostInputLevelPos) * pttTextMetrics.height; + } else { + pttTextContainer.height = pttTextMetrics.height; + } } - onXChanged: rightMostInputLevelPos = x + width } } From 24d6646e8d23f6aab6b441a7b097e17bd41ddbb4 Mon Sep 17 00:00:00 2001 From: r3tk0n Date: Thu, 7 Mar 2019 11:45:59 -0800 Subject: [PATCH 089/446] Fix activation / deactivation criteria for PTT controller module. --- scripts/system/controllers/controllerModules/pushToTalk.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/system/controllers/controllerModules/pushToTalk.js b/scripts/system/controllers/controllerModules/pushToTalk.js index e764b228c9..557476ccd7 100644 --- a/scripts/system/controllers/controllerModules/pushToTalk.js +++ b/scripts/system/controllers/controllerModules/pushToTalk.js @@ -25,12 +25,12 @@ Script.include("/~/system/libraries/controllers.js"); this.shouldTalk = function (controllerData) { // Set up test against controllerData here... - var gripVal = controllerData.secondaryValues[this.hand]; + var gripVal = controllerData.secondaryValues[LEFT_HAND] && controllerData.secondaryValues[RIGHT_HAND]; return (gripVal) ? true : false; }; this.shouldStopTalking = function (controllerData) { - var gripVal = controllerData.secondaryValues[this.hand]; + var gripVal = controllerData.secondaryValues[LEFT_HAND] && controllerData.secondaryValues[RIGHT_HAND]; return (gripVal) ? false : true; }; From 5f48a6d1044fd11cab6110db39eca367448ab0e4 Mon Sep 17 00:00:00 2001 From: r3tk0n Date: Thu, 7 Mar 2019 12:35:04 -0800 Subject: [PATCH 090/446] Fix typo. --- scripts/system/controllers/controllerModules/pushToTalk.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/controllerModules/pushToTalk.js b/scripts/system/controllers/controllerModules/pushToTalk.js index 557476ccd7..dd959ae6fb 100644 --- a/scripts/system/controllers/controllerModules/pushToTalk.js +++ b/scripts/system/controllers/controllerModules/pushToTalk.js @@ -35,7 +35,7 @@ Script.include("/~/system/libraries/controllers.js"); }; this.isReady = function (controllerData, deltaTime) { - if (HMD.active() && Audio.pushToTalk && this.shouldTalk(controllerData)) { + if (HMD.active && Audio.pushToTalk && this.shouldTalk(controllerData)) { Audio.pushingToTalk = true; returnMakeRunningValues(true, [], []); } From 18b86d550de3f71290b354e1e4ff602ba4dd03ff Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Thu, 7 Mar 2019 12:36:56 -0800 Subject: [PATCH 091/446] adding PushToTalk action --- interface/src/Application.cpp | 11 +++++++++++ libraries/controllers/src/controllers/Actions.cpp | 4 ++++ libraries/controllers/src/controllers/Actions.h | 1 + .../controllers/controllerModules/pushToTalk.js | 2 +- 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index dad86e748e..c0cacd4e40 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1601,12 +1601,23 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(userInputMapper.data(), &UserInputMapper::actionEvent, [this](int action, float state) { using namespace controller; auto tabletScriptingInterface = DependencyManager::get(); + auto audioScriptingInterface = reinterpret_cast(DependencyManager::get().data()); { auto actionEnum = static_cast(action); int key = Qt::Key_unknown; static int lastKey = Qt::Key_unknown; bool navAxis = false; switch (actionEnum) { + case Action::TOGGLE_PUSHTOTALK: + if (audioScriptingInterface->getPTT()) { + qDebug() << "State is " << state; + if (state > 0.0f) { + audioScriptingInterface->setPushingToTalk(false); + } else if (state < 0.0f) { + audioScriptingInterface->setPushingToTalk(true); + } + } + case Action::UI_NAV_VERTICAL: navAxis = true; if (state > 0.0f) { diff --git a/libraries/controllers/src/controllers/Actions.cpp b/libraries/controllers/src/controllers/Actions.cpp index 5a396231b6..57be2f788b 100644 --- a/libraries/controllers/src/controllers/Actions.cpp +++ b/libraries/controllers/src/controllers/Actions.cpp @@ -180,6 +180,7 @@ namespace controller { * third person, to full screen mirror, then back to first person and repeat. * ContextMenunumbernumberShow / hide the tablet. * ToggleMutenumbernumberToggle the microphone mute. + * TogglePushToTalknumbernumberToggle push to talk. * ToggleOverlaynumbernumberToggle the display of overlays. * SprintnumbernumberSet avatar sprint mode. * ReticleClicknumbernumberSet mouse-pressed. @@ -245,6 +246,8 @@ namespace controller { * ContextMenu instead. * TOGGLE_MUTEnumbernumberDeprecated: Use * ToggleMute instead. + * TOGGLE_PUSHTOTALKnumbernumberDeprecated: Use + * TogglePushToTalk instead. * SPRINTnumbernumberDeprecated: Use * Sprint instead. * LONGITUDINAL_BACKWARDnumbernumberDeprecated: Use @@ -411,6 +414,7 @@ namespace controller { makeButtonPair(Action::ACTION2, "SecondaryAction"), makeButtonPair(Action::CONTEXT_MENU, "ContextMenu"), makeButtonPair(Action::TOGGLE_MUTE, "ToggleMute"), + makeButtonPair(Action::TOGGLE_PUSHTOTALK, "TogglePushToTalk"), makeButtonPair(Action::CYCLE_CAMERA, "CycleCamera"), makeButtonPair(Action::TOGGLE_OVERLAY, "ToggleOverlay"), makeButtonPair(Action::SPRINT, "Sprint"), diff --git a/libraries/controllers/src/controllers/Actions.h b/libraries/controllers/src/controllers/Actions.h index a12a3d60a9..3e99d8d147 100644 --- a/libraries/controllers/src/controllers/Actions.h +++ b/libraries/controllers/src/controllers/Actions.h @@ -60,6 +60,7 @@ enum class Action { CONTEXT_MENU, TOGGLE_MUTE, + TOGGLE_PUSHTOTALK, CYCLE_CAMERA, TOGGLE_OVERLAY, diff --git a/scripts/system/controllers/controllerModules/pushToTalk.js b/scripts/system/controllers/controllerModules/pushToTalk.js index dd959ae6fb..9d6435f497 100644 --- a/scripts/system/controllers/controllerModules/pushToTalk.js +++ b/scripts/system/controllers/controllerModules/pushToTalk.js @@ -72,4 +72,4 @@ Script.include("/~/system/libraries/controllers.js"); }; Script.scriptEnding.connect(cleanup); -}()); // END LOCAL_SCOPE \ No newline at end of file +}()); // END LOCAL_SCOPE From 48d1ec850c64e16822ce495cf55d0f1c8a9b60cf Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Thu, 7 Mar 2019 12:54:32 -0800 Subject: [PATCH 092/446] removing debug statement --- interface/src/Application.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index c0cacd4e40..fc9fcd1bbb 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1610,7 +1610,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo switch (actionEnum) { case Action::TOGGLE_PUSHTOTALK: if (audioScriptingInterface->getPTT()) { - qDebug() << "State is " << state; if (state > 0.0f) { audioScriptingInterface->setPushingToTalk(false); } else if (state < 0.0f) { From e0fe11056e8e49e529eddc12930b50f737d55966 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Fri, 8 Mar 2019 16:37:41 -0800 Subject: [PATCH 093/446] fixing compile error --- interface/src/scripting/Audio.h | 40 --------------------------------- 1 file changed, 40 deletions(-) diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index ce857211e0..10aceb02fb 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -263,46 +263,6 @@ signals: */ void pushToTalkHMDChanged(bool enabled); - /**jsdoc - * Triggered when desktop audio input is muted or unmuted. - * @function Audio.desktopMutedChanged - * @param {boolean} isMuted - true if the audio input is muted for desktop mode, otherwise false. - * @returns {Signal} - */ - void desktopMutedChanged(bool isMuted); - - /**jsdoc - * Triggered when HMD audio input is muted or unmuted. - * @function Audio.hmdMutedChanged - * @param {boolean} isMuted - true if the audio input is muted for HMD mode, otherwise false. - * @returns {Signal} - */ - void hmdMutedChanged(bool isMuted); - - /** - * Triggered when Push-to-Talk has been enabled or disabled. - * @function Audio.pushToTalkChanged - * @param {boolean} enabled - true if Push-to-Talk is enabled, otherwise false. - * @returns {Signal} - */ - void pushToTalkChanged(bool enabled); - - /** - * Triggered when Push-to-Talk has been enabled or disabled for desktop mode. - * @function Audio.pushToTalkDesktopChanged - * @param {boolean} enabled - true if Push-to-Talk is emabled for Desktop mode, otherwise false. - * @returns {Signal} - */ - void pushToTalkDesktopChanged(bool enabled); - - /** - * Triggered when Push-to-Talk has been enabled or disabled for HMD mode. - * @function Audio.pushToTalkHMDChanged - * @param {boolean} enabled - true if Push-to-Talk is emabled for HMD mode, otherwise false. - * @returns {Signal} - */ - void pushToTalkHMDChanged(bool enabled); - /**jsdoc * Triggered when the audio input noise reduction is enabled or disabled. * @function Audio.noiseReductionChanged From 687745dc7e3f3aa44a69db36066840ff0b11ad8f Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 8 Mar 2019 17:17:10 -0800 Subject: [PATCH 094/446] Remove old recursive scripts that are empty. Don't add unneeded delays. --- tools/nitpick/src/TestCreator.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tools/nitpick/src/TestCreator.cpp b/tools/nitpick/src/TestCreator.cpp index f87134ce5b..587490bb64 100644 --- a/tools/nitpick/src/TestCreator.cpp +++ b/tools/nitpick/src/TestCreator.cpp @@ -824,6 +824,10 @@ void TestCreator::createRecursiveScript(const QString& directory, bool interacti // If 'directories' is empty, this means that this recursive script has no tests to call, so it is redundant if (directories.length() == 0) { + QString testRecursivePathname = directory + "/" + TEST_RECURSIVE_FILENAME; + if (QFile::exists(testRecursivePathname)) { + QFile::remove(testRecursivePathname); + } return; } @@ -856,10 +860,7 @@ void TestCreator::createRecursiveScript(const QString& directory, bool interacti textStream << " nitpick = createNitpick(Script.resolvePath(\".\"));" << endl; textStream << " testsRootPath = nitpick.getTestsRootPath();" << endl << endl; textStream << " nitpick.enableRecursive();" << endl; - textStream << " nitpick.enableAuto();" << endl << endl; - textStream << " if (typeof Test !== 'undefined') {" << endl; - textStream << " Test.wait(10000);" << endl; - textStream << " }" << endl; + textStream << " nitpick.enableAuto();" << endl; textStream << "} else {" << endl; textStream << " depth++" << endl; textStream << "}" << endl << endl; From 74320fe7be5c1ed0a8f9bc2160d1dceb3e523523 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Fri, 8 Mar 2019 17:24:43 -0800 Subject: [PATCH 095/446] Case 20622 - Qml Marketplace - Fix categories dropdown UI issues --- .../hifi/commerce/marketplace/Marketplace.qml | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index 07ded49956..6f8150028a 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -382,6 +382,7 @@ Rectangle { id: categoriesDropdown anchors.fill: parent; + anchors.topMargin: 2 visible: false z: 10 @@ -420,6 +421,7 @@ Rectangle { model: categoriesModel delegate: ItemDelegate { + id: categoriesItemDelegate height: 34 width: parent.width @@ -431,6 +433,8 @@ Rectangle { color: hifi.colors.white visible: true + border.color: hifi.colors.blueHighlight + border.width: 0 RalewayRegular { id: categoriesItemText @@ -439,7 +443,7 @@ Rectangle { anchors.fill:parent text: model.name - color: ListView.isCurrentItem ? hifi.colors.lightBlueHighlight : hifi.colors.baseGray + color: categoriesItemDelegate.ListView.isCurrentItem ? hifi.colors.blueHighlight : hifi.colors.baseGray horizontalAlignment: Text.AlignLeft verticalAlignment: Text.AlignVCenter size: 14 @@ -449,16 +453,22 @@ Rectangle { MouseArea { anchors.fill: parent z: 10 - hoverEnabled: true propagateComposedEvents: false - onEntered: { - categoriesItem.color = ListView.isCurrentItem ? hifi.colors.white : hifi.colors.lightBlueHighlight; + onPositionChanged: { + // Must use onPositionChanged and not onEntered + // due to a QML bug where a mouseenter event was + // being fired on open of the categories list even + // though the mouse was outside the borders + categoriesItem.border.width = 2; + } + onExited: { + categoriesItem.border.width = 0; } - onExited: { - categoriesItem.color = ListView.isCurrentItem ? hifi.colors.lightBlueHighlight : hifi.colors.white; + onCanceled: { + categoriesItem.border.width = 0; } onClicked: { From 3464fe09c1b0ea2b3671941e09bbe91dbe6f36bf Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Fri, 8 Mar 2019 17:39:11 -0800 Subject: [PATCH 096/446] Applying the hero changes to master soon to be rc81 --- .../src/avatars/AvatarMixerClientData.cpp | 4 + assignment-client/src/avatars/MixerAvatar.h | 3 - .../qml/+android_interface/Stats.qml | 4 + interface/resources/qml/Stats.qml | 4 + interface/src/avatar/AvatarManager.cpp | 183 +++++++++++------- interface/src/avatar/AvatarManager.h | 4 + interface/src/avatar/OtherAvatar.cpp | 12 -- interface/src/ui/Stats.cpp | 2 + interface/src/ui/Stats.h | 18 ++ libraries/avatars/src/AvatarData.cpp | 13 +- libraries/avatars/src/AvatarData.h | 18 +- 11 files changed, 179 insertions(+), 86 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index cef4383aee..557c5c9fe3 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -130,12 +130,16 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared } _lastReceivedSequenceNumber = sequenceNumber; glm::vec3 oldPosition = getPosition(); + bool oldHasPriority = _avatar->getHasPriority(); // compute the offset to the data payload if (!_avatar->parseDataFromBuffer(message.readWithoutCopy(message.getBytesLeftToRead()))) { return false; } + // Regardless of what the client says, restore the priority as we know it without triggering any update. + _avatar->setHasPriorityWithoutTimestampReset(oldHasPriority); + auto newPosition = getPosition(); if (newPosition != oldPosition) { //#define AVATAR_HERO_TEST_HACK diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h index 4c3ded4582..3e80704495 100644 --- a/assignment-client/src/avatars/MixerAvatar.h +++ b/assignment-client/src/avatars/MixerAvatar.h @@ -19,11 +19,8 @@ class MixerAvatar : public AvatarData { public: - bool getHasPriority() const { return _hasPriority; } - void setHasPriority(bool hasPriority) { _hasPriority = hasPriority; } private: - bool _hasPriority { false }; }; using MixerAvatarSharedPointer = std::shared_ptr; diff --git a/interface/resources/qml/+android_interface/Stats.qml b/interface/resources/qml/+android_interface/Stats.qml index fe56f3797b..54f6086a86 100644 --- a/interface/resources/qml/+android_interface/Stats.qml +++ b/interface/resources/qml/+android_interface/Stats.qml @@ -113,6 +113,10 @@ Item { visible: root.expanded text: "Avatars Updated: " + root.updatedAvatarCount } + StatText { + visible: root.expanded + text: "Heroes Count/Updated: " + root.heroAvatarCount + "/" + root.updatedHeroAvatarCount + } StatText { visible: root.expanded text: "Avatars NOT Updated: " + root.notUpdatedAvatarCount diff --git a/interface/resources/qml/Stats.qml b/interface/resources/qml/Stats.qml index 3b703d72e6..6748418d19 100644 --- a/interface/resources/qml/Stats.qml +++ b/interface/resources/qml/Stats.qml @@ -115,6 +115,10 @@ Item { visible: root.expanded text: "Avatars Updated: " + root.updatedAvatarCount } + StatText { + visible: root.expanded + text: "Heroes Count/Updated: " + root.heroAvatarCount + "/" + root.updatedHeroAvatarCount + } StatText { visible: root.expanded text: "Avatars NOT Updated: " + root.notUpdatedAvatarCount diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 55025b3b23..c66c0a30cb 100755 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -232,96 +232,142 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { auto avatarMap = getHashCopy(); const auto& views = qApp->getConicalViews(); - PrioritySortUtil::PriorityQueue sortedAvatars(views, - AvatarData::_avatarSortCoefficientSize, - AvatarData::_avatarSortCoefficientCenter, - AvatarData::_avatarSortCoefficientAge); - sortedAvatars.reserve(avatarMap.size() - 1); // don't include MyAvatar + // Prepare 2 queues for heros and for crowd avatars + using AvatarPriorityQueue = PrioritySortUtil::PriorityQueue; + // Keep two independent queues, one for heroes and one for the riff-raff. + enum PriorityVariants + { + kHero = 0, + kNonHero, + NumVariants + }; + AvatarPriorityQueue avatarPriorityQueues[NumVariants] = { + { views, + AvatarData::_avatarSortCoefficientSize, + AvatarData::_avatarSortCoefficientCenter, + AvatarData::_avatarSortCoefficientAge }, + { views, + AvatarData::_avatarSortCoefficientSize, + AvatarData::_avatarSortCoefficientCenter, + AvatarData::_avatarSortCoefficientAge } }; + // Reserve space + //avatarPriorityQueues[kHero].reserve(10); // just few + avatarPriorityQueues[kNonHero].reserve(avatarMap.size() - 1); // don't include MyAvatar // Build vector and compute priorities auto nodeList = DependencyManager::get(); AvatarHash::iterator itr = avatarMap.begin(); while (itr != avatarMap.end()) { - const auto& avatar = std::static_pointer_cast(*itr); + auto avatar = std::static_pointer_cast(*itr); // DO NOT update _myAvatar! Its update has already been done earlier in the main loop. // DO NOT update or fade out uninitialized Avatars if (avatar != _myAvatar && avatar->isInitialized() && !nodeList->isPersonalMutingNode(avatar->getID())) { - sortedAvatars.push(SortableAvatar(avatar)); + if (avatar->getHasPriority()) { + avatarPriorityQueues[kHero].push(SortableAvatar(avatar)); + } else { + avatarPriorityQueues[kNonHero].push(SortableAvatar(avatar)); + } } ++itr; } - // Sort - const auto& sortedAvatarVector = sortedAvatars.getSortedVector(); + + _numHeroAvatars = (int)avatarPriorityQueues[kHero].size(); // process in sorted order uint64_t startTime = usecTimestampNow(); - uint64_t updateExpiry = startTime + MAX_UPDATE_AVATARS_TIME_BUDGET; + + const uint64_t MAX_UPDATE_HEROS_TIME_BUDGET = uint64_t(0.8 * MAX_UPDATE_AVATARS_TIME_BUDGET); + + uint64_t updatePriorityExpiries[NumVariants] = { startTime + MAX_UPDATE_HEROS_TIME_BUDGET, startTime + MAX_UPDATE_AVATARS_TIME_BUDGET }; + int numHerosUpdated = 0; int numAvatarsUpdated = 0; - int numAVatarsNotUpdated = 0; + int numAvatarsNotUpdated = 0; render::Transaction renderTransaction; workload::Transaction workloadTransaction; - for (auto it = sortedAvatarVector.begin(); it != sortedAvatarVector.end(); ++it) { - const SortableAvatar& sortData = *it; - const auto avatar = std::static_pointer_cast(sortData.getAvatar()); - if (!avatar->_isClientAvatar) { - avatar->setIsClientAvatar(true); - } - // TODO: to help us scale to more avatars it would be nice to not have to poll this stuff every update - if (avatar->getSkeletonModel()->isLoaded()) { - // remove the orb if it is there - avatar->removeOrb(); - if (avatar->needsPhysicsUpdate()) { - _avatarsToChangeInPhysics.insert(avatar); - } - } else { - avatar->updateOrbPosition(); - } + + for (int p = kHero; p < NumVariants; p++) { + auto& priorityQueue = avatarPriorityQueues[p]; + // Sorting the current queue HERE as part of the measured timing. + const auto& sortedAvatarVector = priorityQueue.getSortedVector(); - // for ALL avatars... - if (_shouldRender) { - avatar->ensureInScene(avatar, qApp->getMain3DScene()); - } - avatar->animateScaleChanges(deltaTime); + auto passExpiry = updatePriorityExpiries[p]; - uint64_t now = usecTimestampNow(); - if (now < updateExpiry) { - // we're within budget - bool inView = sortData.getPriority() > OUT_OF_VIEW_THRESHOLD; - if (inView && avatar->hasNewJointData()) { - numAvatarsUpdated++; + for (auto it = sortedAvatarVector.begin(); it != sortedAvatarVector.end(); ++it) { + const SortableAvatar& sortData = *it; + const auto avatar = std::static_pointer_cast(sortData.getAvatar()); + if (!avatar->_isClientAvatar) { + avatar->setIsClientAvatar(true); } - auto transitStatus = avatar->_transit.update(deltaTime, avatar->_serverPosition, _transitConfig); - if (avatar->getIsNewAvatar() && (transitStatus == AvatarTransit::Status::START_TRANSIT || transitStatus == AvatarTransit::Status::ABORT_TRANSIT)) { - avatar->_transit.reset(); - avatar->setIsNewAvatar(false); - } - avatar->simulate(deltaTime, inView); - if (avatar->getSkeletonModel()->isLoaded() && avatar->getWorkloadRegion() == workload::Region::R1) { - _myAvatar->addAvatarHandsToFlow(avatar); - } - avatar->updateRenderItem(renderTransaction); - avatar->updateSpaceProxy(workloadTransaction); - avatar->setLastRenderUpdateTime(startTime); - } else { - // we've spent our full time budget --> bail on the rest of the avatar updates - // --> more avatars may freeze until their priority trickles up - // --> some scale animations may glitch - // --> some avatar velocity measurements may be a little off - - // no time to simulate, but we take the time to count how many were tragically missed - while (it != sortedAvatarVector.end()) { - const SortableAvatar& newSortData = *it; - const auto& newAvatar = newSortData.getAvatar(); - bool inView = newSortData.getPriority() > OUT_OF_VIEW_THRESHOLD; - // Once we reach an avatar that's not in view, all avatars after it will also be out of view - if (!inView) { - break; + // TODO: to help us scale to more avatars it would be nice to not have to poll this stuff every update + if (avatar->getSkeletonModel()->isLoaded()) { + // remove the orb if it is there + avatar->removeOrb(); + if (avatar->needsPhysicsUpdate()) { + _avatarsToChangeInPhysics.insert(avatar); } - numAVatarsNotUpdated += (int)(newAvatar->hasNewJointData()); - ++it; + } else { + avatar->updateOrbPosition(); } - break; + + // for ALL avatars... + if (_shouldRender) { + avatar->ensureInScene(avatar, qApp->getMain3DScene()); + } + + avatar->animateScaleChanges(deltaTime); + + uint64_t now = usecTimestampNow(); + if (now < passExpiry) { + // we're within budget + bool inView = sortData.getPriority() > OUT_OF_VIEW_THRESHOLD; + if (inView && avatar->hasNewJointData()) { + numAvatarsUpdated++; + } + auto transitStatus = avatar->_transit.update(deltaTime, avatar->_serverPosition, _transitConfig); + if (avatar->getIsNewAvatar() && (transitStatus == AvatarTransit::Status::START_TRANSIT || + transitStatus == AvatarTransit::Status::ABORT_TRANSIT)) { + avatar->_transit.reset(); + avatar->setIsNewAvatar(false); + } + avatar->simulate(deltaTime, inView); + if (avatar->getSkeletonModel()->isLoaded() && avatar->getWorkloadRegion() == workload::Region::R1) { + _myAvatar->addAvatarHandsToFlow(avatar); + } + avatar->updateRenderItem(renderTransaction); + avatar->updateSpaceProxy(workloadTransaction); + avatar->setLastRenderUpdateTime(startTime); + } else { + // we've spent our time budget for this priority bucket + // let's deal with the reminding avatars if this pass and BREAK from the for loop + + if (p == kHero) { + // Hero, + // --> put them back in the non hero queue + + auto& crowdQueue = avatarPriorityQueues[kNonHero]; + while (it != sortedAvatarVector.end()) { + crowdQueue.push(SortableAvatar((*it).getAvatar())); + ++it; + } + } else { + // Non Hero + // --> bail on the rest of the avatar updates + // --> more avatars may freeze until their priority trickles up + // --> some scale animations may glitch + // --> some avatar velocity measurements may be a little off + + // no time to simulate, but we take the time to count how many were tragically missed + numAvatarsNotUpdated = sortedAvatarVector.end() - it; + } + + // We had to cut short this pass, we must break out of the for loop here + break; + } + } + + if (p == kHero) { + numHerosUpdated = numAvatarsUpdated; } } @@ -337,7 +383,8 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { _space->enqueueTransaction(workloadTransaction); _numAvatarsUpdated = numAvatarsUpdated; - _numAvatarsNotUpdated = numAVatarsNotUpdated; + _numAvatarsNotUpdated = numAvatarsNotUpdated; + _numHeroAvatarsUpdated = numHerosUpdated; simulateAvatarFades(deltaTime); diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 51352ec861..2b58b14d11 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -90,6 +90,8 @@ public: int getNumAvatarsUpdated() const { return _numAvatarsUpdated; } int getNumAvatarsNotUpdated() const { return _numAvatarsNotUpdated; } + int getNumHeroAvatars() const { return _numHeroAvatars; } + int getNumHeroAvatarsUpdated() const { return _numHeroAvatarsUpdated; } float getAvatarSimulationTime() const { return _avatarSimulationTime; } void updateMyAvatar(float deltaTime); @@ -242,6 +244,8 @@ private: RateCounter<> _myAvatarSendRate; int _numAvatarsUpdated { 0 }; int _numAvatarsNotUpdated { 0 }; + int _numHeroAvatars{ 0 }; + int _numHeroAvatarsUpdated{ 0 }; float _avatarSimulationTime { 0.0f }; bool _shouldRender { true }; bool _myAvatarDataPacketsPaused { false }; diff --git a/interface/src/avatar/OtherAvatar.cpp b/interface/src/avatar/OtherAvatar.cpp index 7848c46eee..11eb6542c4 100755 --- a/interface/src/avatar/OtherAvatar.cpp +++ b/interface/src/avatar/OtherAvatar.cpp @@ -200,17 +200,6 @@ void OtherAvatar::resetDetailedMotionStates() { void OtherAvatar::setWorkloadRegion(uint8_t region) { _workloadRegion = region; - QString printRegion = ""; - if (region == workload::Region::R1) { - printRegion = "R1"; - } else if (region == workload::Region::R2) { - printRegion = "R2"; - } else if (region == workload::Region::R3) { - printRegion = "R3"; - } else { - printRegion = "invalid"; - } - qCDebug(avatars) << "Setting workload region to " << printRegion; computeShapeLOD(); } @@ -235,7 +224,6 @@ void OtherAvatar::computeShapeLOD() { if (newLOD != _bodyLOD) { _bodyLOD = newLOD; if (isInPhysicsSimulation()) { - qCDebug(avatars) << "Changing to body LOD " << newLOD; _needsReinsertion = true; } } diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index e3697ee8ec..ecdae0b375 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -125,8 +125,10 @@ void Stats::updateStats(bool force) { auto avatarManager = DependencyManager::get(); // we need to take one avatar out so we don't include ourselves STAT_UPDATE(avatarCount, avatarManager->size() - 1); + STAT_UPDATE(heroAvatarCount, avatarManager->getNumHeroAvatars()); STAT_UPDATE(physicsObjectCount, qApp->getNumCollisionObjects()); STAT_UPDATE(updatedAvatarCount, avatarManager->getNumAvatarsUpdated()); + STAT_UPDATE(updatedHeroAvatarCount, avatarManager->getNumHeroAvatarsUpdated()); STAT_UPDATE(notUpdatedAvatarCount, avatarManager->getNumAvatarsNotUpdated()); STAT_UPDATE(serverCount, (int)nodeList->size()); STAT_UPDATE_FLOAT(renderrate, qApp->getRenderLoopRate(), 0.1f); diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index 36e92b00af..0f563a6935 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -49,8 +49,10 @@ private: \ * @property {number} presentdroprate - Read-only. * @property {number} gameLoopRate - Read-only. * @property {number} avatarCount - Read-only. + * @property {number} heroAvatarCount - Read-only. * @property {number} physicsObjectCount - Read-only. * @property {number} updatedAvatarCount - Read-only. + * @property {number} updatedHeroAvatarCount - Read-only. * @property {number} notUpdatedAvatarCount - Read-only. * @property {number} packetInCount - Read-only. * @property {number} packetOutCount - Read-only. @@ -203,8 +205,10 @@ class Stats : public QQuickItem { STATS_PROPERTY(float, presentdroprate, 0) STATS_PROPERTY(int, gameLoopRate, 0) STATS_PROPERTY(int, avatarCount, 0) + STATS_PROPERTY(int, heroAvatarCount, 0) STATS_PROPERTY(int, physicsObjectCount, 0) STATS_PROPERTY(int, updatedAvatarCount, 0) + STATS_PROPERTY(int, updatedHeroAvatarCount, 0) STATS_PROPERTY(int, notUpdatedAvatarCount, 0) STATS_PROPERTY(int, packetInCount, 0) STATS_PROPERTY(int, packetOutCount, 0) @@ -436,6 +440,13 @@ signals: */ void avatarCountChanged(); + /**jsdoc + * Triggered when the value of the heroAvatarCount property changes. + * @function Stats.heroAvatarCountChanged + * @returns {Signal} + */ + void heroAvatarCountChanged(); + /**jsdoc * Triggered when the value of the updatedAvatarCount property changes. * @function Stats.updatedAvatarCountChanged @@ -443,6 +454,13 @@ signals: */ void updatedAvatarCountChanged(); + /**jsdoc + * Triggered when the value of the updatedHeroAvatarCount property changes. + * @function Stats.updatedHeroAvatarCountChanged + * @returns {Signal} + */ + void updatedHeroAvatarCountChanged(); + /**jsdoc * Triggered when the value of the notUpdatedAvatarCount property changes. * @function Stats.notUpdatedAvatarCountChanged diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index c733cfa291..26407c3564 100755 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -564,6 +564,11 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent setAtBit16(flags, COLLIDE_WITH_OTHER_AVATARS); } + // Avatar has hero priority + if (getHasPriority()) { + setAtBit16(flags, HAS_HERO_PRIORITY); + } + data->flags = flags; destinationBuffer += sizeof(AvatarDataPacket::AdditionalFlags); @@ -1152,7 +1157,8 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { auto newHasProceduralEyeFaceMovement = oneAtBit16(bitItems, PROCEDURAL_EYE_FACE_MOVEMENT); auto newHasProceduralBlinkFaceMovement = oneAtBit16(bitItems, PROCEDURAL_BLINK_FACE_MOVEMENT); auto newCollideWithOtherAvatars = oneAtBit16(bitItems, COLLIDE_WITH_OTHER_AVATARS); - + auto newHasPriority = oneAtBit16(bitItems, HAS_HERO_PRIORITY); + bool keyStateChanged = (_keyState != newKeyState); bool handStateChanged = (_handState != newHandState); bool faceStateChanged = (_headData->_isFaceTrackerConnected != newFaceTrackerConnected); @@ -1161,8 +1167,10 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { bool proceduralEyeFaceMovementChanged = (_headData->getHasProceduralEyeFaceMovement() != newHasProceduralEyeFaceMovement); bool proceduralBlinkFaceMovementChanged = (_headData->getHasProceduralBlinkFaceMovement() != newHasProceduralBlinkFaceMovement); bool collideWithOtherAvatarsChanged = (_collideWithOtherAvatars != newCollideWithOtherAvatars); + bool hasPriorityChanged = (getHasPriority() != newHasPriority); bool somethingChanged = keyStateChanged || handStateChanged || faceStateChanged || eyeStateChanged || audioEnableFaceMovementChanged || - proceduralEyeFaceMovementChanged || proceduralBlinkFaceMovementChanged || collideWithOtherAvatarsChanged; + proceduralEyeFaceMovementChanged || + proceduralBlinkFaceMovementChanged || collideWithOtherAvatarsChanged || hasPriorityChanged; _keyState = newKeyState; _handState = newHandState; @@ -1172,6 +1180,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { _headData->setHasProceduralEyeFaceMovement(newHasProceduralEyeFaceMovement); _headData->setHasProceduralBlinkFaceMovement(newHasProceduralBlinkFaceMovement); _collideWithOtherAvatars = newCollideWithOtherAvatars; + setHasPriorityWithoutTimestampReset(newHasPriority); sourceBuffer += sizeof(AvatarDataPacket::AdditionalFlags); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 63396a59ac..95bbcbeb16 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -100,6 +100,9 @@ const quint32 AVATAR_MOTION_SCRIPTABLE_BITS = // Procedural audio to mouth movement is enabled 8th bit // Procedural Blink is enabled 9th bit // Procedural Eyelid is enabled 10th bit +// Procedural PROCEDURAL_BLINK_FACE_MOVEMENT is enabled 11th bit +// Procedural Collide with other avatars is enabled 12th bit +// Procedural Has Hero Priority is enabled 13th bit const int KEY_STATE_START_BIT = 0; // 1st and 2nd bits const int HAND_STATE_START_BIT = 2; // 3rd and 4th bits @@ -111,7 +114,7 @@ const int AUDIO_ENABLED_FACE_MOVEMENT = 8; // 9th bit const int PROCEDURAL_EYE_FACE_MOVEMENT = 9; // 10th bit const int PROCEDURAL_BLINK_FACE_MOVEMENT = 10; // 11th bit const int COLLIDE_WITH_OTHER_AVATARS = 11; // 12th bit - +const int HAS_HERO_PRIORITY = 12; // 13th bit (be scared) const char HAND_STATE_NULL = 0; const char LEFT_HAND_POINTING_FLAG = 1; @@ -1121,6 +1124,18 @@ public: int getAverageBytesReceivedPerSecond() const; int getReceiveRate() const; + // An Avatar can be set Priority from the AvatarMixer side. + bool getHasPriority() const { return _hasPriority; } + // regular setHasPriority does a check of state changed and if true reset 'additionalFlagsChanged' timestamp + void setHasPriority(bool hasPriority) { + if (_hasPriority != hasPriority) { + _additionalFlagsChanged = usecTimestampNow(); + _hasPriority = hasPriority; + } + } + // In some cases, we want to assign the hasPRiority flag without reseting timestamp + void setHasPriorityWithoutTimestampReset(bool hasPriority) { _hasPriority = hasPriority; } + const glm::vec3& getTargetVelocity() const { return _targetVelocity; } void clearRecordingBasis(); @@ -1498,6 +1513,7 @@ protected: bool _isNewAvatar { true }; bool _isClientAvatar { false }; bool _collideWithOtherAvatars { true }; + bool _hasPriority{ false }; // null unless MyAvatar or ScriptableAvatar sending traits data to mixer std::unique_ptr _clientTraitsHandler; From 24286273b402d83009fa7387900514d68e6e73ef Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 9 Mar 2019 19:22:07 +1300 Subject: [PATCH 097/446] Miscellaneous JSDoc fixes --- interface/src/ui/overlays/Overlays.cpp | 48 +++++++++++++------------- libraries/avatars/src/AvatarData.h | 2 +- libraries/shared/src/BillboardMode.h | 8 +++-- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 1bcb040a77..eec6eddf44 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -1711,9 +1711,9 @@ QVector Overlays::findOverlays(const glm::vec3& center, float radius) { * @property {number} parentJointIndex=65535 - Integer value specifying the skeleton joint that the overlay is attached to if * parentID is an avatar skeleton. A value of 65535 means "no joint". * - * @property {boolean} isFacingAvatar - If true< / code>, the overlay is rotated to face the user's camera about an axis + * @property {boolean} isFacingAvatar - If true, the overlay is rotated to face the user's camera about an axis * parallel to the user's avatar's "up" direction. - * @property {string} text="" - The text to display.Text does not automatically wrap; use \n< / code> for a line break. + * @property {string} text="" - The text to display.Text does not automatically wrap; use \n for a line break. * @property {number} textAlpha=1 - The text alpha value. * @property {Color} backgroundColor=0,0,0 - The background color. * @property {number} backgroundAlpha=0.7 - The background alpha value. @@ -1929,39 +1929,39 @@ QVector Overlays::findOverlays(const glm::vec3& center, float radius) { * * @property {number} startAt = 0 - The counter - clockwise angle from the overlay's x-axis that drawing starts at, in degrees. * @property {number} endAt = 360 - The counter - clockwise angle from the overlay's x-axis that drawing ends at, in degrees. - * @property {number} outerRadius = 1 - The outer radius of the overlay, in meters.Synonym: radius< / code>. + * @property {number} outerRadius = 1 - The outer radius of the overlay, in meters.Synonym: radius. * @property {number} innerRadius = 0 - The inner radius of the overlay, in meters. * @property {Color} color = 255, 255, 255 - The color of the overlay.Setting this value also sets the values of - * innerStartColor< / code>, innerEndColor< / code>, outerStartColor< / code>, and outerEndColor< / code>. - * @property {Color} startColor - Sets the values of innerStartColor< / code> and outerStartColor< / code>. - * Write - only.< / em> - * @property {Color} endColor - Sets the values of innerEndColor< / code> and outerEndColor< / code>. - * Write - only.< / em> - * @property {Color} innerColor - Sets the values of innerStartColor< / code> and innerEndColor< / code>. - * Write - only.< / em> - * @property {Color} outerColor - Sets the values of outerStartColor< / code> and outerEndColor< / code>. - * Write - only.< / em> + * innerStartColor, innerEndColor, outerStartColor, and outerEndColor. + * @property {Color} startColor - Sets the values of innerStartColor and outerStartColor. + * Write - only. + * @property {Color} endColor - Sets the values of innerEndColor and outerEndColor. + * Write - only. + * @property {Color} innerColor - Sets the values of innerStartColor and innerEndColor. + * Write - only. + * @property {Color} outerColor - Sets the values of outerStartColor and outerEndColor. + * Write - only. * @property {Color} innerStartcolor - The color at the inner start point of the overlay. * @property {Color} innerEndColor - The color at the inner end point of the overlay. * @property {Color} outerStartColor - The color at the outer start point of the overlay. * @property {Color} outerEndColor - The color at the outer end point of the overlay. - * @property {number} alpha = 0.5 - The opacity of the overlay, 0.0< / code> -1.0< / code>.Setting this value also sets - * the values of innerStartAlpha< / code>, innerEndAlpha< / code>, outerStartAlpha< / code>, and - * outerEndAlpha< / code>.Synonym: Alpha< / code>; write - only< / em>. - * @property {number} startAlpha - Sets the values of innerStartAlpha< / code> and outerStartAlpha< / code>. - * Write - only.< / em> - * @property {number} endAlpha - Sets the values of innerEndAlpha< / code> and outerEndAlpha< / code>. - * Write - only.< / em> - * @property {number} innerAlpha - Sets the values of innerStartAlpha< / code> and innerEndAlpha< / code>. - * Write - only.< / em> - * @property {number} outerAlpha - Sets the values of outerStartAlpha< / code> and outerEndAlpha< / code>. - * Write - only.< / em> + * @property {number} alpha = 0.5 - The opacity of the overlay, 0.0 -1.0.Setting this value also sets + * the values of innerStartAlpha, innerEndAlpha, outerStartAlpha, and + * outerEndAlpha.Synonym: Alpha; write - only. + * @property {number} startAlpha - Sets the values of innerStartAlpha and outerStartAlpha. + * Write - only. + * @property {number} endAlpha - Sets the values of innerEndAlpha and outerEndAlpha. + * Write - only. + * @property {number} innerAlpha - Sets the values of innerStartAlpha and innerEndAlpha. + * Write - only. + * @property {number} outerAlpha - Sets the values of outerStartAlpha and outerEndAlpha. + * Write - only. * @property {number} innerStartAlpha = 0 - The alpha at the inner start point of the overlay. * @property {number} innerEndAlpha = 0 - The alpha at the inner end point of the overlay. * @property {number} outerStartAlpha = 0 - The alpha at the outer start point of the overlay. * @property {number} outerEndAlpha = 0 - The alpha at the outer end point of the overlay. * - * @property {boolean} hasTickMarks = false - If true< / code>, tick marks are drawn. + * @property {boolean} hasTickMarks = false - If true, tick marks are drawn. * @property {number} majorTickMarksAngle = 0 - The angle between major tick marks, in degrees. * @property {number} minorTickMarksAngle = 0 - The angle between minor tick marks, in degrees. * @property {number} majorTickMarksLength = 0 - The length of the major tick marks, in meters.A positive value draws tick marks diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 858af7ba3c..e0437cfeb1 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -1034,7 +1034,7 @@ public: * @param {string} name - The name of the joint. * @returns {number} The index of the joint if valid, otherwise -1. * @example Report the index of your avatar's left arm joint. - * print(JSON.stringify(MyAvatar.getJointIndex("LeftArm")); + * print(JSON.stringify(MyAvatar.getJointIndex("LeftArm"))); * * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ diff --git a/libraries/shared/src/BillboardMode.h b/libraries/shared/src/BillboardMode.h index 050f939941..700127aff1 100644 --- a/libraries/shared/src/BillboardMode.h +++ b/libraries/shared/src/BillboardMode.h @@ -18,9 +18,11 @@ * ValueDescription * * - * noneThe entity will not be billboarded. - * yawThe entity will yaw, but not pitch, to face the camera. Its actual rotation will be ignored. - * fullThe entity will be billboarded to face the camera. Its actual rotation will be ignored. + * "none"The entity will not be billboarded. + * "yaw"The entity will yaw, but not pitch, to face the camera. Its actual rotation will be + * ignored. + * "full"The entity will be billboarded to face the camera. Its actual rotation will be + * ignored. * * * @typedef {string} BillboardMode From 7419f9899e4af5b753966ea30b259359698d1d0a Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Sat, 9 Mar 2019 11:54:04 -0800 Subject: [PATCH 098/446] Modified thresholds to reduce false positives. --- tools/nitpick/src/TestCreator.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/nitpick/src/TestCreator.h b/tools/nitpick/src/TestCreator.h index f2bd520574..b4ce56a7d5 100644 --- a/tools/nitpick/src/TestCreator.h +++ b/tools/nitpick/src/TestCreator.h @@ -121,8 +121,8 @@ private: const QString TEST_RESULTS_FOLDER { "TestResults" }; const QString TEST_RESULTS_FILENAME { "TestResults.txt" }; - const double THRESHOLD_GLOBAL{ 0.9998 }; - const double THRESHOLD_LOCAL { 0.7500 }; + const double THRESHOLD_GLOBAL{ 0.9995 }; + const double THRESHOLD_LOCAL { 0.6 }; QDir _imageDirectory; From 04c6c425125d4da61863662ac83f1242c1496705 Mon Sep 17 00:00:00 2001 From: r3tk0n Date: Sun, 10 Mar 2019 14:28:31 -0700 Subject: [PATCH 099/446] Fix build error from merge. --- interface/src/scripting/Audio.cpp | 40 ------------------------------- 1 file changed, 40 deletions(-) diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 0a859c4dcc..669856198d 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -211,12 +211,6 @@ void Audio::setPTTHMD(bool enabled) { } } -bool Audio::getPTTHMD() const { - return resultWithReadLock([&] { - return _pttHMD; - }); -} - void Audio::saveData() { _desktopMutedSetting.set(getMutedDesktop()); _hmdMutedSetting.set(getMutedHMD()); @@ -237,40 +231,6 @@ bool Audio::getPTTHMD() const { }); } -void Audio::saveData() { - _desktopMutedSetting.set(getMutedDesktop()); - _hmdMutedSetting.set(getMutedHMD()); - _pttDesktopSetting.set(getPTTDesktop()); - _pttHMDSetting.set(getPTTHMD()); -} - -void Audio::loadData() { - _desktopMuted = _desktopMutedSetting.get(); - _hmdMuted = _hmdMutedSetting.get(); - _pttDesktop = _pttDesktopSetting.get(); - _pttHMD = _pttHMDSetting.get(); -} - -bool Audio::getPTTHMD() const { - return resultWithReadLock([&] { - return _pttHMD; - }); -} - -void Audio::saveData() { - _desktopMutedSetting.set(getMutedDesktop()); - _hmdMutedSetting.set(getMutedHMD()); - _pttDesktopSetting.set(getPTTDesktop()); - _pttHMDSetting.set(getPTTHMD()); -} - -void Audio::loadData() { - _desktopMuted = _desktopMutedSetting.get(); - _hmdMuted = _hmdMutedSetting.get(); - _pttDesktop = _pttDesktopSetting.get(); - _pttHMD = _pttHMDSetting.get(); -} - bool Audio::noiseReductionEnabled() const { return resultWithReadLock([&] { return _enableNoiseReduction; From 2eaa1e63d333a2872683001d04cb24e724ac1f40 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Sun, 10 Mar 2019 17:07:02 -0700 Subject: [PATCH 100/446] switch from column layout to item --- interface/resources/qml/hifi/audio/Audio.qml | 74 +++----------------- 1 file changed, 8 insertions(+), 66 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index 569cd23176..ce968090b4 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -166,10 +166,12 @@ Rectangle { Separator {} - ColumnLayout { - spacing: muteMic.spacing; + Item { + width: rightMostInputLevelPos + height: pttTextContainer.height + pttCheckBox.height + margins.paddings + 10 AudioControls.CheckBox { - spacing: muteMic.spacing + id: pttCheckBox + anchors.top: parent.top text: qsTr("Push To Talk (T)"); checked: isVR ? AudioScriptingInterface.pushToTalkHMD : AudioScriptingInterface.pushToTalkDesktop; onClicked: { @@ -189,69 +191,9 @@ Rectangle { } Item { id: pttTextContainer - x: margins.paddings; - width: rightMostInputLevelPos - height: pttTextMetrics.height - visible: true - TextMetrics { - id: pttTextMetrics - text: pttText.text - font: pttText.font - } - RalewayRegular { - id: pttText - wrapMode: Text.WordWrap - color: hifi.colors.white; - width: parent.width; - font.italic: true - size: 16; - text: isVR ? qsTr("Press and hold grip triggers on both of your controllers to unmute.") : - qsTr("Press and hold the button \"T\" to unmute."); - onTextChanged: { - if (pttTextMetrics.width > rightMostInputLevelPos) { - pttTextContainer.height = Math.ceil(pttTextMetrics.width / rightMostInputLevelPos) * pttTextMetrics.height; - } else { - pttTextContainer.height = pttTextMetrics.height; - } - } - } - Component.onCompleted: { - if (pttTextMetrics.width > rightMostInputLevelPos) { - pttTextContainer.height = Math.ceil(pttTextMetrics.width / rightMostInputLevelPos) * pttTextMetrics.height; - } else { - pttTextContainer.height = pttTextMetrics.height; - } - } - } - } - - Separator {} - - ColumnLayout { - spacing: muteMic.spacing; - AudioControls.CheckBox { - spacing: muteMic.spacing - text: qsTr("Push To Talk (T)"); - checked: isVR ? AudioScriptingInterface.pushToTalkHMD : AudioScriptingInterface.pushToTalkDesktop; - onClicked: { - if (isVR) { - AudioScriptingInterface.pushToTalkHMD = checked; - } else { - AudioScriptingInterface.pushToTalkDesktop = checked; - } - checked = Qt.binding(function() { - if (isVR) { - return AudioScriptingInterface.pushToTalkHMD; - } else { - return AudioScriptingInterface.pushToTalkDesktop; - } - }); // restore binding - } - } - Item { - id: pttTextContainer - x: margins.paddings; - width: rightMostInputLevelPos + anchors.top: pttCheckBox.bottom + anchors.topMargin: 10 + width: parent.width height: pttTextMetrics.height visible: true TextMetrics { From 54973375a4f543557a7547828e3c8896db5bdb20 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Sun, 10 Mar 2019 17:18:18 -0700 Subject: [PATCH 101/446] fixng ptt checkbox --- interface/resources/qml/hifi/audio/Audio.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index ce968090b4..46ae937614 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -171,6 +171,8 @@ Rectangle { height: pttTextContainer.height + pttCheckBox.height + margins.paddings + 10 AudioControls.CheckBox { id: pttCheckBox + spacing: muteMic.spacing; + width: rightMostInputLevelPos anchors.top: parent.top text: qsTr("Push To Talk (T)"); checked: isVR ? AudioScriptingInterface.pushToTalkHMD : AudioScriptingInterface.pushToTalkDesktop; From cf3694e8e343f0dd39e8875c689214ea4611ed33 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Sun, 10 Mar 2019 19:00:41 -0700 Subject: [PATCH 102/446] fixing error in master + getting ptt to show up --- interface/resources/qml/hifi/audio/Audio.qml | 114 +++++++++---------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index a07fb1cb95..376aa39269 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -104,7 +104,6 @@ Rectangle { RowLayout { x: 2 * margins.paddings; - spacing: columnOne.width; width: parent.width; // mute is in its own row @@ -170,66 +169,66 @@ Rectangle { } } } + } - Separator {} + Separator {} - Item { - width: rightMostInputLevelPos - height: pttTextContainer.height + pttCheckBox.height + margins.paddings + 10 - AudioControls.CheckBox { - id: pttCheckBox - spacing: muteMic.spacing; - width: rightMostInputLevelPos - anchors.top: parent.top - text: qsTr("Push To Talk (T)"); - checked: isVR ? AudioScriptingInterface.pushToTalkHMD : AudioScriptingInterface.pushToTalkDesktop; - onClicked: { - if (isVR) { - AudioScriptingInterface.pushToTalkHMD = checked; + + ColumnLayout { + id: pttColumn + spacing: 24; + x: 2 * margins.paddings; + HifiControlsUit.Switch { + id: pttSwitch + height: root.switchHeight; + switchWidth: root.switchWidth; + labelTextOn: qsTr("Push To Talk (T)"); + backgroundOnColor: "#E3E3E3"; + checked: (bar.currentIndex === 1 && isVR) || + (bar.currentIndex === 0 && !isVR) ? AudioScriptingInterface.pushToTalkDesktop : AudioScriptingInterface.pushToTalkHMD; + onCheckedChanged: { + if ((bar.currentIndex === 1 && isVR) || + (bar.currentIndex === 0 && !isVR)) { + AudioScriptingInterface.pushToTalkDesktop = checked; + } else { + AudioScriptingInterface.pushToTalkHMD = checked; + } + checked = Qt.binding(function() { + if ((bar.currentIndex === 1 && isVR) || + (bar.currentIndex === 0 && !isVR)) { + return AudioScriptingInterface.pushToTalkDesktop; } else { - AudioScriptingInterface.pushToTalkDesktop = checked; + return AudioScriptingInterface.pushToTalkHMD; } - checked = Qt.binding(function() { - if (isVR) { - return AudioScriptingInterface.pushToTalkHMD; - } else { - return AudioScriptingInterface.pushToTalkDesktop; - } - }); // restore binding - } + }); // restore binding } - Item { - id: pttTextContainer - anchors.top: pttCheckBox.bottom - anchors.topMargin: 10 - width: parent.width - height: pttTextMetrics.height - visible: true - TextMetrics { - id: pttTextMetrics - text: pttText.text - font: pttText.font - } - RalewayRegular { - id: pttText - wrapMode: Text.WordWrap - color: hifi.colors.white; - width: parent.width; - font.italic: true - size: 16; - text: isVR ? qsTr("Press and hold grip triggers on both of your controllers to unmute.") : - qsTr("Press and hold the button \"T\" to unmute."); - onTextChanged: { - if (pttTextMetrics.width > rightMostInputLevelPos) { - pttTextContainer.height = Math.ceil(pttTextMetrics.width / rightMostInputLevelPos) * pttTextMetrics.height; - } else { - pttTextContainer.height = pttTextMetrics.height; - } - } - } - Component.onCompleted: { - if (pttTextMetrics.width > rightMostInputLevelPos) { - pttTextContainer.height = Math.ceil(pttTextMetrics.width / rightMostInputLevelPos) * pttTextMetrics.height; + } + Item { + id: pttTextContainer + width: rightMostInputLevelPos + height: pttTextMetrics.height + anchors.left: parent.left + anchors.leftMargin: -margins.padding + TextMetrics { + id: pttTextMetrics + text: pttText.text + font: pttText.font + } + RalewayRegular { + id: pttText + color: hifi.colors.white; + width: parent.width; + wrapMode: (bar.currentIndex === 1 && isVR) || + (bar.currentIndex === 0 && !isVR) ? Text.NoWrap : Text.WordWrap; + font.italic: true + size: 16; + + text: (bar.currentIndex === 1 && isVR) || + (bar.currentIndex === 0 && !isVR) ? qsTr("Press and hold the button \"T\" to unmute.") : + qsTr("Press and hold grip triggers on both of your controllers to unmute."); + onTextChanged: { + if (pttTextMetrics.width > pttTextContainer.width) { + pttTextContainer.height = Math.ceil(pttTextMetrics.width / pttTextContainer.width) * pttTextMetrics.height; } else { pttTextContainer.height = pttTextMetrics.height; } @@ -240,6 +239,7 @@ Rectangle { Separator {} + Item { x: margins.paddings; width: parent.width - margins.paddings*2 @@ -293,7 +293,7 @@ Rectangle { text: devicename onPressed: { if (!checked) { - stereoMic.checked = false; + stereoInput.checked = false; AudioScriptingInterface.setStereoInput(false); // the next selected audio device might not support stereo AudioScriptingInterface.setInputDevice(info, bar.currentIndex === 1); } From 20487b2ad1adb1a10b16bac8cb48e3bf12e93e44 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Sun, 10 Mar 2019 19:39:45 -0700 Subject: [PATCH 103/446] connecting pushingToTalkChanged with handlePushedToTalk --- interface/src/Application.cpp | 19 +++++++++---------- interface/src/Application.h | 2 -- interface/src/scripting/Audio.cpp | 3 ++- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index fc9fcd1bbb..215736001c 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1435,8 +1435,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo }); connect(this, &Application::activeDisplayPluginChanged, reinterpret_cast(audioScriptingInterface.data()), &scripting::Audio::onContextChanged); - connect(this, &Application::pushedToTalk, - reinterpret_cast(audioScriptingInterface.data()), &scripting::Audio::handlePushedToTalk); } // Create the rendering engine. This can be slow on some machines due to lots of @@ -1609,13 +1607,12 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo bool navAxis = false; switch (actionEnum) { case Action::TOGGLE_PUSHTOTALK: - if (audioScriptingInterface->getPTT()) { - if (state > 0.0f) { - audioScriptingInterface->setPushingToTalk(false); - } else if (state < 0.0f) { - audioScriptingInterface->setPushingToTalk(true); - } + if (state > 0.0f) { + audioScriptingInterface->setPushingToTalk(false); + } else if (state < 0.0f) { + audioScriptingInterface->setPushingToTalk(true); } + break; case Action::UI_NAV_VERTICAL: navAxis = true; @@ -4218,7 +4215,8 @@ void Application::keyPressEvent(QKeyEvent* event) { break; case Qt::Key_T: - emit pushedToTalk(true); + auto audioScriptingInterface = reinterpret_cast(DependencyManager::get().data()); + audioScriptingInterface->setPushingToTalk(true); break; case Qt::Key_P: { @@ -4329,7 +4327,8 @@ void Application::keyReleaseEvent(QKeyEvent* event) { switch (event->key()) { case Qt::Key_T: - emit pushedToTalk(false); + auto audioScriptingInterface = reinterpret_cast(DependencyManager::get().data()); + audioScriptingInterface->setPushingToTalk(false); break; } } diff --git a/interface/src/Application.h b/interface/src/Application.h index 1c86326f90..a8cc9450c5 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -358,8 +358,6 @@ signals: void miniTabletEnabledChanged(bool enabled); - void pushedToTalk(bool enabled); - public slots: QVector pasteEntities(float x, float y, float z); bool exportEntities(const QString& filename, const QVector& entityIDs, const glm::vec3* givenOffset = nullptr); diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 669856198d..c4dfcffb61 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -40,6 +40,8 @@ Audio::Audio() : _devices(_contextIsHMD) { connect(client, &AudioClient::inputLoudnessChanged, this, &Audio::onInputLoudnessChanged); connect(client, &AudioClient::inputVolumeChanged, this, &Audio::setInputVolume); connect(this, &Audio::contextChanged, &_devices, &AudioDevices::onContextChanged); + // when pushing to talk changed, handle it. + connect(this, &Audio::pushingToTalkChanged, this, &Audio::handlePushedToTalk); enableNoiseReduction(enableNoiseReductionSetting.get()); onContextChanged(); } @@ -344,7 +346,6 @@ void Audio::handlePushedToTalk(bool enabled) { } else { setMuted(true); } - setPushingToTalk(enabled); } } From 7b51061b5f3eee889b6d5f7ea4cec5e502518650 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Sun, 10 Mar 2019 19:46:01 -0700 Subject: [PATCH 104/446] fixing initialization in switch statement --- interface/src/Application.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 215736001c..3230419816 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4047,6 +4047,7 @@ void Application::keyPressEvent(QKeyEvent* event) { _keysPressed.insert(event->key(), *event); } + auto audioScriptingInterface = reinterpret_cast(DependencyManager::get().data()); _controllerScriptingInterface->emitKeyPressEvent(event); // send events to any registered scripts // if one of our scripts have asked to capture this event, then stop processing it if (_controllerScriptingInterface->isKeyCaptured(event) || isInterstitialMode()) { @@ -4215,7 +4216,6 @@ void Application::keyPressEvent(QKeyEvent* event) { break; case Qt::Key_T: - auto audioScriptingInterface = reinterpret_cast(DependencyManager::get().data()); audioScriptingInterface->setPushingToTalk(true); break; @@ -4325,9 +4325,9 @@ void Application::keyReleaseEvent(QKeyEvent* event) { _keyboardMouseDevice->keyReleaseEvent(event); } + auto audioScriptingInterface = reinterpret_cast(DependencyManager::get().data()); switch (event->key()) { case Qt::Key_T: - auto audioScriptingInterface = reinterpret_cast(DependencyManager::get().data()); audioScriptingInterface->setPushingToTalk(false); break; } From ec0cf3ee3ace28f9c2bc39fa350190238f15a46c Mon Sep 17 00:00:00 2001 From: r3tk0n Date: Mon, 11 Mar 2019 09:58:20 -0700 Subject: [PATCH 105/446] Fix typo. --- .../controllerModules/pushToTalk.js | 123 +++++++++--------- 1 file changed, 62 insertions(+), 61 deletions(-) diff --git a/scripts/system/controllers/controllerModules/pushToTalk.js b/scripts/system/controllers/controllerModules/pushToTalk.js index 9d6435f497..6b1bacc367 100644 --- a/scripts/system/controllers/controllerModules/pushToTalk.js +++ b/scripts/system/controllers/controllerModules/pushToTalk.js @@ -11,65 +11,66 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); Script.include("/~/system/libraries/controllers.js"); -(function() { // BEGIN LOCAL_SCOPE - function PushToTalkHandler() { - var _this = this; - this.active = false; - //var pttMapping, mappingName; - - this.setup = function() { - //mappingName = 'Hifi-PTT-Dev-' + Math.random(); - //pttMapping = Controller.newMapping(mappingName); - //pttMapping.enable(); - }; - - this.shouldTalk = function (controllerData) { - // Set up test against controllerData here... - var gripVal = controllerData.secondaryValues[LEFT_HAND] && controllerData.secondaryValues[RIGHT_HAND]; - return (gripVal) ? true : false; - }; - - this.shouldStopTalking = function (controllerData) { - var gripVal = controllerData.secondaryValues[LEFT_HAND] && controllerData.secondaryValues[RIGHT_HAND]; - return (gripVal) ? false : true; - }; - - this.isReady = function (controllerData, deltaTime) { - if (HMD.active && Audio.pushToTalk && this.shouldTalk(controllerData)) { - Audio.pushingToTalk = true; - returnMakeRunningValues(true, [], []); - } - - return makeRunningValues(false, [], []); - }; - - this.run = function (controllerData, deltaTime) { - if (this.shouldStopTalking(controllerData) || !Audio.pushToTalk) { - Audio.pushingToTalk = false; - return makeRunningValues(false, [], []); - } - - return makeRunningValues(true, [], []); - }; - - this.cleanup = function () { - //pttMapping.disable(); - }; - - this.parameters = makeDispatcherModuleParameters( - 950, - ["head"], - [], - 100); - } - - var pushToTalk = new PushToTalkHandler(); - enableDispatcherModule("PushToTalk", pushToTalk); - - function cleanup () { - pushToTalk.cleanup(); - disableDispatcherModule("PushToTalk"); - }; - - Script.scriptEnding.connect(cleanup); +(function () { // BEGIN LOCAL_SCOPE + function PushToTalkHandler() { + var _this = this; + this.active = false; + //var pttMapping, mappingName; + + this.setup = function () { + //mappingName = 'Hifi-PTT-Dev-' + Math.random(); + //pttMapping = Controller.newMapping(mappingName); + //pttMapping.enable(); + }; + + this.shouldTalk = function (controllerData) { + // Set up test against controllerData here... + var gripVal = controllerData.secondaryValues[LEFT_HAND] && controllerData.secondaryValues[RIGHT_HAND]; + return (gripVal) ? true : false; + }; + + this.shouldStopTalking = function (controllerData) { + var gripVal = controllerData.secondaryValues[LEFT_HAND] && controllerData.secondaryValues[RIGHT_HAND]; + return (gripVal) ? false : true; + }; + + this.isReady = function (controllerData, deltaTime) { + if (HMD.active && Audio.pushToTalk && this.shouldTalk(controllerData)) { + Audio.pushingToTalk = true; + return makeRunningValues(true, [], []); + } + + return makeRunningValues(false, [], []); + }; + + this.run = function (controllerData, deltaTime) { + if (this.shouldStopTalking(controllerData) || !Audio.pushToTalk) { + Audio.pushingToTalk = false; + print("Stop pushing to talk."); + return makeRunningValues(false, [], []); + } + + return makeRunningValues(true, [], []); + }; + + this.cleanup = function () { + //pttMapping.disable(); + }; + + this.parameters = makeDispatcherModuleParameters( + 950, + ["head"], + [], + 100); + } + + var pushToTalk = new PushToTalkHandler(); + enableDispatcherModule("PushToTalk", pushToTalk); + + function cleanup() { + pushToTalk.cleanup(); + disableDispatcherModule("PushToTalk"); + }; + + Script.scriptEnding.connect(cleanup); }()); // END LOCAL_SCOPE From c1ed01115de29730e9d2132d5f63e13d143f04a3 Mon Sep 17 00:00:00 2001 From: r3tk0n Date: Mon, 11 Mar 2019 09:58:35 -0700 Subject: [PATCH 106/446] Fix logic for HMD vs Desktop. --- interface/resources/qml/hifi/audio/Audio.qml | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index 376aa39269..ecc3297d9f 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -184,18 +184,15 @@ Rectangle { switchWidth: root.switchWidth; labelTextOn: qsTr("Push To Talk (T)"); backgroundOnColor: "#E3E3E3"; - checked: (bar.currentIndex === 1 && isVR) || - (bar.currentIndex === 0 && !isVR) ? AudioScriptingInterface.pushToTalkDesktop : AudioScriptingInterface.pushToTalkHMD; + checked: (bar.currentIndex === 0) ? AudioScriptingInterface.pushToTalkDesktop : AudioScriptingInterface.pushToTalkHMD; onCheckedChanged: { - if ((bar.currentIndex === 1 && isVR) || - (bar.currentIndex === 0 && !isVR)) { + if (bar.currentIndex === 0) { AudioScriptingInterface.pushToTalkDesktop = checked; } else { AudioScriptingInterface.pushToTalkHMD = checked; } checked = Qt.binding(function() { - if ((bar.currentIndex === 1 && isVR) || - (bar.currentIndex === 0 && !isVR)) { + if (bar.currentIndex === 0) { return AudioScriptingInterface.pushToTalkDesktop; } else { return AudioScriptingInterface.pushToTalkHMD; @@ -218,13 +215,11 @@ Rectangle { id: pttText color: hifi.colors.white; width: parent.width; - wrapMode: (bar.currentIndex === 1 && isVR) || - (bar.currentIndex === 0 && !isVR) ? Text.NoWrap : Text.WordWrap; + wrapMode: (bar.currentIndex === 0) ? Text.NoWrap : Text.WordWrap; font.italic: true size: 16; - text: (bar.currentIndex === 1 && isVR) || - (bar.currentIndex === 0 && !isVR) ? qsTr("Press and hold the button \"T\" to unmute.") : + text: (bar.currentIndex === 0) ? qsTr("Press and hold the button \"T\" to unmute.") : qsTr("Press and hold grip triggers on both of your controllers to unmute."); onTextChanged: { if (pttTextMetrics.width > pttTextContainer.width) { From e4e8a61328d1a2ec9c70608e216b8e4dbf1625b2 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Mon, 11 Mar 2019 10:46:51 -0700 Subject: [PATCH 107/446] Case 21326 - missing marketplaceInject.js The re-addition of marketplaceInject.js didn't merge from 80 for some reason. --- scripts/system/html/js/marketplacesInject.js | 744 +++++++++++++++++++ 1 file changed, 744 insertions(+) create mode 100644 scripts/system/html/js/marketplacesInject.js diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js new file mode 100644 index 0000000000..8d408169ba --- /dev/null +++ b/scripts/system/html/js/marketplacesInject.js @@ -0,0 +1,744 @@ +/* global $, window, MutationObserver */ + +// +// marketplacesInject.js +// +// Created by David Rowe on 12 Nov 2016. +// Copyright 2016 High Fidelity, Inc. +// +// Injected into marketplace Web pages. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function () { + // Event bridge messages. + var CLARA_IO_DOWNLOAD = "CLARA.IO DOWNLOAD"; + var CLARA_IO_STATUS = "CLARA.IO STATUS"; + var CLARA_IO_CANCEL_DOWNLOAD = "CLARA.IO CANCEL DOWNLOAD"; + var CLARA_IO_CANCELLED_DOWNLOAD = "CLARA.IO CANCELLED DOWNLOAD"; + var GOTO_DIRECTORY = "GOTO_DIRECTORY"; + var GOTO_MARKETPLACE = "GOTO_MARKETPLACE"; + var QUERY_CAN_WRITE_ASSETS = "QUERY_CAN_WRITE_ASSETS"; + var CAN_WRITE_ASSETS = "CAN_WRITE_ASSETS"; + var WARN_USER_NO_PERMISSIONS = "WARN_USER_NO_PERMISSIONS"; + + var canWriteAssets = false; + var xmlHttpRequest = null; + var isPreparing = false; // Explicitly track download request status. + + var limitedCommerce = false; + var commerceMode = false; + var userIsLoggedIn = false; + var walletNeedsSetup = false; + var marketplaceBaseURL = "https://highfidelity.com"; + var messagesWaiting = false; + + function injectCommonCode(isDirectoryPage) { + // Supporting styles from marketplaces.css. + // Glyph font family, size, and spacing adjusted because HiFi-Glyphs cannot be used cross-domain. + $("head").append( + '' + ); + + // Supporting styles from edit-style.css. + // Font family, size, and position adjusted because Raleway-Bold cannot be used cross-domain. + $("head").append( + '' + ); + + // Footer. + var isInitialHiFiPage = location.href === (marketplaceBaseURL + "/marketplace?"); + $("body").append( + '
' + + (!isInitialHiFiPage ? '' : '') + + (isInitialHiFiPage ? '🛈 Get items from Clara.io!' : '') + + (!isDirectoryPage ? '' : '') + + (isDirectoryPage ? '🛈 Select a marketplace to explore.' : '') + + '
' + ); + + // Footer actions. + $("#back-button").on("click", function () { + if (document.referrer !== "") { + window.history.back(); + } else { + var params = { type: GOTO_MARKETPLACE }; + var itemIdMatch = location.search.match(/itemId=([^&]*)/); + if (itemIdMatch && itemIdMatch.length === 2) { + params.itemId = itemIdMatch[1]; + } + EventBridge.emitWebEvent(JSON.stringify(params)); + } + }); + $("#all-markets").on("click", function () { + EventBridge.emitWebEvent(JSON.stringify({ + type: GOTO_DIRECTORY + })); + }); + } + + function injectDirectoryCode() { + + // Remove e-mail hyperlink. + var letUsKnow = $("#letUsKnow"); + letUsKnow.replaceWith(letUsKnow.html()); + + // Add button links. + + $('#exploreClaraMarketplace').on('click', function () { + window.location = "https://clara.io/library?gameCheck=true&public=true"; + }); + $('#exploreHifiMarketplace').on('click', function () { + EventBridge.emitWebEvent(JSON.stringify({ + type: GOTO_MARKETPLACE + })); + }); + } + + emitWalletSetupEvent = function () { + EventBridge.emitWebEvent(JSON.stringify({ + type: "WALLET_SETUP" + })); + }; + + function maybeAddSetupWalletButton() { + if (!$('body').hasClass("walletsetup-injected") && userIsLoggedIn && walletNeedsSetup) { + $('body').addClass("walletsetup-injected"); + + var resultsElement = document.getElementById('results'); + var setupWalletElement = document.createElement('div'); + setupWalletElement.classList.add("row"); + setupWalletElement.id = "setupWalletDiv"; + setupWalletElement.style = "height:60px;margin:20px 10px 10px 10px;padding:12px 5px;" + + "background-color:#D6F4D8;border-color:#aee9b2;border-width:2px;border-style:solid;border-radius:5px;"; + + var span = document.createElement('span'); + span.style = "margin:10px 5px;color:#1b6420;font-size:15px;"; + span.innerHTML = "Activate your Wallet to get money and shop in Marketplace."; + + var xButton = document.createElement('a'); + xButton.id = "xButton"; + xButton.setAttribute('href', "#"); + xButton.style = "width:50px;height:100%;margin:0;color:#ccc;font-size:20px;"; + xButton.innerHTML = "X"; + xButton.onclick = function () { + setupWalletElement.remove(); + dummyRow.remove(); + }; + + setupWalletElement.appendChild(span); + setupWalletElement.appendChild(xButton); + + resultsElement.insertBefore(setupWalletElement, resultsElement.firstChild); + + // Dummy row for padding + var dummyRow = document.createElement('div'); + dummyRow.classList.add("row"); + dummyRow.style = "height:15px;"; + resultsElement.insertBefore(dummyRow, resultsElement.firstChild); + } + } + + function maybeAddLogInButton() { + if (!$('body').hasClass("login-injected") && !userIsLoggedIn) { + $('body').addClass("login-injected"); + var resultsElement = document.getElementById('results'); + if (!resultsElement) { // If we're on the main page, this will evaluate to `true` + resultsElement = document.getElementById('item-show'); + resultsElement.style = 'margin-top:0;'; + } + var logInElement = document.createElement('div'); + logInElement.classList.add("row"); + logInElement.id = "logInDiv"; + logInElement.style = "height:60px;margin:20px 10px 10px 10px;padding:5px;" + + "background-color:#D6F4D8;border-color:#aee9b2;border-width:2px;border-style:solid;border-radius:5px;"; + + var button = document.createElement('a'); + button.classList.add("btn"); + button.classList.add("btn-default"); + button.id = "logInButton"; + button.setAttribute('href', "#"); + button.innerHTML = "LOG IN"; + button.style = "width:80px;height:100%;margin-top:0;margin-left:10px;padding:13px;font-weight:bold;background:linear-gradient(white, #ccc);"; + button.onclick = function () { + EventBridge.emitWebEvent(JSON.stringify({ + type: "LOGIN" + })); + }; + + var span = document.createElement('span'); + span.style = "margin:10px;color:#1b6420;font-size:15px;"; + span.innerHTML = "to get items from the Marketplace."; + + var xButton = document.createElement('a'); + xButton.id = "xButton"; + xButton.setAttribute('href', "#"); + xButton.style = "width:50px;height:100%;margin:0;color:#ccc;font-size:20px;"; + xButton.innerHTML = "X"; + xButton.onclick = function () { + logInElement.remove(); + dummyRow.remove(); + }; + + logInElement.appendChild(button); + logInElement.appendChild(span); + logInElement.appendChild(xButton); + + resultsElement.insertBefore(logInElement, resultsElement.firstChild); + + // Dummy row for padding + var dummyRow = document.createElement('div'); + dummyRow.classList.add("row"); + dummyRow.style = "height:15px;"; + resultsElement.insertBefore(dummyRow, resultsElement.firstChild); + } + } + + function changeDropdownMenu() { + var logInOrOutButton = document.createElement('a'); + logInOrOutButton.id = "logInOrOutButton"; + logInOrOutButton.setAttribute('href', "#"); + logInOrOutButton.innerHTML = userIsLoggedIn ? "Log Out" : "Log In"; + logInOrOutButton.onclick = function () { + EventBridge.emitWebEvent(JSON.stringify({ + type: "LOGIN" + })); + }; + + $($('.dropdown-menu').find('li')[0]).append(logInOrOutButton); + + $('a[href="/marketplace?view=mine"]').each(function () { + $(this).attr('href', '#'); + $(this).on('click', function () { + EventBridge.emitWebEvent(JSON.stringify({ + type: "MY_ITEMS" + })); + }); + }); + } + + function buyButtonClicked(id, referrer, edition) { + EventBridge.emitWebEvent(JSON.stringify({ + type: "CHECKOUT", + itemId: id, + referrer: referrer, + itemEdition: edition + })); + } + + function injectBuyButtonOnMainPage() { + var cost; + + // Unbind original mouseenter and mouseleave behavior + $('body').off('mouseenter', '#price-or-edit .price'); + $('body').off('mouseleave', '#price-or-edit .price'); + + $('.grid-item').find('#price-or-edit').each(function () { + $(this).css({ "margin-top": "0" }); + }); + + $('.grid-item').find('#price-or-edit').find('a').each(function() { + if ($(this).attr('href') !== '#') { // Guard necessary because of the AJAX nature of Marketplace site + $(this).attr('data-href', $(this).attr('href')); + $(this).attr('href', '#'); + } + cost = $(this).closest('.col-xs-3').find('.item-cost').text(); + var costInt = parseInt(cost, 10); + + $(this).closest('.col-xs-3').prev().attr("class", 'col-xs-6'); + $(this).closest('.col-xs-3').attr("class", 'col-xs-6'); + + var priceElement = $(this).find('.price'); + var available = true; + + if (priceElement.text() === 'invalidated' || + priceElement.text() === 'sold out' || + priceElement.text() === 'not for sale') { + available = false; + priceElement.css({ + "padding": "3px 5px 10px 5px", + "height": "40px", + "background": "linear-gradient(#a2a2a2, #fefefe)", + "color": "#000", + "font-weight": "600", + "line-height": "34px" + }); + } else { + priceElement.css({ + "padding": "3px 5px", + "height": "40px", + "background": "linear-gradient(#00b4ef, #0093C5)", + "color": "#FFF", + "font-weight": "600", + "line-height": "34px" + }); + } + + if (parseInt(cost) > 0) { + priceElement.css({ "width": "auto" }); + + if (available) { + priceElement.html(' ' + cost); + } + + priceElement.css({ "min-width": priceElement.width() + 30 }); + } + }); + + // change pricing to GET/BUY on button hover + $('body').on('mouseenter', '#price-or-edit .price', function () { + var $this = $(this); + var buyString = "BUY"; + var getString = "GET"; + // Protection against the button getting stuck in the "BUY"/"GET" state. + // That happens when the browser gets two MOUSEENTER events before getting a + // MOUSELEAVE event. Also, if not available for sale, just return. + if ($this.text() === buyString || + $this.text() === getString || + $this.text() === 'invalidated' || + $this.text() === 'sold out' || + $this.text() === 'not for sale' ) { + return; + } + $this.data('initialHtml', $this.html()); + + var cost = $(this).parent().siblings().text(); + if (parseInt(cost) > 0) { + $this.text(buyString); + } + if (parseInt(cost) == 0) { + $this.text(getString); + } + }); + + $('body').on('mouseleave', '#price-or-edit .price', function () { + var $this = $(this); + $this.html($this.data('initialHtml')); + }); + + + $('.grid-item').find('#price-or-edit').find('a').on('click', function () { + var price = $(this).closest('.grid-item').find('.price').text(); + if (price === 'invalidated' || + price === 'sold out' || + price === 'not for sale') { + return false; + } + buyButtonClicked($(this).closest('.grid-item').attr('data-item-id'), + "mainPage", + -1); + }); + } + + function injectUnfocusOnSearch() { + // unfocus input field on search, thus hiding virtual keyboard + $('#search-box').on('submit', function () { + if (document.activeElement) { + document.activeElement.blur(); + } + }); + } + + // fix for 10108 - marketplace category cannot scroll + function injectAddScrollbarToCategories() { + $('#categories-dropdown').on('show.bs.dropdown', function () { + $('body > div.container').css('display', 'none') + $('#categories-dropdown > ul.dropdown-menu').css({ 'overflow': 'auto', 'height': 'calc(100vh - 110px)' }); + }); + + $('#categories-dropdown').on('hide.bs.dropdown', function () { + $('body > div.container').css('display', ''); + $('#categories-dropdown > ul.dropdown-menu').css({ 'overflow': '', 'height': '' }); + }); + } + + function injectHiFiCode() { + if (commerceMode) { + maybeAddLogInButton(); + maybeAddSetupWalletButton(); + + if (!$('body').hasClass("code-injected")) { + + $('body').addClass("code-injected"); + changeDropdownMenu(); + + var target = document.getElementById('templated-items'); + // MutationObserver is necessary because the DOM is populated after the page is loaded. + // We're searching for changes to the element whose ID is '#templated-items' - this is + // the element that gets filled in by the AJAX. + var observer = new MutationObserver(function (mutations) { + mutations.forEach(function (mutation) { + injectBuyButtonOnMainPage(); + }); + }); + var config = { attributes: true, childList: true, characterData: true }; + observer.observe(target, config); + + // Try this here in case it works (it will if the user just pressed the "back" button, + // since that doesn't trigger another AJAX request. + injectBuyButtonOnMainPage(); + } + } + + injectUnfocusOnSearch(); + injectAddScrollbarToCategories(); + } + + function injectHiFiItemPageCode() { + if (commerceMode) { + maybeAddLogInButton(); + + if (!$('body').hasClass("code-injected")) { + + $('body').addClass("code-injected"); + changeDropdownMenu(); + + var purchaseButton = $('#side-info').find('.btn').first(); + + var href = purchaseButton.attr('href'); + purchaseButton.attr('href', '#'); + var cost = $('.item-cost').text(); + var costInt = parseInt(cost, 10); + var availability = $.trim($('.item-availability').text()); + if (limitedCommerce && (costInt > 0)) { + availability = ''; + } + if (availability === 'available') { + purchaseButton.css({ + "background": "linear-gradient(#00b4ef, #0093C5)", + "color": "#FFF", + "font-weight": "600", + "padding-bottom": "10px" + }); + } else { + purchaseButton.css({ + "background": "linear-gradient(#a2a2a2, #fefefe)", + "color": "#000", + "font-weight": "600", + "padding-bottom": "10px" + }); + } + + var type = $('.item-type').text(); + var isUpdating = window.location.href.indexOf('edition=') > -1; + var urlParams = new URLSearchParams(window.location.search); + if (isUpdating) { + purchaseButton.html('UPDATE FOR FREE'); + } else if (availability !== 'available') { + purchaseButton.html('UNAVAILABLE ' + (availability ? ('(' + availability + ')') : '')); + } else if (parseInt(cost) > 0 && $('#side-info').find('#buyItemButton').size() === 0) { + purchaseButton.html('PURCHASE ' + cost); + } + + purchaseButton.on('click', function () { + if ('available' === availability || isUpdating) { + buyButtonClicked(window.location.pathname.split("/")[3], + "itemPage", + urlParams.get('edition')); + } + }); + } + } + + injectUnfocusOnSearch(); + } + + function updateClaraCode() { + // Have to repeatedly update Clara page because its content can change dynamically without location.href changing. + + // Clara library page. + if (location.href.indexOf("clara.io/library") !== -1) { + // Make entries navigate to "Image" view instead of default "Real Time" view. + var elements = $("a.thumbnail"); + for (var i = 0, length = elements.length; i < length; i++) { + var value = elements[i].getAttribute("href"); + if (value.slice(-6) !== "/image") { + elements[i].setAttribute("href", value + "/image"); + } + } + } + + // Clara item page. + if (location.href.indexOf("clara.io/view/") !== -1) { + // Make site navigation links retain gameCheck etc. parameters. + var element = $("a[href^=\'/library\']")[0]; + var parameters = "?gameCheck=true&public=true"; + var href = element.getAttribute("href"); + if (href.slice(-parameters.length) !== parameters) { + element.setAttribute("href", href + parameters); + } + + // Remove unwanted buttons and replace download options with a single "Download to High Fidelity" button. + var buttons = $("a.embed-button").parent("div"); + var downloadFBX; + if (buttons.find("div.btn-group").length > 0) { + buttons.children(".btn-primary, .btn-group , .embed-button").each(function () { this.remove(); }); + if ($("#hifi-download-container").length === 0) { // Button hasn't been moved already. + downloadFBX = $(' Download to High Fidelity'); + buttons.prepend(downloadFBX); + downloadFBX[0].addEventListener("click", startAutoDownload); + } + } + + // Move the "Download to High Fidelity" button to be more visible on tablet. + if ($("#hifi-download-container").length === 0 && window.innerWidth < 700) { + var downloadContainer = $('
'); + $(".top-title .col-sm-4").append(downloadContainer); + downloadContainer.append(downloadFBX); + } + } + } + + // Automatic download to High Fidelity. + function startAutoDownload() { + // One file request at a time. + if (isPreparing) { + console.log("WARNING: Clara.io FBX: Prepare only one download at a time"); + return; + } + + // User must be able to write to Asset Server. + if (!canWriteAssets) { + console.log("ERROR: Clara.io FBX: File download cancelled because no permissions to write to Asset Server"); + EventBridge.emitWebEvent(JSON.stringify({ + type: WARN_USER_NO_PERMISSIONS + })); + return; + } + + // User must be logged in. + var loginButton = $("#topnav a[href='/signup']"); + if (loginButton.length > 0) { + loginButton[0].click(); + return; + } + + // Obtain zip file to download for requested asset. + // Reference: https://clara.io/learn/sdk/api/export + + //var XMLHTTPREQUEST_URL = "https://clara.io/api/scenes/{uuid}/export/fbx?zip=true¢erScene=true&alignSceneGround=true&fbxUnit=Meter&fbxVersion=7&fbxEmbedTextures=true&imageFormat=WebGL"; + // 13 Jan 2017: Specify FBX version 5 and remove some options in order to make Clara.io site more likely to + // be successful in generating zip files. + var XMLHTTPREQUEST_URL = "https://clara.io/api/scenes/{uuid}/export/fbx?fbxUnit=Meter&fbxVersion=5&fbxEmbedTextures=true&imageFormat=WebGL"; + + var uuid = location.href.match(/\/view\/([a-z0-9\-]*)/)[1]; + var url = XMLHTTPREQUEST_URL.replace("{uuid}", uuid); + + xmlHttpRequest = new XMLHttpRequest(); + var responseTextIndex = 0; + var zipFileURL = ""; + + xmlHttpRequest.onreadystatechange = function () { + // Messages are appended to responseText; process the new ones. + var message = this.responseText.slice(responseTextIndex); + var statusMessage = ""; + + if (isPreparing) { // Ignore messages in flight after finished/cancelled. + var lines = message.split(/[\n\r]+/); + + for (var i = 0, length = lines.length; i < length; i++) { + if (lines[i].slice(0, 5) === "data:") { + // Parse line. + var data; + try { + data = JSON.parse(lines[i].slice(5)); + } + catch (e) { + data = {}; + } + + // Extract zip file URL. + if (data.hasOwnProperty("files") && data.files.length > 0) { + zipFileURL = data.files[0].url; + } + } + } + + if (statusMessage !== "") { + // Update the UI with the most recent status message. + EventBridge.emitWebEvent(JSON.stringify({ + type: CLARA_IO_STATUS, + status: statusMessage + })); + } + } + + responseTextIndex = this.responseText.length; + }; + + // Note: onprogress doesn't have computable total length so can't use it to determine % complete. + + xmlHttpRequest.onload = function () { + var statusMessage = ""; + + if (!isPreparing) { + return; + } + + isPreparing = false; + + var HTTP_OK = 200; + if (this.status !== HTTP_OK) { + EventBridge.emitWebEvent(JSON.stringify({ + type: CLARA_IO_STATUS, + status: statusMessage + })); + } else if (zipFileURL.slice(-4) !== ".zip") { + EventBridge.emitWebEvent(JSON.stringify({ + type: CLARA_IO_STATUS, + status: (statusMessage + ": " + zipFileURL) + })); + } else { + EventBridge.emitWebEvent(JSON.stringify({ + type: CLARA_IO_DOWNLOAD + })); + } + + xmlHttpRequest = null; + } + + isPreparing = true; + EventBridge.emitWebEvent(JSON.stringify({ + type: CLARA_IO_STATUS, + status: "Initiating download" + })); + + xmlHttpRequest.open("POST", url, true); + xmlHttpRequest.setRequestHeader("Accept", "text/event-stream"); + xmlHttpRequest.send(); + } + + function injectClaraCode() { + + // Make space for marketplaces footer in Clara pages. + $("head").append( + '' + ); + + // Condense space. + $("head").append( + '' + ); + + // Move "Download to High Fidelity" button. + $("head").append( + '' + ); + + // Update code injected per page displayed. + var updateClaraCodeInterval = undefined; + updateClaraCode(); + updateClaraCodeInterval = setInterval(function () { + updateClaraCode(); + }, 1000); + + window.addEventListener("unload", function () { + clearInterval(updateClaraCodeInterval); + updateClaraCodeInterval = undefined; + }); + + EventBridge.emitWebEvent(JSON.stringify({ + type: QUERY_CAN_WRITE_ASSETS + })); + } + + function cancelClaraDownload() { + isPreparing = false; + + if (xmlHttpRequest) { + xmlHttpRequest.abort(); + xmlHttpRequest = null; + console.log("Clara.io FBX: File download cancelled"); + EventBridge.emitWebEvent(JSON.stringify({ + type: CLARA_IO_CANCELLED_DOWNLOAD + })); + } + } + + function injectCode() { + var DIRECTORY = 0; + var HIFI = 1; + var CLARA = 2; + var HIFI_ITEM_PAGE = 3; + var pageType = DIRECTORY; + + if (location.href.indexOf(marketplaceBaseURL + "/") !== -1) { pageType = HIFI; } + if (location.href.indexOf("clara.io/") !== -1) { pageType = CLARA; } + if (location.href.indexOf(marketplaceBaseURL + "/marketplace/items/") !== -1) { pageType = HIFI_ITEM_PAGE; } + + injectCommonCode(pageType === DIRECTORY); + switch (pageType) { + case DIRECTORY: + injectDirectoryCode(); + break; + case HIFI: + injectHiFiCode(); + break; + case CLARA: + injectClaraCode(); + break; + case HIFI_ITEM_PAGE: + injectHiFiItemPageCode(); + break; + + } + } + + function onLoad() { + EventBridge.scriptEventReceived.connect(function (message) { + message = JSON.parse(message); + if (message.type === CAN_WRITE_ASSETS) { + canWriteAssets = message.canWriteAssets; + } else if (message.type === CLARA_IO_CANCEL_DOWNLOAD) { + cancelClaraDownload(); + } else if (message.type === "marketplaces") { + if (message.action === "commerceSetting") { + limitedCommerce = !!message.data.limitedCommerce; + commerceMode = !!message.data.commerceMode; + userIsLoggedIn = !!message.data.userIsLoggedIn; + walletNeedsSetup = !!message.data.walletNeedsSetup; + marketplaceBaseURL = message.data.metaverseServerURL; + if (marketplaceBaseURL.indexOf('metaverse.') !== -1) { + marketplaceBaseURL = marketplaceBaseURL.replace('metaverse.', ''); + } + messagesWaiting = message.data.messagesWaiting; + injectCode(); + } + } + }); + + // Request commerce setting + // Code is injected into the webpage after the setting comes back. + EventBridge.emitWebEvent(JSON.stringify({ + type: "REQUEST_SETTING" + })); + } + + // Load / unload. + window.addEventListener("load", onLoad); // More robust to Web site issues than using $(document).ready(). + window.addEventListener("page:change", onLoad); // Triggered after Marketplace HTML is changed +}()); From 8aedc98a584870156998ba615bd935511621e147 Mon Sep 17 00:00:00 2001 From: r3tk0n Date: Mon, 11 Mar 2019 10:54:48 -0700 Subject: [PATCH 108/446] Fix QML formatting issue. --- interface/resources/qml/hifi/audio/Audio.qml | 57 ++++++++------------ 1 file changed, 21 insertions(+), 36 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index ecc3297d9f..5e849acedf 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -171,15 +171,14 @@ Rectangle { } } - Separator {} - - - ColumnLayout { - id: pttColumn - spacing: 24; - x: 2 * margins.paddings; + Separator { id: pttStartSeparator; } + Item { + width: rightMostInputLevelPos; + height: pttSwitch.height + pttText.height + 24; HifiControlsUit.Switch { id: pttSwitch + x: 2 * margins.paddings; + anchors.top: parent.top; height: root.switchHeight; switchWidth: root.switchWidth; labelTextOn: qsTr("Push To Talk (T)"); @@ -200,39 +199,25 @@ Rectangle { }); // restore binding } } - Item { - id: pttTextContainer - width: rightMostInputLevelPos - height: pttTextMetrics.height - anchors.left: parent.left - anchors.leftMargin: -margins.padding - TextMetrics { - id: pttTextMetrics - text: pttText.text - font: pttText.font - } - RalewayRegular { - id: pttText - color: hifi.colors.white; - width: parent.width; - wrapMode: (bar.currentIndex === 0) ? Text.NoWrap : Text.WordWrap; - font.italic: true - size: 16; + RalewayRegular { + id: pttText + x: 2 * margins.paddings; + color: hifi.colors.white; + anchors.bottom: parent.bottom; + width: rightMostInputLevelPos; + height: paintedHeight; + wrapMode: Text.WordWrap; + font.italic: true + size: 16; - text: (bar.currentIndex === 0) ? qsTr("Press and hold the button \"T\" to unmute.") : - qsTr("Press and hold grip triggers on both of your controllers to unmute."); - onTextChanged: { - if (pttTextMetrics.width > pttTextContainer.width) { - pttTextContainer.height = Math.ceil(pttTextMetrics.width / pttTextContainer.width) * pttTextMetrics.height; - } else { - pttTextContainer.height = pttTextMetrics.height; - } - } - } + text: (bar.currentIndex === 0) ? qsTr("Press and hold the button \"T\" to unmute.") : + qsTr("Press and hold grip triggers on both of your controllers to unmute."); } } - Separator {} + Separator { + id: pttEndSeparator; + } Item { From 4371723145a2c30d0e57ee2a3613d88fdb4a706a Mon Sep 17 00:00:00 2001 From: danteruiz Date: Mon, 11 Mar 2019 11:16:53 -0700 Subject: [PATCH 109/446] fix soft entity popping --- .../entities-renderer/src/RenderableModelEntityItem.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 03c50008a0..643e5afb70 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -181,9 +181,11 @@ void RenderableModelEntityItem::updateModelBounds() { updateRenderItems = true; } - if (model->getScaleToFitDimensions() != getScaledDimensions() || - model->getRegistrationPoint() != getRegistrationPoint() || - !model->getIsScaledToFit()) { + bool overridingModelTransform = model->isOverridingModelTransformAndOffset(); + if (!overridingModelTransform && + (model->getScaleToFitDimensions() != getScaledDimensions() || + model->getRegistrationPoint() != getRegistrationPoint() || + !model->getIsScaledToFit())) { // The machinery for updateModelBounds will give existing models the opportunity to fix their // translation/rotation/scale/registration. The first two are straightforward, but the latter two // have guards to make sure they don't happen after they've already been set. Here we reset those guards. From b7e1798d1bf9a50003c095ee70180b23279c5e91 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 11 Mar 2019 11:28:30 -0700 Subject: [PATCH 110/446] better handling of unrigged vertices on skinned mesh --- libraries/fbx/src/FBXSerializer.cpp | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 5246242a1e..ca3659636f 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -1043,7 +1043,11 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr cluster.transformLink = createMat4(values); } } - clusters.insert(getID(object.properties), cluster); + + // skip empty clusters + if (cluster.indices.size() > 0 && cluster.weights.size() > 0) { + clusters.insert(getID(object.properties), cluster); + } } else if (object.properties.last() == "BlendShapeChannel") { QByteArray name = object.properties.at(1).toByteArray(); @@ -1510,19 +1514,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr const HFMCluster& hfmCluster = extracted.mesh.clusters.at(i); int jointIndex = hfmCluster.jointIndex; HFMJoint& joint = hfmModel.joints[jointIndex]; - glm::mat4 transformJointToMesh = inverseModelTransform * joint.bindTransform; - glm::vec3 boneEnd = extractTranslation(transformJointToMesh); - glm::vec3 boneBegin = boneEnd; - glm::vec3 boneDirection; - float boneLength = 0.0f; - if (joint.parentIndex != -1) { - boneBegin = extractTranslation(inverseModelTransform * hfmModel.joints[joint.parentIndex].bindTransform); - boneDirection = boneEnd - boneBegin; - boneLength = glm::length(boneDirection); - if (boneLength > EPSILON) { - boneDirection /= boneLength; - } - } glm::mat4 meshToJoint = glm::inverse(joint.bindTransform) * modelTransform; ShapeVertices& points = shapeVertices.at(jointIndex); @@ -1575,16 +1566,19 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr int j = i * WEIGHTS_PER_VERTEX; // normalize weights into uint16_t - float totalWeight = weightAccumulators[j]; - for (int k = j + 1; k < j + WEIGHTS_PER_VERTEX; ++k) { + float totalWeight = 0.0f; + for (int k = j; k < j + WEIGHTS_PER_VERTEX; ++k) { totalWeight += weightAccumulators[k]; } + + const float ALMOST_HALF = 0.499f; if (totalWeight > 0.0f) { - const float ALMOST_HALF = 0.499f; float weightScalingFactor = (float)(UINT16_MAX) / totalWeight; for (int k = j; k < j + WEIGHTS_PER_VERTEX; ++k) { extracted.mesh.clusterWeights[k] = (uint16_t)(weightScalingFactor * weightAccumulators[k] + ALMOST_HALF); } + } else { + extracted.mesh.clusterWeights[j] = (uint16_t)((float)(UINT16_MAX) + ALMOST_HALF); } } } else { From 97b01bad70d081f3245028618913c4f3e0c5f63a Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 11 Mar 2019 12:23:26 -0700 Subject: [PATCH 111/446] tellPhysics to children when animating model --- libraries/entities-renderer/src/RenderableModelEntityItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 03c50008a0..54254ef26c 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -1032,7 +1032,7 @@ void RenderableModelEntityItem::copyAnimationJointDataToModel() { }); if (changed) { - locationChanged(false, true); + locationChanged(true, true); } } From 2fb5e1ebc263efc96dedbe4fbf0ca7eba30f7c2d Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 11 Mar 2019 12:36:29 -0700 Subject: [PATCH 112/446] quiet some logging --- scripts/system/miniTablet.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/system/miniTablet.js b/scripts/system/miniTablet.js index 449921514c..91c8b1edcf 100644 --- a/scripts/system/miniTablet.js +++ b/scripts/system/miniTablet.js @@ -1048,6 +1048,7 @@ // Track grabbed state and item. switch (message.action) { case "grab": + case "equip": grabbingHand = HAND_NAMES.indexOf(message.joint); grabbedItem = message.grabbedEntity; break; @@ -1056,7 +1057,7 @@ grabbedItem = null; break; default: - error("Unexpected grab message!"); + error("Unexpected grab message: " + JSON.stringify(message)); return; } @@ -1144,4 +1145,4 @@ setUp(); Script.scriptEnding.connect(tearDown); -}()); \ No newline at end of file +}()); From e515e9cc66d627c543e3e50d8983ffdb38936703 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 11 Mar 2019 12:38:54 -0700 Subject: [PATCH 113/446] fix cloneEntity function --- scripts/system/libraries/cloneEntityUtils.js | 15 ++++++--------- .../system/libraries/controllerDispatcherUtils.js | 4 +++- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/scripts/system/libraries/cloneEntityUtils.js b/scripts/system/libraries/cloneEntityUtils.js index e0f4aba84a..f789e19cd8 100644 --- a/scripts/system/libraries/cloneEntityUtils.js +++ b/scripts/system/libraries/cloneEntityUtils.js @@ -5,8 +5,8 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -/* global entityIsCloneable:true, getGrabbableData:true, cloneEntity:true, propsAreCloneDynamic:true, Script, - propsAreCloneDynamic:true, Entities*/ +/* global entityIsCloneable:true, cloneEntity:true, propsAreCloneDynamic:true, Script, + propsAreCloneDynamic:true, Entities, Uuid */ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); @@ -47,13 +47,10 @@ propsAreCloneDynamic = function(props) { }; cloneEntity = function(props) { - var entityToClone = props.id; - var props = Entities.getEntityProperties(entityToClone, ['certificateID', 'certificateType']) - var certificateID = props.certificateID; - // ensure entity is cloneable and does not have a certificate ID, whereas cloneable limits - // will now be handled by the server where the entity add will fail if limit reached - if (entityIsCloneable(props) && (!!certificateID || props.certificateType.indexOf('domainUnlimited') >= 0)) { - var cloneID = Entities.cloneEntity(entityToClone); + var entityIDToClone = props.id; + if (entityIsCloneable(props) && + (Uuid.isNull(props.certificateID) || props.certificateType.indexOf('domainUnlimited') >= 0)) { + var cloneID = Entities.cloneEntity(entityIDToClone); return cloneID; } return null; diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js index 385ed954b0..5cb95f625d 100644 --- a/scripts/system/libraries/controllerDispatcherUtils.js +++ b/scripts/system/libraries/controllerDispatcherUtils.js @@ -156,7 +156,9 @@ DISPATCHER_PROPERTIES = [ "grab.equippableIndicatorOffset", "userData", "avatarEntity", - "owningAvatarID" + "owningAvatarID", + "certificateID", + "certificateType" ]; // priority -- a lower priority means the module will be asked sooner than one with a higher priority in a given update step From 6032fde5e55fb6688f43f701fefcb20b132052d2 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Mon, 11 Mar 2019 12:43:34 -0700 Subject: [PATCH 114/446] Unique command line to run Interface on Quest. --- tools/nitpick/src/Nitpick.cpp | 2 +- tools/nitpick/src/TestRunnerMobile.cpp | 28 +++++++++++++++++--------- tools/nitpick/src/TestRunnerMobile.h | 2 ++ 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/tools/nitpick/src/Nitpick.cpp b/tools/nitpick/src/Nitpick.cpp index cf50774617..e72de9d1ad 100644 --- a/tools/nitpick/src/Nitpick.cpp +++ b/tools/nitpick/src/Nitpick.cpp @@ -38,7 +38,7 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { _ui.plainTextEdit->setReadOnly(true); - setWindowTitle("Nitpick - v3.1.2"); + setWindowTitle("Nitpick - v3.1.3"); clientProfiles << "VR-High" << "Desktop-High" << "Desktop-Low" << "Mobile-Touch" << "VR-Standalone"; _ui.clientProfileComboBox->insertItems(0, clientProfiles); diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 4d0d18ef3d..284b4de15c 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -98,12 +98,12 @@ void TestRunnerMobile::connectDevice() { QString deviceID = tokens[0]; QString modelID = tokens[3].split(':')[1]; - QString modelName = "UNKNOWN"; + _modelName = "UNKNOWN"; if (modelNames.count(modelID) == 1) { - modelName = modelNames[modelID]; + _modelName = modelNames[modelID]; } - _detectedDeviceLabel->setText(modelName + " [" + deviceID + "]"); + _detectedDeviceLabel->setText(_modelName + " [" + deviceID + "]"); _pullFolderButton->setEnabled(true); _folderLineEdit->setEnabled(true); _downloadAPKPushbutton->setEnabled(true); @@ -198,14 +198,22 @@ void TestRunnerMobile::runInterface() { ? QString("https://raw.githubusercontent.com/") + nitpick->getSelectedUser() + "/hifi_tests/" + nitpick->getSelectedBranch() + "/tests/testRecursive.js" : _scriptURL->text(); + // Quest and Android have different commands to run interface + QString startCommand; + if (_modelName == "Quest") { + startCommand = "io.highfidelity.questInterface/.PermissionsChecker"; + } else { + startCommand = "io.highfidelity.hifiinterface/.PermissionChecker"; + } + QString command = _adbInterface->getAdbCommand() + - " shell am start -n io.highfidelity.hifiinterface/.PermissionChecker" + - " --es args \\\"" + - " --url file:///~/serverless/tutorial.json" + - " --no-updater" + - " --no-login-suggestion" + - " --testScript " + testScript + " quitWhenFinished" + - " --testResultsLocation /sdcard/snapshots" + + " shell am start -n " + startCommand + + " --es args \\\"" + + " --url file:///~/serverless/tutorial.json" + + " --no-updater" + + " --no-login-suggestion" + + " --testScript " + testScript + " quitWhenFinished" + + " --testResultsLocation /sdcard/snapshots" + "\\\""; appendLog(command); diff --git a/tools/nitpick/src/TestRunnerMobile.h b/tools/nitpick/src/TestRunnerMobile.h index f7b16da6f8..7dbf5456b3 100644 --- a/tools/nitpick/src/TestRunnerMobile.h +++ b/tools/nitpick/src/TestRunnerMobile.h @@ -75,5 +75,7 @@ private: std::map modelNames; AdbInterface* _adbInterface; + + QString _modelName; }; #endif From 577ff9695fb1e59cec27e96b16ca89a0bb9e9433 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Mon, 11 Mar 2019 13:53:19 -0700 Subject: [PATCH 115/446] TEST!!! --- interface/src/Application.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 6d9a1823a1..3d6a26d7e3 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1795,7 +1795,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo }); connect(offscreenUi.data(), &OffscreenUi::keyboardFocusActive, [this]() { -#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML) +#if !defined(DISABLE_QML) // Do not show login dialog if requested not to on the command line QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY); int index = arguments().indexOf(hifiNoLoginCommandLineKey); From 07ddd4e1dd1d716d78c9c29db8b6d6ec880d7005 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 11 Mar 2019 14:27:47 -0700 Subject: [PATCH 116/446] moving key press detection to JSON --- .../resources/controllers/keyboardMouse.json | 1 + interface/src/Application.cpp | 15 ++------------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/interface/resources/controllers/keyboardMouse.json b/interface/resources/controllers/keyboardMouse.json index 74c11203ef..9b3c711c63 100644 --- a/interface/resources/controllers/keyboardMouse.json +++ b/interface/resources/controllers/keyboardMouse.json @@ -5,6 +5,7 @@ { "from": "Keyboard.D", "when": ["Keyboard.RightMouseButton", "!Keyboard.Control"], "to": "Actions.LATERAL_RIGHT" }, { "from": "Keyboard.E", "when": "!Keyboard.Control", "to": "Actions.LATERAL_RIGHT" }, { "from": "Keyboard.Q", "when": "!Keyboard.Control", "to": "Actions.LATERAL_LEFT" }, + { "from": "Keyboard.T", "when": "!Keyboard.Control", "to": "Actions.TogglePushToTalk" }, { "comment" : "Mouse turn need to be small continuous increments", "from": { "makeAxis" : [ diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 3230419816..de4a6bb167 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1608,9 +1608,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo switch (actionEnum) { case Action::TOGGLE_PUSHTOTALK: if (state > 0.0f) { - audioScriptingInterface->setPushingToTalk(false); - } else if (state < 0.0f) { audioScriptingInterface->setPushingToTalk(true); + } else if (state <= 0.0f) { + audioScriptingInterface->setPushingToTalk(false); } break; @@ -4047,7 +4047,6 @@ void Application::keyPressEvent(QKeyEvent* event) { _keysPressed.insert(event->key(), *event); } - auto audioScriptingInterface = reinterpret_cast(DependencyManager::get().data()); _controllerScriptingInterface->emitKeyPressEvent(event); // send events to any registered scripts // if one of our scripts have asked to capture this event, then stop processing it if (_controllerScriptingInterface->isKeyCaptured(event) || isInterstitialMode()) { @@ -4215,10 +4214,6 @@ void Application::keyPressEvent(QKeyEvent* event) { } break; - case Qt::Key_T: - audioScriptingInterface->setPushingToTalk(true); - break; - case Qt::Key_P: { if (!isShifted && !isMeta && !isOption && !event->isAutoRepeat()) { AudioInjectorOptions options; @@ -4325,12 +4320,6 @@ void Application::keyReleaseEvent(QKeyEvent* event) { _keyboardMouseDevice->keyReleaseEvent(event); } - auto audioScriptingInterface = reinterpret_cast(DependencyManager::get().data()); - switch (event->key()) { - case Qt::Key_T: - audioScriptingInterface->setPushingToTalk(false); - break; - } } void Application::focusOutEvent(QFocusEvent* event) { From c9cb284c19534899224ac9162316b9d811446059 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Mon, 11 Mar 2019 14:33:00 -0700 Subject: [PATCH 117/446] Case 21467 - only update search when search field content has changed --- .../qml/hifi/commerce/marketplace/Marketplace.qml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index 07ded49956..fdeca07561 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -359,9 +359,11 @@ Rectangle { } onAccepted: { - root.searchString = searchField.text; - getMarketplaceItems(); - searchField.forceActiveFocus(); + if(root.searchString !== searchField.text) { + root.searchString = searchField.text; + getMarketplaceItems(); + searchField.forceActiveFocus(); + } } onActiveFocusChanged: { From b24b7fed3d96038aa5c55b2463534868935e1737 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 11 Mar 2019 15:34:41 -0700 Subject: [PATCH 118/446] the root node isn't the first onegit add ../.git add ../. --- libraries/fbx/src/FBXSerializer.cpp | 12 ++++++------ libraries/render-utils/src/CauterizedModel.cpp | 8 ++++---- libraries/render-utils/src/Model.cpp | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index ca3659636f..52f4189bdb 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -1486,8 +1486,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } - // if we don't have a skinned joint, parent to the model itself - if (extracted.mesh.clusters.isEmpty()) { + // the last cluster is the root cluster + { HFMCluster cluster; cluster.jointIndex = modelIDs.indexOf(modelID); if (cluster.jointIndex == -1) { @@ -1498,13 +1498,11 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } // whether we're skinned depends on how many clusters are attached - const HFMCluster& firstHFMCluster = extracted.mesh.clusters.at(0); - glm::mat4 inverseModelTransform = glm::inverse(modelTransform); if (clusterIDs.size() > 1) { // this is a multi-mesh joint const int WEIGHTS_PER_VERTEX = 4; int numClusterIndices = extracted.mesh.vertices.size() * WEIGHTS_PER_VERTEX; - extracted.mesh.clusterIndices.fill(0, numClusterIndices); + extracted.mesh.clusterIndices.fill(extracted.mesh.clusters.size() - 1, numClusterIndices); QVector weightAccumulators; weightAccumulators.fill(0.0f, numClusterIndices); @@ -1526,6 +1524,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr int newIndex = it.value(); // remember vertices with at least 1/4 weight + // FIXME: vertices with no weightpainting won't get recorded here const float EXPANSION_WEIGHT_THRESHOLD = 0.25f; if (weight >= EXPANSION_WEIGHT_THRESHOLD) { // transform to joint-frame and save for later @@ -1582,7 +1581,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } } else { - // this is a single-mesh joint + // this is a single-joint mesh + const HFMCluster& firstHFMCluster = extracted.mesh.clusters.at(0); int jointIndex = firstHFMCluster.jointIndex; HFMJoint& joint = hfmModel.joints[jointIndex]; diff --git a/libraries/render-utils/src/CauterizedModel.cpp b/libraries/render-utils/src/CauterizedModel.cpp index cfb78d6bbc..cfdcec6e99 100644 --- a/libraries/render-utils/src/CauterizedModel.cpp +++ b/libraries/render-utils/src/CauterizedModel.cpp @@ -245,7 +245,7 @@ void CauterizedModel::updateRenderItems() { Transform renderTransform = modelTransform; if (useDualQuaternionSkinning) { - if (meshState.clusterDualQuaternions.size() == 1) { + if (meshState.clusterDualQuaternions.size() <= 2) { const auto& dq = meshState.clusterDualQuaternions[0]; Transform transform(dq.getRotation(), dq.getScale(), @@ -253,7 +253,7 @@ void CauterizedModel::updateRenderItems() { renderTransform = modelTransform.worldTransform(transform); } } else { - if (meshState.clusterMatrices.size() == 1) { + if (meshState.clusterMatrices.size() <= 2) { renderTransform = modelTransform.worldTransform(Transform(meshState.clusterMatrices[0])); } } @@ -261,7 +261,7 @@ void CauterizedModel::updateRenderItems() { renderTransform = modelTransform; if (useDualQuaternionSkinning) { - if (cauterizedMeshState.clusterDualQuaternions.size() == 1) { + if (cauterizedMeshState.clusterDualQuaternions.size() <= 2) { const auto& dq = cauterizedMeshState.clusterDualQuaternions[0]; Transform transform(dq.getRotation(), dq.getScale(), @@ -269,7 +269,7 @@ void CauterizedModel::updateRenderItems() { renderTransform = modelTransform.worldTransform(Transform(transform)); } } else { - if (cauterizedMeshState.clusterMatrices.size() == 1) { + if (cauterizedMeshState.clusterMatrices.size() <= 2) { renderTransform = modelTransform.worldTransform(Transform(cauterizedMeshState.clusterMatrices[0])); } } diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index a8d3e504f1..3c6565fca9 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -247,7 +247,7 @@ void Model::updateRenderItems() { Transform renderTransform = modelTransform; if (useDualQuaternionSkinning) { - if (meshState.clusterDualQuaternions.size() == 1) { + if (meshState.clusterDualQuaternions.size() <= 2) { const auto& dq = meshState.clusterDualQuaternions[0]; Transform transform(dq.getRotation(), dq.getScale(), @@ -255,7 +255,7 @@ void Model::updateRenderItems() { renderTransform = modelTransform.worldTransform(Transform(transform)); } } else { - if (meshState.clusterMatrices.size() == 1) { + if (meshState.clusterMatrices.size() <= 2) { renderTransform = modelTransform.worldTransform(Transform(meshState.clusterMatrices[0])); } } From ca5ff3381b154d7466a6010e9b344e80cb94e407 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 11 Mar 2019 16:02:18 -0700 Subject: [PATCH 119/446] changing position of the mute warning setting --- interface/resources/qml/hifi/audio/Audio.qml | 54 ++++++++++---------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index a5138b3dd9..1a0457fd0a 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -125,22 +125,6 @@ Rectangle { } } - HifiControlsUit.Switch { - id: stereoInput; - height: root.switchHeight; - switchWidth: root.switchWidth; - labelTextOn: qsTr("Stereo input"); - backgroundOnColor: "#E3E3E3"; - checked: AudioScriptingInterface.isStereoInput; - onCheckedChanged: { - AudioScriptingInterface.isStereoInput = checked; - checked = Qt.binding(function() { return AudioScriptingInterface.isStereoInput; }); // restore binding - } - } - } - - ColumnLayout { - spacing: 24; HifiControlsUit.Switch { height: root.switchHeight; switchWidth: root.switchWidth; @@ -152,6 +136,23 @@ Rectangle { checked = Qt.binding(function() { return AudioScriptingInterface.noiseReduction; }); // restore binding } } + } + + ColumnLayout { + spacing: 24; + HifiControlsUit.Switch { + id: warnMutedSwitch + height: root.switchHeight; + switchWidth: root.switchWidth; + labelTextOn: qsTr("Warn when muted"); + backgroundOnColor: "#E3E3E3"; + checked: AudioScriptingInterface.warnWhenMuted; + onClicked: { + AudioScriptingInterface.warnWhenMuted = checked; + checked = Qt.binding(function() { return AudioScriptingInterface.warnWhenMuted; }); // restore binding + } + } + HifiControlsUit.Switch { id: audioLevelSwitch @@ -165,19 +166,20 @@ Rectangle { checked = Qt.binding(function() { return AvatarInputs.showAudioTools; }); // restore binding } } - } - RowLayout { - spacing: muteMic.spacing*2; - AudioControls.CheckBox { - spacing: muteMic.spacing - text: qsTr("Warn when muted"); - checked: AudioScriptingInterface.warnWhenMuted; - onClicked: { - AudioScriptingInterface.warnWhenMuted = checked; - checked = Qt.binding(function() { return AudioScriptingInterface.warnWhenMuted; }); // restore binding + HifiControlsUit.Switch { + id: stereoInput; + height: root.switchHeight; + switchWidth: root.switchWidth; + labelTextOn: qsTr("Stereo input"); + backgroundOnColor: "#E3E3E3"; + checked: AudioScriptingInterface.isStereoInput; + onCheckedChanged: { + AudioScriptingInterface.isStereoInput = checked; + checked = Qt.binding(function() { return AudioScriptingInterface.isStereoInput; }); // restore binding } } + } } From 80821e8b7e354ebd1d77a737b74c1e009e942538 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 11 Mar 2019 16:15:47 -0700 Subject: [PATCH 120/446] changing mic bar indicator when muted in PTT --- interface/resources/qml/hifi/audio/Audio.qml | 62 +++++++++---------- interface/resources/qml/hifi/audio/MicBar.qml | 6 +- 2 files changed, 32 insertions(+), 36 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index 5e849acedf..faa4f1de2f 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -140,6 +140,29 @@ Rectangle { checked = Qt.binding(function() { return AudioScriptingInterface.isStereoInput; }); // restore binding } } + + HifiControlsUit.Switch { + id: pttSwitch + height: root.switchHeight; + switchWidth: root.switchWidth; + labelTextOn: qsTr("Push To Talk (T)"); + backgroundOnColor: "#E3E3E3"; + checked: (bar.currentIndex === 0) ? AudioScriptingInterface.pushToTalkDesktop : AudioScriptingInterface.pushToTalkHMD; + onCheckedChanged: { + if (bar.currentIndex === 0) { + AudioScriptingInterface.pushToTalkDesktop = checked; + } else { + AudioScriptingInterface.pushToTalkHMD = checked; + } + checked = Qt.binding(function() { + if (bar.currentIndex === 0) { + return AudioScriptingInterface.pushToTalkDesktop; + } else { + return AudioScriptingInterface.pushToTalkHMD; + } + }); // restore binding + } + } } ColumnLayout { @@ -171,53 +194,26 @@ Rectangle { } } - Separator { id: pttStartSeparator; } Item { + anchors.left: parent.left width: rightMostInputLevelPos; - height: pttSwitch.height + pttText.height + 24; - HifiControlsUit.Switch { - id: pttSwitch - x: 2 * margins.paddings; - anchors.top: parent.top; - height: root.switchHeight; - switchWidth: root.switchWidth; - labelTextOn: qsTr("Push To Talk (T)"); - backgroundOnColor: "#E3E3E3"; - checked: (bar.currentIndex === 0) ? AudioScriptingInterface.pushToTalkDesktop : AudioScriptingInterface.pushToTalkHMD; - onCheckedChanged: { - if (bar.currentIndex === 0) { - AudioScriptingInterface.pushToTalkDesktop = checked; - } else { - AudioScriptingInterface.pushToTalkHMD = checked; - } - checked = Qt.binding(function() { - if (bar.currentIndex === 0) { - return AudioScriptingInterface.pushToTalkDesktop; - } else { - return AudioScriptingInterface.pushToTalkHMD; - } - }); // restore binding - } - } + height: pttText.height; RalewayRegular { id: pttText - x: 2 * margins.paddings; + x: margins.paddings; color: hifi.colors.white; - anchors.bottom: parent.bottom; width: rightMostInputLevelPos; height: paintedHeight; wrapMode: Text.WordWrap; font.italic: true size: 16; - text: (bar.currentIndex === 0) ? qsTr("Press and hold the button \"T\" to unmute.") : - qsTr("Press and hold grip triggers on both of your controllers to unmute."); + text: (bar.currentIndex === 0) ? qsTr("Press and hold the button \"T\" to talk.") : + qsTr("Press and hold grip triggers on both of your controllers to talk."); } } - Separator { - id: pttEndSeparator; - } + Separator { } Item { diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index 491b9f9554..f51da9c381 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -159,7 +159,7 @@ Rectangle { color: parent.color; - text: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) ? "MUTED PTT-(T)" : (AudioScriptingInterface.muted ? "MUTED" : "MUTE"); + text: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) ? (HMD.active ? "MUTED PTT" : "MUTED PTT-(T)") : (AudioScriptingInterface.muted ? "MUTED" : "MUTE"); font.pointSize: 12; } @@ -169,7 +169,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - width: AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk ? 25 : 50; + width: AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk ? (HMD.active ? 27 : 25) : 50; height: 4; color: parent.color; } @@ -180,7 +180,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - width: AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk ? 25 : 50; + width: AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk ? (HMD.active ? 27 : 25) : 50; height: 4; color: parent.color; } From 3c5fd069595dec2595dd74fbf4e78dee30f5a0a7 Mon Sep 17 00:00:00 2001 From: Jason Najera <39922250+r3tk0n@users.noreply.github.com> Date: Mon, 11 Mar 2019 16:16:58 -0700 Subject: [PATCH 121/446] Remove useless comment. Comment was not helpful. --- interface/src/scripting/Audio.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index c4dfcffb61..6b4ad80231 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -40,7 +40,6 @@ Audio::Audio() : _devices(_contextIsHMD) { connect(client, &AudioClient::inputLoudnessChanged, this, &Audio::onInputLoudnessChanged); connect(client, &AudioClient::inputVolumeChanged, this, &Audio::setInputVolume); connect(this, &Audio::contextChanged, &_devices, &AudioDevices::onContextChanged); - // when pushing to talk changed, handle it. connect(this, &Audio::pushingToTalkChanged, this, &Audio::handlePushedToTalk); enableNoiseReduction(enableNoiseReductionSetting.get()); onContextChanged(); From 84b177996b346deaf82712c4ff74f0ebfa8c608b Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 11 Mar 2019 16:43:26 -0700 Subject: [PATCH 122/446] removing dead code --- .../controllers/controllerModules/pushToTalk.js | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/scripts/system/controllers/controllerModules/pushToTalk.js b/scripts/system/controllers/controllerModules/pushToTalk.js index 6b1bacc367..11335ba2f5 100644 --- a/scripts/system/controllers/controllerModules/pushToTalk.js +++ b/scripts/system/controllers/controllerModules/pushToTalk.js @@ -11,17 +11,10 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); Script.include("/~/system/libraries/controllers.js"); -(function () { // BEGIN LOCAL_SCOPE +(function() { // BEGIN LOCAL_SCOPE function PushToTalkHandler() { var _this = this; this.active = false; - //var pttMapping, mappingName; - - this.setup = function () { - //mappingName = 'Hifi-PTT-Dev-' + Math.random(); - //pttMapping = Controller.newMapping(mappingName); - //pttMapping.enable(); - }; this.shouldTalk = function (controllerData) { // Set up test against controllerData here... @@ -53,10 +46,6 @@ Script.include("/~/system/libraries/controllers.js"); return makeRunningValues(true, [], []); }; - this.cleanup = function () { - //pttMapping.disable(); - }; - this.parameters = makeDispatcherModuleParameters( 950, ["head"], @@ -68,9 +57,8 @@ Script.include("/~/system/libraries/controllers.js"); enableDispatcherModule("PushToTalk", pushToTalk); function cleanup() { - pushToTalk.cleanup(); disableDispatcherModule("PushToTalk"); }; Script.scriptEnding.connect(cleanup); -}()); // END LOCAL_SCOPE +}()); // END LOCAL_SCOPE From d4b77d15cc4a84ea2eea91cd3eb13ea7da9b5ac8 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Mon, 11 Mar 2019 17:11:09 -0700 Subject: [PATCH 123/446] Use correct snapshots folder. Fix bug in APK installer. --- tools/nitpick/src/TestRunnerMobile.cpp | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index d7800f35b4..62630cc7b3 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -43,7 +43,7 @@ TestRunnerMobile::TestRunnerMobile( _installAPKPushbutton = installAPKPushbutton; _runInterfacePushbutton = runInterfacePushbutton; - folderLineEdit->setText("/sdcard/DCIM/TEST"); + folderLineEdit->setText("/sdcard/snapshots"); modelNames["SM_G955U1"] = "Samsung S8+ unlocked"; modelNames["SM_N960U1"] = "Samsung Note 9 unlocked"; @@ -163,22 +163,16 @@ void TestRunnerMobile::installAPK() { _adbInterface = new AdbInterface(); } - if (_installerFilename.isNull()) { - QString installerPathname = QFileDialog::getOpenFileName(nullptr, "Please select the APK", _workingFolder, - "Available APKs (*.apk)" - ); + QString installerPathname = QFileDialog::getOpenFileName(nullptr, "Please select the APK", _workingFolder, + "Available APKs (*.apk)" + ); - if (installerPathname.isNull()) { - return; - } - - // Remove the path - QStringList parts = installerPathname.split('/'); - _installerFilename = parts[parts.length() - 1]; + if (installerPathname.isNull()) { + return; } _statusLabel->setText("Installing"); - QString command = _adbInterface->getAdbCommand() + " install -r -d " + _workingFolder + "/" + _installerFilename + " >" + _workingFolder + "/installOutput.txt"; + QString command = _adbInterface->getAdbCommand() + " install -r -d " + installerPathname + " >" + _workingFolder + "/installOutput.txt"; appendLog(command); system(command.toStdString().c_str()); _statusLabel->setText("Installation complete"); From 2b32b77bed0875cb72c98948f82fcc7aa2513b37 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 11 Mar 2019 17:32:40 -0700 Subject: [PATCH 124/446] handle case when clusterMatrices.size() == 0 --- .vs/slnx.sqlite | Bin 0 -> 77824 bytes libraries/render-utils/src/CauterizedModel.cpp | 8 ++++---- libraries/render-utils/src/Model.cpp | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 .vs/slnx.sqlite diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..4227190a376249e4edb90c9be12165dbacd46945 GIT binary patch literal 77824 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 Date: Mon, 11 Mar 2019 17:50:20 -0700 Subject: [PATCH 125/446] debug statements to find the node parsing error --- libraries/animation/src/AnimClip.cpp | 2 +- libraries/fbx/src/FBXSerializer.cpp | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index 4fe02e9307..5d846a8f84 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -154,7 +154,7 @@ void AnimClip::copyFromNetworkAnim() { const float avatarToAnimationHeightRatio = avatarHeightInMeters / animHeightInMeters; const float unitsRatio = 1.0f / (avatarUnitScale / animationUnitScale); const float parentScaleRatio = 1.0f / (avatarHipsParentScale / animHipsParentScale); - + qCDebug(animation) << " avatar unit scale " << avatarUnitScale << " animation unit scale " << animationUnitScale << " avatar height " << avatarHeightInMeters << " animation height " << animHeightInMeters << " avatar scale " << avatarHipsParentScale << " animation scale " << animHipsParentScale; boneLengthScale = avatarToAnimationHeightRatio * unitsRatio * parentScaleRatio; } diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 5246242a1e..e6e3d73815 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -142,6 +142,7 @@ glm::mat4 getGlobalTransform(const QMultiMap& _connectionParen visitedNodes.append(nodeID); // Append each node we visit const FBXModel& fbxModel = fbxModels.value(nodeID); + qCDebug(modelformat) << "this fbx model name is " << fbxModel.name; globalTransform = glm::translate(fbxModel.translation) * fbxModel.preTransform * glm::mat4_cast(fbxModel.preRotation * fbxModel.rotation * fbxModel.postRotation) * fbxModel.postTransform * globalTransform; if (fbxModel.hasGeometricOffset) { @@ -201,13 +202,23 @@ public: void appendModelIDs(const QString& parentID, const QMultiMap& connectionChildMap, QHash& fbxModels, QSet& remainingModels, QVector& modelIDs, bool isRootNode = false) { if (remainingModels.contains(parentID)) { + qCDebug(modelformat) << " remaining models contains parent " << parentID; modelIDs.append(parentID); remainingModels.remove(parentID); } - int parentIndex = isRootNode ? -1 : modelIDs.size() - 1; + int parentIndex = 1000; + if (isRootNode) { + qCDebug(modelformat) << " found a root node " << parentID; + parentIndex = -1; + } else { + parentIndex = modelIDs.size() - 1; + } + //int parentIndex = isRootNode ? -1 : modelIDs.size() - 1; foreach (const QString& childID, connectionChildMap.values(parentID)) { + qCDebug(modelformat) << " searching children, parent id " << parentID; if (remainingModels.contains(childID)) { FBXModel& fbxModel = fbxModels[childID]; + qCDebug(modelformat) << " child id " << fbxModel.name; if (fbxModel.parentIndex == -1) { fbxModel.parentIndex = parentIndex; appendModelIDs(childID, connectionChildMap, fbxModels, remainingModels, modelIDs); @@ -441,6 +452,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr QString hifiGlobalNodeID; unsigned int meshIndex = 0; haveReportedUnhandledRotationOrder = false; + int nodeParentId = -1; foreach (const FBXNode& child, node.children) { if (child.name == "FBXHeaderExtension") { @@ -497,6 +509,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } else if (child.name == "Objects") { foreach (const FBXNode& object, child.children) { + nodeParentId++; if (object.name == "Geometry") { if (object.properties.at(2) == "Mesh") { meshes.insert(getID(object.properties), extractMesh(object, meshIndex)); @@ -505,6 +518,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr blendshapes.append(extracted); } } else if (object.name == "Model") { + qCDebug(modelformat) << "model name from object properties " << getName(object.properties) << " node parentID " << nodeParentId; QString name = getName(object.properties); QString id = getID(object.properties); modelIDsToNames.insert(id, name); @@ -1181,6 +1195,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr #endif } + // TODO: check if is code is needed if (!lights.empty()) { if (hifiGlobalNodeID.isEmpty()) { @@ -1211,6 +1226,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr for (QHash::const_iterator fbxModel = fbxModels.constBegin(); fbxModel != fbxModels.constEnd(); fbxModel++) { // models with clusters must be parented to the cluster top // Unless the model is a root node. + qCDebug(modelformat) << "fbx model name " << fbxModel.key(); bool isARootNode = !modelIDs.contains(_connectionParentMap.value(fbxModel.key())); if (!isARootNode) { foreach(const QString& deformerID, _connectionChildMap.values(fbxModel.key())) { @@ -1266,6 +1282,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr joint.parentIndex = fbxModel.parentIndex; int jointIndex = hfmModel.joints.size(); + qCDebug(modelformat) << "fbx joint name " << fbxModel.name << " joint index " << jointIndex << " parent index " << joint.parentIndex; + joint.translation = fbxModel.translation; // these are usually in centimeters joint.preTransform = fbxModel.preTransform; joint.preRotation = fbxModel.preRotation; From f0bf87b3c33fe27a6e1c5d6ae1c2a42811abb05c Mon Sep 17 00:00:00 2001 From: David Back Date: Mon, 11 Mar 2019 17:59:48 -0700 Subject: [PATCH 126/446] fix joint out not being renamed with jointRotationOffset2 --- .../src/model-baker/PrepareJointsTask.cpp | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp index b8bcdb386e..a746b76c1f 100644 --- a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp +++ b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp @@ -101,23 +101,24 @@ void PrepareJointsTask::run(const baker::BakeContextPointer& context, const Inpu } if (newJointRot) { - for (const auto& jointIn : jointsIn) { + for (auto& jointOut : jointsOut) { - auto jointNameMapKey = jointNameMapping.key(jointIn.name); - int mappedIndex = jointIndices.value(jointIn.name); + auto jointNameMapKey = jointNameMapping.key(jointOut.name); + int mappedIndex = jointIndices.value(jointOut.name); if (jointNameMapping.contains(jointNameMapKey)) { - // delete and replace with hifi name - jointIndices.remove(jointIn.name); - jointIndices.insert(jointNameMapKey, mappedIndex); + jointIndices.remove(jointOut.name); + jointOut.name = jointNameMapKey; + jointIndices.insert(jointOut.name, mappedIndex); } else { // nothing mapped to this fbx joint name - if (jointNameMapping.contains(jointIn.name)) { + if (jointNameMapping.contains(jointOut.name)) { // but the name is in the list of hifi names is mapped to a different joint - int extraIndex = jointIndices.value(jointIn.name); - jointIndices.remove(jointIn.name); - jointIndices.insert("", extraIndex); + int extraIndex = jointIndices.value(jointOut.name); + jointIndices.remove(jointOut.name); + jointOut.name = ""; + jointIndices.insert(jointOut.name, extraIndex); } } } From 065897a8f341ee38058c155a1626daa8982c9132 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Mon, 11 Mar 2019 18:56:23 -0700 Subject: [PATCH 127/446] TEST!!! --- interface/src/Application.cpp | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 3d6a26d7e3..852c4eb695 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1795,19 +1795,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo }); connect(offscreenUi.data(), &OffscreenUi::keyboardFocusActive, [this]() { -#if !defined(DISABLE_QML) - // Do not show login dialog if requested not to on the command line - QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY); - int index = arguments().indexOf(hifiNoLoginCommandLineKey); - if (index != -1) { - resumeAfterLoginDialogActionTaken(); - return; - } - - showLoginScreen(); -#else - resumeAfterLoginDialogActionTaken(); -#endif + resumeAfterLoginDialogActionTaken(); }); // Make sure we don't time out during slow operations at startup From 80168058f7a427b49a87a0bdbbd68e595e429629 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Mon, 11 Mar 2019 20:22:07 -0700 Subject: [PATCH 128/446] Correct APK installation. --- tools/nitpick/src/TestRunnerMobile.cpp | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 284b4de15c..eaebb6ca5a 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -164,22 +164,20 @@ void TestRunnerMobile::installAPK() { _adbInterface = new AdbInterface(); } - if (_installerFilename.isNull()) { - QString installerPathname = QFileDialog::getOpenFileName(nullptr, "Please select the APK", _workingFolder, - "Available APKs (*.apk)" - ); + QString installerPathname = QFileDialog::getOpenFileName(nullptr, "Please select the APK", _workingFolder, + "Available APKs (*.apk)" + ); - if (installerPathname.isNull()) { - return; - } - - // Remove the path - QStringList parts = installerPathname.split('/'); - _installerFilename = parts[parts.length() - 1]; + if (installerPathname.isNull()) { + return; } + // Remove the path + QStringList parts = installerPathname.split('/'); + _installerFilename = parts[parts.length() - 1]; + _statusLabel->setText("Installing"); - QString command = _adbInterface->getAdbCommand() + " install -r -d " + _workingFolder + "/" + _installerFilename + " >" + _workingFolder + "/installOutput.txt"; + QString command = _adbInterface->getAdbCommand() + " install -r -d " + installerPathname + " >" + _workingFolder + "/installOutput.txt"; appendLog(command); system(command.toStdString().c_str()); _statusLabel->setText("Installation complete"); From 1a9f33ef660228e0e947cc8bc443b9655640d046 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Mon, 11 Mar 2019 20:24:34 -0700 Subject: [PATCH 129/446] Removed test. --- interface/src/Application - Copy.cpp | 9191 ++++++++++++++++++++++++++ interface/src/Application.cpp | 14 +- 2 files changed, 9204 insertions(+), 1 deletion(-) create mode 100644 interface/src/Application - Copy.cpp diff --git a/interface/src/Application - Copy.cpp b/interface/src/Application - Copy.cpp new file mode 100644 index 0000000000..ca8883f660 --- /dev/null +++ b/interface/src/Application - Copy.cpp @@ -0,0 +1,9191 @@ +// +// Application.cpp +// interface/src +// +// Created by Andrzej Kapolka on 5/10/13. +// Copyright 2013 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 +// + + +#include "Application.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include + + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "ui/overlays/ContextOverlayInterface.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "LocationBookmarks.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "recording/ClipCache.h" + +#include "AudioClient.h" +#include "audio/AudioScope.h" +#include "avatar/AvatarManager.h" +#include "avatar/MyHead.h" +#include "avatar/AvatarPackager.h" +#include "avatar/MyCharacterController.h" +#include "CrashRecoveryHandler.h" +#include "CrashHandler.h" +#include "devices/DdeFaceTracker.h" +#include "DiscoverabilityManager.h" +#include "GLCanvas.h" +#include "InterfaceDynamicFactory.h" +#include "InterfaceLogging.h" +#include "LODManager.h" +#include "ModelPackager.h" +#include "scripting/Audio.h" +#include "networking/CloseEventSender.h" +#include "scripting/TestScriptingInterface.h" +#include "scripting/PlatformInfoScriptingInterface.h" +#include "scripting/AssetMappingsScriptingInterface.h" +#include "scripting/ClipboardScriptingInterface.h" +#include "scripting/DesktopScriptingInterface.h" +#include "scripting/AccountServicesScriptingInterface.h" +#include "scripting/HMDScriptingInterface.h" +#include "scripting/MenuScriptingInterface.h" +#include "graphics-scripting/GraphicsScriptingInterface.h" +#include "scripting/SettingsScriptingInterface.h" +#include "scripting/WindowScriptingInterface.h" +#include "scripting/ControllerScriptingInterface.h" +#include "scripting/RatesScriptingInterface.h" +#include "scripting/SelectionScriptingInterface.h" +#include "scripting/WalletScriptingInterface.h" +#include "scripting/TTSScriptingInterface.h" +#include "scripting/KeyboardScriptingInterface.h" + + + +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) +#include "SpeechRecognizer.h" +#endif +#include "ui/ResourceImageItem.h" +#include "ui/AddressBarDialog.h" +#include "ui/AvatarInputs.h" +#include "ui/DialogsManager.h" +#include "ui/LoginDialog.h" +#include "ui/Snapshot.h" +#include "ui/SnapshotAnimated.h" +#include "ui/StandAloneJSConsole.h" +#include "ui/Stats.h" +#include "ui/AnimStats.h" +#include "ui/UpdateDialog.h" +#include "ui/DomainConnectionModel.h" +#include "ui/Keyboard.h" +#include "Util.h" +#include "InterfaceParentFinder.h" +#include "ui/OctreeStatsProvider.h" + +#include "avatar/GrabManager.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "commerce/Ledger.h" +#include "commerce/Wallet.h" +#include "commerce/QmlCommerce.h" +#include "commerce/QmlMarketplace.h" +#include "ResourceRequestObserver.h" + +#include "webbrowser/WebBrowserSuggestionsEngine.h" +#include + + +#include "AboutUtil.h" + +#if defined(Q_OS_WIN) +#include + +#ifdef DEBUG_EVENT_QUEUE +// This is a HACK that uses private headers included with the qt source distrubution. +// To use this feature you need to add these directores to your include path: +// E:/Qt/5.10.1/Src/qtbase/include/QtCore/5.10.1/QtCore +// E:/Qt/5.10.1/Src/qtbase/include/QtCore/5.10.1 +#define QT_BOOTSTRAPPED +#include +#include +#undef QT_BOOTSTRAPPED +#endif + +// On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU +// FIXME seems to be broken. +extern "C" { + _declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001; +} +#endif + +#if defined(Q_OS_ANDROID) +#include +#include "AndroidHelper.h" +#endif + +#include "graphics/RenderEventHandler.h" + +Q_LOGGING_CATEGORY(trace_app_input_mouse, "trace.app.input.mouse") + +using namespace std; + +static QTimer locationUpdateTimer; +static QTimer identityPacketTimer; +static QTimer pingTimer; + +#if defined(Q_OS_ANDROID) +static bool DISABLE_WATCHDOG = true; +#else +static const QString DISABLE_WATCHDOG_FLAG{ "HIFI_DISABLE_WATCHDOG" }; +static bool DISABLE_WATCHDOG = nsightActive() || QProcessEnvironment::systemEnvironment().contains(DISABLE_WATCHDOG_FLAG); +#endif + +#if defined(USE_GLES) +static bool DISABLE_DEFERRED = true; +#else +static const QString RENDER_FORWARD{ "HIFI_RENDER_FORWARD" }; +static bool DISABLE_DEFERRED = QProcessEnvironment::systemEnvironment().contains(RENDER_FORWARD); +#endif + +#if !defined(Q_OS_ANDROID) +static const uint32_t MAX_CONCURRENT_RESOURCE_DOWNLOADS = 16; +#else +static const uint32_t MAX_CONCURRENT_RESOURCE_DOWNLOADS = 4; +#endif + +// For processing on QThreadPool, we target a number of threads after reserving some +// based on how many are being consumed by the application and the display plugin. However, +// we will never drop below the 'min' value +static const int MIN_PROCESSING_THREAD_POOL_SIZE = 1; + +static const QString SNAPSHOT_EXTENSION = ".jpg"; +static const QString JPG_EXTENSION = ".jpg"; +static const QString PNG_EXTENSION = ".png"; +static const QString SVO_EXTENSION = ".svo"; +static const QString SVO_JSON_EXTENSION = ".svo.json"; +static const QString JSON_GZ_EXTENSION = ".json.gz"; +static const QString JSON_EXTENSION = ".json"; +static const QString JS_EXTENSION = ".js"; +static const QString FST_EXTENSION = ".fst"; +static const QString FBX_EXTENSION = ".fbx"; +static const QString OBJ_EXTENSION = ".obj"; +static const QString AVA_JSON_EXTENSION = ".ava.json"; +static const QString WEB_VIEW_TAG = "noDownload=true"; +static const QString ZIP_EXTENSION = ".zip"; +static const QString CONTENT_ZIP_EXTENSION = ".content.zip"; + +static const float MIRROR_FULLSCREEN_DISTANCE = 0.789f; + +static const quint64 TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS = 1 * USECS_PER_SECOND; + +static const QString INFO_EDIT_ENTITIES_PATH = "html/edit-commands.html"; +static const QString INFO_HELP_PATH = "html/tabletHelp.html"; + +static const unsigned int THROTTLED_SIM_FRAMERATE = 15; +static const int THROTTLED_SIM_FRAME_PERIOD_MS = MSECS_PER_SECOND / THROTTLED_SIM_FRAMERATE; +static const int ENTITY_SERVER_ADDED_TIMEOUT = 5000; +static const int ENTITY_SERVER_CONNECTION_TIMEOUT = 5000; + +static const float INITIAL_QUERY_RADIUS = 10.0f; // priority radius for entities before physics enabled + +static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); + +Setting::Handle maxOctreePacketsPerSecond{"maxOctreePPS", DEFAULT_MAX_OCTREE_PPS}; + +Setting::Handle loginDialogPoppedUp{"loginDialogPoppedUp", false}; + +static const QString STANDARD_TO_ACTION_MAPPING_NAME = "Standard to Action"; +static const QString NO_MOVEMENT_MAPPING_NAME = "Standard to Action (No Movement)"; +static const QString NO_MOVEMENT_MAPPING_JSON = PathUtils::resourcesPath() + "/controllers/standard_nomovement.json"; + +static const QString MARKETPLACE_CDN_HOSTNAME = "mpassets.highfidelity.com"; +static const int INTERVAL_TO_CHECK_HMD_WORN_STATUS = 500; // milliseconds +static const QString DESKTOP_DISPLAY_PLUGIN_NAME = "Desktop"; +static const QString ACTIVE_DISPLAY_PLUGIN_SETTING_NAME = "activeDisplayPlugin"; +static const QString SYSTEM_TABLET = "com.highfidelity.interface.tablet.system"; +static const QString KEEP_ME_LOGGED_IN_SETTING_NAME = "keepMeLoggedIn"; + +static const float FOCUS_HIGHLIGHT_EXPANSION_FACTOR = 1.05f; + +#if defined(Q_OS_ANDROID) +static const QString TESTER_FILE = "/sdcard/_hifi_test_device.txt"; +#endif +const std::vector> Application::_acceptedExtensions { + { SVO_EXTENSION, &Application::importSVOFromURL }, + { SVO_JSON_EXTENSION, &Application::importSVOFromURL }, + { AVA_JSON_EXTENSION, &Application::askToWearAvatarAttachmentUrl }, + { JSON_EXTENSION, &Application::importJSONFromURL }, + { JS_EXTENSION, &Application::askToLoadScript }, + { FST_EXTENSION, &Application::askToSetAvatarUrl }, + { JSON_GZ_EXTENSION, &Application::askToReplaceDomainContent }, + { CONTENT_ZIP_EXTENSION, &Application::askToReplaceDomainContent }, + { ZIP_EXTENSION, &Application::importFromZIP }, + { JPG_EXTENSION, &Application::importImage }, + { PNG_EXTENSION, &Application::importImage } +}; + +class DeadlockWatchdogThread : public QThread { +public: + static const unsigned long HEARTBEAT_UPDATE_INTERVAL_SECS = 1; + static const unsigned long MAX_HEARTBEAT_AGE_USECS = 120 * USECS_PER_SECOND; // 2 mins with no checkin probably a deadlock + static const int WARNING_ELAPSED_HEARTBEAT = 500 * USECS_PER_MSEC; // warn if elapsed heartbeat average is large + static const int HEARTBEAT_SAMPLES = 100000; // ~5 seconds worth of samples + + // Set the heartbeat on launch + DeadlockWatchdogThread() { + setObjectName("Deadlock Watchdog"); + // Give the heartbeat an initial value + _heartbeat = usecTimestampNow(); + _paused = false; + connect(qApp, &QCoreApplication::aboutToQuit, [this] { + _quit = true; + }); + } + + void setMainThreadID(Qt::HANDLE threadID) { + _mainThreadID = threadID; + } + + static void updateHeartbeat() { + auto now = usecTimestampNow(); + auto elapsed = now - _heartbeat; + _movingAverage.addSample(elapsed); + _heartbeat = now; + } + + void deadlockDetectionCrash() { + setCrashAnnotation("_mod_faulting_tid", std::to_string((uint64_t)_mainThreadID)); + setCrashAnnotation("deadlock", "1"); + uint32_t* crashTrigger = nullptr; + *crashTrigger = 0xDEAD10CC; + } + + static void withPause(const std::function& lambda) { + pause(); + lambda(); + resume(); + } + static void pause() { + _paused = true; + } + + static void resume() { + // Update the heartbeat BEFORE resuming the checks + updateHeartbeat(); + _paused = false; + } + + void run() override { + while (!_quit) { + QThread::sleep(HEARTBEAT_UPDATE_INTERVAL_SECS); + // Don't do heartbeat detection under nsight + if (_paused) { + continue; + } + uint64_t lastHeartbeat = _heartbeat; // sample atomic _heartbeat, because we could context switch away and have it updated on us + uint64_t now = usecTimestampNow(); + auto lastHeartbeatAge = (now > lastHeartbeat) ? now - lastHeartbeat : 0; + auto elapsedMovingAverage = _movingAverage.getAverage(); + + if (elapsedMovingAverage > _maxElapsedAverage) { + qCDebug(interfaceapp_deadlock) << "DEADLOCK WATCHDOG WARNING:" + << "lastHeartbeatAge:" << lastHeartbeatAge + << "elapsedMovingAverage:" << elapsedMovingAverage + << "maxElapsed:" << _maxElapsed + << "PREVIOUS maxElapsedAverage:" << _maxElapsedAverage + << "NEW maxElapsedAverage:" << elapsedMovingAverage << "** NEW MAX ELAPSED AVERAGE **" + << "samples:" << _movingAverage.getSamples(); + _maxElapsedAverage = elapsedMovingAverage; + } + if (lastHeartbeatAge > _maxElapsed) { + qCDebug(interfaceapp_deadlock) << "DEADLOCK WATCHDOG WARNING:" + << "lastHeartbeatAge:" << lastHeartbeatAge + << "elapsedMovingAverage:" << elapsedMovingAverage + << "PREVIOUS maxElapsed:" << _maxElapsed + << "NEW maxElapsed:" << lastHeartbeatAge << "** NEW MAX ELAPSED **" + << "maxElapsedAverage:" << _maxElapsedAverage + << "samples:" << _movingAverage.getSamples(); + _maxElapsed = lastHeartbeatAge; + } + if (elapsedMovingAverage > WARNING_ELAPSED_HEARTBEAT) { + qCDebug(interfaceapp_deadlock) << "DEADLOCK WATCHDOG WARNING:" + << "lastHeartbeatAge:" << lastHeartbeatAge + << "elapsedMovingAverage:" << elapsedMovingAverage << "** OVER EXPECTED VALUE **" + << "maxElapsed:" << _maxElapsed + << "maxElapsedAverage:" << _maxElapsedAverage + << "samples:" << _movingAverage.getSamples(); + } + + if (lastHeartbeatAge > MAX_HEARTBEAT_AGE_USECS) { + qCDebug(interfaceapp_deadlock) << "DEADLOCK DETECTED -- " + << "lastHeartbeatAge:" << lastHeartbeatAge + << "[ lastHeartbeat :" << lastHeartbeat + << "now:" << now << " ]" + << "elapsedMovingAverage:" << elapsedMovingAverage + << "maxElapsed:" << _maxElapsed + << "maxElapsedAverage:" << _maxElapsedAverage + << "samples:" << _movingAverage.getSamples(); + + // Don't actually crash in debug builds, in case this apparent deadlock is simply from + // the developer actively debugging code + #ifdef NDEBUG + deadlockDetectionCrash(); + #endif + } + } + } + + static std::atomic _paused; + static std::atomic _heartbeat; + static std::atomic _maxElapsed; + static std::atomic _maxElapsedAverage; + static ThreadSafeMovingAverage _movingAverage; + + bool _quit { false }; + + Qt::HANDLE _mainThreadID = nullptr; +}; + +std::atomic DeadlockWatchdogThread::_paused; +std::atomic DeadlockWatchdogThread::_heartbeat; +std::atomic DeadlockWatchdogThread::_maxElapsed; +std::atomic DeadlockWatchdogThread::_maxElapsedAverage; +ThreadSafeMovingAverage DeadlockWatchdogThread::_movingAverage; + +bool isDomainURL(QUrl url) { + if (!url.isValid()) { + return false; + } + if (url.scheme() == URL_SCHEME_HIFI) { + return true; + } + if (url.scheme() != HIFI_URL_SCHEME_FILE) { + // TODO -- once Octree::readFromURL no-longer takes over the main event-loop, serverless-domain urls can + // be loaded over http(s) + // && url.scheme() != HIFI_URL_SCHEME_HTTP && + // url.scheme() != HIFI_URL_SCHEME_HTTPS + return false; + } + if (url.path().endsWith(".json", Qt::CaseInsensitive) || + url.path().endsWith(".json.gz", Qt::CaseInsensitive)) { + return true; + } + return false; +} + +#ifdef Q_OS_WIN +class MyNativeEventFilter : public QAbstractNativeEventFilter { +public: + static MyNativeEventFilter& getInstance() { + static MyNativeEventFilter staticInstance; + return staticInstance; + } + + bool nativeEventFilter(const QByteArray &eventType, void* msg, long* result) Q_DECL_OVERRIDE { + if (eventType == "windows_generic_MSG") { + MSG* message = (MSG*)msg; + + if (message->message == UWM_IDENTIFY_INSTANCES) { + *result = UWM_IDENTIFY_INSTANCES; + return true; + } + + if (message->message == UWM_SHOW_APPLICATION) { + MainWindow* applicationWindow = qApp->getWindow(); + if (applicationWindow->isMinimized()) { + applicationWindow->showNormal(); // Restores to windowed or maximized state appropriately. + } + qApp->setActiveWindow(applicationWindow); // Flashes the taskbar icon if not focus. + return true; + } + + if (message->message == WM_COPYDATA) { + COPYDATASTRUCT* pcds = (COPYDATASTRUCT*)(message->lParam); + QUrl url = QUrl((const char*)(pcds->lpData)); + if (isDomainURL(url)) { + DependencyManager::get()->handleLookupString(url.toString()); + return true; + } + } + + if (message->message == WM_DEVICECHANGE) { + const float MIN_DELTA_SECONDS = 2.0f; // de-bounce signal + static float lastTriggerTime = 0.0f; + const float deltaSeconds = secTimestampNow() - lastTriggerTime; + lastTriggerTime = secTimestampNow(); + if (deltaSeconds > MIN_DELTA_SECONDS) { + Midi::USBchanged(); // re-scan the MIDI bus + } + } + } + return false; + } +}; +#endif + +class LambdaEvent : public QEvent { + std::function _fun; +public: + LambdaEvent(const std::function & fun) : + QEvent(static_cast(ApplicationEvent::Lambda)), _fun(fun) { + } + LambdaEvent(std::function && fun) : + QEvent(static_cast(ApplicationEvent::Lambda)), _fun(fun) { + } + void call() const { _fun(); } +}; + +void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { + QString logMessage = LogHandler::getInstance().printMessage((LogMsgType) type, context, message); + + if (!logMessage.isEmpty()) { +#ifdef Q_OS_ANDROID + const char * local=logMessage.toStdString().c_str(); + switch (type) { + case QtDebugMsg: + __android_log_write(ANDROID_LOG_DEBUG,"Interface",local); + break; + case QtInfoMsg: + __android_log_write(ANDROID_LOG_INFO,"Interface",local); + break; + case QtWarningMsg: + __android_log_write(ANDROID_LOG_WARN,"Interface",local); + break; + case QtCriticalMsg: + __android_log_write(ANDROID_LOG_ERROR,"Interface",local); + break; + case QtFatalMsg: + default: + __android_log_write(ANDROID_LOG_FATAL,"Interface",local); + abort(); + } +#else + qApp->getLogger()->addMessage(qPrintable(logMessage)); +#endif + } +} + + +class ApplicationMeshProvider : public scriptable::ModelProviderFactory { +public: + virtual scriptable::ModelProviderPointer lookupModelProvider(const QUuid& uuid) override { + bool success; + if (auto nestable = DependencyManager::get()->find(uuid, success).lock()) { + auto type = nestable->getNestableType(); +#ifdef SCRIPTABLE_MESH_DEBUG + qCDebug(interfaceapp) << "ApplicationMeshProvider::lookupModelProvider" << uuid << SpatiallyNestable::nestableTypeToString(type); +#endif + switch (type) { + case NestableType::Entity: + return getEntityModelProvider(static_cast(uuid)); + case NestableType::Avatar: + return getAvatarModelProvider(uuid); + } + } + return nullptr; + } + +private: + scriptable::ModelProviderPointer getEntityModelProvider(EntityItemID entityID) { + scriptable::ModelProviderPointer provider; + auto entityTreeRenderer = qApp->getEntities(); + auto entityTree = entityTreeRenderer->getTree(); + if (auto entity = entityTree->findEntityByID(entityID)) { + if (auto renderer = entityTreeRenderer->renderableForEntityId(entityID)) { + provider = std::dynamic_pointer_cast(renderer); + provider->modelProviderType = NestableType::Entity; + } else { + qCWarning(interfaceapp) << "no renderer for entity ID" << entityID.toString(); + } + } + return provider; + } + + scriptable::ModelProviderPointer getAvatarModelProvider(QUuid sessionUUID) { + scriptable::ModelProviderPointer provider; + auto avatarManager = DependencyManager::get(); + if (auto avatar = avatarManager->getAvatarBySessionID(sessionUUID)) { + provider = std::dynamic_pointer_cast(avatar); + provider->modelProviderType = NestableType::Avatar; + } + return provider; + } +}; + +/**jsdoc + *

The Controller.Hardware.Application object has properties representing Interface's state. The property + * values are integer IDs, uniquely identifying each output. Read-only. These can be mapped to actions or functions or + * Controller.Standard items in a {@link RouteObject} mapping (e.g., using the {@link RouteObject#when} method). + * Each data value is either 1.0 for "true" or 0.0 for "false".

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
PropertyTypeDataDescription
CameraFirstPersonnumbernumberThe camera is in first-person mode. + *
CameraThirdPersonnumbernumberThe camera is in third-person mode. + *
CameraFSMnumbernumberThe camera is in full screen mirror mode.
CameraIndependentnumbernumberThe camera is in independent mode.
CameraEntitynumbernumberThe camera is in entity mode.
InHMDnumbernumberThe user is in HMD mode.
AdvancedMovementnumbernumberAdvanced movement controls are enabled. + *
SnapTurnnumbernumberSnap turn is enabled.
GroundednumbernumberThe user's avatar is on the ground.
NavigationFocusednumbernumberNot used.
+ * @typedef {object} Controller.Hardware-Application + */ + +static const QString STATE_IN_HMD = "InHMD"; +static const QString STATE_CAMERA_FULL_SCREEN_MIRROR = "CameraFSM"; +static const QString STATE_CAMERA_FIRST_PERSON = "CameraFirstPerson"; +static const QString STATE_CAMERA_THIRD_PERSON = "CameraThirdPerson"; +static const QString STATE_CAMERA_ENTITY = "CameraEntity"; +static const QString STATE_CAMERA_INDEPENDENT = "CameraIndependent"; +static const QString STATE_SNAP_TURN = "SnapTurn"; +static const QString STATE_ADVANCED_MOVEMENT_CONTROLS = "AdvancedMovement"; +static const QString STATE_GROUNDED = "Grounded"; +static const QString STATE_NAV_FOCUSED = "NavigationFocused"; +static const QString STATE_PLATFORM_WINDOWS = "PlatformWindows"; +static const QString STATE_PLATFORM_MAC = "PlatformMac"; +static const QString STATE_PLATFORM_ANDROID = "PlatformAndroid"; + +// Statically provided display and input plugins +extern DisplayPluginList getDisplayPlugins(); +extern InputPluginList getInputPlugins(); +extern void saveInputPluginSettings(const InputPluginList& plugins); + +// Parameters used for running tests from teh command line +const QString TEST_SCRIPT_COMMAND{ "--testScript" }; +const QString TEST_QUIT_WHEN_FINISHED_OPTION{ "quitWhenFinished" }; +const QString TEST_RESULTS_LOCATION_COMMAND{ "--testResultsLocation" }; + +bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { + const char** constArgv = const_cast(argv); + + qInstallMessageHandler(messageHandler); + + // HRS: I could not figure out how to move these any earlier in startup, so when using this option, be sure to also supply + // --allowMultipleInstances + auto reportAndQuit = [&](const char* commandSwitch, std::function report) { + const char* reportfile = getCmdOption(argc, constArgv, commandSwitch); + // Reports to the specified file, because stdout is set up to be captured for logging. + if (reportfile) { + FILE* fp = fopen(reportfile, "w"); + if (fp) { + report(fp); + fclose(fp); + if (!runningMarkerExisted) { // don't leave ours around + RunningMarker runingMarker(RUNNING_MARKER_FILENAME); + runingMarker.deleteRunningMarkerFile(); // happens in deleter, but making the side-effect explicit. + } + _exit(0); + } + } + }; + reportAndQuit("--protocolVersion", [&](FILE* fp) { + auto version = protocolVersionsSignatureBase64(); + fputs(version.toLatin1().data(), fp); + }); + reportAndQuit("--version", [&](FILE* fp) { + fputs(BuildInfo::VERSION.toLatin1().data(), fp); + }); + + const char* portStr = getCmdOption(argc, constArgv, "--listenPort"); + const int listenPort = portStr ? atoi(portStr) : INVALID_PORT; + + static const auto SUPPRESS_SETTINGS_RESET = "--suppress-settings-reset"; + bool suppressPrompt = cmdOptionExists(argc, const_cast(argv), SUPPRESS_SETTINGS_RESET); + + // set the OCULUS_STORE property so the oculus plugin can know if we ran from the Oculus Store + static const auto OCULUS_STORE_ARG = "--oculus-store"; + bool isStore = cmdOptionExists(argc, const_cast(argv), OCULUS_STORE_ARG); + qApp->setProperty(hifi::properties::OCULUS_STORE, isStore); + + // Ignore any previous crashes if running from command line with a test script. + bool inTestMode { false }; + for (int i = 0; i < argc; ++i) { + QString parameter(argv[i]); + if (parameter == TEST_SCRIPT_COMMAND) { + inTestMode = true; + break; + } + } + + bool previousSessionCrashed { false }; + if (!inTestMode) { + previousSessionCrashed = CrashRecoveryHandler::checkForResetSettings(runningMarkerExisted, suppressPrompt); + } + + // get dir to use for cache + static const auto CACHE_SWITCH = "--cache"; + QString cacheDir = getCmdOption(argc, const_cast(argv), CACHE_SWITCH); + if (!cacheDir.isEmpty()) { + qApp->setProperty(hifi::properties::APP_LOCAL_DATA_PATH, cacheDir); + } + + { + const QString resourcesBinaryFile = PathUtils::getRccPath(); + if (!QFile::exists(resourcesBinaryFile)) { + throw std::runtime_error("Unable to find primary resources"); + } + if (!QResource::registerResource(resourcesBinaryFile)) { + throw std::runtime_error("Unable to load primary resources"); + } + } + + // Tell the plugin manager about our statically linked plugins + DependencyManager::set(); + auto pluginManager = PluginManager::getInstance(); + pluginManager->setInputPluginProvider([] { return getInputPlugins(); }); + pluginManager->setDisplayPluginProvider([] { return getDisplayPlugins(); }); + pluginManager->setInputPluginSettingsPersister([](const InputPluginList& plugins) { saveInputPluginSettings(plugins); }); + if (auto steamClient = pluginManager->getSteamClientPlugin()) { + steamClient->init(); + } + if (auto oculusPlatform = pluginManager->getOculusPlatformPlugin()) { + oculusPlatform->init(); + } + + PROFILE_SET_THREAD_NAME("Main Thread"); + +#if defined(Q_OS_WIN) + // Select appropriate audio DLL + QString audioDLLPath = QCoreApplication::applicationDirPath(); + if (IsWindows8OrGreater()) { + audioDLLPath += "/audioWin8"; + } else { + audioDLLPath += "/audioWin7"; + } + QCoreApplication::addLibraryPath(audioDLLPath); +#endif + + DependencyManager::registerInheritance(); + DependencyManager::registerInheritance(); + DependencyManager::registerInheritance(); + DependencyManager::registerInheritance(); + + // Set dependencies + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); +#if defined(Q_OS_ANDROID) + DependencyManager::set(); // use the default user agent getter +#else + DependencyManager::set(std::bind(&Application::getUserAgent, qApp)); +#endif + DependencyManager::set(); + DependencyManager::set(ScriptEngine::CLIENT_SCRIPT); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(NodeType::Agent, listenPort); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); // ModelFormatRegistry must be defined before ModelCache. See the ModelCache constructor. + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(true); + DependencyManager::set(); + DependencyManager::registerInheritance(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) + DependencyManager::set(); +#endif + DependencyManager::set(); + DependencyManager::set(); +#if !defined(DISABLE_QML) + DependencyManager::set(); +#endif + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + controller::StateController::setStateVariables({ { STATE_IN_HMD, STATE_CAMERA_FULL_SCREEN_MIRROR, + STATE_CAMERA_FIRST_PERSON, STATE_CAMERA_THIRD_PERSON, STATE_CAMERA_ENTITY, STATE_CAMERA_INDEPENDENT, + STATE_SNAP_TURN, STATE_ADVANCED_MOVEMENT_CONTROLS, STATE_GROUNDED, STATE_NAV_FOCUSED, + STATE_PLATFORM_WINDOWS, STATE_PLATFORM_MAC, STATE_PLATFORM_ANDROID } }); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(true, qApp, qApp); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(nullptr, qApp->getOcteeSceneStats()); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + + return previousSessionCrashed; +} + +// FIXME move to header, or better yet, design some kind of UI manager +// to take care of highlighting keyboard focused items, rather than +// continuing to overburden Application.cpp +QUuid _keyboardFocusHighlightID; + +OffscreenGLCanvas* _qmlShareContext { nullptr }; + +// FIXME hack access to the internal share context for the Chromium helper +// Normally we'd want to use QWebEngine::initialize(), but we can't because +// our primary context is a QGLWidget, which can't easily be initialized to share +// from a QOpenGLContext. +// +// So instead we create a new offscreen context to share with the QGLWidget, +// and manually set THAT to be the shared context for the Chromium helper +#if !defined(DISABLE_QML) +OffscreenGLCanvas* _chromiumShareContext { nullptr }; +#endif + +Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); +Q_GUI_EXPORT QOpenGLContext *qt_gl_global_share_context(); + +Setting::Handle sessionRunTime{ "sessionRunTime", 0 }; + +const float DEFAULT_HMD_TABLET_SCALE_PERCENT = 60.0f; +const float DEFAULT_DESKTOP_TABLET_SCALE_PERCENT = 75.0f; +const bool DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR = true; +const bool DEFAULT_HMD_TABLET_BECOMES_TOOLBAR = false; +const bool DEFAULT_PREFER_STYLUS_OVER_LASER = false; +const bool DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS = false; +const QString DEFAULT_CURSOR_NAME = "DEFAULT"; +const bool DEFAULT_MINI_TABLET_ENABLED = true; + +QSharedPointer getOffscreenUI() { +#if !defined(DISABLE_QML) + return DependencyManager::get(); +#else + return nullptr; +#endif +} + +Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bool runningMarkerExisted) : + QApplication(argc, argv), + _window(new MainWindow(desktop())), + _sessionRunTimer(startupTimer), +#ifndef Q_OS_ANDROID + _logger(new FileLogger(this)), +#endif + _previousSessionCrashed(setupEssentials(argc, argv, runningMarkerExisted)), + _entitySimulation(new PhysicalEntitySimulation()), + _physicsEngine(new PhysicsEngine(Vectors::ZERO)), + _entityClipboard(new EntityTree()), + _previousScriptLocation("LastScriptLocation", DESKTOP_LOCATION), + _fieldOfView("fieldOfView", DEFAULT_FIELD_OF_VIEW_DEGREES), + _hmdTabletScale("hmdTabletScale", DEFAULT_HMD_TABLET_SCALE_PERCENT), + _desktopTabletScale("desktopTabletScale", DEFAULT_DESKTOP_TABLET_SCALE_PERCENT), + _firstRun(Settings::firstRun, true), + _desktopTabletBecomesToolbarSetting("desktopTabletBecomesToolbar", DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR), + _hmdTabletBecomesToolbarSetting("hmdTabletBecomesToolbar", DEFAULT_HMD_TABLET_BECOMES_TOOLBAR), + _preferStylusOverLaserSetting("preferStylusOverLaser", DEFAULT_PREFER_STYLUS_OVER_LASER), + _preferAvatarFingerOverStylusSetting("preferAvatarFingerOverStylus", DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS), + _constrainToolbarPosition("toolbar/constrainToolbarToCenterX", true), + _preferredCursor("preferredCursor", DEFAULT_CURSOR_NAME), + _miniTabletEnabledSetting("miniTabletEnabled", DEFAULT_MINI_TABLET_ENABLED), + _scaleMirror(1.0f), + _mirrorYawOffset(0.0f), + _raiseMirror(0.0f), + _enableProcessOctreeThread(true), + _lastNackTime(usecTimestampNow()), + _lastSendDownstreamAudioStats(usecTimestampNow()), + _notifiedPacketVersionMismatchThisDomain(false), + _maxOctreePPS(maxOctreePacketsPerSecond.get()), + _lastFaceTrackerUpdate(0), + _snapshotSound(nullptr), + _sampleSound(nullptr) +{ + + auto steamClient = PluginManager::getInstance()->getSteamClientPlugin(); + setProperty(hifi::properties::STEAM, (steamClient && steamClient->isRunning())); + setProperty(hifi::properties::CRASHED, _previousSessionCrashed); + + { + const QStringList args = arguments(); + + for (int i = 0; i < args.size() - 1; ++i) { + if (args.at(i) == TEST_SCRIPT_COMMAND && (i + 1) < args.size()) { + QString testScriptPath = args.at(i + 1); + + // If the URL scheme is http(s) or ftp, then use as is, else - treat it as a local file + // This is done so as not break previous command line scripts + if (testScriptPath.left(HIFI_URL_SCHEME_HTTP.length()) == HIFI_URL_SCHEME_HTTP || + testScriptPath.left(HIFI_URL_SCHEME_FTP.length()) == HIFI_URL_SCHEME_FTP) { + + setProperty(hifi::properties::TEST, QUrl::fromUserInput(testScriptPath)); + } else if (QFileInfo(testScriptPath).exists()) { + setProperty(hifi::properties::TEST, QUrl::fromLocalFile(testScriptPath)); + } + + // quite when finished parameter must directly follow the test script + if ((i + 2) < args.size() && args.at(i + 2) == TEST_QUIT_WHEN_FINISHED_OPTION) { + quitWhenFinished = true; + } + } else if (args.at(i) == TEST_RESULTS_LOCATION_COMMAND) { + // Set test snapshot location only if it is a writeable directory + QString path(args.at(i + 1)); + + QFileInfo fileInfo(path); + if (fileInfo.isDir() && fileInfo.isWritable()) { + TestScriptingInterface::getInstance()->setTestResultsLocation(path); + } + } + } + } + + // make sure the debug draw singleton is initialized on the main thread. + DebugDraw::getInstance().removeMarker(""); + + PluginContainer* pluginContainer = dynamic_cast(this); // set the container for any plugins that care + PluginManager::getInstance()->setContainer(pluginContainer); + + QThreadPool::globalInstance()->setMaxThreadCount(MIN_PROCESSING_THREAD_POOL_SIZE); + thread()->setPriority(QThread::HighPriority); + thread()->setObjectName("Main Thread"); + + setInstance(this); + + auto controllerScriptingInterface = DependencyManager::get().data(); + _controllerScriptingInterface = dynamic_cast(controllerScriptingInterface); + connect(PluginManager::getInstance().data(), &PluginManager::inputDeviceRunningChanged, + controllerScriptingInterface, &controller::ScriptingInterface::updateRunningInputDevices); + + EntityTree::setEntityClicksCapturedOperator([this] { + return _controllerScriptingInterface->areEntityClicksCaptured(); + }); + + _entityClipboard->createRootElement(); + +#ifdef Q_OS_WIN + installNativeEventFilter(&MyNativeEventFilter::getInstance()); +#endif + + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "styles/Inconsolata.otf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/fontawesome-webfont.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/hifi-glyphs.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/AnonymousPro-Regular.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/FiraSans-Regular.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/FiraSans-SemiBold.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Light.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Regular.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/rawline-500.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Bold.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-SemiBold.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Cairo-SemiBold.ttf"); + _window->setWindowTitle("High Fidelity Interface"); + + Model::setAbstractViewStateInterface(this); // The model class will sometimes need to know view state details from us + + auto nodeList = DependencyManager::get(); + nodeList->startThread(); + nodeList->setFlagTimeForConnectionStep(true); + + // move the AddressManager to the NodeList thread so that domain resets due to domain changes always occur + // before we tell MyAvatar to go to a new location in the new domain + auto addressManager = DependencyManager::get(); + addressManager->moveToThread(nodeList->thread()); + + const char** constArgv = const_cast(argv); + if (cmdOptionExists(argc, constArgv, "--disableWatchdog")) { + DISABLE_WATCHDOG = true; + } + // Set up a watchdog thread to intentionally crash the application on deadlocks + if (!DISABLE_WATCHDOG) { + auto deadlockWatchdogThread = new DeadlockWatchdogThread(); + deadlockWatchdogThread->setMainThreadID(QThread::currentThreadId()); + deadlockWatchdogThread->start(); + } + + // Set File Logger Session UUID + auto avatarManager = DependencyManager::get(); + auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr; + if (avatarManager) { + workload::SpacePointer space = getEntities()->getWorkloadSpace(); + avatarManager->setSpace(space); + } + auto accountManager = DependencyManager::get(); + +#ifndef Q_OS_ANDROID + _logger->setSessionID(accountManager->getSessionID()); +#endif + + setCrashAnnotation("metaverse_session_id", accountManager->getSessionID().toString().toStdString()); + setCrashAnnotation("main_thread_id", std::to_string((size_t)QThread::currentThreadId())); + + if (steamClient) { + qCDebug(interfaceapp) << "[VERSION] SteamVR buildID:" << steamClient->getSteamVRBuildID(); + } + setCrashAnnotation("steam", property(hifi::properties::STEAM).toBool() ? "1" : "0"); + + qCDebug(interfaceapp) << "[VERSION] Build sequence:" << qPrintable(applicationVersion()); + qCDebug(interfaceapp) << "[VERSION] MODIFIED_ORGANIZATION:" << BuildInfo::MODIFIED_ORGANIZATION; + qCDebug(interfaceapp) << "[VERSION] VERSION:" << BuildInfo::VERSION; + qCDebug(interfaceapp) << "[VERSION] BUILD_TYPE_STRING:" << BuildInfo::BUILD_TYPE_STRING; + qCDebug(interfaceapp) << "[VERSION] BUILD_GLOBAL_SERVICES:" << BuildInfo::BUILD_GLOBAL_SERVICES; +#if USE_STABLE_GLOBAL_SERVICES + qCDebug(interfaceapp) << "[VERSION] We will use STABLE global services."; +#else + qCDebug(interfaceapp) << "[VERSION] We will use DEVELOPMENT global services."; +#endif + + bool isStore = property(hifi::properties::OCULUS_STORE).toBool(); + + DependencyManager::get()->setLimitedCommerce(isStore); // Or we could make it a separate arg, or if either arg is set, etc. And should this instead by a hifi::properties? + + updateHeartbeat(); + + // setup a timer for domain-server check ins + QTimer* domainCheckInTimer = new QTimer(this); + QWeakPointer nodeListWeak = nodeList; + connect(domainCheckInTimer, &QTimer::timeout, [this, nodeListWeak] { + auto nodeList = nodeListWeak.lock(); + if (!isServerlessMode() && nodeList) { + nodeList->sendDomainServerCheckIn(); + } + }); + domainCheckInTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS); + connect(this, &QCoreApplication::aboutToQuit, [domainCheckInTimer] { + domainCheckInTimer->stop(); + domainCheckInTimer->deleteLater(); + }); + + { + auto audioIO = DependencyManager::get().data(); + audioIO->setPositionGetter([] { + auto avatarManager = DependencyManager::get(); + auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr; + + return myAvatar ? myAvatar->getPositionForAudio() : Vectors::ZERO; + }); + audioIO->setOrientationGetter([] { + auto avatarManager = DependencyManager::get(); + auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr; + + return myAvatar ? myAvatar->getOrientationForAudio() : Quaternions::IDENTITY; + }); + + recording::Frame::registerFrameHandler(AudioConstants::getAudioFrameName(), [&audioIO](recording::Frame::ConstPointer frame) { + audioIO->handleRecordedAudioInput(frame->data); + }); + + connect(audioIO, &AudioClient::inputReceived, [](const QByteArray& audio) { + static auto recorder = DependencyManager::get(); + if (recorder->isRecording()) { + static const recording::FrameType AUDIO_FRAME_TYPE = recording::Frame::registerFrameType(AudioConstants::getAudioFrameName()); + recorder->recordFrame(AUDIO_FRAME_TYPE, audio); + } + }); + audioIO->startThread(); + } + + // Make sure we don't time out during slow operations at startup + updateHeartbeat(); + + // Setup MessagesClient + DependencyManager::get()->startThread(); + + const DomainHandler& domainHandler = nodeList->getDomainHandler(); + + connect(&domainHandler, SIGNAL(domainURLChanged(QUrl)), SLOT(domainURLChanged(QUrl))); + connect(&domainHandler, SIGNAL(redirectToErrorDomainURL(QUrl)), SLOT(goToErrorDomainURL(QUrl))); + connect(&domainHandler, &DomainHandler::domainURLChanged, [](QUrl domainURL){ + setCrashAnnotation("domain", domainURL.toString().toStdString()); + }); + connect(&domainHandler, SIGNAL(resetting()), SLOT(resettingDomain())); + connect(&domainHandler, SIGNAL(connectedToDomain(QUrl)), SLOT(updateWindowTitle())); + connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle())); + connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, [this]() { + auto tabletScriptingInterface = DependencyManager::get(); + if (tabletScriptingInterface) { + tabletScriptingInterface->setQmlTabletRoot(SYSTEM_TABLET, nullptr); + } + auto entityScriptingInterface = DependencyManager::get(); + entityScriptingInterface->deleteEntity(getTabletScreenID()); + entityScriptingInterface->deleteEntity(getTabletHomeButtonID()); + entityScriptingInterface->deleteEntity(getTabletFrameID()); + _failedToConnectToEntityServer = false; + }); + + _entityServerConnectionTimer.setSingleShot(true); + connect(&_entityServerConnectionTimer, &QTimer::timeout, this, &Application::setFailedToConnectToEntityServer); + + connect(&domainHandler, &DomainHandler::connectedToDomain, this, [this]() { + if (!isServerlessMode()) { + _entityServerConnectionTimer.setInterval(ENTITY_SERVER_ADDED_TIMEOUT); + _entityServerConnectionTimer.start(); + _failedToConnectToEntityServer = false; + } + }); + connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &Application::domainConnectionRefused); + + nodeList->getDomainHandler().setErrorDomainURL(QUrl(REDIRECT_HIFI_ADDRESS)); + + // We could clear ATP assets only when changing domains, but it's possible that the domain you are connected + // to has gone down and switched to a new content set, so when you reconnect the cached ATP assets will no longer be valid. + connect(&domainHandler, &DomainHandler::disconnectedFromDomain, DependencyManager::get().data(), &ScriptCache::clearATPScriptsFromCache); + + // update our location every 5 seconds in the metaverse server, assuming that we are authenticated with one + const qint64 DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS = 5 * MSECS_PER_SECOND; + + auto discoverabilityManager = DependencyManager::get(); + connect(&locationUpdateTimer, &QTimer::timeout, discoverabilityManager.data(), &DiscoverabilityManager::updateLocation); + connect(&locationUpdateTimer, &QTimer::timeout, + DependencyManager::get().data(), &AddressManager::storeCurrentAddress); + locationUpdateTimer.start(DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS); + + // if we get a domain change, immediately attempt update location in metaverse server + connect(&nodeList->getDomainHandler(), &DomainHandler::connectedToDomain, + discoverabilityManager.data(), &DiscoverabilityManager::updateLocation); + + // send a location update immediately + discoverabilityManager->updateLocation(); + + connect(nodeList.data(), &NodeList::nodeAdded, this, &Application::nodeAdded); + connect(nodeList.data(), &NodeList::nodeKilled, this, &Application::nodeKilled); + connect(nodeList.data(), &NodeList::nodeActivated, this, &Application::nodeActivated); + connect(nodeList.data(), &NodeList::uuidChanged, myAvatar.get(), &MyAvatar::setSessionUUID); + connect(nodeList.data(), &NodeList::uuidChanged, this, &Application::setSessionUUID); + connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &Application::notifyPacketVersionMismatch); + + // you might think we could just do this in NodeList but we only want this connection for Interface + connect(&nodeList->getDomainHandler(), SIGNAL(limitOfSilentDomainCheckInsReached()), + nodeList.data(), SLOT(reset())); + + auto dialogsManager = DependencyManager::get(); +#if defined(Q_OS_ANDROID) + connect(accountManager.data(), &AccountManager::authRequired, this, []() { + auto addressManager = DependencyManager::get(); + AndroidHelper::instance().showLoginDialog(addressManager->currentAddress()); + }); +#else + connect(accountManager.data(), &AccountManager::authRequired, dialogsManager.data(), &DialogsManager::showLoginDialog); +#endif + connect(accountManager.data(), &AccountManager::usernameChanged, this, &Application::updateWindowTitle); + + // set the account manager's root URL and trigger a login request if we don't have the access token + accountManager->setIsAgent(true); + accountManager->setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL()); + + // use our MyAvatar position and quat for address manager path + addressManager->setPositionGetter([this]{ return getMyAvatar()->getWorldFeetPosition(); }); + addressManager->setOrientationGetter([this]{ return getMyAvatar()->getWorldOrientation(); }); + + connect(addressManager.data(), &AddressManager::hostChanged, this, &Application::updateWindowTitle); + connect(this, &QCoreApplication::aboutToQuit, addressManager.data(), &AddressManager::storeCurrentAddress); + + connect(this, &Application::activeDisplayPluginChanged, this, &Application::updateThreadPoolCount); + connect(this, &Application::activeDisplayPluginChanged, this, [](){ + qApp->setProperty(hifi::properties::HMD, qApp->isHMDMode()); + auto displayPlugin = qApp->getActiveDisplayPlugin(); + setCrashAnnotation("display_plugin", displayPlugin->getName().toStdString()); + setCrashAnnotation("hmd", displayPlugin->isHmd() ? "1" : "0"); + }); + connect(this, &Application::activeDisplayPluginChanged, this, &Application::updateSystemTabletMode); + connect(this, &Application::activeDisplayPluginChanged, this, [&](){ + if (getLoginDialogPoppedUp()) { + auto dialogsManager = DependencyManager::get(); + auto keyboard = DependencyManager::get(); + if (_firstRun.get()) { + // display mode changed. Don't allow auto-switch to work after this session. + _firstRun.set(false); + } + if (isHMDMode()) { + emit loginDialogFocusDisabled(); + dialogsManager->hideLoginDialog(); + createLoginDialog(); + } else { + DependencyManager::get()->deleteEntity(_loginDialogID); + _loginDialogID = QUuid(); + _loginStateManager.tearDown(); + dialogsManager->showLoginDialog(); + emit loginDialogFocusEnabled(); + } + } + }); + + // Save avatar location immediately after a teleport. + connect(myAvatar.get(), &MyAvatar::positionGoneTo, + DependencyManager::get().data(), &AddressManager::storeCurrentAddress); + + connect(myAvatar.get(), &MyAvatar::skeletonModelURLChanged, [](){ + QUrl avatarURL = qApp->getMyAvatar()->getSkeletonModelURL(); + setCrashAnnotation("avatar", avatarURL.toString().toStdString()); + }); + + + // Inititalize sample before registering + _sampleSound = DependencyManager::get()->getSound(PathUtils::resourcesUrl("sounds/sample.wav")); + + { + auto scriptEngines = DependencyManager::get().data(); + scriptEngines->registerScriptInitializer([this](ScriptEnginePointer engine) { + registerScriptEngineWithApplicationServices(engine); + }); + + connect(scriptEngines, &ScriptEngines::scriptCountChanged, this, [this] { + auto scriptEngines = DependencyManager::get(); + if (scriptEngines->getRunningScripts().isEmpty()) { + getMyAvatar()->clearScriptableSettings(); + } + }, Qt::QueuedConnection); + + connect(scriptEngines, &ScriptEngines::scriptsReloading, this, [this] { + getEntities()->reloadEntityScripts(); + loadAvatarScripts(getMyAvatar()->getScriptUrls()); + }, Qt::QueuedConnection); + + connect(scriptEngines, &ScriptEngines::scriptLoadError, + this, [](const QString& filename, const QString& error) { + OffscreenUi::asyncWarning(nullptr, "Error Loading Script", filename + " failed to load."); + }, Qt::QueuedConnection); + } + +#ifdef _WIN32 + WSADATA WsaData; + int wsaresult = WSAStartup(MAKEWORD(2, 2), &WsaData); +#endif + + // tell the NodeList instance who to tell the domain server we care about + nodeList->addSetOfNodeTypesToNodeInterestSet(NodeSet() << NodeType::AudioMixer << NodeType::AvatarMixer + << NodeType::EntityServer << NodeType::AssetServer << NodeType::MessagesMixer << NodeType::EntityScriptServer); + + // connect to the packet sent signal of the _entityEditSender + connect(&_entityEditSender, &EntityEditPacketSender::packetSent, this, &Application::packetSent); + connect(&_entityEditSender, &EntityEditPacketSender::addingEntityWithCertificate, this, &Application::addingEntityWithCertificate); + + QString concurrentDownloadsStr = getCmdOption(argc, constArgv, "--concurrent-downloads"); + bool success; + uint32_t concurrentDownloads = concurrentDownloadsStr.toUInt(&success); + if (!success) { + concurrentDownloads = MAX_CONCURRENT_RESOURCE_DOWNLOADS; + } + ResourceCache::setRequestLimit(concurrentDownloads); + + // perhaps override the avatar url. Since we will test later for validity + // we don't need to do so here. + QString avatarURL = getCmdOption(argc, constArgv, "--avatarURL"); + _avatarOverrideUrl = QUrl::fromUserInput(avatarURL); + + // If someone specifies both --avatarURL and --replaceAvatarURL, + // the replaceAvatarURL wins. So only set the _overrideUrl if this + // does have a non-empty string. + QString replaceURL = getCmdOption(argc, constArgv, "--replaceAvatarURL"); + if (!replaceURL.isEmpty()) { + _avatarOverrideUrl = QUrl::fromUserInput(replaceURL); + _saveAvatarOverrideUrl = true; + } + + _glWidget = new GLCanvas(); + getApplicationCompositor().setRenderingWidget(_glWidget); + _window->setCentralWidget(_glWidget); + + _window->restoreGeometry(); + _window->setVisible(true); + + _glWidget->setFocusPolicy(Qt::StrongFocus); + _glWidget->setFocus(); + + if (cmdOptionExists(argc, constArgv, "--system-cursor")) { + _preferredCursor.set(Cursor::Manager::getIconName(Cursor::Icon::SYSTEM)); + } + showCursor(Cursor::Manager::lookupIcon(_preferredCursor.get())); + + // enable mouse tracking; otherwise, we only get drag events + _glWidget->setMouseTracking(true); + // Make sure the window is set to the correct size by processing the pending events + QCoreApplication::processEvents(); + + // Create the main thread context, the GPU backend + initializeGL(); + qCDebug(interfaceapp, "Initialized GL"); + + // Initialize the display plugin architecture + initializeDisplayPlugins(); + qCDebug(interfaceapp, "Initialized Display"); + + // An audio device changed signal received before the display plugins are set up will cause a crash, + // so we defer the setup of the `scripting::Audio` class until this point + { + auto audioScriptingInterface = DependencyManager::set(); + auto audioIO = DependencyManager::get().data(); + connect(audioIO, &AudioClient::mutedByMixer, audioScriptingInterface.data(), &AudioScriptingInterface::mutedByMixer); + connect(audioIO, &AudioClient::receivedFirstPacket, audioScriptingInterface.data(), &AudioScriptingInterface::receivedFirstPacket); + connect(audioIO, &AudioClient::disconnected, audioScriptingInterface.data(), &AudioScriptingInterface::disconnected); + connect(audioIO, &AudioClient::muteEnvironmentRequested, [](glm::vec3 position, float radius) { + auto audioClient = DependencyManager::get(); + auto audioScriptingInterface = DependencyManager::get(); + auto myAvatarPosition = DependencyManager::get()->getMyAvatar()->getWorldPosition(); + float distance = glm::distance(myAvatarPosition, position); + + if (distance < radius) { + audioClient->setMuted(true); + audioScriptingInterface->environmentMuted(); + } + }); + connect(this, &Application::activeDisplayPluginChanged, + reinterpret_cast(audioScriptingInterface.data()), &scripting::Audio::onContextChanged); + } + + // Create the rendering engine. This can be slow on some machines due to lots of + // GPU pipeline creation. + initializeRenderEngine(); + qCDebug(interfaceapp, "Initialized Render Engine."); + + // Overlays need to exist before we set the ContextOverlayInterface dependency + _overlays.init(); // do this before scripts load + DependencyManager::set(); + + // Initialize the user interface and menu system + // Needs to happen AFTER the render engine initialization to access its configuration + initializeUi(); + + init(); + qCDebug(interfaceapp, "init() complete."); + + // create thread for parsing of octree data independent of the main network and rendering threads + _octreeProcessor.initialize(_enableProcessOctreeThread); + connect(&_octreeProcessor, &OctreePacketProcessor::packetVersionMismatch, this, &Application::notifyPacketVersionMismatch); + _entityEditSender.initialize(_enableProcessOctreeThread); + + _idleLoopStdev.reset(); + + // update before the first render + update(0); + + // Make sure we don't time out during slow operations at startup + updateHeartbeat(); + + static const QString TESTER = "HIFI_TESTER"; + bool isTester = false; +#if defined (Q_OS_ANDROID) + // Since we cannot set environment variables in Android we use a file presence + // to denote that this is a testing device + QFileInfo check_tester_file(TESTER_FILE); + isTester = check_tester_file.exists() && check_tester_file.isFile(); +#endif + + constexpr auto INSTALLER_INI_NAME = "installer.ini"; + auto iniPath = QDir(applicationDirPath()).filePath(INSTALLER_INI_NAME); + QFile installerFile { iniPath }; + std::unordered_map installerKeyValues; + if (installerFile.open(QIODevice::ReadOnly)) { + while (!installerFile.atEnd()) { + auto line = installerFile.readLine(); + if (!line.isEmpty()) { + auto index = line.indexOf("="); + if (index >= 0) { + installerKeyValues[line.mid(0, index).trimmed()] = line.mid(index + 1).trimmed(); + } + } + } + } + + // In practice we shouldn't run across installs that don't have a known installer type. + // Client or Client+Server installs should always have the installer.ini next to their + // respective interface.exe, and Steam installs will be detected as such. If a user were + // to delete the installer.ini, though, and as an example, we won't know the context of the + // original install. + constexpr auto INSTALLER_KEY_TYPE = "type"; + constexpr auto INSTALLER_KEY_CAMPAIGN = "campaign"; + constexpr auto INSTALLER_TYPE_UNKNOWN = "unknown"; + constexpr auto INSTALLER_TYPE_STEAM = "steam"; + + auto typeIt = installerKeyValues.find(INSTALLER_KEY_TYPE); + QString installerType = INSTALLER_TYPE_UNKNOWN; + if (typeIt == installerKeyValues.end()) { + if (property(hifi::properties::STEAM).toBool()) { + installerType = INSTALLER_TYPE_STEAM; + } + } else { + installerType = typeIt->second; + } + + auto campaignIt = installerKeyValues.find(INSTALLER_KEY_CAMPAIGN); + QString installerCampaign = campaignIt != installerKeyValues.end() ? campaignIt->second : ""; + + qDebug() << "Detected installer type:" << installerType; + qDebug() << "Detected installer campaign:" << installerCampaign; + + auto& userActivityLogger = UserActivityLogger::getInstance(); + if (userActivityLogger.isEnabled()) { + // sessionRunTime will be reset soon by loadSettings. Grab it now to get previous session value. + // The value will be 0 if the user blew away settings this session, which is both a feature and a bug. + static const QString TESTER = "HIFI_TESTER"; + auto gpuIdent = GPUIdent::getInstance(); + auto glContextData = getGLContextData(); + QJsonObject properties = { + { "version", applicationVersion() }, + { "tester", QProcessEnvironment::systemEnvironment().contains(TESTER) || isTester }, + { "installer_campaign", installerCampaign }, + { "installer_type", installerType }, + { "build_type", BuildInfo::BUILD_TYPE_STRING }, + { "previousSessionCrashed", _previousSessionCrashed }, + { "previousSessionRuntime", sessionRunTime.get() }, + { "cpu_architecture", QSysInfo::currentCpuArchitecture() }, + { "kernel_type", QSysInfo::kernelType() }, + { "kernel_version", QSysInfo::kernelVersion() }, + { "os_type", QSysInfo::productType() }, + { "os_version", QSysInfo::productVersion() }, + { "gpu_name", gpuIdent->getName() }, + { "gpu_driver", gpuIdent->getDriver() }, + { "gpu_memory", static_cast(gpuIdent->getMemory()) }, + { "gl_version_int", glVersionToInteger(glContextData.value("version").toString()) }, + { "gl_version", glContextData["version"] }, + { "gl_vender", glContextData["vendor"] }, + { "gl_sl_version", glContextData["sl_version"] }, + { "gl_renderer", glContextData["renderer"] }, + { "ideal_thread_count", QThread::idealThreadCount() } + }; + auto macVersion = QSysInfo::macVersion(); + if (macVersion != QSysInfo::MV_None) { + properties["os_osx_version"] = QSysInfo::macVersion(); + } + auto windowsVersion = QSysInfo::windowsVersion(); + if (windowsVersion != QSysInfo::WV_None) { + properties["os_win_version"] = QSysInfo::windowsVersion(); + } + + ProcessorInfo procInfo; + if (getProcessorInfo(procInfo)) { + properties["processor_core_count"] = procInfo.numProcessorCores; + properties["logical_processor_count"] = procInfo.numLogicalProcessors; + properties["processor_l1_cache_count"] = procInfo.numProcessorCachesL1; + properties["processor_l2_cache_count"] = procInfo.numProcessorCachesL2; + properties["processor_l3_cache_count"] = procInfo.numProcessorCachesL3; + } + + properties["first_run"] = _firstRun.get(); + + // add the user's machine ID to the launch event + QString machineFingerPrint = uuidStringWithoutCurlyBraces(FingerprintUtils::getMachineFingerprint()); + properties["machine_fingerprint"] = machineFingerPrint; + + userActivityLogger.logAction("launch", properties); + } + + _entityEditSender.setMyAvatar(myAvatar.get()); + + // The entity octree will have to know about MyAvatar for the parentJointName import + getEntities()->getTree()->setMyAvatar(myAvatar); + _entityClipboard->setMyAvatar(myAvatar); + + // For now we're going to set the PPS for outbound packets to be super high, this is + // probably not the right long term solution. But for now, we're going to do this to + // allow you to move an entity around in your hand + _entityEditSender.setPacketsPerSecond(3000); // super high!! + + // Make sure we don't time out during slow operations at startup + updateHeartbeat(); + + connect(this, SIGNAL(aboutToQuit()), this, SLOT(onAboutToQuit())); + + // FIXME -- I'm a little concerned about this. + connect(myAvatar->getSkeletonModel().get(), &SkeletonModel::skeletonLoaded, + this, &Application::checkSkeleton, Qt::QueuedConnection); + + // Setup the userInputMapper with the actions + auto userInputMapper = DependencyManager::get(); + connect(userInputMapper.data(), &UserInputMapper::actionEvent, [this](int action, float state) { + using namespace controller; + auto tabletScriptingInterface = DependencyManager::get(); + { + auto actionEnum = static_cast(action); + int key = Qt::Key_unknown; + static int lastKey = Qt::Key_unknown; + bool navAxis = false; + switch (actionEnum) { + case Action::UI_NAV_VERTICAL: + navAxis = true; + if (state > 0.0f) { + key = Qt::Key_Up; + } else if (state < 0.0f) { + key = Qt::Key_Down; + } + break; + + case Action::UI_NAV_LATERAL: + navAxis = true; + if (state > 0.0f) { + key = Qt::Key_Right; + } else if (state < 0.0f) { + key = Qt::Key_Left; + } + break; + + case Action::UI_NAV_GROUP: + navAxis = true; + if (state > 0.0f) { + key = Qt::Key_Tab; + } else if (state < 0.0f) { + key = Qt::Key_Backtab; + } + break; + + case Action::UI_NAV_BACK: + key = Qt::Key_Escape; + break; + + case Action::UI_NAV_SELECT: + key = Qt::Key_Return; + break; + default: + break; + } + + auto window = tabletScriptingInterface->getTabletWindow(); + if (navAxis && window) { + if (lastKey != Qt::Key_unknown) { + QKeyEvent event(QEvent::KeyRelease, lastKey, Qt::NoModifier); + sendEvent(window, &event); + lastKey = Qt::Key_unknown; + } + + if (key != Qt::Key_unknown) { + QKeyEvent event(QEvent::KeyPress, key, Qt::NoModifier); + sendEvent(window, &event); + tabletScriptingInterface->processEvent(&event); + lastKey = key; + } + } else if (key != Qt::Key_unknown && window) { + if (state) { + QKeyEvent event(QEvent::KeyPress, key, Qt::NoModifier); + sendEvent(window, &event); + tabletScriptingInterface->processEvent(&event); + } else { + QKeyEvent event(QEvent::KeyRelease, key, Qt::NoModifier); + sendEvent(window, &event); + } + return; + } + } + + if (action == controller::toInt(controller::Action::RETICLE_CLICK)) { + auto reticlePos = getApplicationCompositor().getReticlePosition(); + QPoint localPos(reticlePos.x, reticlePos.y); // both hmd and desktop already handle this in our coordinates. + if (state) { + QMouseEvent mousePress(QEvent::MouseButtonPress, localPos, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); + sendEvent(_glWidget, &mousePress); + _reticleClickPressed = true; + } else { + QMouseEvent mouseRelease(QEvent::MouseButtonRelease, localPos, Qt::LeftButton, Qt::NoButton, Qt::NoModifier); + sendEvent(_glWidget, &mouseRelease); + _reticleClickPressed = false; + } + return; // nothing else to do + } + + if (state) { + if (action == controller::toInt(controller::Action::TOGGLE_MUTE)) { + auto audioClient = DependencyManager::get(); + audioClient->setMuted(!audioClient->isMuted()); + } else if (action == controller::toInt(controller::Action::CYCLE_CAMERA)) { + cycleCamera(); + } else if (action == controller::toInt(controller::Action::CONTEXT_MENU) && !isInterstitialMode()) { + toggleTabletUI(); + } else if (action == controller::toInt(controller::Action::RETICLE_X)) { + auto oldPos = getApplicationCompositor().getReticlePosition(); + getApplicationCompositor().setReticlePosition({ oldPos.x + state, oldPos.y }); + } else if (action == controller::toInt(controller::Action::RETICLE_Y)) { + auto oldPos = getApplicationCompositor().getReticlePosition(); + getApplicationCompositor().setReticlePosition({ oldPos.x, oldPos.y + state }); + } else if (action == controller::toInt(controller::Action::TOGGLE_OVERLAY)) { + toggleOverlays(); + } + } + }); + + _applicationStateDevice = userInputMapper->getStateDevice(); + + _applicationStateDevice->setInputVariant(STATE_IN_HMD, []() -> float { + return qApp->isHMDMode() ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_CAMERA_FULL_SCREEN_MIRROR, []() -> float { + return qApp->getCamera().getMode() == CAMERA_MODE_MIRROR ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_CAMERA_FIRST_PERSON, []() -> float { + return qApp->getCamera().getMode() == CAMERA_MODE_FIRST_PERSON ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_CAMERA_THIRD_PERSON, []() -> float { + return qApp->getCamera().getMode() == CAMERA_MODE_THIRD_PERSON ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_CAMERA_ENTITY, []() -> float { + return qApp->getCamera().getMode() == CAMERA_MODE_ENTITY ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_CAMERA_INDEPENDENT, []() -> float { + return qApp->getCamera().getMode() == CAMERA_MODE_INDEPENDENT ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_SNAP_TURN, []() -> float { + return qApp->getMyAvatar()->getSnapTurn() ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_ADVANCED_MOVEMENT_CONTROLS, []() -> float { + return qApp->getMyAvatar()->useAdvancedMovementControls() ? 1 : 0; + }); + + _applicationStateDevice->setInputVariant(STATE_GROUNDED, []() -> float { + return qApp->getMyAvatar()->getCharacterController()->onGround() ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_NAV_FOCUSED, []() -> float { + auto offscreenUi = getOffscreenUI(); + return offscreenUi ? (offscreenUi->navigationFocused() ? 1 : 0) : 0; + }); + _applicationStateDevice->setInputVariant(STATE_PLATFORM_WINDOWS, []() -> float { +#if defined(Q_OS_WIN) + return 1; +#else + return 0; +#endif + }); + _applicationStateDevice->setInputVariant(STATE_PLATFORM_MAC, []() -> float { +#if defined(Q_OS_MAC) + return 1; +#else + return 0; +#endif + }); + _applicationStateDevice->setInputVariant(STATE_PLATFORM_ANDROID, []() -> float { +#if defined(Q_OS_ANDROID) + return 1 ; +#else + return 0; +#endif + }); + + + // Setup the _keyboardMouseDevice, _touchscreenDevice, _touchscreenVirtualPadDevice and the user input mapper with the default bindings + userInputMapper->registerDevice(_keyboardMouseDevice->getInputDevice()); + // if the _touchscreenDevice is not supported it will not be registered + if (_touchscreenDevice) { + userInputMapper->registerDevice(_touchscreenDevice->getInputDevice()); + } + if (_touchscreenVirtualPadDevice) { + userInputMapper->registerDevice(_touchscreenVirtualPadDevice->getInputDevice()); + } + + QString scriptsSwitch = QString("--").append(SCRIPTS_SWITCH); + _defaultScriptsLocation = getCmdOption(argc, constArgv, scriptsSwitch.toStdString().c_str()); + + // Make sure we don't time out during slow operations at startup + updateHeartbeat(); + + loadSettings(); + + updateVerboseLogging(); + + // Now that we've loaded the menu and thus switched to the previous display plugin + // we can unlock the desktop repositioning code, since all the positions will be + // relative to the desktop size for this plugin + auto offscreenUi = getOffscreenUI(); + connect(offscreenUi.data(), &OffscreenUi::desktopReady, []() { + auto offscreenUi = getOffscreenUI(); + auto desktop = offscreenUi->getDesktop(); + if (desktop) { + desktop->setProperty("repositionLocked", false); + } + }); + + connect(offscreenUi.data(), &OffscreenUi::keyboardFocusActive, [this]() { +#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML) + // Do not show login dialog if requested not to on the command line + QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY); + int index = arguments().indexOf(hifiNoLoginCommandLineKey); + if (index != -1) { + resumeAfterLoginDialogActionTaken(); + return; + } + + showLoginScreen(); +#else + resumeAfterLoginDialogActionTaken(); +#endif + }); + + // Make sure we don't time out during slow operations at startup + updateHeartbeat(); + QTimer* settingsTimer = new QTimer(); + moveToNewNamedThread(settingsTimer, "Settings Thread", [this, settingsTimer]{ + // This needs to run on the settings thread, so we need to pass the `settingsTimer` as the + // receiver object, otherwise it will run on the application thread and trigger a warning + // about trying to kill the timer on the main thread. + connect(qApp, &Application::beforeAboutToQuit, settingsTimer, [this, settingsTimer]{ + // Disconnect the signal from the save settings + QObject::disconnect(settingsTimer, &QTimer::timeout, this, &Application::saveSettings); + // Stop the settings timer + settingsTimer->stop(); + // Delete it (this will trigger the thread destruction + settingsTimer->deleteLater(); + // Mark the settings thread as finished, so we know we can safely save in the main application + // shutdown code + _settingsGuard.trigger(); + }); + + int SAVE_SETTINGS_INTERVAL = 10 * MSECS_PER_SECOND; // Let's save every seconds for now + settingsTimer->setSingleShot(false); + settingsTimer->setInterval(SAVE_SETTINGS_INTERVAL); // 10s, Qt::CoarseTimer acceptable + QObject::connect(settingsTimer, &QTimer::timeout, this, &Application::saveSettings); + settingsTimer->start(); + }, QThread::LowestPriority); + + if (Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson)) { + getMyAvatar()->setBoomLength(MyAvatar::ZOOM_MIN); // So that camera doesn't auto-switch to third person. + } else if (Menu::getInstance()->isOptionChecked(MenuOption::IndependentMode)) { + Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); + cameraMenuChanged(); + } else if (Menu::getInstance()->isOptionChecked(MenuOption::CameraEntityMode)) { + Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); + cameraMenuChanged(); + } + + { + auto audioIO = DependencyManager::get().data(); + // set the local loopback interface for local sounds + AudioInjector::setLocalAudioInterface(audioIO); + auto audioScriptingInterface = DependencyManager::get(); + audioScriptingInterface->setLocalAudioInterface(audioIO); + connect(audioIO, &AudioClient::noiseGateOpened, audioScriptingInterface.data(), &AudioScriptingInterface::noiseGateOpened); + connect(audioIO, &AudioClient::noiseGateClosed, audioScriptingInterface.data(), &AudioScriptingInterface::noiseGateClosed); + connect(audioIO, &AudioClient::inputReceived, audioScriptingInterface.data(), &AudioScriptingInterface::inputReceived); + } + + this->installEventFilter(this); + + + +#ifdef HAVE_DDE + auto ddeTracker = DependencyManager::get(); + ddeTracker->init(); + connect(ddeTracker.data(), &FaceTracker::muteToggled, this, &Application::faceTrackerMuteToggled); +#endif + +#ifdef HAVE_IVIEWHMD + auto eyeTracker = DependencyManager::get(); + eyeTracker->init(); + setActiveEyeTracker(); +#endif + + // If launched from Steam, let it handle updates + const QString HIFI_NO_UPDATER_COMMAND_LINE_KEY = "--no-updater"; + bool noUpdater = arguments().indexOf(HIFI_NO_UPDATER_COMMAND_LINE_KEY) != -1; + bool buildCanUpdate = BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Stable + || BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Master; + if (!noUpdater && buildCanUpdate) { + constexpr auto INSTALLER_TYPE_CLIENT_ONLY = "client_only"; + + auto applicationUpdater = DependencyManager::set(); + + AutoUpdater::InstallerType type = installerType == INSTALLER_TYPE_CLIENT_ONLY + ? AutoUpdater::InstallerType::CLIENT_ONLY : AutoUpdater::InstallerType::FULL; + + applicationUpdater->setInstallerType(type); + applicationUpdater->setInstallerCampaign(installerCampaign); + connect(applicationUpdater.data(), &AutoUpdater::newVersionIsAvailable, dialogsManager.data(), &DialogsManager::showUpdateDialog); + applicationUpdater->checkForUpdate(); + } + + Menu::getInstance()->setIsOptionChecked(MenuOption::ActionMotorControl, true); + +// FIXME spacemouse code still needs cleanup +#if 0 + // the 3Dconnexion device wants to be initialized after a window is displayed. + SpacemouseManager::getInstance().init(); +#endif + + // If the user clicks on an object, we will check that it's a web surface, and if so, set the focus to it + auto pointerManager = DependencyManager::get(); + auto keyboardFocusOperator = [this](const QUuid& id, const PointerEvent& event) { + if (event.shouldFocus()) { + auto keyboard = DependencyManager::get(); + if (getEntities()->wantsKeyboardFocus(id)) { + setKeyboardFocusEntity(id); + } else if (!keyboard->containsID(id)) { // FIXME: this is a hack to make the keyboard work for now, since the keys would otherwise steal focus + setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); + } + } + }; + connect(pointerManager.data(), &PointerManager::triggerBeginEntity, keyboardFocusOperator); + connect(pointerManager.data(), &PointerManager::triggerBeginOverlay, keyboardFocusOperator); + + auto entityScriptingInterface = DependencyManager::get(); + connect(entityScriptingInterface.data(), &EntityScriptingInterface::deletingEntity, this, [this](const EntityItemID& entityItemID) { + if (entityItemID == _keyboardFocusedEntity.get()) { + setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); + } + }, Qt::QueuedConnection); + + EntityTreeRenderer::setAddMaterialToEntityOperator([this](const QUuid& entityID, graphics::MaterialLayer material, const std::string& parentMaterialName) { + if (_aboutToQuit) { + return false; + } + + auto renderable = getEntities()->renderableForEntityId(entityID); + if (renderable) { + renderable->addMaterial(material, parentMaterialName); + return true; + } + + return false; + }); + EntityTreeRenderer::setRemoveMaterialFromEntityOperator([this](const QUuid& entityID, graphics::MaterialPointer material, const std::string& parentMaterialName) { + if (_aboutToQuit) { + return false; + } + + auto renderable = getEntities()->renderableForEntityId(entityID); + if (renderable) { + renderable->removeMaterial(material, parentMaterialName); + return true; + } + + return false; + }); + + EntityTreeRenderer::setAddMaterialToAvatarOperator([](const QUuid& avatarID, graphics::MaterialLayer material, const std::string& parentMaterialName) { + auto avatarManager = DependencyManager::get(); + auto avatar = avatarManager->getAvatarBySessionID(avatarID); + if (avatar) { + avatar->addMaterial(material, parentMaterialName); + return true; + } + return false; + }); + EntityTreeRenderer::setRemoveMaterialFromAvatarOperator([](const QUuid& avatarID, graphics::MaterialPointer material, const std::string& parentMaterialName) { + auto avatarManager = DependencyManager::get(); + auto avatar = avatarManager->getAvatarBySessionID(avatarID); + if (avatar) { + avatar->removeMaterial(material, parentMaterialName); + return true; + } + return false; + }); + + EntityTree::setGetEntityObjectOperator([this](const QUuid& id) -> QObject* { + auto entities = getEntities(); + if (auto entity = entities->renderableForEntityId(id)) { + return qobject_cast(entity.get()); + } + return nullptr; + }); + + EntityTree::setTextSizeOperator([this](const QUuid& id, const QString& text) { + auto entities = getEntities(); + if (auto entity = entities->renderableForEntityId(id)) { + if (auto renderable = std::dynamic_pointer_cast(entity)) { + return renderable->textSize(text); + } + } + return QSizeF(0.0f, 0.0f); + }); + + connect(this, &Application::aboutToQuit, [this]() { + setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); + }); + + // Add periodic checks to send user activity data + static int CHECK_NEARBY_AVATARS_INTERVAL_MS = 10000; + static int NEARBY_AVATAR_RADIUS_METERS = 10; + + // setup the stats interval depending on if the 1s faster hearbeat was requested + static const QString FAST_STATS_ARG = "--fast-heartbeat"; + static int SEND_STATS_INTERVAL_MS = arguments().indexOf(FAST_STATS_ARG) != -1 ? 1000 : 10000; + + static glm::vec3 lastAvatarPosition = myAvatar->getWorldPosition(); + static glm::mat4 lastHMDHeadPose = getHMDSensorPose(); + static controller::Pose lastLeftHandPose = myAvatar->getLeftHandPose(); + static controller::Pose lastRightHandPose = myAvatar->getRightHandPose(); + + // Periodically send fps as a user activity event + QTimer* sendStatsTimer = new QTimer(this); + sendStatsTimer->setInterval(SEND_STATS_INTERVAL_MS); // 10s, Qt::CoarseTimer acceptable + connect(sendStatsTimer, &QTimer::timeout, this, [this]() { + + QJsonObject properties = {}; + MemoryInfo memInfo; + if (getMemoryInfo(memInfo)) { + properties["system_memory_total"] = static_cast(memInfo.totalMemoryBytes); + properties["system_memory_used"] = static_cast(memInfo.usedMemoryBytes); + properties["process_memory_used"] = static_cast(memInfo.processUsedMemoryBytes); + } + + // content location and build info - useful for filtering stats + auto addressManager = DependencyManager::get(); + auto currentDomain = addressManager->currentShareableAddress(true).toString(); // domain only + auto currentPath = addressManager->currentPath(true); // with orientation + properties["current_domain"] = currentDomain; + properties["current_path"] = currentPath; + properties["build_version"] = BuildInfo::VERSION; + + auto displayPlugin = qApp->getActiveDisplayPlugin(); + + properties["render_rate"] = getRenderLoopRate(); + properties["target_render_rate"] = getTargetRenderFrameRate(); + properties["present_rate"] = displayPlugin->presentRate(); + properties["new_frame_present_rate"] = displayPlugin->newFramePresentRate(); + properties["dropped_frame_rate"] = displayPlugin->droppedFrameRate(); + properties["stutter_rate"] = displayPlugin->stutterRate(); + properties["game_rate"] = getGameLoopRate(); + properties["has_async_reprojection"] = displayPlugin->hasAsyncReprojection(); + properties["hardware_stats"] = displayPlugin->getHardwareStats(); + + // deadlock watchdog related stats + properties["deadlock_watchdog_maxElapsed"] = (int)DeadlockWatchdogThread::_maxElapsed; + properties["deadlock_watchdog_maxElapsedAverage"] = (int)DeadlockWatchdogThread::_maxElapsedAverage; + + auto nodeList = DependencyManager::get(); + properties["packet_rate_in"] = nodeList->getInboundPPS(); + properties["packet_rate_out"] = nodeList->getOutboundPPS(); + properties["kbps_in"] = nodeList->getInboundKbps(); + properties["kbps_out"] = nodeList->getOutboundKbps(); + + SharedNodePointer entityServerNode = nodeList->soloNodeOfType(NodeType::EntityServer); + SharedNodePointer audioMixerNode = nodeList->soloNodeOfType(NodeType::AudioMixer); + SharedNodePointer avatarMixerNode = nodeList->soloNodeOfType(NodeType::AvatarMixer); + SharedNodePointer assetServerNode = nodeList->soloNodeOfType(NodeType::AssetServer); + SharedNodePointer messagesMixerNode = nodeList->soloNodeOfType(NodeType::MessagesMixer); + properties["entity_ping"] = entityServerNode ? entityServerNode->getPingMs() : -1; + properties["audio_ping"] = audioMixerNode ? audioMixerNode->getPingMs() : -1; + properties["avatar_ping"] = avatarMixerNode ? avatarMixerNode->getPingMs() : -1; + properties["asset_ping"] = assetServerNode ? assetServerNode->getPingMs() : -1; + properties["messages_ping"] = messagesMixerNode ? messagesMixerNode->getPingMs() : -1; + properties["atp_in_kbps"] = assetServerNode ? assetServerNode->getInboundKbps() : 0.0f; + + auto loadingRequests = ResourceCache::getLoadingRequests(); + + QJsonArray loadingRequestsStats; + for (const auto& request : loadingRequests) { + QJsonObject requestStats; + requestStats["filename"] = request->getURL().fileName(); + requestStats["received"] = request->getBytesReceived(); + requestStats["total"] = request->getBytesTotal(); + requestStats["attempts"] = (int)request->getDownloadAttempts(); + loadingRequestsStats.append(requestStats); + } + + properties["active_downloads"] = loadingRequests.size(); + properties["pending_downloads"] = (int)ResourceCache::getPendingRequestCount(); + properties["active_downloads_details"] = loadingRequestsStats; + + auto statTracker = DependencyManager::get(); + + properties["processing_resources"] = statTracker->getStat("Processing").toInt(); + properties["pending_processing_resources"] = statTracker->getStat("PendingProcessing").toInt(); + + QJsonObject startedRequests; + startedRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_STARTED).toInt(); + startedRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_STARTED).toInt(); + startedRequests["file"] = statTracker->getStat(STAT_FILE_REQUEST_STARTED).toInt(); + startedRequests["total"] = startedRequests["atp"].toInt() + startedRequests["http"].toInt() + + startedRequests["file"].toInt(); + properties["started_requests"] = startedRequests; + + QJsonObject successfulRequests; + successfulRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_SUCCESS).toInt(); + successfulRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_SUCCESS).toInt(); + successfulRequests["file"] = statTracker->getStat(STAT_FILE_REQUEST_SUCCESS).toInt(); + successfulRequests["total"] = successfulRequests["atp"].toInt() + successfulRequests["http"].toInt() + + successfulRequests["file"].toInt(); + properties["successful_requests"] = successfulRequests; + + QJsonObject failedRequests; + failedRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_FAILED).toInt(); + failedRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_FAILED).toInt(); + failedRequests["file"] = statTracker->getStat(STAT_FILE_REQUEST_FAILED).toInt(); + failedRequests["total"] = failedRequests["atp"].toInt() + failedRequests["http"].toInt() + + failedRequests["file"].toInt(); + properties["failed_requests"] = failedRequests; + + QJsonObject cacheRequests; + cacheRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_CACHE).toInt(); + cacheRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_CACHE).toInt(); + cacheRequests["total"] = cacheRequests["atp"].toInt() + cacheRequests["http"].toInt(); + properties["cache_requests"] = cacheRequests; + + QJsonObject atpMappingRequests; + atpMappingRequests["started"] = statTracker->getStat(STAT_ATP_MAPPING_REQUEST_STARTED).toInt(); + atpMappingRequests["failed"] = statTracker->getStat(STAT_ATP_MAPPING_REQUEST_FAILED).toInt(); + atpMappingRequests["successful"] = statTracker->getStat(STAT_ATP_MAPPING_REQUEST_SUCCESS).toInt(); + properties["atp_mapping_requests"] = atpMappingRequests; + + properties["throttled"] = _displayPlugin ? _displayPlugin->isThrottled() : false; + + QJsonObject bytesDownloaded; + auto atpBytes = statTracker->getStat(STAT_ATP_RESOURCE_TOTAL_BYTES).toLongLong(); + auto httpBytes = statTracker->getStat(STAT_HTTP_RESOURCE_TOTAL_BYTES).toLongLong(); + auto fileBytes = statTracker->getStat(STAT_FILE_RESOURCE_TOTAL_BYTES).toLongLong(); + bytesDownloaded["atp"] = atpBytes; + bytesDownloaded["http"] = httpBytes; + bytesDownloaded["file"] = fileBytes; + bytesDownloaded["total"] = atpBytes + httpBytes + fileBytes; + properties["bytes_downloaded"] = bytesDownloaded; + + auto myAvatar = getMyAvatar(); + glm::vec3 avatarPosition = myAvatar->getWorldPosition(); + properties["avatar_has_moved"] = lastAvatarPosition != avatarPosition; + lastAvatarPosition = avatarPosition; + + auto entityScriptingInterface = DependencyManager::get(); + auto entityActivityTracking = entityScriptingInterface->getActivityTracking(); + entityScriptingInterface->resetActivityTracking(); + properties["added_entity_cnt"] = entityActivityTracking.addedEntityCount; + properties["deleted_entity_cnt"] = entityActivityTracking.deletedEntityCount; + properties["edited_entity_cnt"] = entityActivityTracking.editedEntityCount; + + NodeToOctreeSceneStats* octreeServerSceneStats = getOcteeSceneStats(); + unsigned long totalServerOctreeElements = 0; + for (NodeToOctreeSceneStatsIterator i = octreeServerSceneStats->begin(); i != octreeServerSceneStats->end(); i++) { + totalServerOctreeElements += i->second.getTotalElements(); + } + + properties["local_octree_elements"] = (qint64) OctreeElement::getInternalNodeCount(); + properties["server_octree_elements"] = (qint64) totalServerOctreeElements; + + properties["active_display_plugin"] = getActiveDisplayPlugin()->getName(); + properties["using_hmd"] = isHMDMode(); + + _autoSwitchDisplayModeSupportedHMDPlugin = nullptr; + foreach(DisplayPluginPointer displayPlugin, PluginManager::getInstance()->getDisplayPlugins()) { + if (displayPlugin->isHmd() && + displayPlugin->getSupportsAutoSwitch()) { + _autoSwitchDisplayModeSupportedHMDPlugin = displayPlugin; + _autoSwitchDisplayModeSupportedHMDPluginName = + _autoSwitchDisplayModeSupportedHMDPlugin->getName(); + _previousHMDWornStatus = + _autoSwitchDisplayModeSupportedHMDPlugin->isDisplayVisible(); + break; + } + } + + if (_autoSwitchDisplayModeSupportedHMDPlugin) { + if (getActiveDisplayPlugin() != _autoSwitchDisplayModeSupportedHMDPlugin && + !_autoSwitchDisplayModeSupportedHMDPlugin->isSessionActive()) { + startHMDStandBySession(); + } + // Poll periodically to check whether the user has worn HMD or not. Switch Display mode accordingly. + // If the user wears HMD then switch to VR mode. If the user removes HMD then switch to Desktop mode. + QTimer* autoSwitchDisplayModeTimer = new QTimer(this); + connect(autoSwitchDisplayModeTimer, SIGNAL(timeout()), this, SLOT(switchDisplayMode())); + autoSwitchDisplayModeTimer->start(INTERVAL_TO_CHECK_HMD_WORN_STATUS); + } + + auto glInfo = getGLContextData(); + properties["gl_info"] = glInfo; + properties["gpu_used_memory"] = (int)BYTES_TO_MB(gpu::Context::getUsedGPUMemSize()); + properties["gpu_free_memory"] = (int)BYTES_TO_MB(gpu::Context::getFreeGPUMemSize()); + properties["gpu_frame_time"] = (float)(qApp->getGPUContext()->getFrameTimerGPUAverage()); + properties["batch_frame_time"] = (float)(qApp->getGPUContext()->getFrameTimerBatchAverage()); + properties["ideal_thread_count"] = QThread::idealThreadCount(); + + auto hmdHeadPose = getHMDSensorPose(); + properties["hmd_head_pose_changed"] = isHMDMode() && (hmdHeadPose != lastHMDHeadPose); + lastHMDHeadPose = hmdHeadPose; + + auto leftHandPose = myAvatar->getLeftHandPose(); + auto rightHandPose = myAvatar->getRightHandPose(); + // controller::Pose considers two poses to be different if either are invalid. In our case, we actually + // want to consider the pose to be unchanged if it was invalid and still is invalid, so we check that first. + properties["hand_pose_changed"] = + ((leftHandPose.valid || lastLeftHandPose.valid) && (leftHandPose != lastLeftHandPose)) + || ((rightHandPose.valid || lastRightHandPose.valid) && (rightHandPose != lastRightHandPose)); + lastLeftHandPose = leftHandPose; + lastRightHandPose = rightHandPose; + + UserActivityLogger::getInstance().logAction("stats", properties); + }); + sendStatsTimer->start(); + + // Periodically check for count of nearby avatars + static int lastCountOfNearbyAvatars = -1; + QTimer* checkNearbyAvatarsTimer = new QTimer(this); + checkNearbyAvatarsTimer->setInterval(CHECK_NEARBY_AVATARS_INTERVAL_MS); // 10 seconds, Qt::CoarseTimer ok + connect(checkNearbyAvatarsTimer, &QTimer::timeout, this, []() { + auto avatarManager = DependencyManager::get(); + int nearbyAvatars = avatarManager->numberOfAvatarsInRange(avatarManager->getMyAvatar()->getWorldPosition(), + NEARBY_AVATAR_RADIUS_METERS) - 1; + if (nearbyAvatars != lastCountOfNearbyAvatars) { + lastCountOfNearbyAvatars = nearbyAvatars; + UserActivityLogger::getInstance().logAction("nearby_avatars", { { "count", nearbyAvatars } }); + } + }); + checkNearbyAvatarsTimer->start(); + + // Track user activity event when we receive a mute packet + auto onMutedByMixer = []() { + UserActivityLogger::getInstance().logAction("received_mute_packet"); + }; + connect(DependencyManager::get().data(), &AudioClient::mutedByMixer, this, onMutedByMixer); + + // Track when the address bar is opened + auto onAddressBarShown = [this]() { + // Record time + UserActivityLogger::getInstance().logAction("opened_address_bar", { { "uptime_ms", _sessionRunTimer.elapsed() } }); + }; + connect(DependencyManager::get().data(), &DialogsManager::addressBarShown, this, onAddressBarShown); + + // Make sure we don't time out during slow operations at startup + updateHeartbeat(); + + OctreeEditPacketSender* packetSender = entityScriptingInterface->getPacketSender(); + EntityEditPacketSender* entityPacketSender = static_cast(packetSender); + entityPacketSender->setMyAvatar(myAvatar.get()); + + connect(this, &Application::applicationStateChanged, this, &Application::activeChanged); + connect(_window, SIGNAL(windowMinimizedChanged(bool)), this, SLOT(windowMinimizedChanged(bool))); + qCDebug(interfaceapp, "Startup time: %4.2f seconds.", (double)startupTimer.elapsed() / 1000.0); + + EntityTreeRenderer::setEntitiesShouldFadeFunction([this]() { + SharedNodePointer entityServerNode = DependencyManager::get()->soloNodeOfType(NodeType::EntityServer); + return entityServerNode && !isPhysicsEnabled(); + }); + + _snapshotSound = DependencyManager::get()->getSound(PathUtils::resourcesUrl("sounds/snapshot/snap.wav")); + + // Monitor model assets (e.g., from Clara.io) added to the world that may need resizing. + static const int ADD_ASSET_TO_WORLD_TIMER_INTERVAL_MS = 1000; + _addAssetToWorldResizeTimer.setInterval(ADD_ASSET_TO_WORLD_TIMER_INTERVAL_MS); // 1s, Qt::CoarseTimer acceptable + connect(&_addAssetToWorldResizeTimer, &QTimer::timeout, this, &Application::addAssetToWorldCheckModelSize); + + // Auto-update and close adding asset to world info message box. + static const int ADD_ASSET_TO_WORLD_INFO_TIMEOUT_MS = 5000; + _addAssetToWorldInfoTimer.setInterval(ADD_ASSET_TO_WORLD_INFO_TIMEOUT_MS); // 5s, Qt::CoarseTimer acceptable + _addAssetToWorldInfoTimer.setSingleShot(true); + connect(&_addAssetToWorldInfoTimer, &QTimer::timeout, this, &Application::addAssetToWorldInfoTimeout); + static const int ADD_ASSET_TO_WORLD_ERROR_TIMEOUT_MS = 8000; + _addAssetToWorldErrorTimer.setInterval(ADD_ASSET_TO_WORLD_ERROR_TIMEOUT_MS); // 8s, Qt::CoarseTimer acceptable + _addAssetToWorldErrorTimer.setSingleShot(true); + connect(&_addAssetToWorldErrorTimer, &QTimer::timeout, this, &Application::addAssetToWorldErrorTimeout); + + connect(this, &QCoreApplication::aboutToQuit, this, &Application::addAssetToWorldMessageClose); + connect(&domainHandler, &DomainHandler::domainURLChanged, this, &Application::addAssetToWorldMessageClose); + connect(&domainHandler, &DomainHandler::redirectToErrorDomainURL, this, &Application::addAssetToWorldMessageClose); + + updateSystemTabletMode(); + + connect(&_myCamera, &Camera::modeUpdated, this, &Application::cameraModeChanged); + + DependencyManager::get()->setShouldPickHUDOperator([]() { return DependencyManager::get()->isHMDMode(); }); + DependencyManager::get()->setCalculatePos2DFromHUDOperator([this](const glm::vec3& intersection) { + const glm::vec2 MARGIN(25.0f); + glm::vec2 maxPos = _controllerScriptingInterface->getViewportDimensions() - MARGIN; + glm::vec2 pos2D = DependencyManager::get()->overlayFromWorldPoint(intersection); + return glm::max(MARGIN, glm::min(pos2D, maxPos)); + }); + + // Setup the mouse ray pick and related operators + { + auto mouseRayPick = std::make_shared(Vectors::ZERO, Vectors::UP, PickFilter(PickScriptingInterface::PICK_ENTITIES() | PickScriptingInterface::PICK_LOCAL_ENTITIES()), 0.0f, true); + mouseRayPick->parentTransform = std::make_shared(); + mouseRayPick->setJointState(PickQuery::JOINT_STATE_MOUSE); + auto mouseRayPickID = DependencyManager::get()->addPick(PickQuery::Ray, mouseRayPick); + DependencyManager::get()->setMouseRayPickID(mouseRayPickID); + } + DependencyManager::get()->setMouseRayPickResultOperator([](unsigned int rayPickID) { + RayToEntityIntersectionResult entityResult; + entityResult.intersects = false; + auto pickResult = DependencyManager::get()->getPrevPickResultTyped(rayPickID); + if (pickResult) { + entityResult.intersects = pickResult->type != IntersectionType::NONE; + if (entityResult.intersects) { + entityResult.intersection = pickResult->intersection; + entityResult.distance = pickResult->distance; + entityResult.surfaceNormal = pickResult->surfaceNormal; + entityResult.entityID = pickResult->objectID; + entityResult.extraInfo = pickResult->extraInfo; + } + } + return entityResult; + }); + DependencyManager::get()->setSetPrecisionPickingOperator([](unsigned int rayPickID, bool value) { + DependencyManager::get()->setPrecisionPicking(rayPickID, value); + }); + + EntityItem::setBillboardRotationOperator([this](const glm::vec3& position, const glm::quat& rotation, BillboardMode billboardMode, const glm::vec3& frustumPos) { + if (billboardMode == BillboardMode::YAW) { + //rotate about vertical to face the camera + glm::vec3 dPosition = frustumPos - position; + // If x and z are 0, atan(x, z) is undefined, so default to 0 degrees + float yawRotation = dPosition.x == 0.0f && dPosition.z == 0.0f ? 0.0f : glm::atan(dPosition.x, dPosition.z); + return glm::quat(glm::vec3(0.0f, yawRotation, 0.0f)); + } else if (billboardMode == BillboardMode::FULL) { + // use the referencial from the avatar, y isn't always up + glm::vec3 avatarUP = DependencyManager::get()->getMyAvatar()->getWorldOrientation() * Vectors::UP; + // check to see if glm::lookAt will work / using glm::lookAt variable name + glm::highp_vec3 s(glm::cross(position - frustumPos, avatarUP)); + + // make sure s is not NaN for any component + if (glm::length2(s) > 0.0f) { + return glm::conjugate(glm::toQuat(glm::lookAt(frustumPos, position, avatarUP))); + } + } + return rotation; + }); + EntityItem::setPrimaryViewFrustumPositionOperator([this]() { + ViewFrustum viewFrustum; + copyViewFrustum(viewFrustum); + return viewFrustum.getPosition(); + }); + + render::entities::WebEntityRenderer::setAcquireWebSurfaceOperator([this](const QString& url, bool htmlContent, QSharedPointer& webSurface, bool& cachedWebSurface) { + bool isTablet = url == TabletScriptingInterface::QML; + if (htmlContent) { + webSurface = DependencyManager::get()->acquire(render::entities::WebEntityRenderer::QML); + cachedWebSurface = true; + auto rootItemLoadedFunctor = [url, webSurface] { + webSurface->getRootItem()->setProperty(render::entities::WebEntityRenderer::URL_PROPERTY, url); + }; + if (webSurface->getRootItem()) { + rootItemLoadedFunctor(); + } else { + QObject::connect(webSurface.data(), &hifi::qml::OffscreenSurface::rootContextCreated, rootItemLoadedFunctor); + } + auto surfaceContext = webSurface->getSurfaceContext(); + surfaceContext->setContextProperty("KeyboardScriptingInterface", DependencyManager::get().data()); + } else { + // FIXME: the tablet should use the OffscreenQmlSurfaceCache + webSurface = QSharedPointer(new OffscreenQmlSurface(), [](OffscreenQmlSurface* webSurface) { + AbstractViewStateInterface::instance()->sendLambdaEvent([webSurface] { + // WebEngineView may run other threads (wasapi), so they must be deleted for a clean shutdown + // if the application has already stopped its event loop, delete must be explicit + delete webSurface; + }); + }); + auto rootItemLoadedFunctor = [webSurface, url, isTablet] { + Application::setupQmlSurface(webSurface->getSurfaceContext(), isTablet || url == LOGIN_DIALOG.toString()); + }; + if (webSurface->getRootItem()) { + rootItemLoadedFunctor(); + } else { + QObject::connect(webSurface.data(), &hifi::qml::OffscreenSurface::rootContextCreated, rootItemLoadedFunctor); + } + webSurface->load(url); + cachedWebSurface = false; + } + const uint8_t DEFAULT_MAX_FPS = 10; + const uint8_t TABLET_FPS = 90; + webSurface->setMaxFps(isTablet ? TABLET_FPS : DEFAULT_MAX_FPS); + }); + render::entities::WebEntityRenderer::setReleaseWebSurfaceOperator([this](QSharedPointer& webSurface, bool& cachedWebSurface, std::vector& connections) { + QQuickItem* rootItem = webSurface->getRootItem(); + + // Fix for crash in QtWebEngineCore when rapidly switching domains + // Call stop on the QWebEngineView before destroying OffscreenQMLSurface. + if (rootItem && !cachedWebSurface) { + // stop loading + QMetaObject::invokeMethod(rootItem, "stop"); + } + + webSurface->pause(); + + for (auto& connection : connections) { + QObject::disconnect(connection); + } + connections.clear(); + + // If the web surface was fetched out of the cache, release it back into the cache + if (cachedWebSurface) { + // If it's going back into the cache make sure to explicitly set the URL to a blank page + // in order to stop any resource consumption or audio related to the page. + if (rootItem) { + rootItem->setProperty("url", "about:blank"); + } + auto offscreenCache = DependencyManager::get(); + if (offscreenCache) { + offscreenCache->release(render::entities::WebEntityRenderer::QML, webSurface); + } + cachedWebSurface = false; + } + webSurface.reset(); + }); + + // Preload Tablet sounds + DependencyManager::get()->setEntityTree(qApp->getEntities()->getTree()); + DependencyManager::get()->preloadSounds(); + DependencyManager::get()->createKeyboard(); + + _pendingIdleEvent = false; + _graphicsEngine.startup(); + + qCDebug(interfaceapp) << "Metaverse session ID is" << uuidStringWithoutCurlyBraces(accountManager->getSessionID()); + +#if defined(Q_OS_ANDROID) + connect(&AndroidHelper::instance(), &AndroidHelper::beforeEnterBackground, this, &Application::beforeEnterBackground); + connect(&AndroidHelper::instance(), &AndroidHelper::enterBackground, this, &Application::enterBackground); + connect(&AndroidHelper::instance(), &AndroidHelper::enterForeground, this, &Application::enterForeground); + connect(&AndroidHelper::instance(), &AndroidHelper::toggleAwayMode, this, &Application::toggleAwayMode); + AndroidHelper::instance().notifyLoadComplete(); +#endif + pauseUntilLoginDetermined(); +} + +void Application::updateVerboseLogging() { + auto menu = Menu::getInstance(); + if (!menu) { + return; + } + bool enable = menu->isOptionChecked(MenuOption::VerboseLogging); + + QString rules = + "hifi.*.info=%1\n" + "hifi.audio-stream.debug=false\n" + "hifi.audio-stream.info=false"; + rules = rules.arg(enable ? "true" : "false"); + QLoggingCategory::setFilterRules(rules); +} + +void Application::domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo) { + DomainHandler::ConnectionRefusedReason reasonCode = static_cast(reasonCodeInt); + + if (reasonCode == DomainHandler::ConnectionRefusedReason::TooManyUsers && !extraInfo.isEmpty()) { + DependencyManager::get()->handleLookupString(extraInfo); + return; + } + + switch (reasonCode) { + case DomainHandler::ConnectionRefusedReason::ProtocolMismatch: + case DomainHandler::ConnectionRefusedReason::TooManyUsers: + case DomainHandler::ConnectionRefusedReason::Unknown: { + QString message = "Unable to connect to the location you are visiting.\n"; + message += reasonMessage; + OffscreenUi::asyncWarning("", message); + getMyAvatar()->setWorldVelocity(glm::vec3(0.0f)); + break; + } + default: + // nothing to do. + break; + } +} + +QString Application::getUserAgent() { + if (QThread::currentThread() != thread()) { + QString userAgent; + + BLOCKING_INVOKE_METHOD(this, "getUserAgent", Q_RETURN_ARG(QString, userAgent)); + + return userAgent; + } + + QString userAgent = "Mozilla/5.0 (HighFidelityInterface/" + BuildInfo::VERSION + "; " + + QSysInfo::productType() + " " + QSysInfo::productVersion() + ")"; + + auto formatPluginName = [](QString name) -> QString { return name.trimmed().replace(" ", "-"); }; + + // For each plugin, add to userAgent + auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); + for (auto& dp : displayPlugins) { + if (dp->isActive() && dp->isHmd()) { + userAgent += " " + formatPluginName(dp->getName()); + } + } + auto inputPlugins= PluginManager::getInstance()->getInputPlugins(); + for (auto& ip : inputPlugins) { + if (ip->isActive()) { + userAgent += " " + formatPluginName(ip->getName()); + } + } + // for codecs, we include all of them, even if not active + auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); + for (auto& cp : codecPlugins) { + userAgent += " " + formatPluginName(cp->getName()); + } + + return userAgent; +} + +void Application::toggleTabletUI(bool shouldOpen) const { + auto hmd = DependencyManager::get(); + if (!(shouldOpen && hmd->getShouldShowTablet())) { + auto HMD = DependencyManager::get(); + HMD->toggleShouldShowTablet(); + + if (!HMD->getShouldShowTablet()) { + DependencyManager::get()->setRaised(false); + _window->activateWindow(); + auto tablet = DependencyManager::get()->getTablet(SYSTEM_TABLET); + tablet->unfocus(); + } + } +} + +void Application::checkChangeCursor() { + QMutexLocker locker(&_changeCursorLock); + if (_cursorNeedsChanging) { +#ifdef Q_OS_MAC + auto cursorTarget = _window; // OSX doesn't seem to provide for hiding the cursor only on the GL widget +#else + // On windows and linux, hiding the top level cursor also means it's invisible when hovering over the + // window menu, which is a pain, so only hide it for the GL surface + auto cursorTarget = _glWidget; +#endif + cursorTarget->setCursor(_desiredCursor); + + _cursorNeedsChanging = false; + } +} + +void Application::showCursor(const Cursor::Icon& cursor) { + QMutexLocker locker(&_changeCursorLock); + + auto managedCursor = Cursor::Manager::instance().getCursor(); + auto curIcon = managedCursor->getIcon(); + if (curIcon != cursor) { + managedCursor->setIcon(cursor); + curIcon = cursor; + } + _desiredCursor = cursor == Cursor::Icon::SYSTEM ? Qt::ArrowCursor : Qt::BlankCursor; + _cursorNeedsChanging = true; +} + +void Application::updateHeartbeat() const { + DeadlockWatchdogThread::updateHeartbeat(); +} + +void Application::onAboutToQuit() { + // quickly save AvatarEntityData before the EntityTree is dismantled + getMyAvatar()->saveAvatarEntityDataToSettings(); + + emit beforeAboutToQuit(); + + if (getLoginDialogPoppedUp() && _firstRun.get()) { + _firstRun.set(false); + } + + foreach(auto inputPlugin, PluginManager::getInstance()->getInputPlugins()) { + if (inputPlugin->isActive()) { + inputPlugin->deactivate(); + } + } + + // The active display plugin needs to be loaded before the menu system is active, + // so its persisted explicitly here + Setting::Handle{ ACTIVE_DISPLAY_PLUGIN_SETTING_NAME }.set(getActiveDisplayPlugin()->getName()); + + loginDialogPoppedUp.set(false); + + getActiveDisplayPlugin()->deactivate(); + if (_autoSwitchDisplayModeSupportedHMDPlugin + && _autoSwitchDisplayModeSupportedHMDPlugin->isSessionActive()) { + _autoSwitchDisplayModeSupportedHMDPlugin->endSession(); + } + // use the CloseEventSender via a QThread to send an event that says the user asked for the app to close + DependencyManager::get()->startThread(); + + // Hide Running Scripts dialog so that it gets destroyed in an orderly manner; prevents warnings at shutdown. +#if !defined(DISABLE_QML) + getOffscreenUI()->hide("RunningScripts"); +#endif + + _aboutToQuit = true; + + cleanupBeforeQuit(); +} + +void Application::cleanupBeforeQuit() { + // add a logline indicating if QTWEBENGINE_REMOTE_DEBUGGING is set or not + QString webengineRemoteDebugging = QProcessEnvironment::systemEnvironment().value("QTWEBENGINE_REMOTE_DEBUGGING", "false"); + qCDebug(interfaceapp) << "QTWEBENGINE_REMOTE_DEBUGGING =" << webengineRemoteDebugging; + + DependencyManager::prepareToExit(); + + if (tracing::enabled()) { + auto tracer = DependencyManager::get(); + tracer->stopTracing(); + auto outputFile = property(hifi::properties::TRACING).toString(); + tracer->serialize(outputFile); + } + + // Stop third party processes so that they're not left running in the event of a subsequent shutdown crash. +#ifdef HAVE_DDE + DependencyManager::get()->setEnabled(false); +#endif +#ifdef HAVE_IVIEWHMD + DependencyManager::get()->setEnabled(false, true); +#endif + AnimDebugDraw::getInstance().shutdown(); + + // FIXME: once we move to shared pointer for the INputDevice we shoud remove this naked delete: + _applicationStateDevice.reset(); + + { + if (_keyboardFocusHighlightID != UNKNOWN_ENTITY_ID) { + DependencyManager::get()->deleteEntity(_keyboardFocusHighlightID); + _keyboardFocusHighlightID = UNKNOWN_ENTITY_ID; + } + } + + { + auto nodeList = DependencyManager::get(); + + // send the domain a disconnect packet, force stoppage of domain-server check-ins + nodeList->getDomainHandler().disconnect(); + nodeList->setIsShuttingDown(true); + + // tell the packet receiver we're shutting down, so it can drop packets + nodeList->getPacketReceiver().setShouldDropPackets(true); + } + + getEntities()->shutdown(); // tell the entities system we're shutting down, so it will stop running scripts + + // Clear any queued processing (I/O, FBX/OBJ/Texture parsing) + QThreadPool::globalInstance()->clear(); + + DependencyManager::destroy(); + + // FIXME: Something is still holding on to the ScriptEnginePointers contained in ScriptEngines, and they hold backpointers to ScriptEngines, + // so this doesn't shut down properly + DependencyManager::get()->shutdownScripting(); // stop all currently running global scripts + // These classes hold ScriptEnginePointers, so they must be destroyed before ScriptEngines + // Must be done after shutdownScripting in case any scripts try to access these things + { + DependencyManager::destroy(); + EntityTreePointer tree = getEntities()->getTree(); + tree->setSimulation(nullptr); + DependencyManager::destroy(); + } + DependencyManager::destroy(); + + bool keepMeLoggedIn = Setting::Handle(KEEP_ME_LOGGED_IN_SETTING_NAME, false).get(); + if (!keepMeLoggedIn) { + DependencyManager::get()->removeAccountFromFile(); + } + + _displayPlugin.reset(); + PluginManager::getInstance()->shutdown(); + + // Cleanup all overlays after the scripts, as scripts might add more + _overlays.cleanupAllOverlays(); + + // first stop all timers directly or by invokeMethod + // depending on what thread they run in + locationUpdateTimer.stop(); + identityPacketTimer.stop(); + pingTimer.stop(); + + // Wait for the settings thread to shut down, and save the settings one last time when it's safe + if (_settingsGuard.wait()) { + // save state + saveSettings(); + } + + _window->saveGeometry(); + + // Destroy third party processes after scripts have finished using them. +#ifdef HAVE_DDE + DependencyManager::destroy(); +#endif +#ifdef HAVE_IVIEWHMD + DependencyManager::destroy(); +#endif + + DependencyManager::destroy(); // Must be destroyed before TabletScriptingInterface + + // stop QML + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + + DependencyManager::destroy(); + + if (_snapshotSoundInjector != nullptr) { + _snapshotSoundInjector->stop(); + } + + // destroy Audio so it and its threads have a chance to go down safely + // this must happen after QML, as there are unexplained audio crashes originating in qtwebengine + QMetaObject::invokeMethod(DependencyManager::get().data(), "stop"); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + + // The PointerManager must be destroyed before the PickManager because when a Pointer is deleted, + // it accesses the PickManager to delete its associated Pick + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + + qCDebug(interfaceapp) << "Application::cleanupBeforeQuit() complete"; +} + +Application::~Application() { + // remove avatars from physics engine + auto avatarManager = DependencyManager::get(); + avatarManager->clearOtherAvatars(); + + PhysicsEngine::Transaction transaction; + avatarManager->buildPhysicsTransaction(transaction); + _physicsEngine->processTransaction(transaction); + avatarManager->handleProcessedPhysicsTransaction(transaction); + + avatarManager->deleteAllAvatars(); + + auto myCharacterController = getMyAvatar()->getCharacterController(); + myCharacterController->clearDetailedMotionStates(); + + myCharacterController->buildPhysicsTransaction(transaction); + _physicsEngine->processTransaction(transaction); + myCharacterController->handleProcessedPhysicsTransaction(transaction); + + _physicsEngine->setCharacterController(nullptr); + + // the _shapeManager should have zero references + _shapeManager.collectGarbage(); + assert(_shapeManager.getNumShapes() == 0); + + // shutdown graphics engine + _graphicsEngine.shutdown(); + + _gameWorkload.shutdown(); + + DependencyManager::destroy(); + + _entityClipboard->eraseAllOctreeElements(); + _entityClipboard.reset(); + + _octreeProcessor.terminate(); + _entityEditSender.terminate(); + + if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { + steamClient->shutdown(); + } + + if (auto oculusPlatform = PluginManager::getInstance()->getOculusPlatformPlugin()) { + oculusPlatform->shutdown(); + } + + DependencyManager::destroy(); + + DependencyManager::destroy(); // must be destroyed before the FramebufferCache + + DependencyManager::destroy(); + + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + + DependencyManager::get()->cleanup(); + + // remove the NodeList from the DependencyManager + DependencyManager::destroy(); + +#if 0 + ConnexionClient::getInstance().destroy(); +#endif + // The window takes ownership of the menu, so this has the side effect of destroying it. + _window->setMenuBar(nullptr); + + _window->deleteLater(); + + // make sure that the quit event has finished sending before we take the application down + auto closeEventSender = DependencyManager::get(); + while (!closeEventSender->hasFinishedQuitEvent() && !closeEventSender->hasTimedOutQuitEvent()) { + // sleep a little so we're not spinning at 100% + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + // quit the thread used by the closure event sender + closeEventSender->thread()->quit(); + + // Can't log to file past this point, FileLogger about to be deleted + qInstallMessageHandler(LogHandler::verboseMessageHandler); +} + +void Application::initializeGL() { + qCDebug(interfaceapp) << "Created Display Window."; + +#ifdef DISABLE_QML + setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity); +#endif + + // initialize glut for shape drawing; Qt apparently initializes it on OS X + if (_isGLInitialized) { + return; + } else { + _isGLInitialized = true; + } + + _glWidget->windowHandle()->setFormat(getDefaultOpenGLSurfaceFormat()); + + // When loading QtWebEngineWidgets, it creates a global share context on startup. + // We have to account for this possibility by checking here for an existing + // global share context + auto globalShareContext = qt_gl_global_share_context(); + +#if !defined(DISABLE_QML) + // Build a shared canvas / context for the Chromium processes + if (!globalShareContext) { + // Chromium rendering uses some GL functions that prevent nSight from capturing + // frames, so we only create the shared context if nsight is NOT active. + if (!nsightActive()) { + _chromiumShareContext = new OffscreenGLCanvas(); + _chromiumShareContext->setObjectName("ChromiumShareContext"); + auto format =QSurfaceFormat::defaultFormat(); +#ifdef Q_OS_MAC + // On mac, the primary shared OpenGL context must be a 3.2 core context, + // or chromium flips out and spews error spam (but renders fine) + format.setMajorVersion(3); + format.setMinorVersion(2); +#endif + _chromiumShareContext->setFormat(format); + _chromiumShareContext->create(); + if (!_chromiumShareContext->makeCurrent()) { + qCWarning(interfaceapp, "Unable to make chromium shared context current"); + } + globalShareContext = _chromiumShareContext->getContext(); + qt_gl_set_global_share_context(globalShareContext); + _chromiumShareContext->doneCurrent(); + } + } +#endif + + + _glWidget->createContext(globalShareContext); + + if (!_glWidget->makeCurrent()) { + qCWarning(interfaceapp, "Unable to make window context current"); + } + +#if !defined(DISABLE_QML) + // Disable signed distance field font rendering on ATI/AMD GPUs, due to + // https://highfidelity.manuscript.com/f/cases/13677/Text-showing-up-white-on-Marketplace-app + std::string vendor{ (const char*)glGetString(GL_VENDOR) }; + if ((vendor.find("AMD") != std::string::npos) || (vendor.find("ATI") != std::string::npos)) { + qputenv("QTWEBENGINE_CHROMIUM_FLAGS", QByteArray("--disable-distance-field-text")); + } +#endif + + if (!globalShareContext) { + globalShareContext = _glWidget->qglContext(); + qt_gl_set_global_share_context(globalShareContext); + } + + // Build a shared canvas / context for the QML rendering +#if !defined(DISABLE_QML) + { + _qmlShareContext = new OffscreenGLCanvas(); + _qmlShareContext->setObjectName("QmlShareContext"); + _qmlShareContext->create(globalShareContext); + if (!_qmlShareContext->makeCurrent()) { + qCWarning(interfaceapp, "Unable to make QML shared context current"); + } + OffscreenQmlSurface::setSharedContext(_qmlShareContext->getContext()); + _qmlShareContext->doneCurrent(); + if (!_glWidget->makeCurrent()) { + qCWarning(interfaceapp, "Unable to make window context current"); + } + } +#endif + + + // Build an offscreen GL context for the main thread. + _glWidget->makeCurrent(); + glClearColor(0.2f, 0.2f, 0.2f, 1); + glClear(GL_COLOR_BUFFER_BIT); + _glWidget->swapBuffers(); + + _graphicsEngine.initializeGPU(_glWidget); +} + +void Application::initializeDisplayPlugins() { + auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); + Setting::Handle activeDisplayPluginSetting{ ACTIVE_DISPLAY_PLUGIN_SETTING_NAME, displayPlugins.at(0)->getName() }; + auto lastActiveDisplayPluginName = activeDisplayPluginSetting.get(); + + auto defaultDisplayPlugin = displayPlugins.at(0); + // Once time initialization code + DisplayPluginPointer targetDisplayPlugin; + foreach(auto displayPlugin, displayPlugins) { + displayPlugin->setContext(_graphicsEngine.getGPUContext()); + if (displayPlugin->getName() == lastActiveDisplayPluginName) { + targetDisplayPlugin = displayPlugin; + } + QObject::connect(displayPlugin.get(), &DisplayPlugin::recommendedFramebufferSizeChanged, + [this](const QSize& size) { resizeGL(); }); + QObject::connect(displayPlugin.get(), &DisplayPlugin::resetSensorsRequested, this, &Application::requestReset); + if (displayPlugin->isHmd()) { + auto hmdDisplayPlugin = dynamic_cast(displayPlugin.get()); + QObject::connect(hmdDisplayPlugin, &HmdDisplayPlugin::hmdMountedChanged, + DependencyManager::get().data(), &HMDScriptingInterface::mountedChanged); + QObject::connect(hmdDisplayPlugin, &HmdDisplayPlugin::hmdVisibleChanged, this, &Application::hmdVisibleChanged); + } + } + + // The default display plugin needs to be activated first, otherwise the display plugin thread + // may be launched by an external plugin, which is bad + setDisplayPlugin(defaultDisplayPlugin); + + // Now set the desired plugin if it's not the same as the default plugin + if (targetDisplayPlugin && (targetDisplayPlugin != defaultDisplayPlugin)) { + setDisplayPlugin(targetDisplayPlugin); + } + + // Submit a default frame to render until the engine starts up + updateRenderArgs(0.0f); +} + +void Application::initializeRenderEngine() { + // FIXME: on low end systems os the shaders take up to 1 minute to compile, so we pause the deadlock watchdog thread. + DeadlockWatchdogThread::withPause([&] { + _graphicsEngine.initializeRender(DISABLE_DEFERRED); + DependencyManager::get()->registerKeyboardHighlighting(); + }); +} + +extern void setupPreferences(); +#if !defined(DISABLE_QML) +static void addDisplayPluginToMenu(const DisplayPluginPointer& displayPlugin, int index, bool active = false); +#endif + +void Application::showLoginScreen() { +#if !defined(DISABLE_QML) + auto accountManager = DependencyManager::get(); + auto dialogsManager = DependencyManager::get(); + if (!accountManager->isLoggedIn()) { + if (!isHMDMode()) { + auto toolbar = DependencyManager::get()->getToolbar("com.highfidelity.interface.toolbar.system"); + toolbar->writeProperty("visible", false); + } + _loginDialogPoppedUp = true; + dialogsManager->showLoginDialog(); + emit loginDialogFocusEnabled(); + QJsonObject loginData = {}; + loginData["action"] = "login dialog popped up"; + UserActivityLogger::getInstance().logAction("encourageLoginDialog", loginData); + _window->setWindowTitle("High Fidelity Interface"); + } else { + resumeAfterLoginDialogActionTaken(); + } + _loginDialogPoppedUp = !accountManager->isLoggedIn(); + loginDialogPoppedUp.set(_loginDialogPoppedUp); +#else + resumeAfterLoginDialogActionTaken(); +#endif +} + +void Application::initializeUi() { + AddressBarDialog::registerType(); + ErrorDialog::registerType(); + LoginDialog::registerType(); + Tooltip::registerType(); + UpdateDialog::registerType(); + QmlContextCallback commerceCallback = [](QQmlContext* context) { + context->setContextProperty("Commerce", DependencyManager::get().data()); + }; + OffscreenQmlSurface::addWhitelistContextHandler({ + QUrl{ "hifi/commerce/checkout/Checkout.qml" }, + QUrl{ "hifi/commerce/common/CommerceLightbox.qml" }, + QUrl{ "hifi/commerce/common/EmulatedMarketplaceHeader.qml" }, + QUrl{ "hifi/commerce/common/FirstUseTutorial.qml" }, + QUrl{ "hifi/commerce/common/sendAsset/SendAsset.qml" }, + QUrl{ "hifi/commerce/common/SortableListModel.qml" }, + QUrl{ "hifi/commerce/inspectionCertificate/InspectionCertificate.qml" }, + QUrl{ "hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml"}, + QUrl{ "hifi/commerce/purchases/PurchasedItem.qml" }, + QUrl{ "hifi/commerce/purchases/Purchases.qml" }, + QUrl{ "hifi/commerce/wallet/Help.qml" }, + QUrl{ "hifi/commerce/wallet/NeedsLogIn.qml" }, + QUrl{ "hifi/commerce/wallet/PassphraseChange.qml" }, + QUrl{ "hifi/commerce/wallet/PassphraseModal.qml" }, + QUrl{ "hifi/commerce/wallet/PassphraseSelection.qml" }, + QUrl{ "hifi/commerce/wallet/Wallet.qml" }, + QUrl{ "hifi/commerce/wallet/WalletHome.qml" }, + QUrl{ "hifi/commerce/wallet/WalletSetup.qml" }, + QUrl{ "hifi/dialogs/security/Security.qml" }, + QUrl{ "hifi/dialogs/security/SecurityImageChange.qml" }, + QUrl{ "hifi/dialogs/security/SecurityImageModel.qml" }, + QUrl{ "hifi/dialogs/security/SecurityImageSelection.qml" }, + QUrl{ "hifi/tablet/TabletMenu.qml" }, + QUrl{ "hifi/commerce/marketplace/Marketplace.qml" }, + }, commerceCallback); + + QmlContextCallback marketplaceCallback = [](QQmlContext* context) { + context->setContextProperty("MarketplaceScriptingInterface", new QmlMarketplace()); + }; + OffscreenQmlSurface::addWhitelistContextHandler({ + QUrl{ "hifi/commerce/marketplace/Marketplace.qml" }, + }, marketplaceCallback); + + QmlContextCallback platformInfoCallback = [](QQmlContext* context) { + context->setContextProperty("PlatformInfo", new PlatformInfoScriptingInterface()); + }; + OffscreenQmlSurface::addWhitelistContextHandler({ + QUrl{ "hifi/commerce/marketplace/Marketplace.qml" }, + }, platformInfoCallback); + + QmlContextCallback ttsCallback = [](QQmlContext* context) { + context->setContextProperty("TextToSpeech", DependencyManager::get().data()); + }; + OffscreenQmlSurface::addWhitelistContextHandler({ + QUrl{ "hifi/tts/TTS.qml" } + }, ttsCallback); + qmlRegisterType("Hifi", 1, 0, "ResourceImageItem"); + qmlRegisterType("Hifi", 1, 0, "Preference"); + qmlRegisterType("HifiWeb", 1, 0, "WebBrowserSuggestionsEngine"); + + { + auto tabletScriptingInterface = DependencyManager::get(); + tabletScriptingInterface->getTablet(SYSTEM_TABLET); + } + + auto offscreenUi = getOffscreenUI(); + connect(offscreenUi.data(), &hifi::qml::OffscreenSurface::rootContextCreated, + this, &Application::onDesktopRootContextCreated); + connect(offscreenUi.data(), &hifi::qml::OffscreenSurface::rootItemCreated, + this, &Application::onDesktopRootItemCreated); + +#if !defined(DISABLE_QML) + offscreenUi->setProxyWindow(_window->windowHandle()); + // OffscreenUi is a subclass of OffscreenQmlSurface specifically designed to + // support the window management and scripting proxies for VR use + DeadlockWatchdogThread::withPause([&] { + offscreenUi->createDesktop(PathUtils::qmlUrl("hifi/Desktop.qml")); + }); + // FIXME either expose so that dialogs can set this themselves or + // do better detection in the offscreen UI of what has focus + offscreenUi->setNavigationFocused(false); +#else + _window->setMenuBar(new Menu()); +#endif + + setupPreferences(); + +#if !defined(DISABLE_QML) + _glWidget->installEventFilter(offscreenUi.data()); + offscreenUi->setMouseTranslator([=](const QPointF& pt) { + QPointF result = pt; + auto displayPlugin = getActiveDisplayPlugin(); + if (displayPlugin->isHmd()) { + getApplicationCompositor().handleRealMouseMoveEvent(false); + auto resultVec = getApplicationCompositor().getReticlePosition(); + result = QPointF(resultVec.x, resultVec.y); + } + return result.toPoint(); + }); + offscreenUi->resume(); +#endif + connect(_window, &MainWindow::windowGeometryChanged, [this](const QRect& r){ + resizeGL(); + if (_touchscreenVirtualPadDevice) { + _touchscreenVirtualPadDevice->resize(); + } + }); + + // This will set up the input plugins UI + _activeInputPlugins.clear(); + foreach(auto inputPlugin, PluginManager::getInstance()->getInputPlugins()) { + if (KeyboardMouseDevice::NAME == inputPlugin->getName()) { + _keyboardMouseDevice = std::dynamic_pointer_cast(inputPlugin); + } + if (TouchscreenDevice::NAME == inputPlugin->getName()) { + _touchscreenDevice = std::dynamic_pointer_cast(inputPlugin); + } + if (TouchscreenVirtualPadDevice::NAME == inputPlugin->getName()) { + _touchscreenVirtualPadDevice = std::dynamic_pointer_cast(inputPlugin); +#if defined(ANDROID_APP_INTERFACE) + auto& virtualPadManager = VirtualPad::Manager::instance(); + connect(&virtualPadManager, &VirtualPad::Manager::hapticFeedbackRequested, + this, [](int duration) { + AndroidHelper::instance().performHapticFeedback(duration); + }); +#endif + } + } + + auto compositorHelper = DependencyManager::get(); + connect(compositorHelper.data(), &CompositorHelper::allowMouseCaptureChanged, this, [=] { + if (isHMDMode()) { + auto compositorHelper = DependencyManager::get(); // don't capture outer smartpointer + showCursor(compositorHelper->getAllowMouseCapture() ? + Cursor::Manager::lookupIcon(_preferredCursor.get()) : + Cursor::Icon::SYSTEM); + } + }); + +#if !defined(DISABLE_QML) + // Pre-create a couple of offscreen surfaces to speed up tablet UI + auto offscreenSurfaceCache = DependencyManager::get(); + offscreenSurfaceCache->setOnRootContextCreated([&](const QString& rootObject, QQmlContext* surfaceContext) { + if (rootObject == TabletScriptingInterface::QML) { + // in Qt 5.10.0 there is already an "Audio" object in the QML context + // though I failed to find it (from QtMultimedia??). So.. let it be "AudioScriptingInterface" + surfaceContext->setContextProperty("AudioScriptingInterface", DependencyManager::get().data()); + surfaceContext->setContextProperty("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED + } + }); + + offscreenSurfaceCache->reserve(TabletScriptingInterface::QML, 1); + offscreenSurfaceCache->reserve(render::entities::WebEntityRenderer::QML, 2); +#endif + + flushMenuUpdates(); + +#if !defined(DISABLE_QML) + // Now that the menu is instantiated, ensure the display plugin menu is properly updated + { + auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); + // first sort the plugins into groupings: standard, advanced, developer + std::stable_sort(displayPlugins.begin(), displayPlugins.end(), + [](const DisplayPluginPointer& a, const DisplayPluginPointer& b) -> bool { return a->getGrouping() < b->getGrouping(); }); + int dpIndex = 1; + // concatenate the groupings into a single list in the order: standard, advanced, developer + for(const auto& displayPlugin : displayPlugins) { + addDisplayPluginToMenu(displayPlugin, dpIndex, _displayPlugin == displayPlugin); + dpIndex++; + } + + // after all plugins have been added to the menu, add a separator to the menu + auto parent = getPrimaryMenu()->getMenu(MenuOption::OutputMenu); + parent->addSeparator(); + } +#endif + + + // The display plugins are created before the menu now, so we need to do this here to hide the menu bar + // now that it exists + if (_window && _window->isFullScreen()) { + setFullscreen(nullptr, true); + } + + + setIsInterstitialMode(true); +} + + +void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) { + auto engine = surfaceContext->engine(); + // in Qt 5.10.0 there is already an "Audio" object in the QML context + // though I failed to find it (from QtMultimedia??). So.. let it be "AudioScriptingInterface" + surfaceContext->setContextProperty("AudioScriptingInterface", DependencyManager::get().data()); + + surfaceContext->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); + surfaceContext->setContextProperty("AudioScope", DependencyManager::get().data()); + + surfaceContext->setContextProperty("Controller", DependencyManager::get().data()); + surfaceContext->setContextProperty("Entities", DependencyManager::get().data()); + _fileDownload = new FileScriptingInterface(engine); + surfaceContext->setContextProperty("File", _fileDownload); + connect(_fileDownload, &FileScriptingInterface::unzipResult, this, &Application::handleUnzip); + surfaceContext->setContextProperty("MyAvatar", getMyAvatar().get()); + surfaceContext->setContextProperty("Messages", DependencyManager::get().data()); + surfaceContext->setContextProperty("Recording", DependencyManager::get().data()); + surfaceContext->setContextProperty("Preferences", DependencyManager::get().data()); + surfaceContext->setContextProperty("AddressManager", DependencyManager::get().data()); + surfaceContext->setContextProperty("FrameTimings", &_graphicsEngine._frameTimingsScriptingInterface); + surfaceContext->setContextProperty("Rates", new RatesScriptingInterface(this)); + + surfaceContext->setContextProperty("TREE_SCALE", TREE_SCALE); + // FIXME Quat and Vec3 won't work with QJSEngine used by QML + surfaceContext->setContextProperty("Quat", new Quat()); + surfaceContext->setContextProperty("Vec3", new Vec3()); + surfaceContext->setContextProperty("Uuid", new ScriptUUID()); + surfaceContext->setContextProperty("Assets", DependencyManager::get().data()); + surfaceContext->setContextProperty("Keyboard", DependencyManager::get().data()); + + surfaceContext->setContextProperty("AvatarList", DependencyManager::get().data()); + surfaceContext->setContextProperty("Users", DependencyManager::get().data()); + + surfaceContext->setContextProperty("UserActivityLogger", DependencyManager::get().data()); + + surfaceContext->setContextProperty("Camera", &_myCamera); + +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) + surfaceContext->setContextProperty("SpeechRecognizer", DependencyManager::get().data()); +#endif + + surfaceContext->setContextProperty("Overlays", &_overlays); + surfaceContext->setContextProperty("Window", DependencyManager::get().data()); + surfaceContext->setContextProperty("Desktop", DependencyManager::get().data()); + surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); + surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); + surfaceContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); + surfaceContext->setContextProperty("AvatarBookmarks", DependencyManager::get().data()); + surfaceContext->setContextProperty("LocationBookmarks", DependencyManager::get().data()); + + // Caches + surfaceContext->setContextProperty("AnimationCache", DependencyManager::get().data()); + surfaceContext->setContextProperty("TextureCache", DependencyManager::get().data()); + surfaceContext->setContextProperty("ModelCache", DependencyManager::get().data()); + surfaceContext->setContextProperty("SoundCache", DependencyManager::get().data()); + + surfaceContext->setContextProperty("InputConfiguration", DependencyManager::get().data()); + + surfaceContext->setContextProperty("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED + surfaceContext->setContextProperty("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED + surfaceContext->setContextProperty("AccountServices", AccountServicesScriptingInterface::getInstance()); + + surfaceContext->setContextProperty("DialogsManager", _dialogsManagerScriptingInterface); + surfaceContext->setContextProperty("FaceTracker", DependencyManager::get().data()); + surfaceContext->setContextProperty("AvatarManager", DependencyManager::get().data()); + surfaceContext->setContextProperty("LODManager", DependencyManager::get().data()); + surfaceContext->setContextProperty("HMD", DependencyManager::get().data()); + surfaceContext->setContextProperty("Scene", DependencyManager::get().data()); + surfaceContext->setContextProperty("Render", _graphicsEngine.getRenderEngine()->getConfiguration().get()); + surfaceContext->setContextProperty("Workload", _gameWorkload._engine->getConfiguration().get()); + surfaceContext->setContextProperty("Reticle", getApplicationCompositor().getReticleInterface()); + surfaceContext->setContextProperty("Snapshot", DependencyManager::get().data()); + + surfaceContext->setContextProperty("ApplicationCompositor", &getApplicationCompositor()); + + surfaceContext->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); + surfaceContext->setContextProperty("Selection", DependencyManager::get().data()); + surfaceContext->setContextProperty("ContextOverlay", DependencyManager::get().data()); + surfaceContext->setContextProperty("WalletScriptingInterface", DependencyManager::get().data()); + surfaceContext->setContextProperty("HiFiAbout", AboutUtil::getInstance()); + surfaceContext->setContextProperty("ResourceRequestObserver", DependencyManager::get().data()); + + if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { + surfaceContext->setContextProperty("Steam", new SteamScriptingInterface(engine, steamClient.get())); + } + + _window->setMenuBar(new Menu()); +} + +void Application::onDesktopRootItemCreated(QQuickItem* rootItem) { + Stats::show(); + AnimStats::show(); + auto surfaceContext = getOffscreenUI()->getSurfaceContext(); + surfaceContext->setContextProperty("Stats", Stats::getInstance()); + surfaceContext->setContextProperty("AnimStats", AnimStats::getInstance()); + +#if !defined(Q_OS_ANDROID) + auto offscreenUi = getOffscreenUI(); + auto qml = PathUtils::qmlUrl("AvatarInputsBar.qml"); + offscreenUi->show(qml, "AvatarInputsBar"); +#endif +} + +void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditionalContextProperties) { + surfaceContext->setContextProperty("Users", DependencyManager::get().data()); + surfaceContext->setContextProperty("HMD", DependencyManager::get().data()); + surfaceContext->setContextProperty("UserActivityLogger", DependencyManager::get().data()); + surfaceContext->setContextProperty("Preferences", DependencyManager::get().data()); + surfaceContext->setContextProperty("Vec3", new Vec3()); + surfaceContext->setContextProperty("Quat", new Quat()); + surfaceContext->setContextProperty("MyAvatar", DependencyManager::get()->getMyAvatar().get()); + surfaceContext->setContextProperty("Entities", DependencyManager::get().data()); + surfaceContext->setContextProperty("Snapshot", DependencyManager::get().data()); + surfaceContext->setContextProperty("KeyboardScriptingInterface", DependencyManager::get().data()); + + if (setAdditionalContextProperties) { + auto tabletScriptingInterface = DependencyManager::get(); + auto flags = tabletScriptingInterface->getFlags(); + + surfaceContext->setContextProperty("offscreenFlags", flags); + surfaceContext->setContextProperty("AddressManager", DependencyManager::get().data()); + + surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); + surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); + + surfaceContext->setContextProperty("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED + surfaceContext->setContextProperty("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED + surfaceContext->setContextProperty("AccountServices", AccountServicesScriptingInterface::getInstance()); + + // in Qt 5.10.0 there is already an "Audio" object in the QML context + // though I failed to find it (from QtMultimedia??). So.. let it be "AudioScriptingInterface" + surfaceContext->setContextProperty("AudioScriptingInterface", DependencyManager::get().data()); + + surfaceContext->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); + surfaceContext->setContextProperty("fileDialogHelper", new FileDialogHelper()); + surfaceContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); + surfaceContext->setContextProperty("Assets", DependencyManager::get().data()); + surfaceContext->setContextProperty("LODManager", DependencyManager::get().data()); + surfaceContext->setContextProperty("OctreeStats", DependencyManager::get().data()); + surfaceContext->setContextProperty("DCModel", DependencyManager::get().data()); + surfaceContext->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); + surfaceContext->setContextProperty("AvatarList", DependencyManager::get().data()); + surfaceContext->setContextProperty("DialogsManager", DialogsManagerScriptingInterface::getInstance()); + surfaceContext->setContextProperty("InputConfiguration", DependencyManager::get().data()); + surfaceContext->setContextProperty("SoundCache", DependencyManager::get().data()); + surfaceContext->setContextProperty("AvatarBookmarks", DependencyManager::get().data()); + surfaceContext->setContextProperty("Render", AbstractViewStateInterface::instance()->getRenderEngine()->getConfiguration().get()); + surfaceContext->setContextProperty("Workload", qApp->getGameWorkload()._engine->getConfiguration().get()); + surfaceContext->setContextProperty("Controller", DependencyManager::get().data()); + surfaceContext->setContextProperty("Pointers", DependencyManager::get().data()); + surfaceContext->setContextProperty("Window", DependencyManager::get().data()); + surfaceContext->setContextProperty("Reticle", qApp->getApplicationCompositor().getReticleInterface()); + surfaceContext->setContextProperty("HiFiAbout", AboutUtil::getInstance()); + surfaceContext->setContextProperty("WalletScriptingInterface", DependencyManager::get().data()); + surfaceContext->setContextProperty("ResourceRequestObserver", DependencyManager::get().data()); + } +} + +void Application::updateCamera(RenderArgs& renderArgs, float deltaTime) { + PROFILE_RANGE(render, __FUNCTION__); + PerformanceTimer perfTimer("updateCamera"); + + glm::vec3 boomOffset; + auto myAvatar = getMyAvatar(); + boomOffset = myAvatar->getModelScale() * myAvatar->getBoomLength() * -IDENTITY_FORWARD; + + // The render mode is default or mirror if the camera is in mirror mode, assigned further below + renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; + + // Always use the default eye position, not the actual head eye position. + // Using the latter will cause the camera to wobble with idle animations, + // or with changes from the face tracker + if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON) { + _thirdPersonHMDCameraBoomValid= false; + if (isHMDMode()) { + mat4 camMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); + _myCamera.setPosition(extractTranslation(camMat)); + _myCamera.setOrientation(glmExtractRotation(camMat)); + } + else { + _myCamera.setPosition(myAvatar->getDefaultEyePosition()); + _myCamera.setOrientation(myAvatar->getMyHead()->getHeadOrientation()); + } + } + else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { + if (isHMDMode()) { + + if (!_thirdPersonHMDCameraBoomValid) { + const glm::vec3 CAMERA_OFFSET = glm::vec3(0.0f, 0.0f, 0.7f); + _thirdPersonHMDCameraBoom = cancelOutRollAndPitch(myAvatar->getHMDSensorOrientation()) * CAMERA_OFFSET; + _thirdPersonHMDCameraBoomValid = true; + } + + glm::mat4 thirdPersonCameraSensorToWorldMatrix = myAvatar->getSensorToWorldMatrix(); + + const glm::vec3 cameraPos = myAvatar->getHMDSensorPosition() + _thirdPersonHMDCameraBoom * myAvatar->getBoomLength(); + glm::mat4 sensorCameraMat = createMatFromQuatAndPos(myAvatar->getHMDSensorOrientation(), cameraPos); + glm::mat4 worldCameraMat = thirdPersonCameraSensorToWorldMatrix * sensorCameraMat; + + _myCamera.setOrientation(glm::normalize(glmExtractRotation(worldCameraMat))); + _myCamera.setPosition(extractTranslation(worldCameraMat)); + } + else { + _thirdPersonHMDCameraBoomValid = false; + + _myCamera.setOrientation(myAvatar->getHead()->getOrientation()); + if (isOptionChecked(MenuOption::CenterPlayerInView)) { + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + _myCamera.getOrientation() * boomOffset); + } + else { + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + myAvatar->getWorldOrientation() * boomOffset); + } + } + } + else if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { + _thirdPersonHMDCameraBoomValid= false; + + if (isHMDMode()) { + auto mirrorBodyOrientation = myAvatar->getWorldOrientation() * glm::quat(glm::vec3(0.0f, PI + _mirrorYawOffset, 0.0f)); + + glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); + // Mirror HMD yaw and roll + glm::vec3 mirrorHmdEulers = glm::eulerAngles(hmdRotation); + mirrorHmdEulers.y = -mirrorHmdEulers.y; + mirrorHmdEulers.z = -mirrorHmdEulers.z; + glm::quat mirrorHmdRotation = glm::quat(mirrorHmdEulers); + + glm::quat worldMirrorRotation = mirrorBodyOrientation * mirrorHmdRotation; + + _myCamera.setOrientation(worldMirrorRotation); + + glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); + // Mirror HMD lateral offsets + hmdOffset.x = -hmdOffset.x; + + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + glm::vec3(0, _raiseMirror * myAvatar->getModelScale(), 0) + + mirrorBodyOrientation * glm::vec3(0.0f, 0.0f, 1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror + + mirrorBodyOrientation * hmdOffset); + } + else { + auto userInputMapper = DependencyManager::get(); + const float YAW_SPEED = TWO_PI / 5.0f; + float deltaYaw = userInputMapper->getActionState(controller::Action::YAW) * YAW_SPEED * deltaTime; + _mirrorYawOffset += deltaYaw; + _myCamera.setOrientation(myAvatar->getWorldOrientation() * glm::quat(glm::vec3(0.0f, PI + _mirrorYawOffset, 0.0f))); + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + glm::vec3(0, _raiseMirror * myAvatar->getModelScale(), 0) + + (myAvatar->getWorldOrientation() * glm::quat(glm::vec3(0.0f, _mirrorYawOffset, 0.0f))) * + glm::vec3(0.0f, 0.0f, -1.0f) * myAvatar->getBoomLength() * _scaleMirror); + } + renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; + } + else if (_myCamera.getMode() == CAMERA_MODE_ENTITY) { + _thirdPersonHMDCameraBoomValid= false; + EntityItemPointer cameraEntity = _myCamera.getCameraEntityPointer(); + if (cameraEntity != nullptr) { + if (isHMDMode()) { + glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); + _myCamera.setOrientation(cameraEntity->getWorldOrientation() * hmdRotation); + glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); + _myCamera.setPosition(cameraEntity->getWorldPosition() + (hmdRotation * hmdOffset)); + } + else { + _myCamera.setOrientation(cameraEntity->getWorldOrientation()); + _myCamera.setPosition(cameraEntity->getWorldPosition()); + } + } + } + // Update camera position + if (!isHMDMode()) { + _myCamera.update(); + } + + renderArgs._cameraMode = (int8_t)_myCamera.getMode(); +} + +void Application::runTests() { + runTimingTests(); + runUnitTests(); +} + +void Application::faceTrackerMuteToggled() { + + QAction* muteAction = Menu::getInstance()->getActionForOption(MenuOption::MuteFaceTracking); + Q_CHECK_PTR(muteAction); + bool isMuted = getSelectedFaceTracker()->isMuted(); + muteAction->setChecked(isMuted); + getSelectedFaceTracker()->setEnabled(!isMuted); + Menu::getInstance()->getActionForOption(MenuOption::CalibrateCamera)->setEnabled(!isMuted); +} + +void Application::setFieldOfView(float fov) { + if (fov != _fieldOfView.get()) { + _fieldOfView.set(fov); + resizeGL(); + } +} + +void Application::setHMDTabletScale(float hmdTabletScale) { + _hmdTabletScale.set(hmdTabletScale); +} + +void Application::setDesktopTabletScale(float desktopTabletScale) { + _desktopTabletScale.set(desktopTabletScale); +} + +void Application::setDesktopTabletBecomesToolbarSetting(bool value) { + _desktopTabletBecomesToolbarSetting.set(value); + updateSystemTabletMode(); +} + +void Application::setHmdTabletBecomesToolbarSetting(bool value) { + _hmdTabletBecomesToolbarSetting.set(value); + updateSystemTabletMode(); +} + +void Application::setPreferStylusOverLaser(bool value) { + _preferStylusOverLaserSetting.set(value); +} + +void Application::setPreferAvatarFingerOverStylus(bool value) { + _preferAvatarFingerOverStylusSetting.set(value); +} + +void Application::setPreferredCursor(const QString& cursorName) { + qCDebug(interfaceapp) << "setPreferredCursor" << cursorName; + _preferredCursor.set(cursorName.isEmpty() ? DEFAULT_CURSOR_NAME : cursorName); + showCursor(Cursor::Manager::lookupIcon(_preferredCursor.get())); +} + +void Application::setSettingConstrainToolbarPosition(bool setting) { + _constrainToolbarPosition.set(setting); + getOffscreenUI()->setConstrainToolbarToCenterX(setting); +} + +void Application::setMiniTabletEnabled(bool enabled) { + _miniTabletEnabledSetting.set(enabled); + emit miniTabletEnabledChanged(enabled); +} + +void Application::showHelp() { + static const QString HAND_CONTROLLER_NAME_VIVE = "vive"; + static const QString HAND_CONTROLLER_NAME_OCULUS_TOUCH = "oculus"; + static const QString HAND_CONTROLLER_NAME_WINDOWS_MR = "windowsMR"; + + static const QString VIVE_PLUGIN_NAME = "HTC Vive"; + static const QString OCULUS_RIFT_PLUGIN_NAME = "Oculus Rift"; + static const QString WINDOWS_MR_PLUGIN_NAME = "WindowsMR"; + + static const QString TAB_KEYBOARD_MOUSE = "kbm"; + static const QString TAB_GAMEPAD = "gamepad"; + static const QString TAB_HAND_CONTROLLERS = "handControllers"; + + QString handControllerName; + QString defaultTab = TAB_KEYBOARD_MOUSE; + + if (PluginUtils::isHMDAvailable(WINDOWS_MR_PLUGIN_NAME)) { + defaultTab = TAB_HAND_CONTROLLERS; + handControllerName = HAND_CONTROLLER_NAME_WINDOWS_MR; + } else if (PluginUtils::isHMDAvailable(VIVE_PLUGIN_NAME)) { + defaultTab = TAB_HAND_CONTROLLERS; + handControllerName = HAND_CONTROLLER_NAME_VIVE; + } else if (PluginUtils::isHMDAvailable(OCULUS_RIFT_PLUGIN_NAME)) { + if (PluginUtils::isOculusTouchControllerAvailable()) { + defaultTab = TAB_HAND_CONTROLLERS; + handControllerName = HAND_CONTROLLER_NAME_OCULUS_TOUCH; + } else if (PluginUtils::isXboxControllerAvailable()) { + defaultTab = TAB_GAMEPAD; + } else { + defaultTab = TAB_KEYBOARD_MOUSE; + } + } else if (PluginUtils::isXboxControllerAvailable()) { + defaultTab = TAB_GAMEPAD; + } else { + defaultTab = TAB_KEYBOARD_MOUSE; + } + + QUrlQuery queryString; + queryString.addQueryItem("handControllerName", handControllerName); + queryString.addQueryItem("defaultTab", defaultTab); + TabletProxy* tablet = dynamic_cast(DependencyManager::get()->getTablet(SYSTEM_TABLET)); + tablet->gotoWebScreen(PathUtils::resourcesUrl() + INFO_HELP_PATH + "?" + queryString.toString()); + DependencyManager::get()->openTablet(); + //InfoView::show(INFO_HELP_PATH, false, queryString.toString()); +} + +void Application::resizeEvent(QResizeEvent* event) { + resizeGL(); +} + +void Application::resizeGL() { + PROFILE_RANGE(render, __FUNCTION__); + if (nullptr == _displayPlugin) { + return; + } + + auto displayPlugin = getActiveDisplayPlugin(); + // Set the desired FBO texture size. If it hasn't changed, this does nothing. + // Otherwise, it must rebuild the FBOs + uvec2 framebufferSize = displayPlugin->getRecommendedRenderSize(); + uvec2 renderSize = uvec2(framebufferSize); + if (_renderResolution != renderSize) { + _renderResolution = renderSize; + DependencyManager::get()->setFrameBufferSize(fromGlm(renderSize)); + } + + auto renderResolutionScale = getRenderResolutionScale(); + if (displayPlugin->getRenderResolutionScale() != renderResolutionScale) { + auto renderConfig = _graphicsEngine.getRenderEngine()->getConfiguration(); + assert(renderConfig); + auto mainView = renderConfig->getConfig("RenderMainView.RenderDeferredTask"); + // mainView can be null if we're rendering in forward mode + if (mainView) { + mainView->setProperty("resolutionScale", renderResolutionScale); + } + displayPlugin->setRenderResolutionScale(renderResolutionScale); + } + + // FIXME the aspect ratio for stereo displays is incorrect based on this. + float aspectRatio = displayPlugin->getRecommendedAspectRatio(); + _myCamera.setProjection(glm::perspective(glm::radians(_fieldOfView.get()), aspectRatio, + DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP)); + // Possible change in aspect ratio + { + QMutexLocker viewLocker(&_viewMutex); + _myCamera.loadViewFrustum(_viewFrustum); + } + +#if !defined(DISABLE_QML) + getOffscreenUI()->resize(fromGlm(displayPlugin->getRecommendedUiSize())); +#endif +} + +void Application::handleSandboxStatus(QNetworkReply* reply) { + PROFILE_RANGE(render, __FUNCTION__); + + bool sandboxIsRunning = SandboxUtils::readStatus(reply->readAll()); + + enum HandControllerType { + Vive, + Oculus + }; + static const std::map MIN_CONTENT_VERSION = { + { Vive, 1 }, + { Oculus, 27 } + }; + + // Get sandbox content set version + auto acDirPath = PathUtils::getAppDataPath() + "../../" + BuildInfo::MODIFIED_ORGANIZATION + "/assignment-client/"; + auto contentVersionPath = acDirPath + "content-version.txt"; + qCDebug(interfaceapp) << "Checking " << contentVersionPath << " for content version"; + int contentVersion = 0; + QFile contentVersionFile(contentVersionPath); + if (contentVersionFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + QString line = contentVersionFile.readAll(); + contentVersion = line.toInt(); // returns 0 if conversion fails + } + + // Get controller availability +#ifdef ANDROID_APP_QUEST_INTERFACE + bool hasHandControllers = true; +#else + bool hasHandControllers = false; + if (PluginUtils::isViveControllerAvailable() || PluginUtils::isOculusTouchControllerAvailable()) { + hasHandControllers = true; + } +#endif + + // Check HMD use (may be technically available without being in use) + bool hasHMD = PluginUtils::isHMDAvailable(); + bool isUsingHMD = _displayPlugin->isHmd(); + bool isUsingHMDAndHandControllers = hasHMD && hasHandControllers && isUsingHMD; + + qCDebug(interfaceapp) << "HMD:" << hasHMD << ", Hand Controllers: " << hasHandControllers << ", Using HMD: " << isUsingHMDAndHandControllers; + + // when --url in command line, teleport to location + const QString HIFI_URL_COMMAND_LINE_KEY = "--url"; + int urlIndex = arguments().indexOf(HIFI_URL_COMMAND_LINE_KEY); + QString addressLookupString; + if (urlIndex != -1) { + QUrl url(arguments().value(urlIndex + 1)); + if (url.scheme() == URL_SCHEME_HIFIAPP) { + Setting::Handle("startUpApp").set(url.path()); + } else { + addressLookupString = url.toString(); + } + } + + static const QString SENT_TO_PREVIOUS_LOCATION = "previous_location"; + static const QString SENT_TO_ENTRY = "entry"; + + QString sentTo; + + // If this is a first run we short-circuit the address passed in + if (_firstRun.get()) { +#if !defined(Q_OS_ANDROID) + DependencyManager::get()->goToEntry(); + sentTo = SENT_TO_ENTRY; +#endif + _firstRun.set(false); + + } else { +#if !defined(Q_OS_ANDROID) + QString goingTo = ""; + if (addressLookupString.isEmpty()) { + if (Menu::getInstance()->isOptionChecked(MenuOption::HomeLocation)) { + auto locationBookmarks = DependencyManager::get(); + addressLookupString = locationBookmarks->addressForBookmark(LocationBookmarks::HOME_BOOKMARK); + goingTo = "home location"; + } else { + goingTo = "previous location"; + } + } + qCDebug(interfaceapp) << "Not first run... going to" << qPrintable(!goingTo.isEmpty() ? goingTo : addressLookupString); + DependencyManager::get()->loadSettings(addressLookupString); + sentTo = SENT_TO_PREVIOUS_LOCATION; +#endif + } + + UserActivityLogger::getInstance().logAction("startup_sent_to", { + { "sent_to", sentTo }, + { "sandbox_is_running", sandboxIsRunning }, + { "has_hmd", hasHMD }, + { "has_hand_controllers", hasHandControllers }, + { "is_using_hmd", isUsingHMD }, + { "is_using_hmd_and_hand_controllers", isUsingHMDAndHandControllers }, + { "content_version", contentVersion } + }); + + _connectionMonitor.init(); +} + +bool Application::importJSONFromURL(const QString& urlString) { + // we only load files that terminate in just .json (not .svo.json and not .ava.json) + QUrl jsonURL { urlString }; + + emit svoImportRequested(urlString); + return true; +} + +bool Application::importSVOFromURL(const QString& urlString) { + emit svoImportRequested(urlString); + return true; +} + +bool Application::importFromZIP(const QString& filePath) { + qDebug() << "A zip file has been dropped in: " << filePath; + QUrl empty; + // handle Blocks download from Marketplace + if (filePath.contains("poly.google.com/downloads")) { + addAssetToWorldFromURL(filePath); + } else { + qApp->getFileDownloadInterface()->runUnzip(filePath, empty, true, true, false); + } + return true; +} + +bool Application::isServerlessMode() const { + auto tree = getEntities()->getTree(); + if (tree) { + return tree->isServerlessMode(); + } + return false; +} + +void Application::setIsInterstitialMode(bool interstitialMode) { + bool enableInterstitial = DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled(); + if (enableInterstitial) { + if (_interstitialMode != interstitialMode) { + _interstitialMode = interstitialMode; + emit interstitialModeChanged(_interstitialMode); + + DependencyManager::get()->setAudioPaused(_interstitialMode); + DependencyManager::get()->setMyAvatarDataPacketsPaused(_interstitialMode); + } + } +} + +void Application::setIsServerlessMode(bool serverlessDomain) { + auto tree = getEntities()->getTree(); + if (tree) { + tree->setIsServerlessMode(serverlessDomain); + } +} + +std::map Application::prepareServerlessDomainContents(QUrl domainURL) { + QUuid serverlessSessionID = QUuid::createUuid(); + getMyAvatar()->setSessionUUID(serverlessSessionID); + auto nodeList = DependencyManager::get(); + nodeList->setSessionUUID(serverlessSessionID); + + // there is no domain-server to tell us our permissions, so enable all + NodePermissions permissions; + permissions.setAll(true); + nodeList->setPermissions(permissions); + + // we can't import directly into the main tree because we would need to lock it, and + // Octree::readFromURL calls loop.exec which can run code which will also attempt to lock the tree. + EntityTreePointer tmpTree(new EntityTree()); + tmpTree->setIsServerlessMode(true); + tmpTree->createRootElement(); + auto myAvatar = getMyAvatar(); + tmpTree->setMyAvatar(myAvatar); + bool success = tmpTree->readFromURL(domainURL.toString()); + if (success) { + tmpTree->reaverageOctreeElements(); + tmpTree->sendEntities(&_entityEditSender, getEntities()->getTree(), 0, 0, 0); + } + std::map namedPaths = tmpTree->getNamedPaths(); + + // we must manually eraseAllOctreeElements(false) else the tmpTree will mem-leak + tmpTree->eraseAllOctreeElements(false); + + return namedPaths; +} + +void Application::loadServerlessDomain(QUrl domainURL) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "loadServerlessDomain", Q_ARG(QUrl, domainURL)); + return; + } + + if (domainURL.isEmpty()) { + return; + } + + auto namedPaths = prepareServerlessDomainContents(domainURL); + auto nodeList = DependencyManager::get(); + + nodeList->getDomainHandler().connectedToServerless(namedPaths); + + _fullSceneReceivedCounter++; +} + +void Application::loadErrorDomain(QUrl domainURL) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "loadErrorDomain", Q_ARG(QUrl, domainURL)); + return; + } + + if (domainURL.isEmpty()) { + return; + } + + auto namedPaths = prepareServerlessDomainContents(domainURL); + auto nodeList = DependencyManager::get(); + + nodeList->getDomainHandler().loadedErrorDomain(namedPaths); + + _fullSceneReceivedCounter++; +} + +bool Application::importImage(const QString& urlString) { + qCDebug(interfaceapp) << "An image file has been dropped in"; + QString filepath(urlString); + filepath.remove("file:///"); + addAssetToWorld(filepath, "", false, false); + return true; +} + +// thread-safe +void Application::onPresent(quint32 frameCount) { + bool expected = false; + if (_pendingIdleEvent.compare_exchange_strong(expected, true)) { + postEvent(this, new QEvent((QEvent::Type)ApplicationEvent::Idle), Qt::HighEventPriority); + } + expected = false; + if (_graphicsEngine.checkPendingRenderEvent() && !isAboutToQuit()) { + postEvent(_graphicsEngine._renderEventHandler, new QEvent((QEvent::Type)ApplicationEvent::Render)); + } +} + +static inline bool isKeyEvent(QEvent::Type type) { + return type == QEvent::KeyPress || type == QEvent::KeyRelease; +} + +bool Application::handleKeyEventForFocusedEntity(QEvent* event) { + if (_keyboardFocusedEntity.get() != UNKNOWN_ENTITY_ID) { + switch (event->type()) { + case QEvent::KeyPress: + case QEvent::KeyRelease: + { + auto eventHandler = getEntities()->getEventHandler(_keyboardFocusedEntity.get()); + if (eventHandler) { + event->setAccepted(false); + QCoreApplication::sendEvent(eventHandler, event); + if (event->isAccepted()) { + _lastAcceptedKeyPress = usecTimestampNow(); + return true; + } + } + break; + } + default: + break; + } + } + + return false; +} + +bool Application::handleFileOpenEvent(QFileOpenEvent* fileEvent) { + QUrl url = fileEvent->url(); + if (!url.isEmpty()) { + QString urlString = url.toString(); + if (canAcceptURL(urlString)) { + return acceptURL(urlString); + } + } + return false; +} + +#ifdef DEBUG_EVENT_QUEUE +static int getEventQueueSize(QThread* thread) { + auto threadData = QThreadData::get2(thread); + QMutexLocker locker(&threadData->postEventList.mutex); + return threadData->postEventList.size(); +} + +static void dumpEventQueue(QThread* thread) { + auto threadData = QThreadData::get2(thread); + QMutexLocker locker(&threadData->postEventList.mutex); + qDebug() << "Event list, size =" << threadData->postEventList.size(); + for (auto& postEvent : threadData->postEventList) { + QEvent::Type type = (postEvent.event ? postEvent.event->type() : QEvent::None); + qDebug() << " " << type; + } +} +#endif // DEBUG_EVENT_QUEUE + +bool Application::event(QEvent* event) { + + if (_aboutToQuit) { + return false; + } + + if (!Menu::getInstance()) { + return false; + } + + // Allow focused Entities to handle keyboard input + if (isKeyEvent(event->type()) && handleKeyEventForFocusedEntity(event)) { + return true; + } + + int type = event->type(); + switch (type) { + case ApplicationEvent::Lambda: + static_cast(event)->call(); + return true; + + // Explicit idle keeps the idle running at a lower interval, but without any rendering + // see (windowMinimizedChanged) + case ApplicationEvent::Idle: + idle(); + +#ifdef DEBUG_EVENT_QUEUE + { + int count = getEventQueueSize(QThread::currentThread()); + if (count > 400) { + dumpEventQueue(QThread::currentThread()); + } + } +#endif // DEBUG_EVENT_QUEUE + + _pendingIdleEvent.store(false); + + return true; + + case QEvent::MouseMove: + mouseMoveEvent(static_cast(event)); + return true; + case QEvent::MouseButtonPress: + mousePressEvent(static_cast(event)); + return true; + case QEvent::MouseButtonDblClick: + mouseDoublePressEvent(static_cast(event)); + return true; + case QEvent::MouseButtonRelease: + mouseReleaseEvent(static_cast(event)); + return true; + case QEvent::KeyPress: + keyPressEvent(static_cast(event)); + return true; + case QEvent::KeyRelease: + keyReleaseEvent(static_cast(event)); + return true; + case QEvent::FocusOut: + focusOutEvent(static_cast(event)); + return true; + case QEvent::TouchBegin: + touchBeginEvent(static_cast(event)); + event->accept(); + return true; + case QEvent::TouchEnd: + touchEndEvent(static_cast(event)); + return true; + case QEvent::TouchUpdate: + touchUpdateEvent(static_cast(event)); + return true; + case QEvent::Gesture: + touchGestureEvent((QGestureEvent*)event); + return true; + case QEvent::Wheel: + wheelEvent(static_cast(event)); + return true; + case QEvent::Drop: + dropEvent(static_cast(event)); + return true; + + case QEvent::FileOpen: + if (handleFileOpenEvent(static_cast(event))) { + return true; + } + break; + + default: + break; + } + + return QApplication::event(event); +} + +bool Application::eventFilter(QObject* object, QEvent* event) { + + if (_aboutToQuit && event->type() != QEvent::DeferredDelete && event->type() != QEvent::Destroy) { + return true; + } + + if (event->type() == QEvent::Leave) { + getApplicationCompositor().handleLeaveEvent(); + } + + if (event->type() == QEvent::ShortcutOverride) { +#if !defined(DISABLE_QML) + if (getOffscreenUI()->shouldSwallowShortcut(event)) { + event->accept(); + return true; + } +#endif + + // Filter out captured keys before they're used for shortcut actions. + if (_controllerScriptingInterface->isKeyCaptured(static_cast(event))) { + event->accept(); + return true; + } + } + + return false; +} + +static bool _altPressed{ false }; + +void Application::keyPressEvent(QKeyEvent* event) { + _altPressed = event->key() == Qt::Key_Alt; + + if (!event->isAutoRepeat()) { + _keysPressed.insert(event->key(), *event); + } + + _controllerScriptingInterface->emitKeyPressEvent(event); // send events to any registered scripts + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isKeyCaptured(event) || isInterstitialMode()) { + return; + } + + if (hasFocus() && getLoginDialogPoppedUp()) { + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->keyReleaseEvent(event); + } + + bool isMeta = event->modifiers().testFlag(Qt::ControlModifier); + bool isOption = event->modifiers().testFlag(Qt::AltModifier); + switch (event->key()) { + case Qt::Key_4: + case Qt::Key_5: + case Qt::Key_6: + case Qt::Key_7: + if (isMeta || isOption) { + unsigned int index = static_cast(event->key() - Qt::Key_1); + auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); + if (index < displayPlugins.size()) { + auto targetPlugin = displayPlugins.at(index); + QString targetName = targetPlugin->getName(); + auto menu = Menu::getInstance(); + QAction* action = menu->getActionForOption(targetName); + if (action && !action->isChecked()) { + action->trigger(); + } + } + } + break; + } + } else if (hasFocus()) { + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->keyPressEvent(event); + } + + bool isShifted = event->modifiers().testFlag(Qt::ShiftModifier); + bool isMeta = event->modifiers().testFlag(Qt::ControlModifier); + bool isOption = event->modifiers().testFlag(Qt::AltModifier); + switch (event->key()) { + case Qt::Key_Enter: + case Qt::Key_Return: + if (isOption) { + if (_window->isFullScreen()) { + unsetFullscreen(); + } else { + setFullscreen(nullptr); + } + } + break; + + case Qt::Key_1: { + Menu* menu = Menu::getInstance(); + menu->triggerOption(MenuOption::FirstPerson); + break; + } + case Qt::Key_2: { + Menu* menu = Menu::getInstance(); + menu->triggerOption(MenuOption::FullscreenMirror); + break; + } + case Qt::Key_3: { + Menu* menu = Menu::getInstance(); + menu->triggerOption(MenuOption::ThirdPerson); + break; + } + case Qt::Key_4: + case Qt::Key_5: + case Qt::Key_6: + case Qt::Key_7: + if (isMeta || isOption) { + unsigned int index = static_cast(event->key() - Qt::Key_1); + auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); + if (index < displayPlugins.size()) { + auto targetPlugin = displayPlugins.at(index); + QString targetName = targetPlugin->getName(); + auto menu = Menu::getInstance(); + QAction* action = menu->getActionForOption(targetName); + if (action && !action->isChecked()) { + action->trigger(); + } + } + } + break; + + case Qt::Key_G: + if (isShifted && isMeta && Menu::getInstance() && Menu::getInstance()->getMenu("Developer")->isVisible()) { + static const QString HIFI_FRAMES_FOLDER_VAR = "HIFI_FRAMES_FOLDER"; + static const QString GPU_FRAME_FOLDER = QProcessEnvironment::systemEnvironment().contains(HIFI_FRAMES_FOLDER_VAR) + ? QProcessEnvironment::systemEnvironment().value(HIFI_FRAMES_FOLDER_VAR) + : "hifiFrames"; + static QString GPU_FRAME_TEMPLATE = GPU_FRAME_FOLDER + "/{DATE}_{TIME}"; + QString fullPath = FileUtils::computeDocumentPath(FileUtils::replaceDateTimeTokens(GPU_FRAME_TEMPLATE)); + if (FileUtils::canCreateFile(fullPath)) { + getActiveDisplayPlugin()->captureFrame(fullPath.toStdString()); + } + } + break; + case Qt::Key_X: + if (isShifted && isMeta) { + auto offscreenUi = getOffscreenUI(); + offscreenUi->togglePinned(); + //offscreenUi->getSurfaceContext()->engine()->clearComponentCache(); + //OffscreenUi::information("Debugging", "Component cache cleared"); + // placeholder for dialogs being converted to QML. + } + break; + + case Qt::Key_Y: + if (isShifted && isMeta) { + getActiveDisplayPlugin()->cycleDebugOutput(); + } + break; + + case Qt::Key_B: + if (isMeta) { + auto offscreenUi = getOffscreenUI(); + offscreenUi->load("Browser.qml"); + } else if (isOption) { + controller::InputRecorder* inputRecorder = controller::InputRecorder::getInstance(); + inputRecorder->stopPlayback(); + } + break; + + case Qt::Key_L: + if (isShifted && isMeta) { + Menu::getInstance()->triggerOption(MenuOption::Log); + } else if (isMeta) { + auto dialogsManager = DependencyManager::get(); + dialogsManager->toggleAddressBar(); + } else if (isShifted) { + Menu::getInstance()->triggerOption(MenuOption::LodTools); + } + break; + + case Qt::Key_R: + if (isMeta && !event->isAutoRepeat()) { + DependencyManager::get()->reloadAllScripts(); + getOffscreenUI()->clearCache(); + } + break; + + case Qt::Key_Asterisk: + Menu::getInstance()->triggerOption(MenuOption::DefaultSkybox); + break; + + case Qt::Key_M: + if (isMeta) { + auto audioClient = DependencyManager::get(); + audioClient->setMuted(!audioClient->isMuted()); + } + break; + + case Qt::Key_N: + if (!isOption && !isShifted && isMeta) { + DependencyManager::get()->toggleIgnoreRadius(); + } + break; + + case Qt::Key_S: + if (isShifted && isMeta && !isOption) { + Menu::getInstance()->triggerOption(MenuOption::SuppressShortTimings); + } + break; + + case Qt::Key_P: { + if (!isShifted && !isMeta && !isOption && !event->isAutoRepeat()) { + AudioInjectorOptions options; + options.localOnly = true; + options.positionSet = false; // system sound + options.stereo = true; + + Setting::Handle notificationSounds{ MenuOption::NotificationSounds, true }; + Setting::Handle notificationSoundSnapshot{ MenuOption::NotificationSoundsSnapshot, true }; + if (notificationSounds.get() && notificationSoundSnapshot.get()) { + if (_snapshotSoundInjector) { + _snapshotSoundInjector->setOptions(options); + _snapshotSoundInjector->restart(); + } else { + _snapshotSoundInjector = AudioInjector::playSound(_snapshotSound, options); + } + } + takeSnapshot(true); + } + break; + } + + case Qt::Key_Apostrophe: { + if (isMeta) { + auto cursor = Cursor::Manager::instance().getCursor(); + auto curIcon = cursor->getIcon(); + if (curIcon == Cursor::Icon::DEFAULT) { + showCursor(Cursor::Icon::RETICLE); + } else if (curIcon == Cursor::Icon::RETICLE) { + showCursor(Cursor::Icon::SYSTEM); + } else if (curIcon == Cursor::Icon::SYSTEM) { + showCursor(Cursor::Icon::LINK); + } else { + showCursor(Cursor::Icon::DEFAULT); + } + } else if (!event->isAutoRepeat()){ + resetSensors(true); + } + break; + } + + case Qt::Key_Backslash: + Menu::getInstance()->triggerOption(MenuOption::Chat); + break; + + case Qt::Key_Slash: + Menu::getInstance()->triggerOption(MenuOption::Stats); + break; + + case Qt::Key_Plus: { + if (isMeta && event->modifiers().testFlag(Qt::KeypadModifier)) { + auto& cursorManager = Cursor::Manager::instance(); + cursorManager.setScale(cursorManager.getScale() * 1.1f); + } else { + getMyAvatar()->increaseSize(); + } + break; + } + + case Qt::Key_Minus: { + if (isMeta && event->modifiers().testFlag(Qt::KeypadModifier)) { + auto& cursorManager = Cursor::Manager::instance(); + cursorManager.setScale(cursorManager.getScale() / 1.1f); + } else { + getMyAvatar()->decreaseSize(); + } + break; + } + + case Qt::Key_Equal: + getMyAvatar()->resetSize(); + break; + case Qt::Key_Escape: { + getActiveDisplayPlugin()->abandonCalibration(); + break; + } + + default: + event->ignore(); + break; + } + } +} + +void Application::keyReleaseEvent(QKeyEvent* event) { + if (!event->isAutoRepeat()) { + _keysPressed.remove(event->key()); + } + +#if defined(Q_OS_ANDROID) + if (event->key() == Qt::Key_Back) { + event->accept(); + AndroidHelper::instance().requestActivity("Home", false); + } +#endif + _controllerScriptingInterface->emitKeyReleaseEvent(event); // send events to any registered scripts + + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isKeyCaptured(event)) { + return; + } + + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->keyReleaseEvent(event); + } +} + +void Application::focusOutEvent(QFocusEvent* event) { + auto inputPlugins = PluginManager::getInstance()->getInputPlugins(); + foreach(auto inputPlugin, inputPlugins) { + if (inputPlugin->isActive()) { + inputPlugin->pluginFocusOutEvent(); + } + } + +// FIXME spacemouse code still needs cleanup +#if 0 + //SpacemouseDevice::getInstance().focusOutEvent(); + //SpacemouseManager::getInstance().getDevice()->focusOutEvent(); + SpacemouseManager::getInstance().ManagerFocusOutEvent(); +#endif + + synthesizeKeyReleasEvents(); +} + +void Application::synthesizeKeyReleasEvents() { + // synthesize events for keys currently pressed, since we may not get their release events + // Because our key event handlers may manipulate _keysPressed, lets swap the keys pressed into a local copy, + // clearing the existing list. + QHash keysPressed; + std::swap(keysPressed, _keysPressed); + for (auto& ev : keysPressed) { + QKeyEvent synthesizedEvent { QKeyEvent::KeyRelease, ev.key(), Qt::NoModifier, ev.text() }; + keyReleaseEvent(&synthesizedEvent); + } +} + +void Application::maybeToggleMenuVisible(QMouseEvent* event) const { +#ifndef Q_OS_MAC + // If in full screen, and our main windows menu bar is hidden, and we're close to the top of the QMainWindow + // then show the menubar. + if (_window->isFullScreen()) { + QMenuBar* menuBar = _window->menuBar(); + if (menuBar) { + static const int MENU_TOGGLE_AREA = 10; + if (!menuBar->isVisible()) { + if (event->pos().y() <= MENU_TOGGLE_AREA) { + menuBar->setVisible(true); + } + } else { + if (event->pos().y() > MENU_TOGGLE_AREA) { + menuBar->setVisible(false); + } + } + } + } +#endif +} + +void Application::mouseMoveEvent(QMouseEvent* event) { + PROFILE_RANGE(app_input_mouse, __FUNCTION__); + + maybeToggleMenuVisible(event); + + auto& compositor = getApplicationCompositor(); + // if this is a real mouse event, and we're in HMD mode, then we should use it to move the + // compositor reticle + // handleRealMouseMoveEvent() will return true, if we shouldn't process the event further + if (!compositor.fakeEventActive() && compositor.handleRealMouseMoveEvent()) { + return; // bail + } + +#if !defined(DISABLE_QML) + auto offscreenUi = getOffscreenUI(); + auto eventPosition = compositor.getMouseEventPosition(event); + QPointF transformedPos = offscreenUi ? offscreenUi->mapToVirtualScreen(eventPosition) : QPointF(); +#else + QPointF transformedPos; +#endif + auto button = event->button(); + auto buttons = event->buttons(); + // Determine if the ReticleClick Action is 1 and if so, fake include the LeftMouseButton + if (_reticleClickPressed) { + if (button == Qt::NoButton) { + button = Qt::LeftButton; + } + buttons |= Qt::LeftButton; + } + + QMouseEvent mappedEvent(event->type(), + transformedPos, + event->screenPos(), button, + buttons, event->modifiers()); + + if (compositor.getReticleVisible() || !isHMDMode() || !compositor.getReticleOverDesktop() || + getOverlays().getOverlayAtPoint(glm::vec2(transformedPos.x(), transformedPos.y())) != UNKNOWN_ENTITY_ID) { + getEntities()->mouseMoveEvent(&mappedEvent); + } + + _controllerScriptingInterface->emitMouseMoveEvent(&mappedEvent); // send events to any registered scripts + + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isMouseCaptured()) { + return; + } + + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->mouseMoveEvent(event); + } +} + +void Application::mousePressEvent(QMouseEvent* event) { + // Inhibit the menu if the user is using alt-mouse dragging + _altPressed = false; + +#if !defined(DISABLE_QML) + auto offscreenUi = getOffscreenUI(); + // If we get a mouse press event it means it wasn't consumed by the offscreen UI, + // hence, we should defocus all of the offscreen UI windows, in order to allow + // keyboard shortcuts not to be swallowed by them. In particular, WebEngineViews + // will consume all keyboard events. + offscreenUi->unfocusWindows(); + + auto eventPosition = getApplicationCompositor().getMouseEventPosition(event); + QPointF transformedPos = offscreenUi->mapToVirtualScreen(eventPosition); +#else + QPointF transformedPos; +#endif + + QMouseEvent mappedEvent(event->type(), transformedPos, event->screenPos(), event->button(), event->buttons(), event->modifiers()); + QUuid result = getEntities()->mousePressEvent(&mappedEvent); + setKeyboardFocusEntity(getEntities()->wantsKeyboardFocus(result) ? result : UNKNOWN_ENTITY_ID); + + _controllerScriptingInterface->emitMousePressEvent(&mappedEvent); // send events to any registered scripts + + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isMouseCaptured()) { + return; + } + +#if defined(Q_OS_MAC) + // Fix for OSX right click dragging on window when coming from a native window + bool isFocussed = hasFocus(); + if (!isFocussed && event->button() == Qt::MouseButton::RightButton) { + setFocus(); + isFocussed = true; + } + + if (isFocussed) { +#else + if (hasFocus()) { +#endif + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->mousePressEvent(event); + } + } +} + +void Application::mouseDoublePressEvent(QMouseEvent* event) { +#if !defined(DISABLE_QML) + auto offscreenUi = getOffscreenUI(); + auto eventPosition = getApplicationCompositor().getMouseEventPosition(event); + QPointF transformedPos = offscreenUi->mapToVirtualScreen(eventPosition); +#else + QPointF transformedPos; +#endif + QMouseEvent mappedEvent(event->type(), + transformedPos, + event->screenPos(), event->button(), + event->buttons(), event->modifiers()); + getEntities()->mouseDoublePressEvent(&mappedEvent); + + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isMouseCaptured()) { + return; + } + + _controllerScriptingInterface->emitMouseDoublePressEvent(event); +} + +void Application::mouseReleaseEvent(QMouseEvent* event) { + +#if !defined(DISABLE_QML) + auto offscreenUi = getOffscreenUI(); + auto eventPosition = getApplicationCompositor().getMouseEventPosition(event); + QPointF transformedPos = offscreenUi->mapToVirtualScreen(eventPosition); +#else + QPointF transformedPos; +#endif + QMouseEvent mappedEvent(event->type(), + transformedPos, + event->screenPos(), event->button(), + event->buttons(), event->modifiers()); + + getEntities()->mouseReleaseEvent(&mappedEvent); + + _controllerScriptingInterface->emitMouseReleaseEvent(&mappedEvent); // send events to any registered scripts + + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isMouseCaptured()) { + return; + } + + if (hasFocus()) { + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->mouseReleaseEvent(event); + } + } +} + +void Application::touchUpdateEvent(QTouchEvent* event) { + _altPressed = false; + + if (event->type() == QEvent::TouchUpdate) { + TouchEvent thisEvent(*event, _lastTouchEvent); + _controllerScriptingInterface->emitTouchUpdateEvent(thisEvent); // send events to any registered scripts + _lastTouchEvent = thisEvent; + } + + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isTouchCaptured()) { + return; + } + + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->touchUpdateEvent(event); + } + if (_touchscreenDevice && _touchscreenDevice->isActive()) { + _touchscreenDevice->touchUpdateEvent(event); + } + if (_touchscreenVirtualPadDevice && _touchscreenVirtualPadDevice->isActive()) { + _touchscreenVirtualPadDevice->touchUpdateEvent(event); + } +} + +void Application::touchBeginEvent(QTouchEvent* event) { + _altPressed = false; + TouchEvent thisEvent(*event); // on touch begin, we don't compare to last event + _controllerScriptingInterface->emitTouchBeginEvent(thisEvent); // send events to any registered scripts + + _lastTouchEvent = thisEvent; // and we reset our last event to this event before we call our update + touchUpdateEvent(event); + + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isTouchCaptured()) { + return; + } + + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->touchBeginEvent(event); + } + if (_touchscreenDevice && _touchscreenDevice->isActive()) { + _touchscreenDevice->touchBeginEvent(event); + } + if (_touchscreenVirtualPadDevice && _touchscreenVirtualPadDevice->isActive()) { + _touchscreenVirtualPadDevice->touchBeginEvent(event); + } + +} + +void Application::touchEndEvent(QTouchEvent* event) { + _altPressed = false; + TouchEvent thisEvent(*event, _lastTouchEvent); + _controllerScriptingInterface->emitTouchEndEvent(thisEvent); // send events to any registered scripts + _lastTouchEvent = thisEvent; + + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isTouchCaptured()) { + return; + } + + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->touchEndEvent(event); + } + if (_touchscreenDevice && _touchscreenDevice->isActive()) { + _touchscreenDevice->touchEndEvent(event); + } + if (_touchscreenVirtualPadDevice && _touchscreenVirtualPadDevice->isActive()) { + _touchscreenVirtualPadDevice->touchEndEvent(event); + } + // put any application specific touch behavior below here.. +} + +void Application::touchGestureEvent(QGestureEvent* event) { + if (_touchscreenDevice && _touchscreenDevice->isActive()) { + _touchscreenDevice->touchGestureEvent(event); + } + if (_touchscreenVirtualPadDevice && _touchscreenVirtualPadDevice->isActive()) { + _touchscreenVirtualPadDevice->touchGestureEvent(event); + } +} + +void Application::wheelEvent(QWheelEvent* event) const { + _altPressed = false; + _controllerScriptingInterface->emitWheelEvent(event); // send events to any registered scripts + + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isWheelCaptured() || getLoginDialogPoppedUp()) { + return; + } + + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->wheelEvent(event); + } +} + +void Application::dropEvent(QDropEvent *event) { + const QMimeData* mimeData = event->mimeData(); + for (auto& url : mimeData->urls()) { + QString urlString = url.toString(); + if (acceptURL(urlString, true)) { + event->acceptProposedAction(); + } + } +} + +void Application::dragEnterEvent(QDragEnterEvent* event) { + event->acceptProposedAction(); +} + +// This is currently not used, but could be invoked if the user wants to go to the place embedded in an +// Interface-taken snapshot. (It was developed for drag and drop, before we had asset-server loading or in-world browsers.) +bool Application::acceptSnapshot(const QString& urlString) { + QUrl url(urlString); + QString snapshotPath = url.toLocalFile(); + + SnapshotMetaData* snapshotData = DependencyManager::get()->parseSnapshotData(snapshotPath); + if (snapshotData) { + if (!snapshotData->getURL().toString().isEmpty()) { + DependencyManager::get()->handleLookupString(snapshotData->getURL().toString()); + } + } else { + OffscreenUi::asyncWarning("", "No location details were found in the file\n" + + snapshotPath + "\nTry dragging in an authentic Hifi snapshot."); + } + return true; +} + +#ifdef Q_OS_WIN +#include +#include +#include +#pragma comment(lib, "pdh.lib") +#pragma comment(lib, "ntdll.lib") + +extern "C" { + enum SYSTEM_INFORMATION_CLASS { + SystemBasicInformation = 0, + SystemProcessorPerformanceInformation = 8, + }; + + struct SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION { + LARGE_INTEGER IdleTime; + LARGE_INTEGER KernelTime; + LARGE_INTEGER UserTime; + LARGE_INTEGER DpcTime; + LARGE_INTEGER InterruptTime; + ULONG InterruptCount; + }; + + struct SYSTEM_BASIC_INFORMATION { + ULONG Reserved; + ULONG TimerResolution; + ULONG PageSize; + ULONG NumberOfPhysicalPages; + ULONG LowestPhysicalPageNumber; + ULONG HighestPhysicalPageNumber; + ULONG AllocationGranularity; + ULONG_PTR MinimumUserModeAddress; + ULONG_PTR MaximumUserModeAddress; + ULONG_PTR ActiveProcessorsAffinityMask; + CCHAR NumberOfProcessors; + }; + + NTSYSCALLAPI NTSTATUS NTAPI NtQuerySystemInformation( + _In_ SYSTEM_INFORMATION_CLASS SystemInformationClass, + _Out_writes_bytes_opt_(SystemInformationLength) PVOID SystemInformation, + _In_ ULONG SystemInformationLength, + _Out_opt_ PULONG ReturnLength + ); + +} +template +NTSTATUS NtQuerySystemInformation(SYSTEM_INFORMATION_CLASS SystemInformationClass, T& t) { + return NtQuerySystemInformation(SystemInformationClass, &t, (ULONG)sizeof(T), nullptr); +} + +template +NTSTATUS NtQuerySystemInformation(SYSTEM_INFORMATION_CLASS SystemInformationClass, std::vector& t) { + return NtQuerySystemInformation(SystemInformationClass, t.data(), (ULONG)(sizeof(T) * t.size()), nullptr); +} + + +template +void updateValueAndDelta(std::pair& pair, T newValue) { + auto& value = pair.first; + auto& delta = pair.second; + delta = (value != 0) ? newValue - value : 0; + value = newValue; +} + +struct MyCpuInfo { + using ValueAndDelta = std::pair; + std::string name; + ValueAndDelta kernel { 0, 0 }; + ValueAndDelta user { 0, 0 }; + ValueAndDelta idle { 0, 0 }; + float kernelUsage { 0.0f }; + float userUsage { 0.0f }; + + void update(const SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION& cpuInfo) { + updateValueAndDelta(kernel, cpuInfo.KernelTime.QuadPart); + updateValueAndDelta(user, cpuInfo.UserTime.QuadPart); + updateValueAndDelta(idle, cpuInfo.IdleTime.QuadPart); + auto totalTime = kernel.second + user.second + idle.second; + if (totalTime != 0) { + kernelUsage = (FLOAT)kernel.second / totalTime; + userUsage = (FLOAT)user.second / totalTime; + } else { + kernelUsage = userUsage = 0.0f; + } + } +}; + +void updateCpuInformation() { + static std::once_flag once; + static SYSTEM_BASIC_INFORMATION systemInfo {}; + static SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION cpuTotals; + static std::vector cpuInfos; + static std::vector myCpuInfos; + static MyCpuInfo myCpuTotals; + std::call_once(once, [&] { + NtQuerySystemInformation( SystemBasicInformation, systemInfo); + cpuInfos.resize(systemInfo.NumberOfProcessors); + myCpuInfos.resize(systemInfo.NumberOfProcessors); + for (size_t i = 0; i < systemInfo.NumberOfProcessors; ++i) { + myCpuInfos[i].name = "cpu." + std::to_string(i); + } + myCpuTotals.name = "cpu.total"; + }); + NtQuerySystemInformation(SystemProcessorPerformanceInformation, cpuInfos); + + // Zero the CPU totals. + memset(&cpuTotals, 0, sizeof(SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION)); + for (size_t i = 0; i < systemInfo.NumberOfProcessors; ++i) { + auto& cpuInfo = cpuInfos[i]; + // KernelTime includes IdleTime. + cpuInfo.KernelTime.QuadPart -= cpuInfo.IdleTime.QuadPart; + + // Update totals + cpuTotals.IdleTime.QuadPart += cpuInfo.IdleTime.QuadPart; + cpuTotals.KernelTime.QuadPart += cpuInfo.KernelTime.QuadPart; + cpuTotals.UserTime.QuadPart += cpuInfo.UserTime.QuadPart; + + // Update friendly structure + auto& myCpuInfo = myCpuInfos[i]; + myCpuInfo.update(cpuInfo); + PROFILE_COUNTER(app, myCpuInfo.name.c_str(), { + { "kernel", myCpuInfo.kernelUsage }, + { "user", myCpuInfo.userUsage } + }); + } + + myCpuTotals.update(cpuTotals); + PROFILE_COUNTER(app, myCpuTotals.name.c_str(), { + { "kernel", myCpuTotals.kernelUsage }, + { "user", myCpuTotals.userUsage } + }); +} + + +static ULARGE_INTEGER lastCPU, lastSysCPU, lastUserCPU; +static int numProcessors; +static HANDLE self; +static PDH_HQUERY cpuQuery; +static PDH_HCOUNTER cpuTotal; + +void initCpuUsage() { + SYSTEM_INFO sysInfo; + FILETIME ftime, fsys, fuser; + + GetSystemInfo(&sysInfo); + numProcessors = sysInfo.dwNumberOfProcessors; + + GetSystemTimeAsFileTime(&ftime); + memcpy(&lastCPU, &ftime, sizeof(FILETIME)); + + self = GetCurrentProcess(); + GetProcessTimes(self, &ftime, &ftime, &fsys, &fuser); + memcpy(&lastSysCPU, &fsys, sizeof(FILETIME)); + memcpy(&lastUserCPU, &fuser, sizeof(FILETIME)); + + PdhOpenQuery(NULL, NULL, &cpuQuery); + PdhAddCounter(cpuQuery, "\\Processor(_Total)\\% Processor Time", NULL, &cpuTotal); + PdhCollectQueryData(cpuQuery); +} + +void getCpuUsage(vec3& systemAndUser) { + FILETIME ftime, fsys, fuser; + ULARGE_INTEGER now, sys, user; + + GetSystemTimeAsFileTime(&ftime); + memcpy(&now, &ftime, sizeof(FILETIME)); + + GetProcessTimes(self, &ftime, &ftime, &fsys, &fuser); + memcpy(&sys, &fsys, sizeof(FILETIME)); + memcpy(&user, &fuser, sizeof(FILETIME)); + systemAndUser.x = (sys.QuadPart - lastSysCPU.QuadPart); + systemAndUser.y = (user.QuadPart - lastUserCPU.QuadPart); + systemAndUser /= (float)(now.QuadPart - lastCPU.QuadPart); + systemAndUser /= (float)numProcessors; + systemAndUser *= 100.0f; + lastCPU = now; + lastUserCPU = user; + lastSysCPU = sys; + + PDH_FMT_COUNTERVALUE counterVal; + PdhCollectQueryData(cpuQuery); + PdhGetFormattedCounterValue(cpuTotal, PDH_FMT_DOUBLE, NULL, &counterVal); + systemAndUser.z = (float)counterVal.doubleValue; +} + +void setupCpuMonitorThread() { + initCpuUsage(); + auto cpuMonitorThread = QThread::currentThread(); + + QTimer* timer = new QTimer(); + timer->setInterval(50); + QObject::connect(timer, &QTimer::timeout, [] { + updateCpuInformation(); + vec3 kernelUserAndSystem; + getCpuUsage(kernelUserAndSystem); + PROFILE_COUNTER(app, "cpuProcess", { { "system", kernelUserAndSystem.x }, { "user", kernelUserAndSystem.y } }); + PROFILE_COUNTER(app, "cpuSystem", { { "system", kernelUserAndSystem.z } }); + }); + QObject::connect(cpuMonitorThread, &QThread::finished, [=] { + timer->deleteLater(); + cpuMonitorThread->deleteLater(); + }); + timer->start(); +} + +#endif + +void Application::idle() { + PerformanceTimer perfTimer("idle"); + + // Update the deadlock watchdog + updateHeartbeat(); + +#if !defined(DISABLE_QML) + auto offscreenUi = getOffscreenUI(); + + // These tasks need to be done on our first idle, because we don't want the showing of + // overlay subwindows to do a showDesktop() until after the first time through + static bool firstIdle = true; + if (firstIdle) { + firstIdle = false; + connect(offscreenUi.data(), &OffscreenUi::showDesktop, this, &Application::showDesktop); + } +#endif + +#ifdef Q_OS_WIN + // If tracing is enabled then monitor the CPU in a separate thread + static std::once_flag once; + std::call_once(once, [&] { + if (trace_app().isDebugEnabled()) { + QThread* cpuMonitorThread = new QThread(qApp); + cpuMonitorThread->setObjectName("cpuMonitorThread"); + QObject::connect(cpuMonitorThread, &QThread::started, [this] { setupCpuMonitorThread(); }); + QObject::connect(qApp, &QCoreApplication::aboutToQuit, cpuMonitorThread, &QThread::quit); + cpuMonitorThread->start(); + } + }); +#endif + + auto displayPlugin = getActiveDisplayPlugin(); +#if !defined(DISABLE_QML) + if (displayPlugin) { + auto uiSize = displayPlugin->getRecommendedUiSize(); + // Bit of a hack since there's no device pixel ratio change event I can find. + if (offscreenUi->size() != fromGlm(uiSize)) { + qCDebug(interfaceapp) << "Device pixel ratio changed, triggering resize to " << uiSize; + offscreenUi->resize(fromGlm(uiSize)); + } + } +#endif + + if (displayPlugin) { + PROFILE_COUNTER_IF_CHANGED(app, "present", float, displayPlugin->presentRate()); + } + PROFILE_COUNTER_IF_CHANGED(app, "renderLoopRate", float, getRenderLoopRate()); + PROFILE_COUNTER_IF_CHANGED(app, "currentDownloads", uint32_t, ResourceCache::getLoadingRequests().length()); + PROFILE_COUNTER_IF_CHANGED(app, "pendingDownloads", uint32_t, ResourceCache::getPendingRequestCount()); + PROFILE_COUNTER_IF_CHANGED(app, "currentProcessing", int, DependencyManager::get()->getStat("Processing").toInt()); + PROFILE_COUNTER_IF_CHANGED(app, "pendingProcessing", int, DependencyManager::get()->getStat("PendingProcessing").toInt()); + auto renderConfig = _graphicsEngine.getRenderEngine()->getConfiguration(); + PROFILE_COUNTER_IF_CHANGED(render, "gpuTime", float, (float)_graphicsEngine.getGPUContext()->getFrameTimerGPUAverage()); + + PROFILE_RANGE(app, __FUNCTION__); + + if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { + steamClient->runCallbacks(); + } + + if (auto oculusPlugin = PluginManager::getInstance()->getOculusPlatformPlugin()) { + oculusPlugin->handleOVREvents(); + } + + float secondsSinceLastUpdate = (float)_lastTimeUpdated.nsecsElapsed() / NSECS_PER_MSEC / MSECS_PER_SECOND; + _lastTimeUpdated.start(); + +#if !defined(DISABLE_QML) + // If the offscreen Ui has something active that is NOT the root, then assume it has keyboard focus. + if (offscreenUi && offscreenUi->getWindow()) { + auto activeFocusItem = offscreenUi->getWindow()->activeFocusItem(); + if (_keyboardDeviceHasFocus && activeFocusItem != offscreenUi->getRootItem()) { + _keyboardMouseDevice->pluginFocusOutEvent(); + _keyboardDeviceHasFocus = false; + synthesizeKeyReleasEvents(); + } else if (activeFocusItem == offscreenUi->getRootItem()) { + _keyboardDeviceHasFocus = true; + } + } +#endif + + checkChangeCursor(); + +#if !defined(DISABLE_QML) + auto stats = Stats::getInstance(); + if (stats) { + stats->updateStats(); + } + auto animStats = AnimStats::getInstance(); + if (animStats) { + animStats->updateStats(); + } +#endif + + // Normally we check PipelineWarnings, but since idle will often take more than 10ms we only show these idle timing + // details if we're in ExtraDebugging mode. However, the ::update() and its subcomponents will show their timing + // details normally. +#ifdef Q_OS_ANDROID + bool showWarnings = false; +#else + bool showWarnings = getLogger()->extraDebugging(); +#endif + PerformanceWarning warn(showWarnings, "idle()"); + + { + _gameWorkload.updateViews(_viewFrustum, getMyAvatar()->getHeadPosition()); + _gameWorkload._engine->run(); + } + { + PerformanceTimer perfTimer("update"); + PerformanceWarning warn(showWarnings, "Application::idle()... update()"); + static const float BIGGEST_DELTA_TIME_SECS = 0.25f; + update(glm::clamp(secondsSinceLastUpdate, 0.0f, BIGGEST_DELTA_TIME_SECS)); + } + + { // Update keyboard focus highlight + if (!_keyboardFocusedEntity.get().isInvalidID()) { + const quint64 LOSE_FOCUS_AFTER_ELAPSED_TIME = 30 * USECS_PER_SECOND; // if idle for 30 seconds, drop focus + quint64 elapsedSinceAcceptedKeyPress = usecTimestampNow() - _lastAcceptedKeyPress; + if (elapsedSinceAcceptedKeyPress > LOSE_FOCUS_AFTER_ELAPSED_TIME) { + setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); + } else { + if (auto entity = getEntities()->getTree()->findEntityByID(_keyboardFocusedEntity.get())) { + EntityItemProperties properties; + properties.setPosition(entity->getWorldPosition()); + properties.setRotation(entity->getWorldOrientation()); + properties.setDimensions(entity->getScaledDimensions() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR); + DependencyManager::get()->editEntity(_keyboardFocusHighlightID, properties); + } + } + } + } + + { + if (_keyboardFocusWaitingOnRenderable && getEntities()->renderableForEntityId(_keyboardFocusedEntity.get())) { + QUuid entityId = _keyboardFocusedEntity.get(); + setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); + _keyboardFocusWaitingOnRenderable = false; + setKeyboardFocusEntity(entityId); + } + } + + { + PerformanceTimer perfTimer("pluginIdle"); + PerformanceWarning warn(showWarnings, "Application::idle()... pluginIdle()"); + getActiveDisplayPlugin()->idle(); + auto inputPlugins = PluginManager::getInstance()->getInputPlugins(); + foreach(auto inputPlugin, inputPlugins) { + if (inputPlugin->isActive()) { + inputPlugin->idle(); + } + } + } + { + PerformanceTimer perfTimer("rest"); + PerformanceWarning warn(showWarnings, "Application::idle()... rest of it"); + _idleLoopStdev.addValue(secondsSinceLastUpdate); + + // Record standard deviation and reset counter if needed + const int STDEV_SAMPLES = 500; + if (_idleLoopStdev.getSamples() > STDEV_SAMPLES) { + _idleLoopMeasuredJitter = _idleLoopStdev.getStDev(); + _idleLoopStdev.reset(); + } + } + + _overlayConductor.update(secondsSinceLastUpdate); + + _gameLoopCounter.increment(); +} + +ivec2 Application::getMouse() const { + return getApplicationCompositor().getReticlePosition(); +} + +FaceTracker* Application::getActiveFaceTracker() { + auto dde = DependencyManager::get(); + + return dde->isActive() ? static_cast(dde.data()) : nullptr; +} + +FaceTracker* Application::getSelectedFaceTracker() { + FaceTracker* faceTracker = nullptr; +#ifdef HAVE_DDE + if (Menu::getInstance()->isOptionChecked(MenuOption::UseCamera)) { + faceTracker = DependencyManager::get().data(); + } +#endif + return faceTracker; +} + +void Application::setActiveFaceTracker() const { +#ifdef HAVE_DDE + bool isMuted = Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking); + bool isUsingDDE = Menu::getInstance()->isOptionChecked(MenuOption::UseCamera); + Menu::getInstance()->getActionForOption(MenuOption::BinaryEyelidControl)->setVisible(isUsingDDE); + Menu::getInstance()->getActionForOption(MenuOption::CoupleEyelids)->setVisible(isUsingDDE); + Menu::getInstance()->getActionForOption(MenuOption::UseAudioForMouth)->setVisible(isUsingDDE); + Menu::getInstance()->getActionForOption(MenuOption::VelocityFilter)->setVisible(isUsingDDE); + Menu::getInstance()->getActionForOption(MenuOption::CalibrateCamera)->setVisible(isUsingDDE); + auto ddeTracker = DependencyManager::get(); + ddeTracker->setIsMuted(isMuted); + ddeTracker->setEnabled(isUsingDDE && !isMuted); +#endif +} + +#ifdef HAVE_IVIEWHMD +void Application::setActiveEyeTracker() { + auto eyeTracker = DependencyManager::get(); + if (!eyeTracker->isInitialized()) { + return; + } + + bool isEyeTracking = Menu::getInstance()->isOptionChecked(MenuOption::SMIEyeTracking); + bool isSimulating = Menu::getInstance()->isOptionChecked(MenuOption::SimulateEyeTracking); + eyeTracker->setEnabled(isEyeTracking, isSimulating); + + Menu::getInstance()->getActionForOption(MenuOption::OnePointCalibration)->setEnabled(isEyeTracking && !isSimulating); + Menu::getInstance()->getActionForOption(MenuOption::ThreePointCalibration)->setEnabled(isEyeTracking && !isSimulating); + Menu::getInstance()->getActionForOption(MenuOption::FivePointCalibration)->setEnabled(isEyeTracking && !isSimulating); +} + +void Application::calibrateEyeTracker1Point() { + DependencyManager::get()->calibrate(1); +} + +void Application::calibrateEyeTracker3Points() { + DependencyManager::get()->calibrate(3); +} + +void Application::calibrateEyeTracker5Points() { + DependencyManager::get()->calibrate(5); +} +#endif + +bool Application::exportEntities(const QString& filename, + const QVector& entityIDs, + const glm::vec3* givenOffset) { + QHash entities; + + auto nodeList = DependencyManager::get(); + const QUuid myAvatarID = nodeList->getSessionUUID(); + + auto entityTree = getEntities()->getTree(); + auto exportTree = std::make_shared(); + exportTree->setMyAvatar(getMyAvatar()); + exportTree->createRootElement(); + glm::vec3 root(TREE_SCALE, TREE_SCALE, TREE_SCALE); + bool success = true; + entityTree->withReadLock([entityIDs, entityTree, givenOffset, myAvatarID, &root, &entities, &success, &exportTree] { + for (auto entityID : entityIDs) { // Gather entities and properties. + auto entityItem = entityTree->findEntityByEntityItemID(entityID); + if (!entityItem) { + qCWarning(interfaceapp) << "Skipping export of" << entityID << "that is not in scene."; + continue; + } + + if (!givenOffset) { + EntityItemID parentID = entityItem->getParentID(); + bool parentIsAvatar = (parentID == AVATAR_SELF_ID || parentID == myAvatarID); + if (!parentIsAvatar && (parentID.isInvalidID() || + !entityIDs.contains(parentID) || + !entityTree->findEntityByEntityItemID(parentID))) { + // If parent wasn't selected, we want absolute position, which isn't in properties. + auto position = entityItem->getWorldPosition(); + root.x = glm::min(root.x, position.x); + root.y = glm::min(root.y, position.y); + root.z = glm::min(root.z, position.z); + } + } + entities[entityID] = entityItem; + } + + if (entities.size() == 0) { + success = false; + return; + } + + if (givenOffset) { + root = *givenOffset; + } + for (EntityItemPointer& entityDatum : entities) { + auto properties = entityDatum->getProperties(); + EntityItemID parentID = properties.getParentID(); + bool parentIsAvatar = (parentID == AVATAR_SELF_ID || parentID == myAvatarID); + if (parentIsAvatar) { + properties.setParentID(AVATAR_SELF_ID); + } else { + if (parentID.isInvalidID()) { + properties.setPosition(properties.getPosition() - root); + } else if (!entities.contains(parentID)) { + entityDatum->globalizeProperties(properties, "Parent %3 of %2 %1 is not selected for export.", -root); + } // else valid parent -- don't offset + } + exportTree->addEntity(entityDatum->getEntityItemID(), properties); + } + }); + if (success) { + success = exportTree->writeToJSONFile(filename.toLocal8Bit().constData()); + + // restore the main window's active state + _window->activateWindow(); + } + return success; +} + +bool Application::exportEntities(const QString& filename, float x, float y, float z, float scale) { + glm::vec3 center(x, y, z); + glm::vec3 minCorner = center - vec3(scale); + float cubeSize = scale * 2; + AACube boundingCube(minCorner, cubeSize); + QVector entities; + auto entityTree = getEntities()->getTree(); + entityTree->withReadLock([&] { + entityTree->evalEntitiesInCube(boundingCube, PickFilter(), entities); + }); + return exportEntities(filename, entities, ¢er); +} + +void Application::loadSettings() { + + sessionRunTime.set(0); // Just clean living. We're about to saveSettings, which will update value. + DependencyManager::get()->loadSettings(); + DependencyManager::get()->loadSettings(); + + // DONT CHECK IN + //DependencyManager::get()->setAutomaticLODAdjust(false); + + auto menu = Menu::getInstance(); + menu->loadSettings(); + + // override the menu option show overlays to always be true on startup + menu->setIsOptionChecked(MenuOption::Overlays, true); + + // If there is a preferred plugin, we probably messed it up with the menu settings, so fix it. + auto pluginManager = PluginManager::getInstance(); + auto plugins = pluginManager->getPreferredDisplayPlugins(); + if (plugins.size() > 0) { + for (auto plugin : plugins) { + if (auto action = menu->getActionForOption(plugin->getName())) { + action->setChecked(true); + action->trigger(); + // Find and activated highest priority plugin, bail for the rest + break; + } + } + } + + bool isFirstPerson = false; + if (_firstRun.get()) { + // If this is our first run, and no preferred devices were set, default to + // an HMD device if available. + auto displayPlugins = pluginManager->getDisplayPlugins(); + for (auto& plugin : displayPlugins) { + if (plugin->isHmd()) { + if (auto action = menu->getActionForOption(plugin->getName())) { + action->setChecked(true); + action->trigger(); + break; + } + } + } + + isFirstPerson = (qApp->isHMDMode()); + + } else { + // if this is not the first run, the camera will be initialized differently depending on user settings + + if (qApp->isHMDMode()) { + // if the HMD is active, use first-person camera, unless the appropriate setting is checked + isFirstPerson = menu->isOptionChecked(MenuOption::FirstPersonHMD); + } else { + // if HMD is not active, only use first person if the menu option is checked + isFirstPerson = menu->isOptionChecked(MenuOption::FirstPerson); + } + } + + // finish initializing the camera, based on everything we checked above. Third person camera will be used if no settings + // dictated that we should be in first person + Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, isFirstPerson); + Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, !isFirstPerson); + _myCamera.setMode((isFirstPerson) ? CAMERA_MODE_FIRST_PERSON : CAMERA_MODE_THIRD_PERSON); + cameraMenuChanged(); + + auto inputs = pluginManager->getInputPlugins(); + for (auto plugin : inputs) { + if (!plugin->isActive()) { + plugin->activate(); + } + } + + getMyAvatar()->loadData(); + _settingsLoaded = true; +} + +void Application::saveSettings() const { + sessionRunTime.set(_sessionRunTimer.elapsed() / MSECS_PER_SECOND); + DependencyManager::get()->saveSettings(); + DependencyManager::get()->saveSettings(); + + Menu::getInstance()->saveSettings(); + getMyAvatar()->saveData(); + PluginManager::getInstance()->saveSettings(); +} + +bool Application::importEntities(const QString& urlOrFilename, const bool isObservable, const qint64 callerId) { + bool success = false; + _entityClipboard->withWriteLock([&] { + _entityClipboard->eraseAllOctreeElements(); + + success = _entityClipboard->readFromURL(urlOrFilename, isObservable, callerId); + if (success) { + _entityClipboard->reaverageOctreeElements(); + } + }); + return success; +} + +QVector Application::pasteEntities(float x, float y, float z) { + return _entityClipboard->sendEntities(&_entityEditSender, getEntities()->getTree(), x, y, z); +} + +void Application::init() { + // Make sure Login state is up to date +#if !defined(DISABLE_QML) + DependencyManager::get()->toggleLoginDialog(); +#endif + DependencyManager::get()->init(); + + _timerStart.start(); + _lastTimeUpdated.start(); + + if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { + // when +connect_lobby in command line, join steam lobby + const QString STEAM_LOBBY_COMMAND_LINE_KEY = "+connect_lobby"; + int lobbyIndex = arguments().indexOf(STEAM_LOBBY_COMMAND_LINE_KEY); + if (lobbyIndex != -1) { + QString lobbyId = arguments().value(lobbyIndex + 1); + steamClient->joinLobby(lobbyId); + } + } + + + qCDebug(interfaceapp) << "Loaded settings"; + + // fire off an immediate domain-server check in now that settings are loaded + if (!isServerlessMode()) { + DependencyManager::get()->sendDomainServerCheckIn(); + } + + // This allows collision to be set up properly for shape entities supported by GeometryCache. + // This is before entity setup to ensure that it's ready for whenever instance collision is initialized. + ShapeEntityItem::setShapeInfoCalulator(ShapeEntityItem::ShapeInfoCalculator(&shapeInfoCalculator)); + + getEntities()->init(); + getEntities()->setEntityLoadingPriorityFunction([this](const EntityItem& item) { + auto dims = item.getScaledDimensions(); + auto maxSize = glm::compMax(dims); + + if (maxSize <= 0.0f) { + return 0.0f; + } + + auto distance = glm::distance(getMyAvatar()->getWorldPosition(), item.getWorldPosition()); + return atan2(maxSize, distance); + }); + + ObjectMotionState::setShapeManager(&_shapeManager); + _physicsEngine->init(); + + EntityTreePointer tree = getEntities()->getTree(); + _entitySimulation->init(tree, _physicsEngine, &_entityEditSender); + tree->setSimulation(_entitySimulation); + + auto entityScriptingInterface = DependencyManager::get(); + + // connect the _entityCollisionSystem to our EntityTreeRenderer since that's what handles running entity scripts + connect(_entitySimulation.get(), &PhysicalEntitySimulation::entityCollisionWithEntity, + getEntities().data(), &EntityTreeRenderer::entityCollisionWithEntity); + + // connect the _entities (EntityTreeRenderer) to our script engine's EntityScriptingInterface for firing + // of events related clicking, hovering over, and entering entities + getEntities()->connectSignalsToSlots(entityScriptingInterface.data()); + + // Make sure any new sounds are loaded as soon as know about them. + connect(tree.get(), &EntityTree::newCollisionSoundURL, this, [this](QUrl newURL, EntityItemID id) { + getEntities()->setCollisionSound(id, DependencyManager::get()->getSound(newURL)); + }, Qt::QueuedConnection); + connect(getMyAvatar().get(), &MyAvatar::newCollisionSoundURL, this, [this](QUrl newURL) { + if (auto avatar = getMyAvatar()) { + auto sound = DependencyManager::get()->getSound(newURL); + avatar->setCollisionSound(sound); + } + }, Qt::QueuedConnection); + + _gameWorkload.startup(getEntities()->getWorkloadSpace(), _graphicsEngine.getRenderScene(), _entitySimulation); + _entitySimulation->setWorkloadSpace(getEntities()->getWorkloadSpace()); +} + +void Application::pauseUntilLoginDetermined() { + if (QThread::currentThread() != qApp->thread()) { + QMetaObject::invokeMethod(this, "pauseUntilLoginDetermined"); + return; + } + + auto myAvatar = getMyAvatar(); + _previousAvatarTargetScale = myAvatar->getTargetScale(); + _previousAvatarSkeletonModel = myAvatar->getSkeletonModelURL().toString(); + myAvatar->setTargetScale(1.0f); + myAvatar->setSkeletonModelURLFromScript(myAvatar->defaultFullAvatarModelUrl().toString()); + myAvatar->setEnableMeshVisible(false); + + _controllerScriptingInterface->disableMapping(STANDARD_TO_ACTION_MAPPING_NAME); + + { + auto userInputMapper = DependencyManager::get(); + if (userInputMapper->loadMapping(NO_MOVEMENT_MAPPING_JSON)) { + _controllerScriptingInterface->enableMapping(NO_MOVEMENT_MAPPING_NAME); + } + } + + const auto& nodeList = DependencyManager::get(); + // save interstitial mode setting until resuming. + _interstitialModeEnabled = nodeList->getDomainHandler().getInterstitialModeEnabled(); + nodeList->getDomainHandler().setInterstitialModeEnabled(false); + + auto menu = Menu::getInstance(); + menu->getMenu("Edit")->setVisible(false); + menu->getMenu("View")->setVisible(false); + menu->getMenu("Navigate")->setVisible(false); + menu->getMenu("Settings")->setVisible(false); + _developerMenuVisible = menu->getMenu("Developer")->isVisible(); + menu->setIsOptionChecked(MenuOption::Stats, false); + if (_developerMenuVisible) { + menu->getMenu("Developer")->setVisible(false); + } + _previousCameraMode = _myCamera.getMode(); + _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); + cameraModeChanged(); + + // disconnect domain handler. + nodeList->getDomainHandler().disconnect(); + +} + +void Application::resumeAfterLoginDialogActionTaken() { + if (QThread::currentThread() != qApp->thread()) { + QMetaObject::invokeMethod(this, "resumeAfterLoginDialogActionTaken"); + return; + } + + if (!isHMDMode() && getDesktopTabletBecomesToolbarSetting()) { + auto toolbar = DependencyManager::get()->getToolbar("com.highfidelity.interface.toolbar.system"); + toolbar->writeProperty("visible", true); + } else { + getApplicationCompositor().getReticleInterface()->setAllowMouseCapture(true); + getApplicationCompositor().getReticleInterface()->setVisible(true); + } + + updateSystemTabletMode(); + + { + auto userInputMapper = DependencyManager::get(); + userInputMapper->unloadMapping(NO_MOVEMENT_MAPPING_JSON); + _controllerScriptingInterface->disableMapping(NO_MOVEMENT_MAPPING_NAME); + } + + auto myAvatar = getMyAvatar(); + myAvatar->setTargetScale(_previousAvatarTargetScale); + myAvatar->setSkeletonModelURLFromScript(_previousAvatarSkeletonModel); + myAvatar->setEnableMeshVisible(true); + + _controllerScriptingInterface->enableMapping(STANDARD_TO_ACTION_MAPPING_NAME); + + const auto& nodeList = DependencyManager::get(); + nodeList->getDomainHandler().setInterstitialModeEnabled(_interstitialModeEnabled); + { + auto scriptEngines = DependencyManager::get().data(); + // this will force the model the look at the correct directory (weird order of operations issue) + scriptEngines->reloadLocalFiles(); + + // if the --scripts command-line argument was used. + if (!_defaultScriptsLocation.exists() && (arguments().indexOf(QString("--").append(SCRIPTS_SWITCH))) != -1) { + scriptEngines->loadDefaultScripts(); + scriptEngines->defaultScriptsLocationOverridden(true); + } else { + scriptEngines->loadScripts(); + } + } + + auto accountManager = DependencyManager::get(); + auto addressManager = DependencyManager::get(); + + // restart domain handler. + nodeList->getDomainHandler().resetting(); + + QVariant testProperty = property(hifi::properties::TEST); + if (testProperty.isValid()) { + const auto testScript = property(hifi::properties::TEST).toUrl(); + // Set last parameter to exit interface when the test script finishes, if so requested + DependencyManager::get()->loadScript(testScript, false, false, false, false, quitWhenFinished); + // This is done so we don't get a "connection time-out" message when we haven't passed in a URL. + if (arguments().contains("--url")) { + auto reply = SandboxUtils::getStatus(); + connect(reply, &QNetworkReply::finished, this, [this, reply] { handleSandboxStatus(reply); }); + } + } else { + auto reply = SandboxUtils::getStatus(); + connect(reply, &QNetworkReply::finished, this, [this, reply] { handleSandboxStatus(reply); }); + } + + auto menu = Menu::getInstance(); + menu->getMenu("Edit")->setVisible(true); + menu->getMenu("View")->setVisible(true); + menu->getMenu("Navigate")->setVisible(true); + menu->getMenu("Settings")->setVisible(true); + menu->getMenu("Developer")->setVisible(_developerMenuVisible); + _myCamera.setMode(_previousCameraMode); + cameraModeChanged(); +} + +void Application::loadAvatarScripts(const QVector& urls) { + auto scriptEngines = DependencyManager::get(); + auto runningScripts = scriptEngines->getRunningScripts(); + for (auto url : urls) { + int index = runningScripts.indexOf(url); + if (index < 0) { + auto scriptEnginePointer = scriptEngines->loadScript(url, false); + if (scriptEnginePointer) { + scriptEnginePointer->setType(ScriptEngine::Type::AVATAR); + } + } + } +} + +void Application::unloadAvatarScripts() { + auto scriptEngines = DependencyManager::get(); + auto urls = scriptEngines->getRunningScripts(); + for (auto url : urls) { + auto scriptEngine = scriptEngines->getScriptEngine(url); + if (scriptEngine->getType() == ScriptEngine::Type::AVATAR) { + scriptEngines->stopScript(url, false); + } + } +} + +void Application::updateLOD(float deltaTime) const { + PerformanceTimer perfTimer("LOD"); + // adjust it unless we were asked to disable this feature, or if we're currently in throttleRendering mode + if (!isThrottleRendering()) { + float presentTime = getActiveDisplayPlugin()->getAveragePresentTime(); + float engineRunTime = (float)(_graphicsEngine.getRenderEngine()->getConfiguration().get()->getCPURunTime()); + float gpuTime = getGPUContext()->getFrameTimerGPUAverage(); + float batchTime = getGPUContext()->getFrameTimerBatchAverage(); + auto lodManager = DependencyManager::get(); + lodManager->setRenderTimes(presentTime, engineRunTime, batchTime, gpuTime); + lodManager->autoAdjustLOD(deltaTime); + } else { + DependencyManager::get()->resetLODAdjust(); + } +} + +void Application::pushPostUpdateLambda(void* key, const std::function& func) { + std::unique_lock guard(_postUpdateLambdasLock); + _postUpdateLambdas[key] = func; +} + +// Called during Application::update immediately before AvatarManager::updateMyAvatar, updating my data that is then sent to everyone. +// (Maybe this code should be moved there?) +// The principal result is to call updateLookAtTargetAvatar() and then setLookAtPosition(). +// Note that it is called BEFORE we update position or joints based on sensors, etc. +void Application::updateMyAvatarLookAtPosition() { + PerformanceTimer perfTimer("lookAt"); + bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); + PerformanceWarning warn(showWarnings, "Application::updateMyAvatarLookAtPosition()"); + + auto myAvatar = getMyAvatar(); + myAvatar->updateLookAtTargetAvatar(); + FaceTracker* faceTracker = getActiveFaceTracker(); + auto eyeTracker = DependencyManager::get(); + + bool isLookingAtSomeone = false; + bool isHMD = qApp->isHMDMode(); + glm::vec3 lookAtSpot; + if (eyeTracker->isTracking() && (isHMD || eyeTracker->isSimulating())) { + // Look at the point that the user is looking at. + glm::vec3 lookAtPosition = eyeTracker->getLookAtPosition(); + if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { + lookAtPosition.x = -lookAtPosition.x; + } + if (isHMD) { + // TODO -- this code is probably wrong, getHeadPose() returns something in sensor frame, not avatar + glm::mat4 headPose = getActiveDisplayPlugin()->getHeadPose(); + glm::quat hmdRotation = glm::quat_cast(headPose); + lookAtSpot = _myCamera.getPosition() + myAvatar->getWorldOrientation() * (hmdRotation * lookAtPosition); + } else { + lookAtSpot = myAvatar->getHead()->getEyePosition() + + (myAvatar->getHead()->getFinalOrientationInWorldFrame() * lookAtPosition); + } + } else { + AvatarSharedPointer lookingAt = myAvatar->getLookAtTargetAvatar().lock(); + bool haveLookAtCandidate = lookingAt && myAvatar.get() != lookingAt.get(); + auto avatar = static_pointer_cast(lookingAt); + bool mutualLookAtSnappingEnabled = avatar && avatar->getLookAtSnappingEnabled() && myAvatar->getLookAtSnappingEnabled(); + if (haveLookAtCandidate && mutualLookAtSnappingEnabled) { + // If I am looking at someone else, look directly at one of their eyes + isLookingAtSomeone = true; + auto lookingAtHead = avatar->getHead(); + + const float MAXIMUM_FACE_ANGLE = 65.0f * RADIANS_PER_DEGREE; + glm::vec3 lookingAtFaceOrientation = lookingAtHead->getFinalOrientationInWorldFrame() * IDENTITY_FORWARD; + glm::vec3 fromLookingAtToMe = glm::normalize(myAvatar->getHead()->getEyePosition() + - lookingAtHead->getEyePosition()); + float faceAngle = glm::angle(lookingAtFaceOrientation, fromLookingAtToMe); + + if (faceAngle < MAXIMUM_FACE_ANGLE) { + // Randomly look back and forth between look targets + eyeContactTarget target = Menu::getInstance()->isOptionChecked(MenuOption::FixGaze) ? + LEFT_EYE : myAvatar->getEyeContactTarget(); + switch (target) { + case LEFT_EYE: + lookAtSpot = lookingAtHead->getLeftEyePosition(); + break; + case RIGHT_EYE: + lookAtSpot = lookingAtHead->getRightEyePosition(); + break; + case MOUTH: + lookAtSpot = lookingAtHead->getMouthPosition(); + break; + } + } else { + // Just look at their head (mid point between eyes) + lookAtSpot = lookingAtHead->getEyePosition(); + } + } else { + // I am not looking at anyone else, so just look forward + auto headPose = myAvatar->getControllerPoseInWorldFrame(controller::Action::HEAD); + if (headPose.isValid()) { + lookAtSpot = transformPoint(headPose.getMatrix(), glm::vec3(0.0f, 0.0f, TREE_SCALE)); + } else { + lookAtSpot = myAvatar->getHead()->getEyePosition() + + (myAvatar->getHead()->getFinalOrientationInWorldFrame() * glm::vec3(0.0f, 0.0f, -TREE_SCALE)); + } + } + + // Deflect the eyes a bit to match the detected gaze from the face tracker if active. + if (faceTracker && !faceTracker->isMuted()) { + float eyePitch = faceTracker->getEstimatedEyePitch(); + float eyeYaw = faceTracker->getEstimatedEyeYaw(); + const float GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT = 0.1f; + glm::vec3 origin = myAvatar->getHead()->getEyePosition(); + float deflection = faceTracker->getEyeDeflection(); + if (isLookingAtSomeone) { + deflection *= GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT; + } + lookAtSpot = origin + _myCamera.getOrientation() * glm::quat(glm::radians(glm::vec3( + eyePitch * deflection, eyeYaw * deflection, 0.0f))) * + glm::inverse(_myCamera.getOrientation()) * (lookAtSpot - origin); + } + } + + myAvatar->getHead()->setLookAtPosition(lookAtSpot); +} + +void Application::updateThreads(float deltaTime) { + PerformanceTimer perfTimer("updateThreads"); + bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); + PerformanceWarning warn(showWarnings, "Application::updateThreads()"); + + // parse voxel packets + if (!_enableProcessOctreeThread) { + _octreeProcessor.threadRoutine(); + _entityEditSender.threadRoutine(); + } +} + +void Application::toggleOverlays() { + auto menu = Menu::getInstance(); + menu->setIsOptionChecked(MenuOption::Overlays, !menu->isOptionChecked(MenuOption::Overlays)); +} + +void Application::setOverlaysVisible(bool visible) { + auto menu = Menu::getInstance(); + menu->setIsOptionChecked(MenuOption::Overlays, visible); +} + +void Application::centerUI() { + _overlayConductor.centerUI(); +} + +void Application::cycleCamera() { + auto menu = Menu::getInstance(); + if (menu->isOptionChecked(MenuOption::FullscreenMirror)) { + + menu->setIsOptionChecked(MenuOption::FullscreenMirror, false); + menu->setIsOptionChecked(MenuOption::FirstPerson, true); + + } else if (menu->isOptionChecked(MenuOption::FirstPerson)) { + + menu->setIsOptionChecked(MenuOption::FirstPerson, false); + menu->setIsOptionChecked(MenuOption::ThirdPerson, true); + + } else if (menu->isOptionChecked(MenuOption::ThirdPerson)) { + + menu->setIsOptionChecked(MenuOption::ThirdPerson, false); + menu->setIsOptionChecked(MenuOption::FullscreenMirror, true); + + } else if (menu->isOptionChecked(MenuOption::IndependentMode) || menu->isOptionChecked(MenuOption::CameraEntityMode)) { + // do nothing if in independent or camera entity modes + return; + } + cameraMenuChanged(); // handle the menu change +} + +void Application::cameraModeChanged() { + switch (_myCamera.getMode()) { + case CAMERA_MODE_FIRST_PERSON: + Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, true); + break; + case CAMERA_MODE_THIRD_PERSON: + Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); + break; + case CAMERA_MODE_MIRROR: + Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, true); + break; + case CAMERA_MODE_INDEPENDENT: + Menu::getInstance()->setIsOptionChecked(MenuOption::IndependentMode, true); + break; + case CAMERA_MODE_ENTITY: + Menu::getInstance()->setIsOptionChecked(MenuOption::CameraEntityMode, true); + break; + default: + break; + } + cameraMenuChanged(); +} + +void Application::changeViewAsNeeded(float boomLength) { + // Switch between first and third person views as needed + // This is called when the boom length has changed + bool boomLengthGreaterThanMinimum = (boomLength > MyAvatar::ZOOM_MIN); + + if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON && boomLengthGreaterThanMinimum) { + Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, false); + Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); + cameraMenuChanged(); + } else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON && !boomLengthGreaterThanMinimum) { + Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, true); + Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, false); + cameraMenuChanged(); + } +} + +void Application::cameraMenuChanged() { + auto menu = Menu::getInstance(); + if (menu->isOptionChecked(MenuOption::FullscreenMirror)) { + if (!isHMDMode() && _myCamera.getMode() != CAMERA_MODE_MIRROR) { + _mirrorYawOffset = 0.0f; + _myCamera.setMode(CAMERA_MODE_MIRROR); + getMyAvatar()->reset(false, false, false); // to reset any active MyAvatar::FollowHelpers + getMyAvatar()->setBoomLength(MyAvatar::ZOOM_DEFAULT); + } + } else if (menu->isOptionChecked(MenuOption::FirstPerson)) { + if (_myCamera.getMode() != CAMERA_MODE_FIRST_PERSON) { + _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); + getMyAvatar()->setBoomLength(MyAvatar::ZOOM_MIN); + } + } else if (menu->isOptionChecked(MenuOption::ThirdPerson)) { + if (_myCamera.getMode() != CAMERA_MODE_THIRD_PERSON) { + _myCamera.setMode(CAMERA_MODE_THIRD_PERSON); + if (getMyAvatar()->getBoomLength() == MyAvatar::ZOOM_MIN) { + getMyAvatar()->setBoomLength(MyAvatar::ZOOM_DEFAULT); + } + } + } else if (menu->isOptionChecked(MenuOption::IndependentMode)) { + if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT) { + _myCamera.setMode(CAMERA_MODE_INDEPENDENT); + } + } else if (menu->isOptionChecked(MenuOption::CameraEntityMode)) { + if (_myCamera.getMode() != CAMERA_MODE_ENTITY) { + _myCamera.setMode(CAMERA_MODE_ENTITY); + } + } +} + +void Application::resetPhysicsReadyInformation() { + // we've changed domains or cleared out caches or something. we no longer know enough about the + // collision information of nearby entities to make running bullet be safe. + _fullSceneReceivedCounter = 0; + _fullSceneCounterAtLastPhysicsCheck = 0; + _gpuTextureMemSizeStabilityCount = 0; + _gpuTextureMemSizeAtLastCheck = 0; + _physicsEnabled = false; + _octreeProcessor.startEntitySequence(); +} + + +void Application::reloadResourceCaches() { + resetPhysicsReadyInformation(); + + // Query the octree to refresh everything in view + _queryExpiry = SteadyClock::now(); + _octreeQuery.incrementConnectionID(); + + queryOctree(NodeType::EntityServer, PacketType::EntityQuery); + + // Clear the entities and their renderables + getEntities()->clear(); + + DependencyManager::get()->clearCache(); + DependencyManager::get()->clearCache(); + + // Clear all the resource caches + DependencyManager::get()->clear(); + DependencyManager::get()->refreshAll(); + DependencyManager::get()->refreshAll(); + MaterialCache::instance().refreshAll(); + DependencyManager::get()->refreshAll(); + ShaderCache::instance().refreshAll(); + DependencyManager::get()->refreshAll(); + DependencyManager::get()->refreshAll(); + + DependencyManager::get()->reset(); // Force redownload of .fst models + + DependencyManager::get()->reloadAllScripts(); + getOffscreenUI()->clearCache(); + + DependencyManager::get()->createKeyboard(); + + getMyAvatar()->resetFullAvatarURL(); +} + +void Application::rotationModeChanged() const { + if (!Menu::getInstance()->isOptionChecked(MenuOption::CenterPlayerInView)) { + getMyAvatar()->setHeadPitch(0); + } +} + +void Application::setKeyboardFocusHighlight(const glm::vec3& position, const glm::quat& rotation, const glm::vec3& dimensions) { + if (qApp->getLoginDialogPoppedUp()) { + return; + } + + auto entityScriptingInterface = DependencyManager::get(); + if (_keyboardFocusHighlightID == UNKNOWN_ENTITY_ID || !entityScriptingInterface->isAddedEntity(_keyboardFocusHighlightID)) { + EntityItemProperties properties; + properties.setType(EntityTypes::Box); + properties.setAlpha(1.0f); + properties.setColor({ 0xFF, 0xEF, 0x00 }); + properties.setPrimitiveMode(PrimitiveMode::LINES); + properties.getPulse().setMin(0.5); + properties.getPulse().setMax(1.0f); + properties.getPulse().setColorMode(PulseMode::IN_PHASE); + properties.setIgnorePickIntersection(true); + _keyboardFocusHighlightID = entityScriptingInterface->addEntityInternal(properties, entity::HostType::LOCAL); + } + + // Position focus + EntityItemProperties properties; + properties.setPosition(position); + properties.setRotation(rotation); + properties.setDimensions(dimensions); + properties.setVisible(true); + entityScriptingInterface->editEntity(_keyboardFocusHighlightID, properties); +} + +QUuid Application::getKeyboardFocusEntity() const { + return _keyboardFocusedEntity.get(); +} + +void Application::setKeyboardFocusEntity(const QUuid& id) { + if (_keyboardFocusedEntity.get() != id) { + if (qApp->getLoginDialogPoppedUp() && !_loginDialogID.isNull()) { + if (id == _loginDialogID) { + emit loginDialogFocusEnabled(); + } else if (!_keyboardFocusWaitingOnRenderable) { + // that's the only entity we want in focus; + return; + } + } + + _keyboardFocusedEntity.set(id); + + auto entityScriptingInterface = DependencyManager::get(); + if (id != UNKNOWN_ENTITY_ID) { + EntityPropertyFlags desiredProperties; + desiredProperties += PROP_VISIBLE; + desiredProperties += PROP_SHOW_KEYBOARD_FOCUS_HIGHLIGHT; + auto properties = entityScriptingInterface->getEntityProperties(id); + if (properties.getVisible()) { + auto entities = getEntities(); + auto entityId = _keyboardFocusedEntity.get(); + auto entityItemRenderable = entities->renderableForEntityId(entityId); + if (!entityItemRenderable) { + _keyboardFocusWaitingOnRenderable = true; + } else if (entityItemRenderable->wantsKeyboardFocus()) { + entities->setProxyWindow(entityId, _window->windowHandle()); + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->pluginFocusOutEvent(); + } + _lastAcceptedKeyPress = usecTimestampNow(); + + if (properties.getShowKeyboardFocusHighlight()) { + if (auto entity = entities->getEntity(entityId)) { + setKeyboardFocusHighlight(entity->getWorldPosition(), entity->getWorldOrientation(), + entity->getScaledDimensions() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR); + return; + } + } + } + } + } + + EntityItemProperties properties; + properties.setVisible(false); + entityScriptingInterface->editEntity(_keyboardFocusHighlightID, properties); + } +} + +void Application::updateDialogs(float deltaTime) const { + PerformanceTimer perfTimer("updateDialogs"); + bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); + PerformanceWarning warn(showWarnings, "Application::updateDialogs()"); + auto dialogsManager = DependencyManager::get(); + + QPointer octreeStatsDialog = dialogsManager->getOctreeStatsDialog(); + if (octreeStatsDialog) { + octreeStatsDialog->update(); + } +} + +void Application::updateSecondaryCameraViewFrustum() { + // TODO: Fix this by modeling the way the secondary camera works on how the main camera works + // ie. Use a camera object stored in the game logic and informs the Engine on where the secondary + // camera should be. + + // Code based on SecondaryCameraJob + auto renderConfig = _graphicsEngine.getRenderEngine()->getConfiguration(); + assert(renderConfig); + auto camera = dynamic_cast(renderConfig->getConfig("SecondaryCamera")); + + if (!camera || !camera->isEnabled()) { + return; + } + + ViewFrustum secondaryViewFrustum; + if (camera->portalProjection && !camera->attachedEntityId.isNull() && !camera->portalEntranceEntityId.isNull()) { + auto entityScriptingInterface = DependencyManager::get(); + EntityItemPointer portalEntrance = qApp->getEntities()->getTree()->findEntityByID(camera->portalEntranceEntityId); + EntityItemPointer portalExit = qApp->getEntities()->getTree()->findEntityByID(camera->attachedEntityId); + + glm::vec3 portalEntrancePropertiesPosition = portalEntrance->getWorldPosition(); + glm::quat portalEntrancePropertiesRotation = portalEntrance->getWorldOrientation(); + glm::mat4 worldFromPortalEntranceRotation = glm::mat4_cast(portalEntrancePropertiesRotation); + glm::mat4 worldFromPortalEntranceTranslation = glm::translate(portalEntrancePropertiesPosition); + glm::mat4 worldFromPortalEntrance = worldFromPortalEntranceTranslation * worldFromPortalEntranceRotation; + glm::mat4 portalEntranceFromWorld = glm::inverse(worldFromPortalEntrance); + + glm::vec3 portalExitPropertiesPosition = portalExit->getWorldPosition(); + glm::quat portalExitPropertiesRotation = portalExit->getWorldOrientation(); + glm::vec3 portalExitPropertiesDimensions = portalExit->getScaledDimensions(); + glm::vec3 halfPortalExitPropertiesDimensions = 0.5f * portalExitPropertiesDimensions; + + glm::mat4 worldFromPortalExitRotation = glm::mat4_cast(portalExitPropertiesRotation); + glm::mat4 worldFromPortalExitTranslation = glm::translate(portalExitPropertiesPosition); + glm::mat4 worldFromPortalExit = worldFromPortalExitTranslation * worldFromPortalExitRotation; + + glm::vec3 mainCameraPositionWorld = getCamera().getPosition(); + glm::vec3 mainCameraPositionPortalEntrance = vec3(portalEntranceFromWorld * vec4(mainCameraPositionWorld, 1.0f)); + mainCameraPositionPortalEntrance = vec3(-mainCameraPositionPortalEntrance.x, mainCameraPositionPortalEntrance.y, + -mainCameraPositionPortalEntrance.z); + glm::vec3 portalExitCameraPositionWorld = vec3(worldFromPortalExit * vec4(mainCameraPositionPortalEntrance, 1.0f)); + + secondaryViewFrustum.setPosition(portalExitCameraPositionWorld); + secondaryViewFrustum.setOrientation(portalExitPropertiesRotation); + + float nearClip = mainCameraPositionPortalEntrance.z + portalExitPropertiesDimensions.z * 2.0f; + // `mainCameraPositionPortalEntrance` should technically be `mainCameraPositionPortalExit`, + // but the values are the same. + glm::vec3 upperRight = halfPortalExitPropertiesDimensions - mainCameraPositionPortalEntrance; + glm::vec3 bottomLeft = -halfPortalExitPropertiesDimensions - mainCameraPositionPortalEntrance; + glm::mat4 frustum = glm::frustum(bottomLeft.x, upperRight.x, bottomLeft.y, upperRight.y, nearClip, camera->farClipPlaneDistance); + secondaryViewFrustum.setProjection(frustum); + } else if (camera->mirrorProjection && !camera->attachedEntityId.isNull()) { + auto entityScriptingInterface = DependencyManager::get(); + auto entityProperties = entityScriptingInterface->getEntityProperties(camera->attachedEntityId); + glm::vec3 mirrorPropertiesPosition = entityProperties.getPosition(); + glm::quat mirrorPropertiesRotation = entityProperties.getRotation(); + glm::vec3 mirrorPropertiesDimensions = entityProperties.getDimensions(); + glm::vec3 halfMirrorPropertiesDimensions = 0.5f * mirrorPropertiesDimensions; + + // setup mirror from world as inverse of world from mirror transformation using inverted x and z for mirrored image + // TODO: we are assuming here that UP is world y-axis + glm::mat4 worldFromMirrorRotation = glm::mat4_cast(mirrorPropertiesRotation) * glm::scale(vec3(-1.0f, 1.0f, -1.0f)); + glm::mat4 worldFromMirrorTranslation = glm::translate(mirrorPropertiesPosition); + glm::mat4 worldFromMirror = worldFromMirrorTranslation * worldFromMirrorRotation; + glm::mat4 mirrorFromWorld = glm::inverse(worldFromMirror); + + // get mirror camera position by reflecting main camera position's z coordinate in mirror space + glm::vec3 mainCameraPositionWorld = getCamera().getPosition(); + glm::vec3 mainCameraPositionMirror = vec3(mirrorFromWorld * vec4(mainCameraPositionWorld, 1.0f)); + glm::vec3 mirrorCameraPositionMirror = vec3(mainCameraPositionMirror.x, mainCameraPositionMirror.y, + -mainCameraPositionMirror.z); + glm::vec3 mirrorCameraPositionWorld = vec3(worldFromMirror * vec4(mirrorCameraPositionMirror, 1.0f)); + + // set frustum position to be mirrored camera and set orientation to mirror's adjusted rotation + glm::quat mirrorCameraOrientation = glm::quat_cast(worldFromMirrorRotation); + secondaryViewFrustum.setPosition(mirrorCameraPositionWorld); + secondaryViewFrustum.setOrientation(mirrorCameraOrientation); + + // build frustum using mirror space translation of mirrored camera + float nearClip = mirrorCameraPositionMirror.z + mirrorPropertiesDimensions.z * 2.0f; + glm::vec3 upperRight = halfMirrorPropertiesDimensions - mirrorCameraPositionMirror; + glm::vec3 bottomLeft = -halfMirrorPropertiesDimensions - mirrorCameraPositionMirror; + glm::mat4 frustum = glm::frustum(bottomLeft.x, upperRight.x, bottomLeft.y, upperRight.y, nearClip, camera->farClipPlaneDistance); + secondaryViewFrustum.setProjection(frustum); + } else { + if (!camera->attachedEntityId.isNull()) { + auto entityScriptingInterface = DependencyManager::get(); + auto entityProperties = entityScriptingInterface->getEntityProperties(camera->attachedEntityId); + secondaryViewFrustum.setPosition(entityProperties.getPosition()); + secondaryViewFrustum.setOrientation(entityProperties.getRotation()); + } else { + secondaryViewFrustum.setPosition(camera->position); + secondaryViewFrustum.setOrientation(camera->orientation); + } + + float aspectRatio = (float)camera->textureWidth / (float)camera->textureHeight; + secondaryViewFrustum.setProjection(camera->vFoV, + aspectRatio, + camera->nearClipPlaneDistance, + camera->farClipPlaneDistance); + } + // Without calculating the bound planes, the secondary camera will use the same culling frustum as the main camera, + // which is not what we want here. + secondaryViewFrustum.calculate(); + + _conicalViews.push_back(secondaryViewFrustum); +} + +static bool domainLoadingInProgress = false; + +void Application::update(float deltaTime) { + PROFILE_RANGE_EX(app, __FUNCTION__, 0xffff0000, (uint64_t)_graphicsEngine._renderFrameCount + 1); + + if (_aboutToQuit) { + return; + } + + + if (!_physicsEnabled) { + if (!domainLoadingInProgress) { + PROFILE_ASYNC_BEGIN(app, "Scene Loading", ""); + domainLoadingInProgress = true; + } + + // we haven't yet enabled physics. we wait until we think we have all the collision information + // for nearby entities before starting bullet up. + quint64 now = usecTimestampNow(); + if (isServerlessMode() || _octreeProcessor.isLoadSequenceComplete()) { + bool enableInterstitial = DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled(); + + if (gpuTextureMemSizeStable() || !enableInterstitial) { + // we've received a new full-scene octree stats packet, or it's been long enough to try again anyway + _lastPhysicsCheckTime = now; + _fullSceneCounterAtLastPhysicsCheck = _fullSceneReceivedCounter; + _lastQueriedViews.clear(); // Force new view. + + // process octree stats packets are sent in between full sends of a scene (this isn't currently true). + // We keep physics disabled until we've received a full scene and everything near the avatar in that + // scene is ready to compute its collision shape. + if (getMyAvatar()->isReadyForPhysics()) { + _physicsEnabled = true; + setIsInterstitialMode(false); + getMyAvatar()->updateMotionBehaviorFromMenu(); + } + } + } + } else if (domainLoadingInProgress) { + domainLoadingInProgress = false; + PROFILE_ASYNC_END(app, "Scene Loading", ""); + } + + auto myAvatar = getMyAvatar(); + { + PerformanceTimer perfTimer("devices"); + + FaceTracker* tracker = getSelectedFaceTracker(); + if (tracker && Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking) != tracker->isMuted()) { + tracker->toggleMute(); + } + + tracker = getActiveFaceTracker(); + if (tracker && !tracker->isMuted()) { + tracker->update(deltaTime); + + // Auto-mute microphone after losing face tracking? + if (tracker->isTracking()) { + _lastFaceTrackerUpdate = usecTimestampNow(); + } else { + const quint64 MUTE_MICROPHONE_AFTER_USECS = 5000000; //5 secs + Menu* menu = Menu::getInstance(); + auto audioClient = DependencyManager::get(); + if (menu->isOptionChecked(MenuOption::AutoMuteAudio) && !audioClient->isMuted()) { + if (_lastFaceTrackerUpdate > 0 + && ((usecTimestampNow() - _lastFaceTrackerUpdate) > MUTE_MICROPHONE_AFTER_USECS)) { + audioClient->setMuted(true); + _lastFaceTrackerUpdate = 0; + } + } else { + _lastFaceTrackerUpdate = 0; + } + } + } else { + _lastFaceTrackerUpdate = 0; + } + + auto userInputMapper = DependencyManager::get(); + + controller::HmdAvatarAlignmentType hmdAvatarAlignmentType; + if (myAvatar->getHmdAvatarAlignmentType() == "eyes") { + hmdAvatarAlignmentType = controller::HmdAvatarAlignmentType::Eyes; + } else { + hmdAvatarAlignmentType = controller::HmdAvatarAlignmentType::Head; + } + + controller::InputCalibrationData calibrationData = { + myAvatar->getSensorToWorldMatrix(), + createMatFromQuatAndPos(myAvatar->getWorldOrientation(), myAvatar->getWorldPosition()), + myAvatar->getHMDSensorMatrix(), + myAvatar->getCenterEyeCalibrationMat(), + myAvatar->getHeadCalibrationMat(), + myAvatar->getSpine2CalibrationMat(), + myAvatar->getHipsCalibrationMat(), + myAvatar->getLeftFootCalibrationMat(), + myAvatar->getRightFootCalibrationMat(), + myAvatar->getRightArmCalibrationMat(), + myAvatar->getLeftArmCalibrationMat(), + myAvatar->getRightHandCalibrationMat(), + myAvatar->getLeftHandCalibrationMat(), + hmdAvatarAlignmentType + }; + + InputPluginPointer keyboardMousePlugin; + for (auto inputPlugin : PluginManager::getInstance()->getInputPlugins()) { + if (inputPlugin->getName() == KeyboardMouseDevice::NAME) { + keyboardMousePlugin = inputPlugin; + } else if (inputPlugin->isActive()) { + inputPlugin->pluginUpdate(deltaTime, calibrationData); + } + } + + userInputMapper->setInputCalibrationData(calibrationData); + userInputMapper->update(deltaTime); + + if (keyboardMousePlugin && keyboardMousePlugin->isActive()) { + keyboardMousePlugin->pluginUpdate(deltaTime, calibrationData); + } + // Transfer the user inputs to the driveKeys + // FIXME can we drop drive keys and just have the avatar read the action states directly? + myAvatar->clearDriveKeys(); + if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT && !isInterstitialMode()) { + if (!_controllerScriptingInterface->areActionsCaptured() && _myCamera.getMode() != CAMERA_MODE_MIRROR) { + myAvatar->setDriveKey(MyAvatar::TRANSLATE_Z, -1.0f * userInputMapper->getActionState(controller::Action::TRANSLATE_Z)); + myAvatar->setDriveKey(MyAvatar::TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y)); + myAvatar->setDriveKey(MyAvatar::TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X)); + if (deltaTime > FLT_EPSILON) { + myAvatar->setDriveKey(MyAvatar::PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); + myAvatar->setDriveKey(MyAvatar::YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); + myAvatar->setDriveKey(MyAvatar::DELTA_PITCH, -1.0f * userInputMapper->getActionState(controller::Action::DELTA_PITCH)); + myAvatar->setDriveKey(MyAvatar::DELTA_YAW, -1.0f * userInputMapper->getActionState(controller::Action::DELTA_YAW)); + myAvatar->setDriveKey(MyAvatar::STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW)); + } + } + myAvatar->setDriveKey(MyAvatar::ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z)); + } + + myAvatar->setSprintMode((bool)userInputMapper->getActionState(controller::Action::SPRINT)); + static const std::vector avatarControllerActions = { + controller::Action::LEFT_HAND, + controller::Action::RIGHT_HAND, + controller::Action::LEFT_FOOT, + controller::Action::RIGHT_FOOT, + controller::Action::HIPS, + controller::Action::SPINE2, + controller::Action::HEAD, + controller::Action::LEFT_HAND_THUMB1, + controller::Action::LEFT_HAND_THUMB2, + controller::Action::LEFT_HAND_THUMB3, + controller::Action::LEFT_HAND_THUMB4, + controller::Action::LEFT_HAND_INDEX1, + controller::Action::LEFT_HAND_INDEX2, + controller::Action::LEFT_HAND_INDEX3, + controller::Action::LEFT_HAND_INDEX4, + controller::Action::LEFT_HAND_MIDDLE1, + controller::Action::LEFT_HAND_MIDDLE2, + controller::Action::LEFT_HAND_MIDDLE3, + controller::Action::LEFT_HAND_MIDDLE4, + controller::Action::LEFT_HAND_RING1, + controller::Action::LEFT_HAND_RING2, + controller::Action::LEFT_HAND_RING3, + controller::Action::LEFT_HAND_RING4, + controller::Action::LEFT_HAND_PINKY1, + controller::Action::LEFT_HAND_PINKY2, + controller::Action::LEFT_HAND_PINKY3, + controller::Action::LEFT_HAND_PINKY4, + controller::Action::RIGHT_HAND_THUMB1, + controller::Action::RIGHT_HAND_THUMB2, + controller::Action::RIGHT_HAND_THUMB3, + controller::Action::RIGHT_HAND_THUMB4, + controller::Action::RIGHT_HAND_INDEX1, + controller::Action::RIGHT_HAND_INDEX2, + controller::Action::RIGHT_HAND_INDEX3, + controller::Action::RIGHT_HAND_INDEX4, + controller::Action::RIGHT_HAND_MIDDLE1, + controller::Action::RIGHT_HAND_MIDDLE2, + controller::Action::RIGHT_HAND_MIDDLE3, + controller::Action::RIGHT_HAND_MIDDLE4, + controller::Action::RIGHT_HAND_RING1, + controller::Action::RIGHT_HAND_RING2, + controller::Action::RIGHT_HAND_RING3, + controller::Action::RIGHT_HAND_RING4, + controller::Action::RIGHT_HAND_PINKY1, + controller::Action::RIGHT_HAND_PINKY2, + controller::Action::RIGHT_HAND_PINKY3, + controller::Action::RIGHT_HAND_PINKY4, + controller::Action::LEFT_ARM, + controller::Action::RIGHT_ARM, + controller::Action::LEFT_SHOULDER, + controller::Action::RIGHT_SHOULDER, + controller::Action::LEFT_FORE_ARM, + controller::Action::RIGHT_FORE_ARM, + controller::Action::LEFT_LEG, + controller::Action::RIGHT_LEG, + controller::Action::LEFT_UP_LEG, + controller::Action::RIGHT_UP_LEG, + controller::Action::LEFT_TOE_BASE, + controller::Action::RIGHT_TOE_BASE + }; + + // copy controller poses from userInputMapper to myAvatar. + glm::mat4 myAvatarMatrix = createMatFromQuatAndPos(myAvatar->getWorldOrientation(), myAvatar->getWorldPosition()); + glm::mat4 worldToSensorMatrix = glm::inverse(myAvatar->getSensorToWorldMatrix()); + glm::mat4 avatarToSensorMatrix = worldToSensorMatrix * myAvatarMatrix; + for (auto& action : avatarControllerActions) { + controller::Pose pose = userInputMapper->getPoseState(action); + myAvatar->setControllerPoseInSensorFrame(action, pose.transform(avatarToSensorMatrix)); + } + + static const std::vector trackedObjectStringLiterals = { + QStringLiteral("_TrackedObject00"), QStringLiteral("_TrackedObject01"), QStringLiteral("_TrackedObject02"), QStringLiteral("_TrackedObject03"), + QStringLiteral("_TrackedObject04"), QStringLiteral("_TrackedObject05"), QStringLiteral("_TrackedObject06"), QStringLiteral("_TrackedObject07"), + QStringLiteral("_TrackedObject08"), QStringLiteral("_TrackedObject09"), QStringLiteral("_TrackedObject10"), QStringLiteral("_TrackedObject11"), + QStringLiteral("_TrackedObject12"), QStringLiteral("_TrackedObject13"), QStringLiteral("_TrackedObject14"), QStringLiteral("_TrackedObject15") + }; + + // Controlled by the Developer > Avatar > Show Tracked Objects menu. + if (_showTrackedObjects) { + static const std::vector trackedObjectActions = { + controller::Action::TRACKED_OBJECT_00, controller::Action::TRACKED_OBJECT_01, controller::Action::TRACKED_OBJECT_02, controller::Action::TRACKED_OBJECT_03, + controller::Action::TRACKED_OBJECT_04, controller::Action::TRACKED_OBJECT_05, controller::Action::TRACKED_OBJECT_06, controller::Action::TRACKED_OBJECT_07, + controller::Action::TRACKED_OBJECT_08, controller::Action::TRACKED_OBJECT_09, controller::Action::TRACKED_OBJECT_10, controller::Action::TRACKED_OBJECT_11, + controller::Action::TRACKED_OBJECT_12, controller::Action::TRACKED_OBJECT_13, controller::Action::TRACKED_OBJECT_14, controller::Action::TRACKED_OBJECT_15 + }; + + int i = 0; + glm::vec4 BLUE(0.0f, 0.0f, 1.0f, 1.0f); + for (auto& action : trackedObjectActions) { + controller::Pose pose = userInputMapper->getPoseState(action); + if (pose.valid) { + glm::vec3 pos = transformPoint(myAvatarMatrix, pose.translation); + glm::quat rot = glmExtractRotation(myAvatarMatrix) * pose.rotation; + DebugDraw::getInstance().addMarker(trackedObjectStringLiterals[i], rot, pos, BLUE); + } else { + DebugDraw::getInstance().removeMarker(trackedObjectStringLiterals[i]); + } + i++; + } + } else if (_prevShowTrackedObjects) { + for (auto& key : trackedObjectStringLiterals) { + DebugDraw::getInstance().removeMarker(key); + } + } + _prevShowTrackedObjects = _showTrackedObjects; + } + + updateThreads(deltaTime); // If running non-threaded, then give the threads some time to process... + updateDialogs(deltaTime); // update various stats dialogs if present + + auto grabManager = DependencyManager::get(); + grabManager->simulateGrabs(); + + // TODO: break these out into distinct perfTimers when they prove interesting + { + PROFILE_RANGE(app, "PickManager"); + PerformanceTimer perfTimer("pickManager"); + DependencyManager::get()->update(); + } + + { + PROFILE_RANGE(app, "PointerManager"); + PerformanceTimer perfTimer("pointerManager"); + DependencyManager::get()->update(); + } + + QSharedPointer avatarManager = DependencyManager::get(); + + { + PROFILE_RANGE(simulation_physics, "Simulation"); + PerformanceTimer perfTimer("simulation"); + + if (_physicsEnabled) { + auto t0 = std::chrono::high_resolution_clock::now(); + auto t1 = t0; + { + PROFILE_RANGE(simulation_physics, "PrePhysics"); + PerformanceTimer perfTimer("prePhysics)"); + { + PROFILE_RANGE(simulation_physics, "RemoveEntities"); + const VectorOfMotionStates& motionStates = _entitySimulation->getObjectsToRemoveFromPhysics(); + { + PROFILE_RANGE_EX(simulation_physics, "NumObjs", 0xffff0000, (uint64_t)motionStates.size()); + _physicsEngine->removeObjects(motionStates); + } + _entitySimulation->deleteObjectsRemovedFromPhysics(); + } + + { + PROFILE_RANGE(simulation_physics, "AddEntities"); + VectorOfMotionStates motionStates; + getEntities()->getTree()->withReadLock([&] { + _entitySimulation->getObjectsToAddToPhysics(motionStates); + PROFILE_RANGE_EX(simulation_physics, "NumObjs", 0xffff0000, (uint64_t)motionStates.size()); + _physicsEngine->addObjects(motionStates); + }); + } + { + VectorOfMotionStates motionStates; + PROFILE_RANGE(simulation_physics, "ChangeEntities"); + getEntities()->getTree()->withReadLock([&] { + _entitySimulation->getObjectsToChange(motionStates); + VectorOfMotionStates stillNeedChange = _physicsEngine->changeObjects(motionStates); + _entitySimulation->setObjectsToChange(stillNeedChange); + }); + } + + _entitySimulation->applyDynamicChanges(); + + t1 = std::chrono::high_resolution_clock::now(); + + { + PROFILE_RANGE(simulation_physics, "Avatars"); + PhysicsEngine::Transaction transaction; + avatarManager->buildPhysicsTransaction(transaction); + _physicsEngine->processTransaction(transaction); + avatarManager->handleProcessedPhysicsTransaction(transaction); + myAvatar->getCharacterController()->buildPhysicsTransaction(transaction); + _physicsEngine->processTransaction(transaction); + myAvatar->getCharacterController()->handleProcessedPhysicsTransaction(transaction); + myAvatar->prepareForPhysicsSimulation(); + _physicsEngine->enableGlobalContactAddedCallback(myAvatar->isFlying()); + } + + { + PROFILE_RANGE(simulation_physics, "PrepareActions"); + _physicsEngine->forEachDynamic([&](EntityDynamicPointer dynamic) { + dynamic->prepareForPhysicsSimulation(); + }); + } + } + auto t2 = std::chrono::high_resolution_clock::now(); + { + PROFILE_RANGE(simulation_physics, "StepPhysics"); + PerformanceTimer perfTimer("stepPhysics"); + getEntities()->getTree()->withWriteLock([&] { + _physicsEngine->stepSimulation(); + }); + } + auto t3 = std::chrono::high_resolution_clock::now(); + { + if (_physicsEngine->hasOutgoingChanges()) { + { + PROFILE_RANGE(simulation_physics, "PostPhysics"); + PerformanceTimer perfTimer("postPhysics"); + // grab the collision events BEFORE handleChangedMotionStates() because at this point + // we have a better idea of which objects we own or should own. + auto& collisionEvents = _physicsEngine->getCollisionEvents(); + + getEntities()->getTree()->withWriteLock([&] { + PROFILE_RANGE(simulation_physics, "HandleChanges"); + PerformanceTimer perfTimer("handleChanges"); + + const VectorOfMotionStates& outgoingChanges = _physicsEngine->getChangedMotionStates(); + _entitySimulation->handleChangedMotionStates(outgoingChanges); + avatarManager->handleChangedMotionStates(outgoingChanges); + + const VectorOfMotionStates& deactivations = _physicsEngine->getDeactivatedMotionStates(); + _entitySimulation->handleDeactivatedMotionStates(deactivations); + }); + + // handleCollisionEvents() AFTER handleChangedMotionStates() + { + PROFILE_RANGE(simulation_physics, "CollisionEvents"); + avatarManager->handleCollisionEvents(collisionEvents); + // Collision events (and their scripts) must not be handled when we're locked, above. (That would risk + // deadlock.) + _entitySimulation->handleCollisionEvents(collisionEvents); + } + + { + PROFILE_RANGE(simulation_physics, "MyAvatar"); + myAvatar->harvestResultsFromPhysicsSimulation(deltaTime); + } + + if (PerformanceTimer::isActive() && + Menu::getInstance()->isOptionChecked(MenuOption::DisplayDebugTimingDetails) && + Menu::getInstance()->isOptionChecked(MenuOption::ExpandPhysicsTiming)) { + _physicsEngine->harvestPerformanceStats(); + } + // NOTE: the PhysicsEngine stats are written to stdout NOT to Qt log framework + _physicsEngine->dumpStatsIfNecessary(); + } + auto t4 = std::chrono::high_resolution_clock::now(); + + // NOTE: the getEntities()->update() call below will wait for lock + // and will provide non-physical entity motion + getEntities()->update(true); // update the models... + + auto t5 = std::chrono::high_resolution_clock::now(); + + workload::Timings timings(6); + timings[0] = t1 - t0; // prePhysics entities + timings[1] = t2 - t1; // prePhysics avatars + timings[2] = t3 - t2; // stepPhysics + timings[3] = t4 - t3; // postPhysics + timings[4] = t5 - t4; // non-physical kinematics + timings[5] = workload::Timing_ns((int32_t)(NSECS_PER_SECOND * deltaTime)); // game loop duration + _gameWorkload.updateSimulationTimings(timings); + } + } + } else { + // update the rendering without any simulation + getEntities()->update(false); + } + // remove recently dead avatarEntities + SetOfEntities deadAvatarEntities; + _entitySimulation->takeDeadAvatarEntities(deadAvatarEntities); + avatarManager->removeDeadAvatarEntities(deadAvatarEntities); + } + + // AvatarManager update + { + { + PROFILE_RANGE(simulation, "OtherAvatars"); + PerformanceTimer perfTimer("otherAvatars"); + avatarManager->updateOtherAvatars(deltaTime); + } + + { + PROFILE_RANGE(simulation, "MyAvatar"); + PerformanceTimer perfTimer("MyAvatar"); + qApp->updateMyAvatarLookAtPosition(); + avatarManager->updateMyAvatar(deltaTime); + } + } + + bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); + PerformanceWarning warn(showWarnings, "Application::update()"); + + updateLOD(deltaTime); + + if (!_loginDialogID.isNull()) { + _loginStateManager.update(getMyAvatar()->getDominantHand(), _loginDialogID); + updateLoginDialogPosition(); + } + + { + PROFILE_RANGE_EX(app, "Overlays", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount()); + PerformanceTimer perfTimer("overlays"); + _overlays.update(deltaTime); + } + + // Update _viewFrustum with latest camera and view frustum data... + // NOTE: we get this from the view frustum, to make it simpler, since the + // loadViewFrumstum() method will get the correct details from the camera + // We could optimize this to not actually load the viewFrustum, since we don't + // actually need to calculate the view frustum planes to send these details + // to the server. + { + QMutexLocker viewLocker(&_viewMutex); + _myCamera.loadViewFrustum(_viewFrustum); + + _conicalViews.clear(); + _conicalViews.push_back(_viewFrustum); + // TODO: Fix this by modeling the way the secondary camera works on how the main camera works + // ie. Use a camera object stored in the game logic and informs the Engine on where the secondary + // camera should be. + updateSecondaryCameraViewFrustum(); + } + + quint64 now = usecTimestampNow(); + + // Update my voxel servers with my current voxel query... + { + PROFILE_RANGE_EX(app, "QueryOctree", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount()); + PerformanceTimer perfTimer("queryOctree"); + QMutexLocker viewLocker(&_viewMutex); + + bool viewIsDifferentEnough = false; + if (_conicalViews.size() == _lastQueriedViews.size()) { + for (size_t i = 0; i < _conicalViews.size(); ++i) { + if (!_conicalViews[i].isVerySimilar(_lastQueriedViews[i])) { + viewIsDifferentEnough = true; + break; + } + } + } else { + viewIsDifferentEnough = true; + } + + + // if it's been a while since our last query or the view has significantly changed then send a query, otherwise suppress it + static const std::chrono::seconds MIN_PERIOD_BETWEEN_QUERIES { 3 }; + auto now = SteadyClock::now(); + if (now > _queryExpiry || viewIsDifferentEnough) { + if (DependencyManager::get()->shouldRenderEntities()) { + queryOctree(NodeType::EntityServer, PacketType::EntityQuery); + } + queryAvatars(); + + _lastQueriedViews = _conicalViews; + _queryExpiry = now + MIN_PERIOD_BETWEEN_QUERIES; + } + } + + // sent nack packets containing missing sequence numbers of received packets from nodes + { + quint64 sinceLastNack = now - _lastNackTime; + const quint64 TOO_LONG_SINCE_LAST_NACK = 1 * USECS_PER_SECOND; + if (sinceLastNack > TOO_LONG_SINCE_LAST_NACK) { + _lastNackTime = now; + sendNackPackets(); + } + } + + // send packet containing downstream audio stats to the AudioMixer + { + quint64 sinceLastNack = now - _lastSendDownstreamAudioStats; + if (sinceLastNack > TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS && !isInterstitialMode()) { + _lastSendDownstreamAudioStats = now; + + QMetaObject::invokeMethod(DependencyManager::get().data(), "sendDownstreamAudioStatsPacket", Qt::QueuedConnection); + } + } + + { + PerformanceTimer perfTimer("avatarManager/postUpdate"); + avatarManager->postUpdate(deltaTime, getMain3DScene()); + } + + { + PROFILE_RANGE_EX(app, "PostUpdateLambdas", 0xffff0000, (uint64_t)0); + PerformanceTimer perfTimer("postUpdateLambdas"); + std::unique_lock guard(_postUpdateLambdasLock); + for (auto& iter : _postUpdateLambdas) { + iter.second(); + } + _postUpdateLambdas.clear(); + } + + + updateRenderArgs(deltaTime); + + { + PerformanceTimer perfTimer("AnimDebugDraw"); + AnimDebugDraw::getInstance().update(); + } + + + { // Game loop is done, mark the end of the frame for the scene transactions and the render loop to take over + PerformanceTimer perfTimer("enqueueFrame"); + getMain3DScene()->enqueueFrame(); + } + + // If the display plugin is inactive then the frames won't be processed so process them here. + if (!getActiveDisplayPlugin()->isActive()) { + getMain3DScene()->processTransactionQueue(); + } +} + +void Application::updateRenderArgs(float deltaTime) { + _graphicsEngine.editRenderArgs([this, deltaTime](AppRenderArgs& appRenderArgs) { + PerformanceTimer perfTimer("editRenderArgs"); + appRenderArgs._headPose = getHMDSensorPose(); + + auto myAvatar = getMyAvatar(); + + // update the avatar with a fresh HMD pose + { + PROFILE_RANGE(render, "/updateAvatar"); + myAvatar->updateFromHMDSensorMatrix(appRenderArgs._headPose); + } + + auto lodManager = DependencyManager::get(); + + float sensorToWorldScale = getMyAvatar()->getSensorToWorldScale(); + appRenderArgs._sensorToWorldScale = sensorToWorldScale; + appRenderArgs._sensorToWorld = getMyAvatar()->getSensorToWorldMatrix(); + { + PROFILE_RANGE(render, "/buildFrustrumAndArgs"); + { + QMutexLocker viewLocker(&_viewMutex); + // adjust near clip plane to account for sensor scaling. + auto adjustedProjection = glm::perspective(glm::radians(_fieldOfView.get()), + getActiveDisplayPlugin()->getRecommendedAspectRatio(), + DEFAULT_NEAR_CLIP * sensorToWorldScale, + DEFAULT_FAR_CLIP); + _viewFrustum.setProjection(adjustedProjection); + _viewFrustum.calculate(); + } + appRenderArgs._renderArgs = RenderArgs(_graphicsEngine.getGPUContext(), lodManager->getOctreeSizeScale(), + lodManager->getBoundaryLevelAdjust(), lodManager->getLODAngleHalfTan(), RenderArgs::DEFAULT_RENDER_MODE, + RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); + appRenderArgs._renderArgs._scene = getMain3DScene(); + + { + QMutexLocker viewLocker(&_viewMutex); + appRenderArgs._renderArgs.setViewFrustum(_viewFrustum); + } + } + { + PROFILE_RANGE(render, "/resizeGL"); + bool showWarnings = false; + bool suppressShortTimings = false; + auto menu = Menu::getInstance(); + if (menu) { + suppressShortTimings = menu->isOptionChecked(MenuOption::SuppressShortTimings); + showWarnings = menu->isOptionChecked(MenuOption::PipelineWarnings); + } + PerformanceWarning::setSuppressShortTimings(suppressShortTimings); + PerformanceWarning warn(showWarnings, "Application::paintGL()"); + resizeGL(); + } + + this->updateCamera(appRenderArgs._renderArgs, deltaTime); + appRenderArgs._eyeToWorld = _myCamera.getTransform(); + appRenderArgs._isStereo = false; + + { + auto hmdInterface = DependencyManager::get(); + float ipdScale = hmdInterface->getIPDScale(); + + // scale IPD by sensorToWorldScale, to make the world seem larger or smaller accordingly. + ipdScale *= sensorToWorldScale; + + auto baseProjection = appRenderArgs._renderArgs.getViewFrustum().getProjection(); + + if (getActiveDisplayPlugin()->isStereo()) { + // Stereo modes will typically have a larger projection matrix overall, + // so we ask for the 'mono' projection matrix, which for stereo and HMD + // plugins will imply the combined projection for both eyes. + // + // This is properly implemented for the Oculus plugins, but for OpenVR + // and Stereo displays I'm not sure how to get / calculate it, so we're + // just relying on the left FOV in each case and hoping that the + // overall culling margin of error doesn't cause popping in the + // right eye. There are FIXMEs in the relevant plugins + _myCamera.setProjection(getActiveDisplayPlugin()->getCullingProjection(baseProjection)); + appRenderArgs._isStereo = true; + + auto& eyeOffsets = appRenderArgs._eyeOffsets; + auto& eyeProjections = appRenderArgs._eyeProjections; + + // FIXME we probably don't need to set the projection matrix every frame, + // only when the display plugin changes (or in non-HMD modes when the user + // changes the FOV manually, which right now I don't think they can. + for_each_eye([&](Eye eye) { + // For providing the stereo eye views, the HMD head pose has already been + // applied to the avatar, so we need to get the difference between the head + // pose applied to the avatar and the per eye pose, and use THAT as + // the per-eye stereo matrix adjustment. + mat4 eyeToHead = getActiveDisplayPlugin()->getEyeToHeadTransform(eye); + // Grab the translation + vec3 eyeOffset = glm::vec3(eyeToHead[3]); + // Apply IPD scaling + mat4 eyeOffsetTransform = glm::translate(mat4(), eyeOffset * -1.0f * ipdScale); + eyeOffsets[eye] = eyeOffsetTransform; + eyeProjections[eye] = getActiveDisplayPlugin()->getEyeProjection(eye, baseProjection); + }); + + // Configure the type of display / stereo + appRenderArgs._renderArgs._displayMode = (isHMDMode() ? RenderArgs::STEREO_HMD : RenderArgs::STEREO_MONITOR); + } + } + + { + QMutexLocker viewLocker(&_viewMutex); + _myCamera.loadViewFrustum(_displayViewFrustum); + appRenderArgs._view = glm::inverse(_displayViewFrustum.getView()); + } + + { + QMutexLocker viewLocker(&_viewMutex); + appRenderArgs._renderArgs.setViewFrustum(_displayViewFrustum); + } + + + // HACK + // load the view frustum + // FIXME: This preDisplayRender call is temporary until we create a separate render::scene for the mirror rendering. + // Then we can move this logic into the Avatar::simulate call. + myAvatar->preDisplaySide(&appRenderArgs._renderArgs); + }); +} + +void Application::queryAvatars() { + if (!isInterstitialMode()) { + auto avatarPacket = NLPacket::create(PacketType::AvatarQuery); + auto destinationBuffer = reinterpret_cast(avatarPacket->getPayload()); + unsigned char* bufferStart = destinationBuffer; + + uint8_t numFrustums = (uint8_t)_conicalViews.size(); + memcpy(destinationBuffer, &numFrustums, sizeof(numFrustums)); + destinationBuffer += sizeof(numFrustums); + + for (const auto& view : _conicalViews) { + destinationBuffer += view.serialize(destinationBuffer); + } + + avatarPacket->setPayloadSize(destinationBuffer - bufferStart); + + DependencyManager::get()->broadcastToNodes(std::move(avatarPacket), NodeSet() << NodeType::AvatarMixer); + } +} + + +int Application::sendNackPackets() { + + // iterates through all nodes in NodeList + auto nodeList = DependencyManager::get(); + + int packetsSent = 0; + + nodeList->eachNode([&](const SharedNodePointer& node){ + + if (node->getActiveSocket() && node->getType() == NodeType::EntityServer) { + + auto nackPacketList = NLPacketList::create(PacketType::OctreeDataNack); + + QUuid nodeUUID = node->getUUID(); + + // if there are octree packets from this node that are waiting to be processed, + // don't send a NACK since the missing packets may be among those waiting packets. + if (_octreeProcessor.hasPacketsToProcessFrom(nodeUUID)) { + return; + } + + QSet missingSequenceNumbers; + _octreeServerSceneStats.withReadLock([&] { + // retrieve octree scene stats of this node + if (_octreeServerSceneStats.find(nodeUUID) == _octreeServerSceneStats.end()) { + return; + } + // get sequence number stats of node, prune its missing set, and make a copy of the missing set + SequenceNumberStats& sequenceNumberStats = _octreeServerSceneStats[nodeUUID].getIncomingOctreeSequenceNumberStats(); + sequenceNumberStats.pruneMissingSet(); + missingSequenceNumbers = sequenceNumberStats.getMissingSet(); + }); + + _isMissingSequenceNumbers = (missingSequenceNumbers.size() != 0); + + // construct nack packet(s) for this node + foreach(const OCTREE_PACKET_SEQUENCE& missingNumber, missingSequenceNumbers) { + nackPacketList->writePrimitive(missingNumber); + } + + if (nackPacketList->getNumPackets()) { + packetsSent += (int)nackPacketList->getNumPackets(); + + // send the packet list + nodeList->sendPacketList(std::move(nackPacketList), *node); + } + } + }); + + return packetsSent; +} + +void Application::queryOctree(NodeType_t serverType, PacketType packetType) { + + if (!_settingsLoaded) { + return; // bail early if settings are not loaded + } + + const bool isModifiedQuery = !_physicsEnabled; + if (isModifiedQuery) { + // Create modified view that is a simple sphere. + bool interstitialModeEnabled = DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled(); + + ConicalViewFrustum sphericalView; + sphericalView.setSimpleRadius(INITIAL_QUERY_RADIUS); + + if (interstitialModeEnabled) { + ConicalViewFrustum farView; + farView.set(_viewFrustum); + _octreeQuery.setConicalViews({ sphericalView, farView }); + } else { + _octreeQuery.setConicalViews({ sphericalView }); + } + + _octreeQuery.setOctreeSizeScale(DEFAULT_OCTREE_SIZE_SCALE); + static constexpr float MIN_LOD_ADJUST = -20.0f; + _octreeQuery.setBoundaryLevelAdjust(MIN_LOD_ADJUST); + } else { + _octreeQuery.setConicalViews(_conicalViews); + auto lodManager = DependencyManager::get(); + _octreeQuery.setOctreeSizeScale(lodManager->getOctreeSizeScale()); + _octreeQuery.setBoundaryLevelAdjust(lodManager->getBoundaryLevelAdjust()); + } + _octreeQuery.setReportInitialCompletion(isModifiedQuery); + + + auto nodeList = DependencyManager::get(); + + auto node = nodeList->soloNodeOfType(serverType); + if (node && node->getActiveSocket()) { + _octreeQuery.setMaxQueryPacketsPerSecond(getMaxOctreePacketsPerSecond()); + + auto queryPacket = NLPacket::create(packetType); + + // encode the query data + auto packetData = reinterpret_cast(queryPacket->getPayload()); + int packetSize = _octreeQuery.getBroadcastData(packetData); + queryPacket->setPayloadSize(packetSize); + + // make sure we still have an active socket + nodeList->sendUnreliablePacket(*queryPacket, *node); + } +} + + +bool Application::isHMDMode() const { + return getActiveDisplayPlugin()->isHmd(); +} + +float Application::getNumCollisionObjects() const { + return _physicsEngine ? _physicsEngine->getNumCollisionObjects() : 0; +} + +float Application::getTargetRenderFrameRate() const { return getActiveDisplayPlugin()->getTargetFrameRate(); } + +QRect Application::getDesirableApplicationGeometry() const { + QRect applicationGeometry = getWindow()->geometry(); + + // If our parent window is on the HMD, then don't use its geometry, instead use + // the "main screen" geometry. + HMDToolsDialog* hmdTools = DependencyManager::get()->getHMDToolsDialog(); + if (hmdTools && hmdTools->hasHMDScreen()) { + QScreen* hmdScreen = hmdTools->getHMDScreen(); + QWindow* appWindow = getWindow()->windowHandle(); + QScreen* appScreen = appWindow->screen(); + + // if our app's screen is the hmd screen, we don't want to place the + // running scripts widget on it. So we need to pick a better screen. + // we will use the screen for the HMDTools since it's a guaranteed + // better screen. + if (appScreen == hmdScreen) { + QScreen* betterScreen = hmdTools->windowHandle()->screen(); + applicationGeometry = betterScreen->geometry(); + } + } + return applicationGeometry; +} + +PickRay Application::computePickRay(float x, float y) const { + vec2 pickPoint { x, y }; + PickRay result; + if (isHMDMode()) { + getApplicationCompositor().computeHmdPickRay(pickPoint, result.origin, result.direction); + } else { + pickPoint /= getCanvasSize(); + if (_myCamera.getMode() == CameraMode::CAMERA_MODE_MIRROR) { + pickPoint.x = 1.0f - pickPoint.x; + } + QMutexLocker viewLocker(&_viewMutex); + _viewFrustum.computePickRay(pickPoint.x, pickPoint.y, result.origin, result.direction); + } + return result; +} + +std::shared_ptr Application::getMyAvatar() const { + return DependencyManager::get()->getMyAvatar(); +} + +glm::vec3 Application::getAvatarPosition() const { + return getMyAvatar()->getWorldPosition(); +} + +void Application::copyViewFrustum(ViewFrustum& viewOut) const { + QMutexLocker viewLocker(&_viewMutex); + viewOut = _viewFrustum; +} + +void Application::copyDisplayViewFrustum(ViewFrustum& viewOut) const { + QMutexLocker viewLocker(&_viewMutex); + viewOut = _displayViewFrustum; +} + +// resentSensors() is a bit of vestigial feature. It used to be used for Oculus DK2 to recenter the view around +// the current head orientation. With the introduction of "room scale" tracking we no longer need that particular +// feature. However, we still use this to reset face trackers, eye trackers, audio and to optionally re-load the avatar +// rig and animations from scratch. +void Application::resetSensors(bool andReload) { + DependencyManager::get()->reset(); + DependencyManager::get()->reset(); + _overlayConductor.centerUI(); + getActiveDisplayPlugin()->resetSensors(); + getMyAvatar()->reset(true, andReload); + QMetaObject::invokeMethod(DependencyManager::get().data(), "reset", Qt::QueuedConnection); +} + +void Application::hmdVisibleChanged(bool visible) { + // TODO + // calling start and stop will change audio input and ouput to default audio devices. + // we need to add a pause/unpause functionality to AudioClient for this to work properly +#if 0 + if (visible) { + QMetaObject::invokeMethod(DependencyManager::get().data(), "start", Qt::QueuedConnection); + } else { + QMetaObject::invokeMethod(DependencyManager::get().data(), "stop", Qt::QueuedConnection); + } +#endif +} + +void Application::updateWindowTitle() const { + + auto nodeList = DependencyManager::get(); + auto accountManager = DependencyManager::get(); + auto isInErrorState = nodeList->getDomainHandler().isInErrorState(); + + QString buildVersion = " - " + + (BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Stable ? QString("Version") : QString("Build")) + + " " + applicationVersion(); + + QString loginStatus = accountManager->isLoggedIn() ? "" : " (NOT LOGGED IN)"; + + QString connectionStatus = isInErrorState ? " (ERROR CONNECTING)" : + nodeList->getDomainHandler().isConnected() ? "" : " (NOT CONNECTED)"; + QString username = accountManager->getAccountInfo().getUsername(); + + setCrashAnnotation("username", username.toStdString()); + + QString currentPlaceName; + if (isServerlessMode()) { + if (isInErrorState) { + currentPlaceName = "serverless: " + nodeList->getDomainHandler().getErrorDomainURL().toString(); + } else { + currentPlaceName = "serverless: " + DependencyManager::get()->getDomainURL().toString(); + } + } else { + currentPlaceName = DependencyManager::get()->getDomainURL().host(); + if (currentPlaceName.isEmpty()) { + currentPlaceName = nodeList->getDomainHandler().getHostname(); + } + } + + QString title = QString() + (!username.isEmpty() ? username + " @ " : QString()) + + currentPlaceName + connectionStatus + loginStatus + buildVersion; + +#ifndef WIN32 + // crashes with vs2013/win32 + qCDebug(interfaceapp, "Application title set to: %s", title.toStdString().c_str()); +#endif + _window->setWindowTitle(title); + + // updateTitleWindow gets called whenever there's a change regarding the domain, so rather + // than placing this within domainURLChanged, it's placed here to cover the other potential cases. + DependencyManager::get< MessagesClient >()->sendLocalMessage("Toolbar-DomainChanged", ""); +} + +void Application::clearDomainOctreeDetails(bool clearAll) { + // before we delete all entities get MyAvatar's AvatarEntityData ready + getMyAvatar()->prepareAvatarEntityDataForReload(); + + // if we're about to quit, we really don't need to do the rest of these things... + if (_aboutToQuit) { + return; + } + + qCDebug(interfaceapp) << "Clearing domain octree details..."; + + resetPhysicsReadyInformation(); + setIsInterstitialMode(true); + + _octreeServerSceneStats.withWriteLock([&] { + _octreeServerSceneStats.clear(); + }); + + // reset the model renderer + clearAll ? getEntities()->clear() : getEntities()->clearDomainAndNonOwnedEntities(); + + auto skyStage = DependencyManager::get()->getSkyStage(); + + skyStage->setBackgroundMode(graphics::SunSkyStage::SKY_DEFAULT); + + DependencyManager::get()->clearUnusedResources(); + DependencyManager::get()->clearUnusedResources(); + MaterialCache::instance().clearUnusedResources(); + DependencyManager::get()->clearUnusedResources(); + ShaderCache::instance().clearUnusedResources(); + DependencyManager::get()->clearUnusedResources(); + DependencyManager::get()->clearUnusedResources(); +} + +void Application::domainURLChanged(QUrl domainURL) { + // disable physics until we have enough information about our new location to not cause craziness. + resetPhysicsReadyInformation(); + setIsServerlessMode(domainURL.scheme() != URL_SCHEME_HIFI); + if (isServerlessMode()) { + loadServerlessDomain(domainURL); + } + updateWindowTitle(); +} + +void Application::goToErrorDomainURL(QUrl errorDomainURL) { + // disable physics until we have enough information about our new location to not cause craziness. + resetPhysicsReadyInformation(); + setIsServerlessMode(errorDomainURL.scheme() != URL_SCHEME_HIFI); + if (isServerlessMode()) { + loadErrorDomain(errorDomainURL); + } + updateWindowTitle(); +} + +void Application::resettingDomain() { + _notifiedPacketVersionMismatchThisDomain = false; + + clearDomainOctreeDetails(false); +} + +void Application::nodeAdded(SharedNodePointer node) const { + if (node->getType() == NodeType::EntityServer) { + if (!_failedToConnectToEntityServer) { + _entityServerConnectionTimer.stop(); + _entityServerConnectionTimer.setInterval(ENTITY_SERVER_CONNECTION_TIMEOUT); + _entityServerConnectionTimer.start(); + } + } +} + +void Application::nodeActivated(SharedNodePointer node) { + if (node->getType() == NodeType::AssetServer) { + // asset server just connected - check if we have the asset browser showing + +#if !defined(DISABLE_QML) + auto offscreenUi = getOffscreenUI(); + + if (offscreenUi) { + auto nodeList = DependencyManager::get(); + + if (nodeList->getThisNodeCanWriteAssets()) { + // call reload on the shown asset browser dialog to get the mappings (if permissions allow) + auto assetDialog = offscreenUi ? offscreenUi->getRootItem()->findChild("AssetServer") : nullptr; + if (assetDialog) { + QMetaObject::invokeMethod(assetDialog, "reload"); + } + } else { + // we switched to an Asset Server that we can't modify, hide the Asset Browser + offscreenUi->hide("AssetServer"); + } + } +#endif + } + + // If we get a new EntityServer activated, reset lastQueried time + // so we will do a proper query during update + if (node->getType() == NodeType::EntityServer) { + _queryExpiry = SteadyClock::now(); + _octreeQuery.incrementConnectionID(); + + if (!_failedToConnectToEntityServer) { + _entityServerConnectionTimer.stop(); + } + } + + if (node->getType() == NodeType::AudioMixer && !isInterstitialMode()) { + DependencyManager::get()->negotiateAudioFormat(); + } + + if (node->getType() == NodeType::AvatarMixer) { + _queryExpiry = SteadyClock::now(); + + // new avatar mixer, send off our identity packet on next update loop + // Reset skeletonModelUrl if the last server modified our choice. + // Override the avatar url (but not model name) here too. + if (_avatarOverrideUrl.isValid()) { + getMyAvatar()->useFullAvatarURL(_avatarOverrideUrl); + } + + if (getMyAvatar()->getFullAvatarURLFromPreferences() != getMyAvatar()->getSkeletonModelURL()) { + getMyAvatar()->resetFullAvatarURL(); + } + getMyAvatar()->markIdentityDataChanged(); + getMyAvatar()->resetLastSent(); + + if (!isInterstitialMode()) { + // transmit a "sendAll" packet to the AvatarMixer we just connected to. + getMyAvatar()->sendAvatarDataPacket(true); + } + } +} + +void Application::nodeKilled(SharedNodePointer node) { + // These are here because connecting NodeList::nodeKilled to OctreePacketProcessor::nodeKilled doesn't work: + // OctreePacketProcessor::nodeKilled is not being called when NodeList::nodeKilled is emitted. + // This may have to do with GenericThread::threadRoutine() blocking the QThread event loop + + _octreeProcessor.nodeKilled(node); + + _entityEditSender.nodeKilled(node); + + if (node->getType() == NodeType::AudioMixer) { + QMetaObject::invokeMethod(DependencyManager::get().data(), "audioMixerKilled"); + } else if (node->getType() == NodeType::EntityServer) { + // we lost an entity server, clear all of the domain octree details + clearDomainOctreeDetails(false); + } else if (node->getType() == NodeType::AssetServer) { + // asset server going away - check if we have the asset browser showing + +#if !defined(DISABLE_QML) + auto offscreenUi = getOffscreenUI(); + auto assetDialog = offscreenUi ? offscreenUi->getRootItem()->findChild("AssetServer") : nullptr; + + if (assetDialog) { + // call reload on the shown asset browser dialog + QMetaObject::invokeMethod(assetDialog, "clear"); + } +#endif + } +} + +void Application::trackIncomingOctreePacket(ReceivedMessage& message, SharedNodePointer sendingNode, bool wasStatsPacket) { + // Attempt to identify the sender from its address. + if (sendingNode) { + const QUuid& nodeUUID = sendingNode->getUUID(); + + // now that we know the node ID, let's add these stats to the stats for that node... + _octreeServerSceneStats.withWriteLock([&] { + if (_octreeServerSceneStats.find(nodeUUID) != _octreeServerSceneStats.end()) { + OctreeSceneStats& stats = _octreeServerSceneStats[nodeUUID]; + stats.trackIncomingOctreePacket(message, wasStatsPacket, sendingNode->getClockSkewUsec()); + } + }); + } +} + +bool Application::gpuTextureMemSizeStable() { + auto renderConfig = qApp->getRenderEngine()->getConfiguration(); + auto renderStats = renderConfig->getConfig("Stats"); + + qint64 textureResourceGPUMemSize = renderStats->textureResourceGPUMemSize; + qint64 texturePopulatedGPUMemSize = renderStats->textureResourcePopulatedGPUMemSize; + qint64 textureTransferSize = renderStats->texturePendingGPUTransferSize; + + if (_gpuTextureMemSizeAtLastCheck == textureResourceGPUMemSize) { + _gpuTextureMemSizeStabilityCount++; + } else { + _gpuTextureMemSizeStabilityCount = 0; + } + _gpuTextureMemSizeAtLastCheck = textureResourceGPUMemSize; + + if (_gpuTextureMemSizeStabilityCount >= _minimumGPUTextureMemSizeStabilityCount) { + return (textureResourceGPUMemSize == texturePopulatedGPUMemSize) && (textureTransferSize == 0); + } + return false; +} + +int Application::processOctreeStats(ReceivedMessage& message, SharedNodePointer sendingNode) { + // parse the incoming stats datas stick it in a temporary object for now, while we + // determine which server it belongs to + int statsMessageLength = 0; + + const QUuid& nodeUUID = sendingNode->getUUID(); + + // now that we know the node ID, let's add these stats to the stats for that node... + _octreeServerSceneStats.withWriteLock([&] { + OctreeSceneStats& octreeStats = _octreeServerSceneStats[nodeUUID]; + statsMessageLength = octreeStats.unpackFromPacket(message); + + if (octreeStats.isFullScene()) { + _fullSceneReceivedCounter++; + } + }); + + return statsMessageLength; +} + +void Application::packetSent(quint64 length) { +} + +void Application::addingEntityWithCertificate(const QString& certificateID, const QString& placeName) { + auto ledger = DependencyManager::get(); + ledger->updateLocation(certificateID, placeName); +} + +void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointer scriptEngine) { + + scriptEngine->setEmitScriptUpdatesFunction([this]() { + SharedNodePointer entityServerNode = DependencyManager::get()->soloNodeOfType(NodeType::EntityServer); + return !entityServerNode || isPhysicsEnabled(); + }); + + // setup the packet sender of the script engine's scripting interfaces so + // we can use the same ones from the application. + auto entityScriptingInterface = DependencyManager::get(); + entityScriptingInterface->setPacketSender(&_entityEditSender); + entityScriptingInterface->setEntityTree(getEntities()->getTree()); + + if (property(hifi::properties::TEST).isValid()) { + scriptEngine->registerGlobalObject("Test", TestScriptingInterface::getInstance()); + } + + scriptEngine->registerGlobalObject("PlatformInfo", PlatformInfoScriptingInterface::getInstance()); + scriptEngine->registerGlobalObject("Rates", new RatesScriptingInterface(this)); + + // hook our avatar and avatar hash map object into this script engine + getMyAvatar()->registerMetaTypes(scriptEngine); + + scriptEngine->registerGlobalObject("AvatarList", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("Camera", &_myCamera); + +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) + scriptEngine->registerGlobalObject("SpeechRecognizer", DependencyManager::get().data()); +#endif + + ClipboardScriptingInterface* clipboardScriptable = new ClipboardScriptingInterface(); + scriptEngine->registerGlobalObject("Clipboard", clipboardScriptable); + connect(scriptEngine.data(), &ScriptEngine::finished, clipboardScriptable, &ClipboardScriptingInterface::deleteLater); + + scriptEngine->registerGlobalObject("Overlays", &_overlays); + qScriptRegisterMetaType(scriptEngine.data(), RayToOverlayIntersectionResultToScriptValue, + RayToOverlayIntersectionResultFromScriptValue); + +#if !defined(DISABLE_QML) + scriptEngine->registerGlobalObject("OffscreenFlags", getOffscreenUI()->getFlags()); + scriptEngine->registerGlobalObject("Desktop", DependencyManager::get().data()); +#endif + + qScriptRegisterMetaType(scriptEngine.data(), wrapperToScriptValue, wrapperFromScriptValue); + qScriptRegisterMetaType(scriptEngine.data(), + wrapperToScriptValue, wrapperFromScriptValue); + scriptEngine->registerGlobalObject("Toolbars", DependencyManager::get().data()); + + qScriptRegisterMetaType(scriptEngine.data(), wrapperToScriptValue, wrapperFromScriptValue); + qScriptRegisterMetaType(scriptEngine.data(), + wrapperToScriptValue, wrapperFromScriptValue); + scriptEngine->registerGlobalObject("Tablet", DependencyManager::get().data()); + // FIXME remove these deprecated names for the tablet scripting interface + scriptEngine->registerGlobalObject("tabletInterface", DependencyManager::get().data()); + + auto toolbarScriptingInterface = DependencyManager::get().data(); + DependencyManager::get().data()->setToolbarScriptingInterface(toolbarScriptingInterface); + + scriptEngine->registerGlobalObject("Window", DependencyManager::get().data()); + scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, + LocationScriptingInterface::locationSetter, "Window"); + // register `location` on the global object. + scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, + LocationScriptingInterface::locationSetter); + + bool clientScript = scriptEngine->isClientScript(); + scriptEngine->registerFunction("OverlayWindow", clientScript ? QmlWindowClass::constructor : QmlWindowClass::restricted_constructor); +#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML) + scriptEngine->registerFunction("OverlayWebWindow", clientScript ? QmlWebWindowClass::constructor : QmlWebWindowClass::restricted_constructor); +#endif + scriptEngine->registerFunction("QmlFragment", clientScript ? QmlFragmentClass::constructor : QmlFragmentClass::restricted_constructor); + + scriptEngine->registerGlobalObject("Menu", MenuScriptingInterface::getInstance()); + scriptEngine->registerGlobalObject("DesktopPreviewProvider", DependencyManager::get().data()); +#if !defined(DISABLE_QML) + scriptEngine->registerGlobalObject("Stats", Stats::getInstance()); +#endif + scriptEngine->registerGlobalObject("Settings", SettingsScriptingInterface::getInstance()); + scriptEngine->registerGlobalObject("Snapshot", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("AudioStats", DependencyManager::get()->getStats().data()); + scriptEngine->registerGlobalObject("AudioScope", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("AvatarBookmarks", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("LocationBookmarks", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("RayPick", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("LaserPointers", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("Picks", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("Pointers", DependencyManager::get().data()); + + // Caches + scriptEngine->registerGlobalObject("AnimationCache", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("TextureCache", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("ModelCache", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("SoundCache", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("DialogsManager", _dialogsManagerScriptingInterface); + + scriptEngine->registerGlobalObject("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED + scriptEngine->registerGlobalObject("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED + scriptEngine->registerGlobalObject("AccountServices", AccountServicesScriptingInterface::getInstance()); + qScriptRegisterMetaType(scriptEngine.data(), DownloadInfoResultToScriptValue, DownloadInfoResultFromScriptValue); + + scriptEngine->registerGlobalObject("FaceTracker", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("AvatarManager", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("LODManager", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("Keyboard", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("Paths", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("HMD", DependencyManager::get().data()); + scriptEngine->registerFunction("HMD", "getHUDLookAtPosition2D", HMDScriptingInterface::getHUDLookAtPosition2D, 0); + scriptEngine->registerFunction("HMD", "getHUDLookAtPosition3D", HMDScriptingInterface::getHUDLookAtPosition3D, 0); + + scriptEngine->registerGlobalObject("Scene", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("Render", _graphicsEngine.getRenderEngine()->getConfiguration().get()); + scriptEngine->registerGlobalObject("Workload", _gameWorkload._engine->getConfiguration().get()); + + GraphicsScriptingInterface::registerMetaTypes(scriptEngine.data()); + scriptEngine->registerGlobalObject("Graphics", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("ScriptDiscoveryService", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("Reticle", getApplicationCompositor().getReticleInterface()); + + scriptEngine->registerGlobalObject("UserActivityLogger", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("Users", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("GooglePoly", DependencyManager::get().data()); + + if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { + scriptEngine->registerGlobalObject("Steam", new SteamScriptingInterface(scriptEngine.data(), steamClient.get())); + } + auto scriptingInterface = DependencyManager::get(); + scriptEngine->registerGlobalObject("Controller", scriptingInterface.data()); + UserInputMapper::registerControllerTypes(scriptEngine.data()); + + auto recordingInterface = DependencyManager::get(); + scriptEngine->registerGlobalObject("Recording", recordingInterface.data()); + + auto entityScriptServerLog = DependencyManager::get(); + scriptEngine->registerGlobalObject("EntityScriptServerLog", entityScriptServerLog.data()); + scriptEngine->registerGlobalObject("AvatarInputs", AvatarInputs::getInstance()); + scriptEngine->registerGlobalObject("Selection", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("ContextOverlay", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("WalletScriptingInterface", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("AddressManager", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("HifiAbout", AboutUtil::getInstance()); + scriptEngine->registerGlobalObject("ResourceRequestObserver", DependencyManager::get().data()); + + registerInteractiveWindowMetaType(scriptEngine.data()); + + auto pickScriptingInterface = DependencyManager::get(); + pickScriptingInterface->registerMetaTypes(scriptEngine.data()); + + // connect this script engines printedMessage signal to the global ScriptEngines these various messages + auto scriptEngines = DependencyManager::get().data(); + connect(scriptEngine.data(), &ScriptEngine::printedMessage, scriptEngines, &ScriptEngines::onPrintedMessage); + connect(scriptEngine.data(), &ScriptEngine::errorMessage, scriptEngines, &ScriptEngines::onErrorMessage); + connect(scriptEngine.data(), &ScriptEngine::warningMessage, scriptEngines, &ScriptEngines::onWarningMessage); + connect(scriptEngine.data(), &ScriptEngine::infoMessage, scriptEngines, &ScriptEngines::onInfoMessage); + connect(scriptEngine.data(), &ScriptEngine::clearDebugWindow, scriptEngines, &ScriptEngines::onClearDebugWindow); + +} + +bool Application::canAcceptURL(const QString& urlString) const { + QUrl url(urlString); + if (url.query().contains(WEB_VIEW_TAG)) { + return false; + } else if (urlString.startsWith(URL_SCHEME_HIFI)) { + return true; + } + QString lowerPath = url.path().toLower(); + for (auto& pair : _acceptedExtensions) { + if (lowerPath.endsWith(pair.first, Qt::CaseInsensitive)) { + return true; + } + } + return false; +} + +bool Application::acceptURL(const QString& urlString, bool defaultUpload) { + QUrl url(urlString); + + if (url.scheme() == URL_SCHEME_HIFI) { + // this is a hifi URL - have the AddressManager handle it + QMetaObject::invokeMethod(DependencyManager::get().data(), "handleLookupString", + Qt::AutoConnection, Q_ARG(const QString&, urlString)); + return true; + } + + QString lowerPath = url.path().toLower(); + for (auto& pair : _acceptedExtensions) { + if (lowerPath.endsWith(pair.first, Qt::CaseInsensitive)) { + AcceptURLMethod method = pair.second; + return (this->*method)(urlString); + } + } + + if (defaultUpload && !url.fileName().isEmpty() && url.isLocalFile()) { + showAssetServerWidget(urlString); + } + return defaultUpload; +} + +void Application::setSessionUUID(const QUuid& sessionUUID) const { + Physics::setSessionUUID(sessionUUID); +} + +bool Application::askToSetAvatarUrl(const QString& url) { + QUrl realUrl(url); + if (realUrl.isLocalFile()) { + OffscreenUi::asyncWarning("", "You can not use local files for avatar components."); + return false; + } + + // Download the FST file, to attempt to determine its model type + QVariantHash fstMapping = FSTReader::downloadMapping(url); + + FSTReader::ModelType modelType = FSTReader::predictModelType(fstMapping); + + QString modelName = fstMapping["name"].toString(); + QString modelLicense = fstMapping["license"].toString(); + + bool agreeToLicense = true; // assume true + //create set avatar callback + auto setAvatar = [=] (QString url, QString modelName) { + ModalDialogListener* dlg = OffscreenUi::asyncQuestion("Set Avatar", + "Would you like to use '" + modelName + "' for your avatar?", + QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok); + QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { + QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); + + bool ok = (QMessageBox::Ok == static_cast(answer.toInt())); + if (ok) { + getMyAvatar()->useFullAvatarURL(url, modelName); + emit fullAvatarURLChanged(url, modelName); + } else { + qCDebug(interfaceapp) << "Declined to use the avatar"; + } + }); + }; + + if (!modelLicense.isEmpty()) { + // word wrap the license text to fit in a reasonable shaped message box. + const int MAX_CHARACTERS_PER_LINE = 90; + modelLicense = simpleWordWrap(modelLicense, MAX_CHARACTERS_PER_LINE); + + ModalDialogListener* dlg = OffscreenUi::asyncQuestion("Avatar Usage License", + modelLicense + "\nDo you agree to these terms?", + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + QObject::connect(dlg, &ModalDialogListener::response, this, [=, &agreeToLicense] (QVariant answer) { + QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); + + agreeToLicense = (static_cast(answer.toInt()) == QMessageBox::Yes); + if (agreeToLicense) { + switch (modelType) { + case FSTReader::HEAD_AND_BODY_MODEL: { + setAvatar(url, modelName); + break; + } + default: + OffscreenUi::asyncWarning("", modelName + "Does not support a head and body as required."); + break; + } + } else { + qCDebug(interfaceapp) << "Declined to agree to avatar license"; + } + + //auto offscreenUi = getOffscreenUI(); + }); + } else { + setAvatar(url, modelName); + } + + return true; +} + + +bool Application::askToLoadScript(const QString& scriptFilenameOrURL) { + QString shortName = scriptFilenameOrURL; + + QUrl scriptURL { scriptFilenameOrURL }; + + if (scriptURL.host().endsWith(MARKETPLACE_CDN_HOSTNAME)) { + int startIndex = shortName.lastIndexOf('/') + 1; + int endIndex = shortName.lastIndexOf('?'); + shortName = shortName.mid(startIndex, endIndex - startIndex); + } + +#ifdef DISABLE_QML + DependencyManager::get()->loadScript(scriptFilenameOrURL); +#else + QString message = "Would you like to run this script:\n" + shortName; + ModalDialogListener* dlg = OffscreenUi::asyncQuestion(getWindow(), "Run Script", message, + QMessageBox::Yes | QMessageBox::No); + + QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { + const QString& fileName = scriptFilenameOrURL; + if (static_cast(answer.toInt()) == QMessageBox::Yes) { + qCDebug(interfaceapp) << "Chose to run the script: " << fileName; + DependencyManager::get()->loadScript(fileName); + } else { + qCDebug(interfaceapp) << "Declined to run the script"; + } + QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); + }); +#endif + return true; +} + +bool Application::askToWearAvatarAttachmentUrl(const QString& url) { + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkRequest networkRequest = QNetworkRequest(url); + networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + QNetworkReply* reply = networkAccessManager.get(networkRequest); + int requestNumber = ++_avatarAttachmentRequest; + connect(reply, &QNetworkReply::finished, [this, reply, url, requestNumber]() { + + if (requestNumber != _avatarAttachmentRequest) { + // this request has been superseded by another more recent request + reply->deleteLater(); + return; + } + + QNetworkReply::NetworkError networkError = reply->error(); + if (networkError == QNetworkReply::NoError) { + // download success + QByteArray contents = reply->readAll(); + + QJsonParseError jsonError; + auto doc = QJsonDocument::fromJson(contents, &jsonError); + if (jsonError.error == QJsonParseError::NoError) { + + auto jsonObject = doc.object(); + + // retrieve optional name field from JSON + QString name = tr("Unnamed Attachment"); + auto nameValue = jsonObject.value("name"); + if (nameValue.isString()) { + name = nameValue.toString(); + } + + auto avatarAttachmentConfirmationTitle = tr("Avatar Attachment Confirmation"); + auto avatarAttachmentConfirmationMessage = tr("Would you like to wear '%1' on your avatar?").arg(name); + ModalDialogListener* dlg = OffscreenUi::asyncQuestion(avatarAttachmentConfirmationTitle, + avatarAttachmentConfirmationMessage, + QMessageBox::Ok | QMessageBox::Cancel); + QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { + QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); + if (static_cast(answer.toInt()) == QMessageBox::Yes) { + // add attachment to avatar + auto myAvatar = getMyAvatar(); + assert(myAvatar); + auto attachmentDataVec = myAvatar->getAttachmentData(); + AttachmentData attachmentData; + attachmentData.fromJson(jsonObject); + attachmentDataVec.push_back(attachmentData); + myAvatar->setAttachmentData(attachmentDataVec); + } else { + qCDebug(interfaceapp) << "User declined to wear the avatar attachment"; + } + }); + } else { + // json parse error + auto avatarAttachmentParseErrorString = tr("Error parsing attachment JSON from url: \"%1\""); + displayAvatarAttachmentWarning(avatarAttachmentParseErrorString.arg(url)); + } + } else { + // download failure + auto avatarAttachmentDownloadErrorString = tr("Error downloading attachment JSON from url: \"%1\""); + displayAvatarAttachmentWarning(avatarAttachmentDownloadErrorString.arg(url)); + } + reply->deleteLater(); + }); + return true; +} + +void Application::replaceDomainContent(const QString& url) { + qCDebug(interfaceapp) << "Attempting to replace domain content"; + QByteArray urlData(url.toUtf8()); + auto limitedNodeList = DependencyManager::get(); + const auto& domainHandler = limitedNodeList->getDomainHandler(); + + auto octreeFilePacket = NLPacket::create(PacketType::DomainContentReplacementFromUrl, urlData.size(), true); + octreeFilePacket->write(urlData); + limitedNodeList->sendPacket(std::move(octreeFilePacket), domainHandler.getSockAddr()); + + auto addressManager = DependencyManager::get(); + addressManager->handleLookupString(DOMAIN_SPAWNING_POINT); + QString newHomeAddress = addressManager->getHost() + DOMAIN_SPAWNING_POINT; + qCDebug(interfaceapp) << "Setting new home bookmark to: " << newHomeAddress; + DependencyManager::get()->setHomeLocationToAddress(newHomeAddress); +} + +bool Application::askToReplaceDomainContent(const QString& url) { + QString methodDetails; + const int MAX_CHARACTERS_PER_LINE = 90; + if (DependencyManager::get()->getThisNodeCanReplaceContent()) { + QUrl originURL { url }; + if (originURL.host().endsWith(MARKETPLACE_CDN_HOSTNAME)) { + // Create a confirmation dialog when this call is made + static const QString infoText = simpleWordWrap("Your domain's content will be replaced with a new content set. " + "If you want to save what you have now, create a backup before proceeding. For more information about backing up " + "and restoring content, visit the documentation page at: ", MAX_CHARACTERS_PER_LINE) + + "\nhttps://docs.highfidelity.com/create-and-explore/start-working-in-your-sandbox/restoring-sandbox-content"; + + ModalDialogListener* dig = OffscreenUi::asyncQuestion("Are you sure you want to replace this domain's content set?", + infoText, QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + QObject::connect(dig, &ModalDialogListener::response, this, [=] (QVariant answer) { + QString details; + if (static_cast(answer.toInt()) == QMessageBox::Yes) { + // Given confirmation, send request to domain server to replace content + replaceDomainContent(url); + details = "SuccessfulRequestToReplaceContent"; + } else { + details = "UserDeclinedToReplaceContent"; + } + QJsonObject messageProperties = { + { "status", details }, + { "content_set_url", url } + }; + UserActivityLogger::getInstance().logAction("replace_domain_content", messageProperties); + QObject::disconnect(dig, &ModalDialogListener::response, this, nullptr); + }); + } else { + methodDetails = "ContentSetDidNotOriginateFromMarketplace"; + QJsonObject messageProperties = { + { "status", methodDetails }, + { "content_set_url", url } + }; + UserActivityLogger::getInstance().logAction("replace_domain_content", messageProperties); + } + } else { + methodDetails = "UserDoesNotHavePermissionToReplaceContent"; + static const QString warningMessage = simpleWordWrap("The domain owner must enable 'Replace Content' " + "permissions for you in this domain's server settings before you can continue.", MAX_CHARACTERS_PER_LINE); + OffscreenUi::asyncWarning("You do not have permissions to replace domain content", warningMessage, + QMessageBox::Ok, QMessageBox::Ok); + + QJsonObject messageProperties = { + { "status", methodDetails }, + { "content_set_url", url } + }; + UserActivityLogger::getInstance().logAction("replace_domain_content", messageProperties); + } + return true; +} + +void Application::displayAvatarAttachmentWarning(const QString& message) const { + auto avatarAttachmentWarningTitle = tr("Avatar Attachment Failure"); + OffscreenUi::asyncWarning(avatarAttachmentWarningTitle, message); +} + +void Application::showDialog(const QUrl& widgetUrl, const QUrl& tabletUrl, const QString& name) const { + auto tablet = DependencyManager::get()->getTablet(SYSTEM_TABLET); + auto hmd = DependencyManager::get(); + bool onTablet = false; + + if (!tablet->getToolbarMode()) { + onTablet = tablet->pushOntoStack(tabletUrl); + if (onTablet) { + toggleTabletUI(true); + } + } else { +#if !defined(DISABLE_QML) + getOffscreenUI()->show(widgetUrl, name); +#endif + } +} + +void Application::showScriptLogs() { + QUrl defaultScriptsLoc = PathUtils::defaultScriptsLocation(); + defaultScriptsLoc.setPath(defaultScriptsLoc.path() + "developer/debugging/debugWindow.js"); + DependencyManager::get()->loadScript(defaultScriptsLoc.toString()); +} + +void Application::showAssetServerWidget(QString filePath) { + if (!DependencyManager::get()->getThisNodeCanWriteAssets() || getLoginDialogPoppedUp()) { + return; + } + static const QUrl url { "hifi/AssetServer.qml" }; + + auto startUpload = [=](QQmlContext* context, QObject* newObject){ + if (!filePath.isEmpty()) { + emit uploadRequest(filePath); + } + }; + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); + auto hmd = DependencyManager::get(); + if (tablet->getToolbarMode()) { + getOffscreenUI()->show(url, "AssetServer", startUpload); + } else { + if (!hmd->getShouldShowTablet() && !isHMDMode()) { + getOffscreenUI()->show(url, "AssetServer", startUpload); + } else { + static const QUrl url("hifi/dialogs/TabletAssetServer.qml"); + if (!tablet->isPathLoaded(url)) { + tablet->pushOntoStack(url); + } + } + } + + startUpload(nullptr, nullptr); +} + +void Application::addAssetToWorldFromURL(QString url) { + + QString filename; + if (url.contains("filename")) { + filename = url.section("filename=", 1, 1); // Filename is in "?filename=" parameter at end of URL. + } + if (url.contains("poly.google.com/downloads")) { + filename = url.section('/', -1); + if (url.contains("noDownload")) { + filename.remove(".zip?noDownload=false"); + } else { + filename.remove(".zip"); + } + + } + + if (!DependencyManager::get()->getThisNodeCanWriteAssets()) { + QString errorInfo = "You do not have permissions to write to the Asset Server."; + qWarning(interfaceapp) << "Error downloading model: " + errorInfo; + addAssetToWorldError(filename, errorInfo); + return; + } + + addAssetToWorldInfo(filename, "Downloading model file " + filename + "."); + + auto request = DependencyManager::get()->createResourceRequest( + nullptr, QUrl(url), true, -1, "Application::addAssetToWorldFromURL"); + connect(request, &ResourceRequest::finished, this, &Application::addAssetToWorldFromURLRequestFinished); + request->send(); +} + +void Application::addAssetToWorldFromURLRequestFinished() { + auto request = qobject_cast(sender()); + auto url = request->getUrl().toString(); + auto result = request->getResult(); + + QString filename; + bool isBlocks = false; + + if (url.contains("filename")) { + filename = url.section("filename=", 1, 1); // Filename is in "?filename=" parameter at end of URL. + } + if (url.contains("poly.google.com/downloads")) { + filename = url.section('/', -1); + if (url.contains("noDownload")) { + filename.remove(".zip?noDownload=false"); + } else { + filename.remove(".zip"); + } + isBlocks = true; + } + + if (result == ResourceRequest::Success) { + QTemporaryDir temporaryDir; + temporaryDir.setAutoRemove(false); + if (temporaryDir.isValid()) { + QString temporaryDirPath = temporaryDir.path(); + QString downloadPath = temporaryDirPath + "/" + filename; + + QFile tempFile(downloadPath); + if (tempFile.open(QIODevice::WriteOnly)) { + tempFile.write(request->getData()); + addAssetToWorldInfoClear(filename); // Remove message from list; next one added will have a different key. + tempFile.close(); + qApp->getFileDownloadInterface()->runUnzip(downloadPath, url, true, false, isBlocks); + } else { + QString errorInfo = "Couldn't open temporary file for download"; + qWarning(interfaceapp) << errorInfo; + addAssetToWorldError(filename, errorInfo); + } + } else { + QString errorInfo = "Couldn't create temporary directory for download"; + qWarning(interfaceapp) << errorInfo; + addAssetToWorldError(filename, errorInfo); + } + } else { + qWarning(interfaceapp) << "Error downloading" << url << ":" << request->getResultString(); + addAssetToWorldError(filename, "Error downloading " + filename + " : " + request->getResultString()); + } + + request->deleteLater(); +} + + +QString filenameFromPath(QString filePath) { + return filePath.right(filePath.length() - filePath.lastIndexOf("/") - 1); +} + +void Application::addAssetToWorldUnzipFailure(QString filePath) { + QString filename = filenameFromPath(QUrl(filePath).toLocalFile()); + qWarning(interfaceapp) << "Couldn't unzip file" << filePath; + addAssetToWorldError(filename, "Couldn't unzip file " + filename + "."); +} + +void Application::addAssetToWorld(QString path, QString zipFile, bool isZip, bool isBlocks) { + // Automatically upload and add asset to world as an alternative manual process initiated by showAssetServerWidget(). + QString mapping; + QString filename = filenameFromPath(path); + if (isZip || isBlocks) { + QString assetName = zipFile.section("/", -1).remove(QRegExp("[.]zip(.*)$")); + QString assetFolder = path.section("model_repo/", -1); + mapping = "/" + assetName + "/" + assetFolder; + } else { + mapping = "/" + filename; + } + + // Test repeated because possibly different code paths. + if (!DependencyManager::get()->getThisNodeCanWriteAssets()) { + QString errorInfo = "You do not have permissions to write to the Asset Server."; + qWarning(interfaceapp) << "Error downloading model: " + errorInfo; + addAssetToWorldError(filename, errorInfo); + return; + } + + addAssetToWorldInfo(filename, "Adding " + mapping.mid(1) + " to the Asset Server."); + + addAssetToWorldWithNewMapping(path, mapping, 0, isZip, isBlocks); +} + +void Application::addAssetToWorldWithNewMapping(QString filePath, QString mapping, int copy, bool isZip, bool isBlocks) { + auto request = DependencyManager::get()->createGetMappingRequest(mapping); + + QObject::connect(request, &GetMappingRequest::finished, this, [=](GetMappingRequest* request) mutable { + const int MAX_COPY_COUNT = 100; // Limit number of duplicate assets; recursion guard. + auto result = request->getError(); + if (result == GetMappingRequest::NotFound) { + addAssetToWorldUpload(filePath, mapping, isZip, isBlocks); + } else if (result != GetMappingRequest::NoError) { + QString errorInfo = "Could not map asset name: " + + mapping.left(mapping.length() - QString::number(copy).length() - 1); + qWarning(interfaceapp) << "Error downloading model: " + errorInfo; + addAssetToWorldError(filenameFromPath(filePath), errorInfo); + } else if (copy < MAX_COPY_COUNT - 1) { + if (copy > 0) { + mapping = mapping.remove(mapping.lastIndexOf("-"), QString::number(copy).length() + 1); + } + copy++; + mapping = mapping.insert(mapping.lastIndexOf("."), "-" + QString::number(copy)); + addAssetToWorldWithNewMapping(filePath, mapping, copy, isZip, isBlocks); + } else { + QString errorInfo = "Too many copies of asset name: " + + mapping.left(mapping.length() - QString::number(copy).length() - 1); + qWarning(interfaceapp) << "Error downloading model: " + errorInfo; + addAssetToWorldError(filenameFromPath(filePath), errorInfo); + } + request->deleteLater(); + }); + + request->start(); +} + +void Application::addAssetToWorldUpload(QString filePath, QString mapping, bool isZip, bool isBlocks) { + qInfo(interfaceapp) << "Uploading" << filePath << "to Asset Server as" << mapping; + auto upload = DependencyManager::get()->createUpload(filePath); + QObject::connect(upload, &AssetUpload::finished, this, [=](AssetUpload* upload, const QString& hash) mutable { + if (upload->getError() != AssetUpload::NoError) { + QString errorInfo = "Could not upload model to the Asset Server."; + qWarning(interfaceapp) << "Error downloading model: " + errorInfo; + addAssetToWorldError(filenameFromPath(filePath), errorInfo); + } else { + addAssetToWorldSetMapping(filePath, mapping, hash, isZip, isBlocks); + } + + // Remove temporary directory created by Clara.io market place download. + int index = filePath.lastIndexOf("/model_repo/"); + if (index > 0) { + QString tempDir = filePath.left(index); + qCDebug(interfaceapp) << "Removing temporary directory at: " + tempDir; + QDir(tempDir).removeRecursively(); + } + + upload->deleteLater(); + }); + + upload->start(); +} + +void Application::addAssetToWorldSetMapping(QString filePath, QString mapping, QString hash, bool isZip, bool isBlocks) { + auto request = DependencyManager::get()->createSetMappingRequest(mapping, hash); + connect(request, &SetMappingRequest::finished, this, [=](SetMappingRequest* request) mutable { + if (request->getError() != SetMappingRequest::NoError) { + QString errorInfo = "Could not set asset mapping."; + qWarning(interfaceapp) << "Error downloading model: " + errorInfo; + addAssetToWorldError(filenameFromPath(filePath), errorInfo); + } else { + // to prevent files that aren't models or texture files from being loaded into world automatically + if ((filePath.toLower().endsWith(OBJ_EXTENSION) || filePath.toLower().endsWith(FBX_EXTENSION)) || + ((filePath.toLower().endsWith(JPG_EXTENSION) || filePath.toLower().endsWith(PNG_EXTENSION)) && + ((!isBlocks) && (!isZip)))) { + addAssetToWorldAddEntity(filePath, mapping); + } else { + qCDebug(interfaceapp) << "Zipped contents are not supported entity files"; + addAssetToWorldInfoDone(filenameFromPath(filePath)); + } + } + request->deleteLater(); + }); + + request->start(); +} + +void Application::addAssetToWorldAddEntity(QString filePath, QString mapping) { + EntityItemProperties properties; + properties.setName(mapping.right(mapping.length() - 1)); + if (filePath.toLower().endsWith(PNG_EXTENSION) || filePath.toLower().endsWith(JPG_EXTENSION)) { + properties.setType(EntityTypes::Image); + properties.setImageURL(QString("atp:" + mapping)); + properties.setKeepAspectRatio(false); + } else { + properties.setType(EntityTypes::Model); + properties.setModelURL("atp:" + mapping); + properties.setShapeType(SHAPE_TYPE_SIMPLE_COMPOUND); + } + properties.setCollisionless(true); // Temporarily set so that doesn't collide with avatar. + properties.setVisible(false); // Temporarily set so that don't see at large unresized dimensions. + bool grabbable = (Menu::getInstance()->isOptionChecked(MenuOption::CreateEntitiesGrabbable)); + properties.setUserData(grabbable ? GRABBABLE_USER_DATA : NOT_GRABBABLE_USER_DATA); + glm::vec3 positionOffset = getMyAvatar()->getWorldOrientation() * (getMyAvatar()->getSensorToWorldScale() * glm::vec3(0.0f, 0.0f, -2.0f)); + properties.setPosition(getMyAvatar()->getWorldPosition() + positionOffset); + properties.setRotation(getMyAvatar()->getWorldOrientation()); + properties.setGravity(glm::vec3(0.0f, 0.0f, 0.0f)); + auto entityID = DependencyManager::get()->addEntity(properties); + + // Note: Model dimensions are not available here; model is scaled per FBX mesh in RenderableModelEntityItem::update() later + // on. But FBX dimensions may be in cm, so we monitor for the dimension change and rescale again if warranted. + + if (entityID == QUuid()) { + QString errorInfo = "Could not add model " + mapping + " to world."; + qWarning(interfaceapp) << "Could not add model to world: " + errorInfo; + addAssetToWorldError(filenameFromPath(filePath), errorInfo); + } else { + // Monitor when asset is rendered in world so that can resize if necessary. + _addAssetToWorldResizeList.insert(entityID, 0); // List value is count of checks performed. + if (!_addAssetToWorldResizeTimer.isActive()) { + _addAssetToWorldResizeTimer.start(); + } + + // Close progress message box. + addAssetToWorldInfoDone(filenameFromPath(filePath)); + } +} + +void Application::addAssetToWorldCheckModelSize() { + if (_addAssetToWorldResizeList.size() == 0) { + return; + } + + auto item = _addAssetToWorldResizeList.begin(); + while (item != _addAssetToWorldResizeList.end()) { + auto entityID = item.key(); + + EntityPropertyFlags propertyFlags; + propertyFlags += PROP_NAME; + propertyFlags += PROP_DIMENSIONS; + auto entityScriptingInterface = DependencyManager::get(); + auto properties = entityScriptingInterface->getEntityProperties(entityID, propertyFlags); + auto name = properties.getName(); + auto dimensions = properties.getDimensions(); + + bool doResize = false; + + const glm::vec3 DEFAULT_DIMENSIONS = glm::vec3(0.1f, 0.1f, 0.1f); + if (dimensions != DEFAULT_DIMENSIONS) { + + // Scale model so that its maximum is exactly specific size. + const float MAXIMUM_DIMENSION = getMyAvatar()->getSensorToWorldScale(); + auto previousDimensions = dimensions; + auto scale = std::min(MAXIMUM_DIMENSION / dimensions.x, std::min(MAXIMUM_DIMENSION / dimensions.y, + MAXIMUM_DIMENSION / dimensions.z)); + dimensions *= scale; + qInfo(interfaceapp) << "Model" << name << "auto-resized from" << previousDimensions << " to " << dimensions; + doResize = true; + + item = _addAssetToWorldResizeList.erase(item); // Finished with this entity; advance to next. + } else { + // Increment count of checks done. + _addAssetToWorldResizeList[entityID]++; + + const int CHECK_MODEL_SIZE_MAX_CHECKS = 300; + if (_addAssetToWorldResizeList[entityID] > CHECK_MODEL_SIZE_MAX_CHECKS) { + // Have done enough checks; model was either the default size or something's gone wrong. + + // Rescale all dimensions. + const glm::vec3 UNIT_DIMENSIONS = glm::vec3(1.0f, 1.0f, 1.0f); + dimensions = UNIT_DIMENSIONS; + qInfo(interfaceapp) << "Model" << name << "auto-resize timed out; resized to " << dimensions; + doResize = true; + + item = _addAssetToWorldResizeList.erase(item); // Finished with this entity; advance to next. + } else { + // No action on this entity; advance to next. + ++item; + } + } + + if (doResize) { + EntityItemProperties properties; + properties.setDimensions(dimensions); + properties.setVisible(true); + if (!name.toLower().endsWith(PNG_EXTENSION) && !name.toLower().endsWith(JPG_EXTENSION)) { + properties.setCollisionless(false); + } + bool grabbable = (Menu::getInstance()->isOptionChecked(MenuOption::CreateEntitiesGrabbable)); + properties.setUserData(grabbable ? GRABBABLE_USER_DATA : NOT_GRABBABLE_USER_DATA); + properties.setLastEdited(usecTimestampNow()); + entityScriptingInterface->editEntity(entityID, properties); + } + } + + // Stop timer if nothing in list to check. + if (_addAssetToWorldResizeList.size() == 0) { + _addAssetToWorldResizeTimer.stop(); + } +} + + +void Application::addAssetToWorldInfo(QString modelName, QString infoText) { + // Displays the most recent info message, subject to being overridden by error messages. + + if (_aboutToQuit) { + return; + } + + /* + Cancel info timer if running. + If list has an entry for modelName, delete it (just one). + Append modelName, infoText to list. + Display infoText in message box unless an error is being displayed (i.e., error timer is running). + Show message box if not already visible. + */ + + _addAssetToWorldInfoTimer.stop(); + + addAssetToWorldInfoClear(modelName); + + _addAssetToWorldInfoKeys.append(modelName); + _addAssetToWorldInfoMessages.append(infoText); + + if (!_addAssetToWorldErrorTimer.isActive()) { + if (!_addAssetToWorldMessageBox) { + _addAssetToWorldMessageBox = getOffscreenUI()->createMessageBox(OffscreenUi::ICON_INFORMATION, + "Downloading Model", "", QMessageBox::NoButton, QMessageBox::NoButton); + connect(_addAssetToWorldMessageBox, SIGNAL(destroyed()), this, SLOT(onAssetToWorldMessageBoxClosed())); + } + + _addAssetToWorldMessageBox->setProperty("text", "\n" + infoText); + _addAssetToWorldMessageBox->setVisible(true); + } +} + +void Application::addAssetToWorldInfoClear(QString modelName) { + // Clears modelName entry from message list without affecting message currently displayed. + + if (_aboutToQuit) { + return; + } + + /* + Delete entry for modelName from list. + */ + + auto index = _addAssetToWorldInfoKeys.indexOf(modelName); + if (index > -1) { + _addAssetToWorldInfoKeys.removeAt(index); + _addAssetToWorldInfoMessages.removeAt(index); + } +} + +void Application::addAssetToWorldInfoDone(QString modelName) { + // Continues to display this message if the latest for a few seconds, then deletes it and displays the next latest. + + if (_aboutToQuit) { + return; + } + + /* + Delete entry for modelName from list. + (Re)start the info timer to update message box. ... onAddAssetToWorldInfoTimeout() + */ + + addAssetToWorldInfoClear(modelName); + _addAssetToWorldInfoTimer.start(); +} + +void Application::addAssetToWorldInfoTimeout() { + if (_aboutToQuit) { + return; + } + + /* + If list not empty, display last message in list (may already be displayed ) unless an error is being displayed. + If list empty, close the message box unless an error is being displayed. + */ + + if (!_addAssetToWorldErrorTimer.isActive() && _addAssetToWorldMessageBox) { + if (_addAssetToWorldInfoKeys.length() > 0) { + _addAssetToWorldMessageBox->setProperty("text", "\n" + _addAssetToWorldInfoMessages.last()); + } else { + disconnect(_addAssetToWorldMessageBox); + _addAssetToWorldMessageBox->setVisible(false); + _addAssetToWorldMessageBox->deleteLater(); + _addAssetToWorldMessageBox = nullptr; + } + } +} + +void Application::addAssetToWorldError(QString modelName, QString errorText) { + // Displays the most recent error message for a few seconds. + + if (_aboutToQuit) { + return; + } + + /* + If list has an entry for modelName, delete it. + Display errorText in message box. + Show message box if not already visible. + (Re)start error timer. ... onAddAssetToWorldErrorTimeout() + */ + + addAssetToWorldInfoClear(modelName); + + if (!_addAssetToWorldMessageBox) { + _addAssetToWorldMessageBox = getOffscreenUI()->createMessageBox(OffscreenUi::ICON_INFORMATION, + "Downloading Model", "", QMessageBox::NoButton, QMessageBox::NoButton); + connect(_addAssetToWorldMessageBox, SIGNAL(destroyed()), this, SLOT(onAssetToWorldMessageBoxClosed())); + } + + _addAssetToWorldMessageBox->setProperty("text", "\n" + errorText); + _addAssetToWorldMessageBox->setVisible(true); + + _addAssetToWorldErrorTimer.start(); +} + +void Application::addAssetToWorldErrorTimeout() { + if (_aboutToQuit) { + return; + } + + /* + If list is not empty, display message from last entry. + If list is empty, close the message box. + */ + + if (_addAssetToWorldMessageBox) { + if (_addAssetToWorldInfoKeys.length() > 0) { + _addAssetToWorldMessageBox->setProperty("text", "\n" + _addAssetToWorldInfoMessages.last()); + } else { + disconnect(_addAssetToWorldMessageBox); + _addAssetToWorldMessageBox->setVisible(false); + _addAssetToWorldMessageBox->deleteLater(); + _addAssetToWorldMessageBox = nullptr; + } + } +} + + +void Application::addAssetToWorldMessageClose() { + // Clear messages, e.g., if Interface is being closed or domain changes. + + /* + Call if user manually closes message box. + Call if domain changes. + Call if application is shutting down. + + Stop timers. + Close the message box if open. + Clear lists. + */ + + _addAssetToWorldInfoTimer.stop(); + _addAssetToWorldErrorTimer.stop(); + + if (_addAssetToWorldMessageBox) { + disconnect(_addAssetToWorldMessageBox); + _addAssetToWorldMessageBox->setVisible(false); + _addAssetToWorldMessageBox->deleteLater(); + _addAssetToWorldMessageBox = nullptr; + } + + _addAssetToWorldInfoKeys.clear(); + _addAssetToWorldInfoMessages.clear(); +} + +void Application::onAssetToWorldMessageBoxClosed() { + if (_addAssetToWorldMessageBox) { + // User manually closed message box; perhaps because it has become stuck, so reset all messages. + qInfo(interfaceapp) << "User manually closed download status message box"; + disconnect(_addAssetToWorldMessageBox); + _addAssetToWorldMessageBox = nullptr; + addAssetToWorldMessageClose(); + } +} + + +void Application::handleUnzip(QString zipFile, QStringList unzipFile, bool autoAdd, bool isZip, bool isBlocks) { + if (autoAdd) { + if (!unzipFile.isEmpty()) { + for (int i = 0; i < unzipFile.length(); i++) { + if (QFileInfo(unzipFile.at(i)).isFile()) { + qCDebug(interfaceapp) << "Preparing file for asset server: " << unzipFile.at(i); + addAssetToWorld(unzipFile.at(i), zipFile, isZip, isBlocks); + } + } + } else { + addAssetToWorldUnzipFailure(zipFile); + } + } else { + showAssetServerWidget(unzipFile.first()); + } +} + +void Application::packageModel() { + ModelPackager::package(); +} + +void Application::openUrl(const QUrl& url) const { + if (!url.isEmpty()) { + if (url.scheme() == URL_SCHEME_HIFI) { + DependencyManager::get()->handleLookupString(url.toString()); + } else if (url.scheme() == URL_SCHEME_HIFIAPP) { + DependencyManager::get()->openSystemApp(url.path()); + } else { + // address manager did not handle - ask QDesktopServices to handle + QDesktopServices::openUrl(url); + } + } +} + +void Application::loadDialog() { + ModalDialogListener* dlg = OffscreenUi::getOpenFileNameAsync(_glWidget, tr("Open Script"), + getPreviousScriptLocation(), + tr("JavaScript Files (*.js)")); + connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { + disconnect(dlg, &ModalDialogListener::response, this, nullptr); + const QString& response = answer.toString(); + if (!response.isEmpty() && QFile(response).exists()) { + setPreviousScriptLocation(QFileInfo(response).absolutePath()); + DependencyManager::get()->loadScript(response, true, false, false, true); // Don't load from cache + } + }); +} + +QString Application::getPreviousScriptLocation() { + QString result = _previousScriptLocation.get(); + return result; +} + +void Application::setPreviousScriptLocation(const QString& location) { + _previousScriptLocation.set(location); +} + +void Application::loadScriptURLDialog() const { + ModalDialogListener* dlg = OffscreenUi::getTextAsync(OffscreenUi::ICON_NONE, "Open and Run Script", "Script URL"); + connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) { + disconnect(dlg, &ModalDialogListener::response, this, nullptr); + const QString& newScript = response.toString(); + if (QUrl(newScript).scheme() == "atp") { + OffscreenUi::asyncWarning("Error Loading Script", "Cannot load client script over ATP"); + } else if (!newScript.isEmpty()) { + DependencyManager::get()->loadScript(newScript.trimmed()); + } + }); +} + +SharedSoundPointer Application::getSampleSound() const { + return _sampleSound; +} + +void Application::loadLODToolsDialog() { + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); + if (tablet->getToolbarMode() || (!tablet->getTabletRoot() && !isHMDMode())) { + auto dialogsManager = DependencyManager::get(); + dialogsManager->lodTools(); + } else { + tablet->pushOntoStack("hifi/dialogs/TabletLODTools.qml"); + } +} + +void Application::loadEntityStatisticsDialog() { + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); + if (tablet->getToolbarMode() || (!tablet->getTabletRoot() && !isHMDMode())) { + auto dialogsManager = DependencyManager::get(); + dialogsManager->octreeStatsDetails(); + } else { + tablet->pushOntoStack("hifi/dialogs/TabletEntityStatistics.qml"); + } +} + +void Application::loadDomainConnectionDialog() { + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); + if (tablet->getToolbarMode() || (!tablet->getTabletRoot() && !isHMDMode())) { + auto dialogsManager = DependencyManager::get(); + dialogsManager->showDomainConnectionDialog(); + } else { + tablet->pushOntoStack("hifi/dialogs/TabletDCDialog.qml"); + } +} + +void Application::toggleLogDialog() { +#ifndef ANDROID_APP_QUEST_INTERFACE + if (getLoginDialogPoppedUp()) { + return; + } + if (! _logDialog) { + + bool keepOnTop =_keepLogWindowOnTop.get(); +#ifdef Q_OS_WIN + _logDialog = new LogDialog(keepOnTop ? qApp->getWindow() : nullptr, getLogger()); +#elif !defined(Q_OS_ANDROID) + _logDialog = new LogDialog(nullptr, getLogger()); + + if (keepOnTop) { + Qt::WindowFlags flags = _logDialog->windowFlags() | Qt::Tool; + _logDialog->setWindowFlags(flags); + } +#endif + } + + if (_logDialog->isVisible()) { + _logDialog->hide(); + } else { + _logDialog->show(); + } +#endif +} + + void Application::recreateLogWindow(int keepOnTop) { + _keepLogWindowOnTop.set(keepOnTop != 0); + if (_logDialog) { + bool toggle = _logDialog->isVisible(); + _logDialog->close(); + _logDialog = nullptr; + + if (toggle) { + toggleLogDialog(); + } + } + } + +void Application::toggleEntityScriptServerLogDialog() { + if (! _entityScriptServerLogDialog) { + _entityScriptServerLogDialog = new EntityScriptServerLogDialog(nullptr); + } + + if (_entityScriptServerLogDialog->isVisible()) { + _entityScriptServerLogDialog->hide(); + } else { + _entityScriptServerLogDialog->show(); + } +} + +void Application::loadAddAvatarBookmarkDialog() const { + auto avatarBookmarks = DependencyManager::get(); +} + +void Application::loadAvatarBrowser() const { + auto tablet = dynamic_cast(DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system")); + // construct the url to the marketplace item + QString url = NetworkingConstants::METAVERSE_SERVER_URL().toString() + "/marketplace?category=avatars"; + + QString MARKETPLACES_INJECT_SCRIPT_PATH = "file:///" + qApp->applicationDirPath() + "/scripts/system/html/js/marketplacesInject.js"; + tablet->gotoWebScreen(url, MARKETPLACES_INJECT_SCRIPT_PATH); + DependencyManager::get()->openTablet(); +} + +void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRatio, const QString& filename) { + postLambdaEvent([notify, includeAnimated, aspectRatio, filename, this] { + // Get a screenshot and save it + QString path = DependencyManager::get()->saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio), filename, + TestScriptingInterface::getInstance()->getTestResultsLocation()); + + // If we're not doing an animated snapshot as well... + if (!includeAnimated) { + if (!path.isEmpty()) { + // Tell the dependency manager that the capture of the still snapshot has taken place. + emit DependencyManager::get()->stillSnapshotTaken(path, notify); + } + } else if (!SnapshotAnimated::isAlreadyTakingSnapshotAnimated()) { + // Get an animated GIF snapshot and save it + SnapshotAnimated::saveSnapshotAnimated(path, aspectRatio, qApp, DependencyManager::get()); + } + }); +} + +void Application::takeSecondaryCameraSnapshot(const bool& notify, const QString& filename) { + postLambdaEvent([notify, filename, this] { + QString snapshotPath = DependencyManager::get()->saveSnapshot(getActiveDisplayPlugin()->getSecondaryCameraScreenshot(), filename, + TestScriptingInterface::getInstance()->getTestResultsLocation()); + + emit DependencyManager::get()->stillSnapshotTaken(snapshotPath, notify); + }); +} + +void Application::takeSecondaryCamera360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat, const bool& notify, const QString& filename) { + postLambdaEvent([notify, filename, cubemapOutputFormat, cameraPosition] { + DependencyManager::get()->save360Snapshot(cameraPosition, cubemapOutputFormat, notify, filename); + }); +} + +void Application::shareSnapshot(const QString& path, const QUrl& href) { + postLambdaEvent([path, href] { + // not much to do here, everything is done in snapshot code... + DependencyManager::get()->uploadSnapshot(path, href); + }); +} + +float Application::getRenderResolutionScale() const { + auto menu = Menu::getInstance(); + if (!menu) { + return 1.0f; + } + if (menu->isOptionChecked(MenuOption::RenderResolutionOne)) { + return 1.0f; + } else if (menu->isOptionChecked(MenuOption::RenderResolutionTwoThird)) { + return 0.666f; + } else if (menu->isOptionChecked(MenuOption::RenderResolutionHalf)) { + return 0.5f; + } else if (menu->isOptionChecked(MenuOption::RenderResolutionThird)) { + return 0.333f; + } else if (menu->isOptionChecked(MenuOption::RenderResolutionQuarter)) { + return 0.25f; + } else { + return 1.0f; + } +} + +void Application::notifyPacketVersionMismatch() { + if (!_notifiedPacketVersionMismatchThisDomain && !isInterstitialMode()) { + _notifiedPacketVersionMismatchThisDomain = true; + + QString message = "The location you are visiting is running an incompatible server version.\n"; + message += "Content may not display properly."; + + OffscreenUi::asyncWarning("", message); + } +} + +void Application::checkSkeleton() const { + if (getMyAvatar()->getSkeletonModel()->isActive() && !getMyAvatar()->getSkeletonModel()->hasSkeleton()) { + qCDebug(interfaceapp) << "MyAvatar model has no skeleton"; + + QString message = "Your selected avatar body has no skeleton.\n\nThe default body will be loaded..."; + OffscreenUi::asyncWarning("", message); + + getMyAvatar()->useFullAvatarURL(AvatarData::defaultFullAvatarModelUrl(), DEFAULT_FULL_AVATAR_MODEL_NAME); + } else { + _physicsEngine->setCharacterController(getMyAvatar()->getCharacterController()); + } +} + +void Application::activeChanged(Qt::ApplicationState state) { + switch (state) { + case Qt::ApplicationActive: + _isForeground = true; + break; + + case Qt::ApplicationSuspended: + case Qt::ApplicationHidden: + case Qt::ApplicationInactive: + default: + _isForeground = false; + break; + } +} + +void Application::windowMinimizedChanged(bool minimized) { + // initialize the _minimizedWindowTimer + static std::once_flag once; + std::call_once(once, [&] { + connect(&_minimizedWindowTimer, &QTimer::timeout, this, [] { + QCoreApplication::postEvent(QCoreApplication::instance(), new QEvent(static_cast(Idle)), Qt::HighEventPriority); + }); + }); + + // avoid rendering to the display plugin but continue posting Idle events, + // so that physics continues to simulate and the deadlock watchdog knows we're alive + if (!minimized && !getActiveDisplayPlugin()->isActive()) { + _minimizedWindowTimer.stop(); + getActiveDisplayPlugin()->activate(); + } else if (minimized && getActiveDisplayPlugin()->isActive()) { + getActiveDisplayPlugin()->deactivate(); + _minimizedWindowTimer.start(THROTTLED_SIM_FRAME_PERIOD_MS); + } +} + +void Application::postLambdaEvent(const std::function& f) { + if (this->thread() == QThread::currentThread()) { + f(); + } else { + QCoreApplication::postEvent(this, new LambdaEvent(f)); + } +} + +void Application::sendLambdaEvent(const std::function& f) { + if (this->thread() == QThread::currentThread()) { + f(); + } else { + LambdaEvent event(f); + QCoreApplication::sendEvent(this, &event); + } +} + +void Application::initPlugins(const QStringList& arguments) { + QCommandLineOption display("display", "Preferred displays", "displays"); + QCommandLineOption disableDisplays("disable-displays", "Displays to disable", "displays"); + QCommandLineOption disableInputs("disable-inputs", "Inputs to disable", "inputs"); + + QCommandLineParser parser; + parser.addOption(display); + parser.addOption(disableDisplays); + parser.addOption(disableInputs); + parser.parse(arguments); + + if (parser.isSet(display)) { + auto preferredDisplays = parser.value(display).split(',', QString::SkipEmptyParts); + qInfo() << "Setting prefered display plugins:" << preferredDisplays; + PluginManager::getInstance()->setPreferredDisplayPlugins(preferredDisplays); + } + + if (parser.isSet(disableDisplays)) { + auto disabledDisplays = parser.value(disableDisplays).split(',', QString::SkipEmptyParts); + qInfo() << "Disabling following display plugins:" << disabledDisplays; + PluginManager::getInstance()->disableDisplays(disabledDisplays); + } + + if (parser.isSet(disableInputs)) { + auto disabledInputs = parser.value(disableInputs).split(',', QString::SkipEmptyParts); + qInfo() << "Disabling following input plugins:" << disabledInputs; + PluginManager::getInstance()->disableInputs(disabledInputs); + } +} + +void Application::shutdownPlugins() { +} + +glm::uvec2 Application::getCanvasSize() const { + return glm::uvec2(_glWidget->width(), _glWidget->height()); +} + +QRect Application::getRenderingGeometry() const { + auto geometry = _glWidget->geometry(); + auto topLeft = geometry.topLeft(); + auto topLeftScreen = _glWidget->mapToGlobal(topLeft); + geometry.moveTopLeft(topLeftScreen); + return geometry; +} + +glm::uvec2 Application::getUiSize() const { + static const uint MIN_SIZE = 1; + glm::uvec2 result(MIN_SIZE); + if (_displayPlugin) { + result = getActiveDisplayPlugin()->getRecommendedUiSize(); + } + return result; +} + +QRect Application::getRecommendedHUDRect() const { + auto uiSize = getUiSize(); + QRect result(0, 0, uiSize.x, uiSize.y); + if (_displayPlugin) { + result = getActiveDisplayPlugin()->getRecommendedHUDRect(); + } + return result; +} + +glm::vec2 Application::getDeviceSize() const { + static const int MIN_SIZE = 1; + glm::vec2 result(MIN_SIZE); + if (_displayPlugin) { + result = getActiveDisplayPlugin()->getRecommendedRenderSize(); + } + return result; +} + +bool Application::isThrottleRendering() const { + if (_displayPlugin) { + return getActiveDisplayPlugin()->isThrottled(); + } + return false; +} + +bool Application::hasFocus() const { + bool result = (QApplication::activeWindow() != nullptr); +#if defined(Q_OS_WIN) + // On Windows, QWidget::activateWindow() - as called in setFocus() - makes the application's taskbar icon flash but doesn't + // take user focus away from their current window. So also check whether the application is the user's current foreground + // window. + result = result && (HWND)QApplication::activeWindow()->winId() == GetForegroundWindow(); +#endif + return result; +} + +void Application::setFocus() { + // Note: Windows doesn't allow a user focus to be taken away from another application. Instead, it changes the color of and + // flashes the taskbar icon. + auto window = qApp->getWindow(); + window->activateWindow(); +} + +void Application::raise() { + auto windowState = qApp->getWindow()->windowState(); + if (windowState & Qt::WindowMinimized) { + if (windowState & Qt::WindowMaximized) { + qApp->getWindow()->showMaximized(); + } else if (windowState & Qt::WindowFullScreen) { + qApp->getWindow()->showFullScreen(); + } else { + qApp->getWindow()->showNormal(); + } + } + qApp->getWindow()->raise(); +} + +void Application::setMaxOctreePacketsPerSecond(int maxOctreePPS) { + if (maxOctreePPS != _maxOctreePPS) { + _maxOctreePPS = maxOctreePPS; + maxOctreePacketsPerSecond.set(_maxOctreePPS); + } +} + +int Application::getMaxOctreePacketsPerSecond() const { + return _maxOctreePPS; +} + +qreal Application::getDevicePixelRatio() { + return (_window && _window->windowHandle()) ? _window->windowHandle()->devicePixelRatio() : 1.0; +} + +DisplayPluginPointer Application::getActiveDisplayPlugin() const { + if (QThread::currentThread() != thread()) { + std::unique_lock lock(_displayPluginLock); + return _displayPlugin; + } + + if (!_aboutToQuit && !_displayPlugin) { + const_cast(this)->updateDisplayMode(); + Q_ASSERT(_displayPlugin); + } + return _displayPlugin; +} + + +#if !defined(DISABLE_QML) +static const char* EXCLUSION_GROUP_KEY = "exclusionGroup"; + +static void addDisplayPluginToMenu(const DisplayPluginPointer& displayPlugin, int index, bool active) { + auto menu = Menu::getInstance(); + QString name = displayPlugin->getName(); + auto grouping = displayPlugin->getGrouping(); + QString groupingMenu { "" }; + Q_ASSERT(!menu->menuItemExists(MenuOption::OutputMenu, name)); + + // assign the meny grouping based on plugin grouping + switch (grouping) { + case Plugin::ADVANCED: + groupingMenu = "Advanced"; + break; + case Plugin::DEVELOPER: + groupingMenu = "Developer"; + break; + default: + groupingMenu = "Standard"; + break; + } + + static QActionGroup* displayPluginGroup = nullptr; + if (!displayPluginGroup) { + displayPluginGroup = new QActionGroup(menu); + displayPluginGroup->setExclusive(true); + } + auto parent = menu->getMenu(MenuOption::OutputMenu); + auto action = menu->addActionToQMenuAndActionHash(parent, + name, QKeySequence(Qt::CTRL + (Qt::Key_0 + index)), qApp, + SLOT(updateDisplayMode()), + QAction::NoRole, Menu::UNSPECIFIED_POSITION, groupingMenu); + + action->setCheckable(true); + action->setChecked(active); + displayPluginGroup->addAction(action); + + action->setProperty(EXCLUSION_GROUP_KEY, QVariant::fromValue(displayPluginGroup)); + Q_ASSERT(menu->menuItemExists(MenuOption::OutputMenu, name)); +} +#endif + +void Application::updateDisplayMode() { + // Unsafe to call this method from anything but the main thread + if (QThread::currentThread() != thread()) { + qFatal("Attempted to switch display plugins from a non-main thread"); + } + + // Once time initialization code that depends on the UI being available + auto displayPlugins = getDisplayPlugins(); + + // Default to the first item on the list, in case none of the menu items match + DisplayPluginPointer newDisplayPlugin = displayPlugins.at(0); + auto menu = getPrimaryMenu(); + if (menu) { + foreach(DisplayPluginPointer displayPlugin, PluginManager::getInstance()->getDisplayPlugins()) { + QString name = displayPlugin->getName(); + QAction* action = menu->getActionForOption(name); + // Menu might have been removed if the display plugin lost + if (!action) { + continue; + } + if (action->isChecked()) { + newDisplayPlugin = displayPlugin; + break; + } + } + } + + if (newDisplayPlugin == _displayPlugin) { + return; + } + + setDisplayPlugin(newDisplayPlugin); +} + +void Application::setDisplayPlugin(DisplayPluginPointer newDisplayPlugin) { + if (newDisplayPlugin == _displayPlugin) { + return; + } + + // FIXME don't have the application directly set the state of the UI, + // instead emit a signal that the display plugin is changing and let + // the desktop lock itself. Reduces coupling between the UI and display + // plugins + auto offscreenUi = getOffscreenUI(); + auto desktop = offscreenUi ? offscreenUi->getDesktop() : nullptr; + auto menu = Menu::getInstance(); + + // Make the switch atomic from the perspective of other threads + { + std::unique_lock lock(_displayPluginLock); + bool wasRepositionLocked = false; + if (desktop) { + // Tell the desktop to no reposition (which requires plugin info), until we have set the new plugin, below. + wasRepositionLocked = desktop->property("repositionLocked").toBool(); + desktop->setProperty("repositionLocked", true); + } + + if (_displayPlugin) { + disconnect(_displayPlugin.get(), &DisplayPlugin::presented, this, &Application::onPresent); + _displayPlugin->deactivate(); + } + + auto oldDisplayPlugin = _displayPlugin; + bool active = newDisplayPlugin->activate(); + + if (!active) { + auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); + + // If the new plugin fails to activate, fallback to last display + qWarning() << "Failed to activate display: " << newDisplayPlugin->getName(); + newDisplayPlugin = oldDisplayPlugin; + + if (newDisplayPlugin) { + qWarning() << "Falling back to last display: " << newDisplayPlugin->getName(); + active = newDisplayPlugin->activate(); + } + + // If there is no last display, or + // If the last display fails to activate, fallback to desktop + if (!active) { + newDisplayPlugin = displayPlugins.at(0); + qWarning() << "Falling back to display: " << newDisplayPlugin->getName(); + active = newDisplayPlugin->activate(); + } + + if (!active) { + qFatal("Failed to activate fallback plugin"); + } + } + + if (offscreenUi) { + offscreenUi->resize(fromGlm(newDisplayPlugin->getRecommendedUiSize())); + } + getApplicationCompositor().setDisplayPlugin(newDisplayPlugin); + _displayPlugin = newDisplayPlugin; + connect(_displayPlugin.get(), &DisplayPlugin::presented, this, &Application::onPresent, Qt::DirectConnection); + if (desktop) { + desktop->setProperty("repositionLocked", wasRepositionLocked); + } + } + + bool isHmd = _displayPlugin->isHmd(); + qCDebug(interfaceapp) << "Entering into" << (isHmd ? "HMD" : "Desktop") << "Mode"; + + // Only log/emit after a successful change + UserActivityLogger::getInstance().logAction("changed_display_mode", { + { "previous_display_mode", _displayPlugin ? _displayPlugin->getName() : "" }, + { "display_mode", newDisplayPlugin ? newDisplayPlugin->getName() : "" }, + { "hmd", isHmd } + }); + emit activeDisplayPluginChanged(); + + // reset the avatar, to set head and hand palms back to a reasonable default pose. + getMyAvatar()->reset(false); + + // switch to first person if entering hmd and setting is checked + if (menu) { + QAction* action = menu->getActionForOption(newDisplayPlugin->getName()); + if (action) { + action->setChecked(true); + } + + if (isHmd && menu->isOptionChecked(MenuOption::FirstPersonHMD)) { + menu->setIsOptionChecked(MenuOption::FirstPerson, true); + cameraMenuChanged(); + } + + // Remove the mirror camera option from menu if in HMD mode + auto mirrorAction = menu->getActionForOption(MenuOption::FullscreenMirror); + mirrorAction->setVisible(!isHmd); + } + + Q_ASSERT_X(_displayPlugin, "Application::updateDisplayMode", "could not find an activated display plugin"); +} + +void Application::switchDisplayMode() { + if (!_autoSwitchDisplayModeSupportedHMDPlugin) { + return; + } + bool currentHMDWornStatus = _autoSwitchDisplayModeSupportedHMDPlugin->isDisplayVisible(); + if (currentHMDWornStatus != _previousHMDWornStatus) { + // Switch to respective mode as soon as currentHMDWornStatus changes + if (currentHMDWornStatus) { + qCDebug(interfaceapp) << "Switching from Desktop to HMD mode"; + endHMDSession(); + setActiveDisplayPlugin(_autoSwitchDisplayModeSupportedHMDPluginName); + } else { + qCDebug(interfaceapp) << "Switching from HMD to desktop mode"; + setActiveDisplayPlugin(DESKTOP_DISPLAY_PLUGIN_NAME); + startHMDStandBySession(); + } + } + _previousHMDWornStatus = currentHMDWornStatus; +} + +void Application::setShowBulletWireframe(bool value) { + _physicsEngine->setShowBulletWireframe(value); +} + +void Application::setShowBulletAABBs(bool value) { + _physicsEngine->setShowBulletAABBs(value); +} + +void Application::setShowBulletContactPoints(bool value) { + _physicsEngine->setShowBulletContactPoints(value); +} + +void Application::setShowBulletConstraints(bool value) { + _physicsEngine->setShowBulletConstraints(value); +} + +void Application::setShowBulletConstraintLimits(bool value) { + _physicsEngine->setShowBulletConstraintLimits(value); +} + +void Application::createLoginDialog() { + const glm::vec3 LOGIN_DIMENSIONS { 0.89f, 0.5f, 0.01f }; + const auto OFFSET = glm::vec2(0.7f, -0.1f); + auto cameraPosition = _myCamera.getPosition(); + auto cameraOrientation = _myCamera.getOrientation(); + auto upVec = getMyAvatar()->getWorldOrientation() * Vectors::UNIT_Y; + auto headLookVec = (cameraOrientation * Vectors::FRONT); + // DEFAULT_DPI / tablet scale percentage + const float DPI = 31.0f / (75.0f / 100.0f); + auto offset = headLookVec * OFFSET.x; + auto position = (cameraPosition + offset) + (upVec * OFFSET.y); + + EntityItemProperties properties; + properties.setType(EntityTypes::Web); + properties.setName("LoginDialogEntity"); + properties.setSourceUrl(LOGIN_DIALOG.toString()); + properties.setPosition(position); + properties.setRotation(cameraOrientation); + properties.setDimensions(LOGIN_DIMENSIONS); + properties.setPrimitiveMode(PrimitiveMode::SOLID); + properties.getGrab().setGrabbable(false); + properties.setIgnorePickIntersection(false); + properties.setAlpha(1.0f); + properties.setDPI(DPI); + properties.setVisible(true); + + auto entityScriptingInterface = DependencyManager::get(); + _loginDialogID = entityScriptingInterface->addEntityInternal(properties, entity::HostType::LOCAL); + + auto keyboard = DependencyManager::get().data(); + if (!keyboard->getAnchorID().isNull() && !_loginDialogID.isNull()) { + auto keyboardLocalOffset = cameraOrientation * glm::vec3(-0.4f * getMyAvatar()->getSensorToWorldScale(), -0.3f, 0.2f); + + EntityItemProperties properties; + properties.setPosition(position + keyboardLocalOffset); + properties.setRotation(cameraOrientation * Quaternions::Y_180); + + entityScriptingInterface->editEntity(keyboard->getAnchorID(), properties); + keyboard->setResetKeyboardPositionOnRaise(false); + } + setKeyboardFocusEntity(_loginDialogID); + emit loginDialogFocusEnabled(); + getApplicationCompositor().getReticleInterface()->setAllowMouseCapture(false); + getApplicationCompositor().getReticleInterface()->setVisible(false); + if (!_loginStateManager.isSetUp()) { + _loginStateManager.setUp(); + } +} + +void Application::updateLoginDialogPosition() { + const float LOOK_AWAY_THRESHOLD_ANGLE = 70.0f; + const auto OFFSET = glm::vec2(0.7f, -0.1f); + + auto entityScriptingInterface = DependencyManager::get(); + EntityPropertyFlags desiredProperties; + desiredProperties += PROP_POSITION; + auto properties = entityScriptingInterface->getEntityProperties(_loginDialogID, desiredProperties); + auto positionVec = properties.getPosition(); + auto cameraPositionVec = _myCamera.getPosition(); + auto cameraOrientation = cancelOutRollAndPitch(_myCamera.getOrientation()); + auto headLookVec = (cameraOrientation * Vectors::FRONT); + auto entityToHeadVec = positionVec - cameraPositionVec; + auto pointAngle = (glm::acos(glm::dot(glm::normalize(entityToHeadVec), glm::normalize(headLookVec))) * 180.0f / PI); + auto upVec = getMyAvatar()->getWorldOrientation() * Vectors::UNIT_Y; + auto offset = headLookVec * OFFSET.x; + auto newPositionVec = (cameraPositionVec + offset) + (upVec * OFFSET.y); + + bool outOfBounds = glm::distance(positionVec, cameraPositionVec) > 1.0f; + + if (pointAngle > LOOK_AWAY_THRESHOLD_ANGLE || outOfBounds) { + { + EntityItemProperties properties; + properties.setPosition(newPositionVec); + properties.setRotation(cameraOrientation); + entityScriptingInterface->editEntity(_loginDialogID, properties); + } + + { + glm::vec3 keyboardLocalOffset = cameraOrientation * glm::vec3(-0.4f * getMyAvatar()->getSensorToWorldScale(), -0.3f, 0.2f); + glm::quat keyboardOrientation = cameraOrientation * glm::quat(glm::radians(glm::vec3(-30.0f, 180.0f, 0.0f))); + + EntityItemProperties properties; + properties.setPosition(newPositionVec + keyboardLocalOffset); + properties.setRotation(keyboardOrientation); + entityScriptingInterface->editEntity(DependencyManager::get()->getAnchorID(), properties); + } + } +} + +bool Application::hasRiftControllers() { + return PluginUtils::isOculusTouchControllerAvailable(); +} + +bool Application::hasViveControllers() { + return PluginUtils::isViveControllerAvailable(); +} + +void Application::onDismissedLoginDialog() { + _loginDialogPoppedUp = false; + loginDialogPoppedUp.set(false); + auto keyboard = DependencyManager::get().data(); + keyboard->setResetKeyboardPositionOnRaise(true); + if (!_loginDialogID.isNull()) { + DependencyManager::get()->deleteEntity(_loginDialogID); + _loginDialogID = QUuid(); + _loginStateManager.tearDown(); + } + resumeAfterLoginDialogActionTaken(); +} + +void Application::setShowTrackedObjects(bool value) { + _showTrackedObjects = value; +} + +void Application::startHMDStandBySession() { + _autoSwitchDisplayModeSupportedHMDPlugin->startStandBySession(); +} + +void Application::endHMDSession() { + _autoSwitchDisplayModeSupportedHMDPlugin->endSession(); +} + +mat4 Application::getEyeProjection(int eye) const { + QMutexLocker viewLocker(&_viewMutex); + if (isHMDMode()) { + return getActiveDisplayPlugin()->getEyeProjection((Eye)eye, _viewFrustum.getProjection()); + } + return _viewFrustum.getProjection(); +} + +mat4 Application::getEyeOffset(int eye) const { + // FIXME invert? + return getActiveDisplayPlugin()->getEyeToHeadTransform((Eye)eye); +} + +mat4 Application::getHMDSensorPose() const { + if (isHMDMode()) { + return getActiveDisplayPlugin()->getHeadPose(); + } + return mat4(); +} + +void Application::deadlockApplication() { + qCDebug(interfaceapp) << "Intentionally deadlocked Interface"; + // Using a loop that will *technically* eventually exit (in ~600 billion years) + // to avoid compiler warnings about a loop that will never exit + for (uint64_t i = 1; i != 0; ++i) { + QThread::sleep(1); + } +} + +// cause main thread to be unresponsive for 35 seconds +void Application::unresponsiveApplication() { + // to avoid compiler warnings about a loop that will never exit + uint64_t start = usecTimestampNow(); + uint64_t UNRESPONSIVE_FOR_SECONDS = 35; + uint64_t UNRESPONSIVE_FOR_USECS = UNRESPONSIVE_FOR_SECONDS * USECS_PER_SECOND; + qCDebug(interfaceapp) << "Intentionally cause Interface to be unresponsive for " << UNRESPONSIVE_FOR_SECONDS << " seconds"; + while (usecTimestampNow() - start < UNRESPONSIVE_FOR_USECS) { + QThread::sleep(1); + } +} + +void Application::setActiveDisplayPlugin(const QString& pluginName) { + DisplayPluginPointer newDisplayPlugin; + for (DisplayPluginPointer displayPlugin : PluginManager::getInstance()->getDisplayPlugins()) { + QString name = displayPlugin->getName(); + if (pluginName == name) { + newDisplayPlugin = displayPlugin; + break; + } + } + + if (newDisplayPlugin) { + setDisplayPlugin(newDisplayPlugin); + } +} + +void Application::handleLocalServerConnection() const { + auto server = qobject_cast(sender()); + + qCDebug(interfaceapp) << "Got connection on local server from additional instance - waiting for parameters"; + + auto socket = server->nextPendingConnection(); + + connect(socket, &QLocalSocket::readyRead, this, &Application::readArgumentsFromLocalSocket); + + qApp->getWindow()->raise(); + qApp->getWindow()->activateWindow(); +} + +void Application::readArgumentsFromLocalSocket() const { + auto socket = qobject_cast(sender()); + + auto message = socket->readAll(); + socket->deleteLater(); + + qCDebug(interfaceapp) << "Read from connection: " << message; + + // If we received a message, try to open it as a URL + if (message.length() > 0) { + qApp->openUrl(QString::fromUtf8(message)); + } +} + +void Application::showDesktop() { +} + +CompositorHelper& Application::getApplicationCompositor() const { + return *DependencyManager::get(); +} + + +// virtual functions required for PluginContainer +ui::Menu* Application::getPrimaryMenu() { + auto appMenu = _window->menuBar(); + auto uiMenu = dynamic_cast(appMenu); + return uiMenu; +} + +void Application::showDisplayPluginsTools(bool show) { + DependencyManager::get()->hmdTools(show); +} + +GLWidget* Application::getPrimaryWidget() { + return _glWidget; +} + +MainWindow* Application::getPrimaryWindow() { + return getWindow(); +} + +QOpenGLContext* Application::getPrimaryContext() { + return _glWidget->qglContext(); +} + +bool Application::makeRenderingContextCurrent() { + return true; +} + +bool Application::isForeground() const { + return _isForeground && !_window->isMinimized(); +} + +// FIXME? perhaps two, one for the main thread and one for the offscreen UI rendering thread? +static const int UI_RESERVED_THREADS = 1; +// Windows won't let you have all the cores +static const int OS_RESERVED_THREADS = 1; + +void Application::updateThreadPoolCount() const { + auto reservedThreads = UI_RESERVED_THREADS + OS_RESERVED_THREADS + _displayPlugin->getRequiredThreadCount(); + auto availableThreads = QThread::idealThreadCount() - reservedThreads; + auto threadPoolSize = std::max(MIN_PROCESSING_THREAD_POOL_SIZE, availableThreads); + qCDebug(interfaceapp) << "Ideal Thread Count " << QThread::idealThreadCount(); + qCDebug(interfaceapp) << "Reserved threads " << reservedThreads; + qCDebug(interfaceapp) << "Setting thread pool size to " << threadPoolSize; + QThreadPool::globalInstance()->setMaxThreadCount(threadPoolSize); +} + +void Application::updateSystemTabletMode() { + if (_settingsLoaded && !getLoginDialogPoppedUp()) { + qApp->setProperty(hifi::properties::HMD, isHMDMode()); + if (isHMDMode()) { + DependencyManager::get()->setToolbarMode(getHmdTabletBecomesToolbarSetting()); + } else { + DependencyManager::get()->setToolbarMode(getDesktopTabletBecomesToolbarSetting()); + } + } +} + +QUuid Application::getTabletScreenID() const { + auto HMD = DependencyManager::get(); + return HMD->getCurrentTabletScreenID(); +} + +QUuid Application::getTabletHomeButtonID() const { + auto HMD = DependencyManager::get(); + return HMD->getCurrentHomeButtonID(); +} + +QUuid Application::getTabletFrameID() const { + auto HMD = DependencyManager::get(); + return HMD->getCurrentTabletFrameID(); +} + +QVector Application::getTabletIDs() const { + // Most important first. + QVector result; + auto HMD = DependencyManager::get(); + result << HMD->getCurrentTabletScreenID(); + result << HMD->getCurrentHomeButtonID(); + result << HMD->getCurrentTabletFrameID(); + return result; +} + +void Application::setAvatarOverrideUrl(const QUrl& url, bool save) { + _avatarOverrideUrl = url; + _saveAvatarOverrideUrl = save; +} + +void Application::saveNextPhysicsStats(QString filename) { + _physicsEngine->saveNextPhysicsStats(filename); +} + +void Application::copyToClipboard(const QString& text) { + if (QThread::currentThread() != qApp->thread()) { + QMetaObject::invokeMethod(this, "copyToClipboard"); + return; + } + + // assume that the address is being copied because the user wants a shareable address + QApplication::clipboard()->setText(text); +} + +QString Application::getGraphicsCardType() { + return GPUIdent::getInstance()->getName(); +} + +#if defined(Q_OS_ANDROID) +void Application::beforeEnterBackground() { + auto nodeList = DependencyManager::get(); + nodeList->setSendDomainServerCheckInEnabled(false); + nodeList->reset(true); + clearDomainOctreeDetails(); +} + + + +void Application::enterBackground() { + QMetaObject::invokeMethod(DependencyManager::get().data(), + "stop", Qt::BlockingQueuedConnection); +// Quest only supports one plugin which can't be deactivated currently +#if !defined(ANDROID_APP_QUEST_INTERFACE) + if (getActiveDisplayPlugin()->isActive()) { + getActiveDisplayPlugin()->deactivate(); + } +#endif +} + +void Application::enterForeground() { + QMetaObject::invokeMethod(DependencyManager::get().data(), + "start", Qt::BlockingQueuedConnection); +// Quest only supports one plugin which can't be deactivated currently +#if !defined(ANDROID_APP_QUEST_INTERFACE) + if (!getActiveDisplayPlugin() || getActiveDisplayPlugin()->isActive() || !getActiveDisplayPlugin()->activate()) { + qWarning() << "Could not re-activate display plugin"; + } +#endif + auto nodeList = DependencyManager::get(); + nodeList->setSendDomainServerCheckInEnabled(true); +} + + +void Application::toggleAwayMode(){ + QKeyEvent event = QKeyEvent (QEvent::KeyPress, Qt::Key_Escape, Qt::NoModifier); + QCoreApplication::sendEvent (this, &event); +} + + +#endif + + +#include "Application.moc" diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 852c4eb695..6d9a1823a1 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1795,7 +1795,19 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo }); connect(offscreenUi.data(), &OffscreenUi::keyboardFocusActive, [this]() { - resumeAfterLoginDialogActionTaken(); +#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML) + // Do not show login dialog if requested not to on the command line + QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY); + int index = arguments().indexOf(hifiNoLoginCommandLineKey); + if (index != -1) { + resumeAfterLoginDialogActionTaken(); + return; + } + + showLoginScreen(); +#else + resumeAfterLoginDialogActionTaken(); +#endif }); // Make sure we don't time out during slow operations at startup From 4c7b292e1afc2961cef5599a800b70f2d06e2c87 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Tue, 12 Mar 2019 10:25:40 -0700 Subject: [PATCH 130/446] Remove unneeded wait. --- tools/nitpick/src/TestCreator.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tools/nitpick/src/TestCreator.cpp b/tools/nitpick/src/TestCreator.cpp index 089e84904a..17a191315c 100644 --- a/tools/nitpick/src/TestCreator.cpp +++ b/tools/nitpick/src/TestCreator.cpp @@ -851,10 +851,7 @@ void TestCreator::createRecursiveScript(const QString& directory, bool interacti textStream << " nitpick = createNitpick(Script.resolvePath(\".\"));" << endl; textStream << " testsRootPath = nitpick.getTestsRootPath();" << endl << endl; textStream << " nitpick.enableRecursive();" << endl; - textStream << " nitpick.enableAuto();" << endl << endl; - textStream << " if (typeof Test !== 'undefined') {" << endl; - textStream << " Test.wait(10000);" << endl; - textStream << " }" << endl; + textStream << " nitpick.enableAuto();" << endl; textStream << "} else {" << endl; textStream << " depth++" << endl; textStream << "}" << endl << endl; From 41662b183bf82e26fb6b0fdf4fc8ff8015893e07 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Tue, 12 Mar 2019 10:45:04 -0700 Subject: [PATCH 131/446] Removed redundant method. --- interface/src/scripting/TestScriptingInterface.cpp | 10 ---------- interface/src/scripting/TestScriptingInterface.h | 7 ------- 2 files changed, 17 deletions(-) diff --git a/interface/src/scripting/TestScriptingInterface.cpp b/interface/src/scripting/TestScriptingInterface.cpp index a9ba165037..c3aeb2643b 100644 --- a/interface/src/scripting/TestScriptingInterface.cpp +++ b/interface/src/scripting/TestScriptingInterface.cpp @@ -199,13 +199,3 @@ void TestScriptingInterface::setOtherAvatarsReplicaCount(int count) { int TestScriptingInterface::getOtherAvatarsReplicaCount() { return qApp->getOtherAvatarsReplicaCount(); } - -QString TestScriptingInterface::getOperatingSystemType() { -#ifdef Q_OS_WIN - return "WINDOWS"; -#elif defined Q_OS_MAC - return "MACOS"; -#else - return "UNKNOWN"; -#endif -} diff --git a/interface/src/scripting/TestScriptingInterface.h b/interface/src/scripting/TestScriptingInterface.h index 26e967c9b5..4a1d1a3eeb 100644 --- a/interface/src/scripting/TestScriptingInterface.h +++ b/interface/src/scripting/TestScriptingInterface.h @@ -163,13 +163,6 @@ public slots: */ Q_INVOKABLE int getOtherAvatarsReplicaCount(); - /**jsdoc - * Returns the Operating Sytem type - * @function Test.getOperatingSystemType - * @returns {string} "WINDOWS", "MACOS" or "UNKNOWN" - */ - QString getOperatingSystemType(); - private: bool waitForCondition(qint64 maxWaitMs, std::function condition); QString _testResultsLocation; From cb311408c68f2d465a80d76e0fdfe979cde96e13 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 11 Mar 2019 13:15:47 -0700 Subject: [PATCH 132/446] Remove _compositeFramebuffer from display plugins --- .../Basic2DWindowOpenGLDisplayPlugin.cpp | 6 +- .../Basic2DWindowOpenGLDisplayPlugin.h | 2 +- .../display-plugins/OpenGLDisplayPlugin.cpp | 175 ++++++++---------- .../src/display-plugins/OpenGLDisplayPlugin.h | 28 ++- .../hmd/DebugHmdDisplayPlugin.h | 2 +- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 64 +++---- .../display-plugins/hmd/HmdDisplayPlugin.h | 11 +- .../stereo/InterleavedStereoDisplayPlugin.cpp | 4 +- .../stereo/InterleavedStereoDisplayPlugin.h | 2 +- .../src/OculusMobileDisplayPlugin.cpp | 10 +- .../src/OculusMobileDisplayPlugin.h | 4 +- .../plugins/src/plugins/DisplayPlugin.cpp | 12 +- libraries/plugins/src/plugins/DisplayPlugin.h | 8 +- .../render-utils/src/RenderCommonTask.cpp | 4 +- libraries/render/src/render/Args.h | 2 +- plugins/oculus/src/OculusDebugDisplayPlugin.h | 2 +- plugins/oculus/src/OculusDisplayPlugin.cpp | 28 ++- plugins/oculus/src/OculusDisplayPlugin.h | 2 +- .../src/OculusLegacyDisplayPlugin.cpp | 4 +- .../src/OculusLegacyDisplayPlugin.h | 2 +- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 16 +- plugins/openvr/src/OpenVrDisplayPlugin.h | 4 +- 22 files changed, 181 insertions(+), 211 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp index 9828a8beda..f55f5919f5 100644 --- a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp @@ -109,7 +109,7 @@ bool Basic2DWindowOpenGLDisplayPlugin::internalActivate() { return Parent::internalActivate(); } -void Basic2DWindowOpenGLDisplayPlugin::compositeExtra() { +void Basic2DWindowOpenGLDisplayPlugin::compositeExtra(const gpu::FramebufferPointer& compositeFramebuffer) { #if defined(Q_OS_ANDROID) auto& virtualPadManager = VirtualPad::Manager::instance(); if(virtualPadManager.getLeftVirtualPad()->isShown()) { @@ -121,7 +121,7 @@ void Basic2DWindowOpenGLDisplayPlugin::compositeExtra() { render([&](gpu::Batch& batch) { batch.enableStereo(false); - batch.setFramebuffer(_compositeFramebuffer); + batch.setFramebuffer(compositeFramebuffer); batch.resetViewTransform(); batch.setProjectionTransform(mat4()); batch.setPipeline(_cursorPipeline); @@ -140,7 +140,7 @@ void Basic2DWindowOpenGLDisplayPlugin::compositeExtra() { }); } #endif - Parent::compositeExtra(); + Parent::compositeExtra(compositeFramebuffer); } static const uint32_t MIN_THROTTLE_CHECK_FRAMES = 60; diff --git a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h index cc304c19c2..d4c321a571 100644 --- a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h @@ -33,7 +33,7 @@ public: virtual bool isThrottled() const override; - virtual void compositeExtra() override; + virtual void compositeExtra(const gpu::FramebufferPointer&) override; virtual void pluginUpdate() override {}; diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index c536e6b6e2..5bc84acc6a 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -379,14 +379,6 @@ void OpenGLDisplayPlugin::customizeContext() { scissorState->setDepthTest(gpu::State::DepthTest(false)); scissorState->setScissorEnable(true); - { -#ifdef Q_OS_ANDROID - gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTextureGammaLinearToSRGB); -#else - gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTexture); -#endif - _simplePipeline = gpu::Pipeline::create(program, scissorState); - } { #ifdef Q_OS_ANDROID gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTextureGammaLinearToSRGB); @@ -396,29 +388,59 @@ void OpenGLDisplayPlugin::customizeContext() { _presentPipeline = gpu::Pipeline::create(program, scissorState); } + + // HUD operator { - gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTexture); - _hudPipeline = gpu::Pipeline::create(program, blendState); - } - { - gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTextureMirroredX); - _mirrorHUDPipeline = gpu::Pipeline::create(program, blendState); + gpu::PipelinePointer hudPipeline; + { + gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTexture); + hudPipeline = gpu::Pipeline::create(program, blendState); + } + + gpu::PipelinePointer hudMirrorPipeline; + { + gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTextureMirroredX); + hudMirrorPipeline = gpu::Pipeline::create(program, blendState); + } + + + _hudOperator = [=](gpu::Batch& batch, const gpu::TexturePointer& hudTexture, const gpu::FramebufferPointer& compositeFramebuffer, bool mirror) { + auto hudStereo = isStereo(); + auto hudCompositeFramebufferSize = compositeFramebuffer->getSize(); + std::array hudEyeViewports; + for_each_eye([&](Eye eye) { + hudEyeViewports[eye] = eyeViewport(eye); + }); + if (hudPipeline && hudTexture) { + batch.enableStereo(false); + batch.setPipeline(mirror ? hudMirrorPipeline : hudPipeline); + batch.setResourceTexture(0, hudTexture); + if (hudStereo) { + for_each_eye([&](Eye eye) { + batch.setViewportTransform(hudEyeViewports[eye]); + batch.draw(gpu::TRIANGLE_STRIP, 4); + }); + } else { + batch.setViewportTransform(ivec4(uvec2(0), hudCompositeFramebufferSize)); + batch.draw(gpu::TRIANGLE_STRIP, 4); + } + } + }; + } + { gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTransformedTexture); _cursorPipeline = gpu::Pipeline::create(program, blendState); } } - updateCompositeFramebuffer(); } void OpenGLDisplayPlugin::uncustomizeContext() { _presentPipeline.reset(); _cursorPipeline.reset(); - _hudPipeline.reset(); - _mirrorHUDPipeline.reset(); - _compositeFramebuffer.reset(); + _hudOperator = DEFAULT_HUD_OPERATOR; withPresentThreadLock([&] { _currentFrame.reset(); _lastFrame = nullptr; @@ -510,24 +532,16 @@ void OpenGLDisplayPlugin::captureFrame(const std::string& filename) const { }); } -void OpenGLDisplayPlugin::renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer& texture, const glm::ivec4& viewport, const glm::ivec4& scissor) { - renderFromTexture(batch, texture, viewport, scissor, nullptr); -} -void OpenGLDisplayPlugin::renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer& texture, const glm::ivec4& viewport, const glm::ivec4& scissor, const gpu::FramebufferPointer& copyFbo /*=gpu::FramebufferPointer()*/) { - auto fbo = gpu::FramebufferPointer(); +void OpenGLDisplayPlugin::renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer& texture, const glm::ivec4& viewport, const glm::ivec4& scissor, const gpu::FramebufferPointer& destFbo, const gpu::FramebufferPointer& copyFbo /*=gpu::FramebufferPointer()*/) { batch.enableStereo(false); batch.resetViewTransform(); - batch.setFramebuffer(fbo); + batch.setFramebuffer(destFbo); batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); batch.setStateScissorRect(scissor); batch.setViewportTransform(viewport); batch.setResourceTexture(0, texture); -#ifndef USE_GLES batch.setPipeline(_presentPipeline); -#else - batch.setPipeline(_simplePipeline); -#endif batch.draw(gpu::TRIANGLE_STRIP, 4); if (copyFbo) { gpu::Vec4i copyFboRect(0, 0, copyFbo->getWidth(), copyFbo->getHeight()); @@ -553,7 +567,7 @@ void OpenGLDisplayPlugin::renderFromTexture(gpu::Batch& batch, const gpu::Textur batch.setViewportTransform(copyFboRect); batch.setStateScissorRect(copyFboRect); batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, {0.0f, 0.0f, 0.0f, 1.0f}); - batch.blit(fbo, sourceRect, copyFbo, copyRect); + batch.blit(destFbo, sourceRect, copyFbo, copyRect); } } @@ -581,41 +595,14 @@ void OpenGLDisplayPlugin::updateFrameData() { }); } -std::function OpenGLDisplayPlugin::getHUDOperator() { - auto hudPipeline = _hudPipeline; - auto hudMirrorPipeline = _mirrorHUDPipeline; - auto hudStereo = isStereo(); - auto hudCompositeFramebufferSize = _compositeFramebuffer->getSize(); - std::array hudEyeViewports; - for_each_eye([&](Eye eye) { - hudEyeViewports[eye] = eyeViewport(eye); - }); - return [=](gpu::Batch& batch, const gpu::TexturePointer& hudTexture, bool mirror) { - if (hudPipeline && hudTexture) { - batch.enableStereo(false); - batch.setPipeline(mirror ? hudMirrorPipeline : hudPipeline); - batch.setResourceTexture(0, hudTexture); - if (hudStereo) { - for_each_eye([&](Eye eye) { - batch.setViewportTransform(hudEyeViewports[eye]); - batch.draw(gpu::TRIANGLE_STRIP, 4); - }); - } else { - batch.setViewportTransform(ivec4(uvec2(0), hudCompositeFramebufferSize)); - batch.draw(gpu::TRIANGLE_STRIP, 4); - } - } - }; -} - -void OpenGLDisplayPlugin::compositePointer() { +void OpenGLDisplayPlugin::compositePointer(const gpu::FramebufferPointer& compositeFramebuffer) { auto& cursorManager = Cursor::Manager::instance(); const auto& cursorData = _cursorsData[cursorManager.getCursor()->getIcon()]; auto cursorTransform = DependencyManager::get()->getReticleTransform(glm::mat4()); render([&](gpu::Batch& batch) { batch.enableStereo(false); batch.setProjectionTransform(mat4()); - batch.setFramebuffer(_compositeFramebuffer); + batch.setFramebuffer(compositeFramebuffer); batch.setPipeline(_cursorPipeline); batch.setResourceTexture(0, cursorData.texture); batch.resetViewTransform(); @@ -626,34 +613,13 @@ void OpenGLDisplayPlugin::compositePointer() { batch.draw(gpu::TRIANGLE_STRIP, 4); }); } else { - batch.setViewportTransform(ivec4(uvec2(0), _compositeFramebuffer->getSize())); + batch.setViewportTransform(ivec4(uvec2(0), compositeFramebuffer->getSize())); batch.draw(gpu::TRIANGLE_STRIP, 4); } }); } -void OpenGLDisplayPlugin::compositeScene() { - render([&](gpu::Batch& batch) { - batch.enableStereo(false); - batch.setFramebuffer(_compositeFramebuffer); - batch.setViewportTransform(ivec4(uvec2(), _compositeFramebuffer->getSize())); - batch.setStateScissorRect(ivec4(uvec2(), _compositeFramebuffer->getSize())); - batch.resetViewTransform(); - batch.setProjectionTransform(mat4()); - batch.setPipeline(_simplePipeline); - batch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); - batch.draw(gpu::TRIANGLE_STRIP, 4); - }); -} - -void OpenGLDisplayPlugin::compositeLayers() { - updateCompositeFramebuffer(); - - { - PROFILE_RANGE_EX(render_detail, "compositeScene", 0xff0077ff, (uint64_t)presentCount()) - compositeScene(); - } - +void OpenGLDisplayPlugin::compositeLayers(const gpu::FramebufferPointer& compositeFramebuffer) { #ifdef HIFI_ENABLE_NSIGHT_DEBUG if (false) // do not draw the HUD if running nsight debug #endif @@ -667,23 +633,35 @@ void OpenGLDisplayPlugin::compositeLayers() { { PROFILE_RANGE_EX(render_detail, "compositeExtra", 0xff0077ff, (uint64_t)presentCount()) - compositeExtra(); + compositeExtra(compositeFramebuffer); } // Draw the pointer last so it's on top of everything auto compositorHelper = DependencyManager::get(); if (compositorHelper->getReticleVisible()) { PROFILE_RANGE_EX(render_detail, "compositePointer", 0xff0077ff, (uint64_t)presentCount()) - compositePointer(); + compositePointer(compositeFramebuffer); } } -void OpenGLDisplayPlugin::internalPresent() { +void OpenGLDisplayPlugin::internalPresent(const gpu::FramebufferPointer& compositeFramebuffer) { render([&](gpu::Batch& batch) { // Note: _displayTexture must currently be the same size as the display. uvec2 dims = _displayTexture ? uvec2(_displayTexture->getDimensions()) : getSurfacePixels(); auto viewport = ivec4(uvec2(0), dims); - renderFromTexture(batch, _displayTexture ? _displayTexture : _compositeFramebuffer->getRenderBuffer(0), viewport, viewport); + + gpu::TexturePointer finalTexture; + if (_displayTexture) { + finalTexture = _displayTexture; + } else if (compositeFramebuffer) { + finalTexture = compositeFramebuffer->getRenderBuffer(0); + } else { + qCWarning(displayPlugins) << "No valid texture for output"; + } + + if (finalTexture) { + renderFromTexture(batch, finalTexture, viewport, viewport); + } }); swapBuffers(); _presentRate.increment(); @@ -700,7 +678,7 @@ void OpenGLDisplayPlugin::present() { } incrementPresentCount(); - if (_currentFrame) { + if (_currentFrame && _currentFrame->framebuffer) { auto correction = getViewCorrection(); getGLBackend()->setCameraCorrection(correction, _prevRenderView); _prevRenderView = correction * _currentFrame->view; @@ -720,18 +698,18 @@ void OpenGLDisplayPlugin::present() { // Write all layers to a local framebuffer { PROFILE_RANGE_EX(render, "composite", 0xff00ffff, frameId) - compositeLayers(); + compositeLayers(_currentFrame->framebuffer); } // Take the composite framebuffer and send it to the output device { PROFILE_RANGE_EX(render, "internalPresent", 0xff00ffff, frameId) - internalPresent(); + internalPresent(_currentFrame->framebuffer); } gpu::Backend::freeGPUMemSize.set(gpu::gl::getFreeDedicatedMemory()); } else if (alwaysPresent()) { - internalPresent(); + internalPresent(nullptr); } _movingAveragePresent.addSample((float)(usecTimestampNow() - startPresent)); } @@ -788,7 +766,12 @@ bool OpenGLDisplayPlugin::setDisplayTexture(const QString& name) { } QImage OpenGLDisplayPlugin::getScreenshot(float aspectRatio) const { - auto size = _compositeFramebuffer->getSize(); + if (!_currentFrame || !_currentFrame->framebuffer) { + return QImage(); + } + + auto compositeFramebuffer = _currentFrame->framebuffer; + auto size = compositeFramebuffer->getSize(); if (isHmd()) { size.x /= 2; } @@ -806,7 +789,7 @@ QImage OpenGLDisplayPlugin::getScreenshot(float aspectRatio) const { auto glBackend = const_cast(*this).getGLBackend(); QImage screenshot(bestSize.x, bestSize.y, QImage::Format_ARGB32); withOtherThreadContext([&] { - glBackend->downloadFramebuffer(_compositeFramebuffer, ivec4(corner, bestSize), screenshot); + glBackend->downloadFramebuffer(compositeFramebuffer, ivec4(corner, bestSize), screenshot); }); return screenshot.mirrored(false, true); } @@ -858,7 +841,7 @@ bool OpenGLDisplayPlugin::beginFrameRender(uint32_t frameIndex) { } ivec4 OpenGLDisplayPlugin::eyeViewport(Eye eye) const { - uvec2 vpSize = _compositeFramebuffer->getSize(); + auto vpSize = glm::uvec2(getRecommendedRenderSize()); vpSize.x /= 2; uvec2 vpPos; if (eye == Eye::Right) { @@ -891,14 +874,6 @@ void OpenGLDisplayPlugin::render(std::function f) { OpenGLDisplayPlugin::~OpenGLDisplayPlugin() { } -void OpenGLDisplayPlugin::updateCompositeFramebuffer() { - auto renderSize = glm::uvec2(getRecommendedRenderSize()); - if (!_compositeFramebuffer || _compositeFramebuffer->getSize() != renderSize) { - _compositeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("OpenGLDisplayPlugin::composite", gpu::Element::COLOR_RGBA_32, renderSize.x, renderSize.y)); - // _compositeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("OpenGLDisplayPlugin::composite", gpu::Element::COLOR_SRGBA_32, renderSize.x, renderSize.y)); - } -} - void OpenGLDisplayPlugin::copyTextureToQuickFramebuffer(NetworkTexturePointer networkTexture, QOpenGLFramebufferObject* target, GLsync* fenceSync) { #if !defined(USE_GLES) auto glBackend = const_cast(*this).getGLBackend(); diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index 49a38ecb4c..3c48e8fc48 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -94,14 +94,10 @@ protected: // is not populated virtual bool alwaysPresent() const { return false; } - void updateCompositeFramebuffer(); - virtual QThread::Priority getPresentPriority() { return QThread::HighPriority; } - virtual void compositeLayers(); - virtual void compositeScene(); - virtual std::function getHUDOperator(); - virtual void compositePointer(); - virtual void compositeExtra() {}; + virtual void compositeLayers(const gpu::FramebufferPointer&); + virtual void compositePointer(const gpu::FramebufferPointer&); + virtual void compositeExtra(const gpu::FramebufferPointer&) {}; // These functions must only be called on the presentation thread virtual void customizeContext(); @@ -116,10 +112,10 @@ protected: virtual void deactivateSession() {} // Plugin specific functionality to send the composed scene to the output window or device - virtual void internalPresent(); + virtual void internalPresent(const gpu::FramebufferPointer&); - void renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer& texture, const glm::ivec4& viewport, const glm::ivec4& scissor, const gpu::FramebufferPointer& fbo); - void renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer& texture, const glm::ivec4& viewport, const glm::ivec4& scissor); + + void renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer& texture, const glm::ivec4& viewport, const glm::ivec4& scissor, const gpu::FramebufferPointer& destFbo = nullptr, const gpu::FramebufferPointer& copyFbo = nullptr); virtual void updateFrameData(); virtual glm::mat4 getViewCorrection() { return glm::mat4(); } @@ -142,14 +138,8 @@ protected: gpu::FramePointer _currentFrame; gpu::Frame* _lastFrame { nullptr }; mat4 _prevRenderView; - gpu::FramebufferPointer _compositeFramebuffer; - gpu::PipelinePointer _hudPipeline; - gpu::PipelinePointer _mirrorHUDPipeline; - gpu::ShaderPointer _mirrorHUDPS; - gpu::PipelinePointer _simplePipeline; - gpu::PipelinePointer _presentPipeline; gpu::PipelinePointer _cursorPipeline; - gpu::TexturePointer _displayTexture{}; + gpu::TexturePointer _displayTexture; float _compositeHUDAlpha { 1.0f }; struct CursorData { @@ -185,5 +175,9 @@ protected: // be serialized through this mutex mutable Mutex _presentMutex; float _hudAlpha{ 1.0f }; + +private: + gpu::PipelinePointer _presentPipeline; + }; diff --git a/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.h index f2b1f36419..95592cc490 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.h @@ -24,7 +24,7 @@ public: protected: void updatePresentPose() override; - void hmdPresent() override {} + void hmdPresent(const gpu::FramebufferPointer&) override {} bool isHmdMounted() const override { return true; } bool internalActivate() override; private: diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 321bcc3fd2..a515232b3f 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -114,20 +114,23 @@ void HmdDisplayPlugin::internalDeactivate() { void HmdDisplayPlugin::customizeContext() { Parent::customizeContext(); - _hudRenderer.build(); + _hudOperator = _hudRenderer.build(); } void HmdDisplayPlugin::uncustomizeContext() { // This stops the weirdness where if the preview was disabled, on switching back to 2D, // the vsync was stuck in the disabled state. No idea why that happens though. _disablePreview = false; - render([&](gpu::Batch& batch) { - batch.enableStereo(false); - batch.resetViewTransform(); - batch.setFramebuffer(_compositeFramebuffer); - batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); - }); - _hudRenderer = HUDRenderer(); + if (_currentFrame && _currentFrame->framebuffer) { + render([&](gpu::Batch& batch) { + batch.enableStereo(false); + batch.resetViewTransform(); + batch.setFramebuffer(_currentFrame->framebuffer); + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); + }); + + } + _hudRenderer = {}; _previewTexture.reset(); Parent::uncustomizeContext(); } @@ -174,11 +177,11 @@ float HmdDisplayPlugin::getLeftCenterPixel() const { return leftCenterPixel; } -void HmdDisplayPlugin::internalPresent() { +void HmdDisplayPlugin::internalPresent(const gpu::FramebufferPointer& compositeFramebuffer) { PROFILE_RANGE_EX(render, __FUNCTION__, 0xff00ff00, (uint64_t)presentCount()) // Composite together the scene, hud and mouse cursor - hmdPresent(); + hmdPresent(compositeFramebuffer); if (_displayTexture) { // Note: _displayTexture must currently be the same size as the display. @@ -260,7 +263,7 @@ void HmdDisplayPlugin::internalPresent() { viewport.z *= 2; } - renderFromTexture(batch, _compositeFramebuffer->getRenderBuffer(0), viewport, scissor, fbo); + renderFromTexture(batch, compositeFramebuffer->getRenderBuffer(0), viewport, scissor, nullptr, fbo); }); swapBuffers(); @@ -345,7 +348,7 @@ glm::mat4 HmdDisplayPlugin::getViewCorrection() { } } -void HmdDisplayPlugin::HUDRenderer::build() { +DisplayPlugin::HUDOperator HmdDisplayPlugin::HUDRenderer::build() { vertices = std::make_shared(); indices = std::make_shared(); @@ -380,7 +383,7 @@ void HmdDisplayPlugin::HUDRenderer::build() { indexCount = numberOfRectangles * TRIANGLE_PER_RECTANGLE * VERTEX_PER_TRANGLE; // Compute indices order - std::vector indices; + std::vector indexData; for (int i = 0; i < stacks - 1; i++) { for (int j = 0; j < slices - 1; j++) { GLushort bottomLeftIndex = i * slices + j; @@ -388,24 +391,21 @@ void HmdDisplayPlugin::HUDRenderer::build() { GLushort topLeftIndex = bottomLeftIndex + slices; GLushort topRightIndex = topLeftIndex + 1; // FIXME make a z-order curve for better vertex cache locality - indices.push_back(topLeftIndex); - indices.push_back(bottomLeftIndex); - indices.push_back(topRightIndex); + indexData.push_back(topLeftIndex); + indexData.push_back(bottomLeftIndex); + indexData.push_back(topRightIndex); - indices.push_back(topRightIndex); - indices.push_back(bottomLeftIndex); - indices.push_back(bottomRightIndex); + indexData.push_back(topRightIndex); + indexData.push_back(bottomLeftIndex); + indexData.push_back(bottomRightIndex); } } - this->indices->append(indices); + indices->append(indexData); format = std::make_shared(); // 1 for everyone format->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); format->setAttribute(gpu::Stream::TEXCOORD, gpu::Stream::TEXCOORD, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV)); uniformsBuffer = std::make_shared(sizeof(Uniforms), nullptr); - updatePipeline(); -} -void HmdDisplayPlugin::HUDRenderer::updatePipeline() { if (!pipeline) { auto program = gpu::Shader::createProgram(shader::render_utils::program::hmd_ui); gpu::StatePointer state = gpu::StatePointer(new gpu::State()); @@ -416,10 +416,6 @@ void HmdDisplayPlugin::HUDRenderer::updatePipeline() { pipeline = gpu::Pipeline::create(program, state); } -} - -std::function HmdDisplayPlugin::HUDRenderer::render(HmdDisplayPlugin& plugin) { - updatePipeline(); auto hudPipeline = pipeline; auto hudFormat = format; @@ -428,9 +424,9 @@ std::function HmdDis auto hudUniformBuffer = uniformsBuffer; auto hudUniforms = uniforms; auto hudIndexCount = indexCount; - return [=](gpu::Batch& batch, const gpu::TexturePointer& hudTexture, bool mirror) { - if (hudPipeline && hudTexture) { - batch.setPipeline(hudPipeline); + return [=](gpu::Batch& batch, const gpu::TexturePointer& hudTexture, const gpu::FramebufferPointer&, const bool mirror) { + if (pipeline && hudTexture) { + batch.setPipeline(pipeline); batch.setInputFormat(hudFormat); gpu::BufferView posView(hudVertices, VERTEX_OFFSET, hudVertices->getSize(), VERTEX_STRIDE, hudFormat->getAttributes().at(gpu::Stream::POSITION)._element); @@ -454,7 +450,7 @@ std::function HmdDis }; } -void HmdDisplayPlugin::compositePointer() { +void HmdDisplayPlugin::compositePointer(const gpu::FramebufferPointer& compositeFramebuffer) { auto& cursorManager = Cursor::Manager::instance(); const auto& cursorData = _cursorsData[cursorManager.getCursor()->getIcon()]; auto compositorHelper = DependencyManager::get(); @@ -463,7 +459,7 @@ void HmdDisplayPlugin::compositePointer() { render([&](gpu::Batch& batch) { // FIXME use standard gpu stereo rendering for this. batch.enableStereo(false); - batch.setFramebuffer(_compositeFramebuffer); + batch.setFramebuffer(compositeFramebuffer); batch.setPipeline(_cursorPipeline); batch.setResourceTexture(0, cursorData.texture); batch.resetViewTransform(); @@ -478,10 +474,6 @@ void HmdDisplayPlugin::compositePointer() { }); } -std::function HmdDisplayPlugin::getHUDOperator() { - return _hudRenderer.render(*this); -} - HmdDisplayPlugin::~HmdDisplayPlugin() { } diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index d8c0ce8e1d..6755c5b7e0 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -53,16 +53,15 @@ signals: void hmdVisibleChanged(bool visible); protected: - virtual void hmdPresent() = 0; + virtual void hmdPresent(const gpu::FramebufferPointer&) = 0; virtual bool isHmdMounted() const = 0; virtual void postPreview() {}; virtual void updatePresentPose(); bool internalActivate() override; void internalDeactivate() override; - std::function getHUDOperator() override; - void compositePointer() override; - void internalPresent() override; + void compositePointer(const gpu::FramebufferPointer&) override; + void internalPresent(const gpu::FramebufferPointer&) override; void customizeContext() override; void uncustomizeContext() override; void updateFrameData() override; @@ -120,8 +119,6 @@ private: static const size_t TEXTURE_OFFSET { offsetof(Vertex, uv) }; static const int VERTEX_STRIDE { sizeof(Vertex) }; - void build(); - void updatePipeline(); - std::function render(HmdDisplayPlugin& plugin); + HUDOperator build(); } _hudRenderer; }; diff --git a/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp index 0ae0f9b1b6..69aa7fc344 100644 --- a/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp @@ -37,13 +37,13 @@ glm::uvec2 InterleavedStereoDisplayPlugin::getRecommendedRenderSize() const { return result; } -void InterleavedStereoDisplayPlugin::internalPresent() { +void InterleavedStereoDisplayPlugin::internalPresent(const gpu::FramebufferPointer& compositeFramebuffer) { render([&](gpu::Batch& batch) { batch.enableStereo(false); batch.resetViewTransform(); batch.setFramebuffer(gpu::FramebufferPointer()); batch.setViewportTransform(ivec4(uvec2(0), getSurfacePixels())); - batch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); + batch.setResourceTexture(0, compositeFramebuffer->getRenderBuffer(0)); batch.setPipeline(_interleavedPresentPipeline); batch.draw(gpu::TRIANGLE_STRIP, 4); }); diff --git a/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.h index debd340f24..52dfa8f402 100644 --- a/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.h @@ -21,7 +21,7 @@ protected: // initialize OpenGL context settings needed by the plugin void customizeContext() override; void uncustomizeContext() override; - void internalPresent() override; + void internalPresent(const gpu::FramebufferPointer&) override; private: static const QString NAME; diff --git a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp index 9809d02866..12a9b12adc 100644 --- a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp +++ b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp @@ -245,7 +245,7 @@ void OculusMobileDisplayPlugin::updatePresentPose() { }); } -void OculusMobileDisplayPlugin::internalPresent() { +void OculusMobileDisplayPlugin::internalPresent(const gpu::FramebufferPointer& compsiteFramebuffer) { VrHandler::pollTask(); if (!vrActive()) { @@ -253,8 +253,12 @@ void OculusMobileDisplayPlugin::internalPresent() { return; } - auto sourceTexture = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0)); - glm::uvec2 sourceSize{ _compositeFramebuffer->getWidth(), _compositeFramebuffer->getHeight() }; + GLuint sourceTexture = 0; + glm::uvec2 sourceSize; + if (compsiteFramebuffer) { + sourceTexture = getGLBackend()->getTextureID(compsiteFramebuffer->getRenderBuffer(0)); + sourceSize = { compsiteFramebuffer->getWidth(), compsiteFramebuffer->getHeight() }; + } VrHandler::presentFrame(sourceTexture, sourceSize, presentTracking); _presentRate.increment(); } diff --git a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.h b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.h index a98989655e..b5f7aa57b0 100644 --- a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.h +++ b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.h @@ -54,8 +54,8 @@ protected: void uncustomizeContext() override; void updatePresentPose() override; - void internalPresent() override; - void hmdPresent() override { throw std::runtime_error("Unused"); } + void internalPresent(const gpu::FramebufferPointer&) override; + void hmdPresent(const gpu::FramebufferPointer&) override { throw std::runtime_error("Unused"); } bool isHmdMounted() const override; bool alwaysPresent() const override { return true; } diff --git a/libraries/plugins/src/plugins/DisplayPlugin.cpp b/libraries/plugins/src/plugins/DisplayPlugin.cpp index 47503e8f85..71db87557c 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.cpp +++ b/libraries/plugins/src/plugins/DisplayPlugin.cpp @@ -2,6 +2,12 @@ #include + +const DisplayPlugin::HUDOperator DisplayPlugin::DEFAULT_HUD_OPERATOR{ std::function() }; + +DisplayPlugin::DisplayPlugin() : _hudOperator{ DEFAULT_HUD_OPERATOR } { +} + int64_t DisplayPlugin::getPaintDelayUsecs() const { std::lock_guard lock(_paintDelayMutex); return _paintDelayTimer.isValid() ? _paintDelayTimer.nsecsElapsed() / NSECS_PER_USEC : 0; @@ -35,8 +41,8 @@ void DisplayPlugin::waitForPresent() { } } -std::function DisplayPlugin::getHUDOperator() { - std::function hudOperator; +std::function DisplayPlugin::getHUDOperator() { + HUDOperator hudOperator; { QMutexLocker locker(&_presentMutex); hudOperator = _hudOperator; @@ -48,3 +54,5 @@ glm::mat4 HmdDisplay::getEyeToHeadTransform(Eye eye) const { static const glm::mat4 xform; return xform; } + + diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index aa52e57c3f..9194fde3ac 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -121,6 +121,8 @@ class DisplayPlugin : public Plugin, public HmdDisplay { Q_OBJECT using Parent = Plugin; public: + DisplayPlugin(); + virtual int getRequiredThreadCount() const { return 0; } virtual bool isHmd() const { return false; } virtual int getHmdScreen() const { return -1; } @@ -214,7 +216,8 @@ public: void waitForPresent(); float getAveragePresentTime() { return _movingAveragePresent.average / (float)USECS_PER_MSEC; } // in msec - std::function getHUDOperator(); + using HUDOperator = std::function; + virtual HUDOperator getHUDOperator() final; static const QString& MENU_PATH(); @@ -231,7 +234,8 @@ protected: gpu::ContextPointer _gpuContext; - std::function _hudOperator { std::function() }; + static const HUDOperator DEFAULT_HUD_OPERATOR; + HUDOperator _hudOperator; MovingAverage _movingAveragePresent; diff --git a/libraries/render-utils/src/RenderCommonTask.cpp b/libraries/render-utils/src/RenderCommonTask.cpp index 385e384efe..e77ccb74a5 100644 --- a/libraries/render-utils/src/RenderCommonTask.cpp +++ b/libraries/render-utils/src/RenderCommonTask.cpp @@ -122,8 +122,8 @@ void CompositeHUD::run(const RenderContextPointer& renderContext, const gpu::Fra if (inputs) { batch.setFramebuffer(inputs); } - if (renderContext->args->_hudOperator) { - renderContext->args->_hudOperator(batch, renderContext->args->_hudTexture, renderContext->args->_renderMode == RenderArgs::RenderMode::MIRROR_RENDER_MODE); + if (renderContext->args->_hudOperator && renderContext->args->_blitFramebuffer) { + renderContext->args->_hudOperator(batch, renderContext->args->_hudTexture, renderContext->args->_blitFramebuffer, renderContext->args->_renderMode == RenderArgs::RenderMode::MIRROR_RENDER_MODE); } }); #endif diff --git a/libraries/render/src/render/Args.h b/libraries/render/src/render/Args.h index b5c98e3428..8b2fff68c6 100644 --- a/libraries/render/src/render/Args.h +++ b/libraries/render/src/render/Args.h @@ -131,7 +131,7 @@ namespace render { render::ScenePointer _scene; int8_t _cameraMode { -1 }; - std::function _hudOperator; + std::function _hudOperator; gpu::TexturePointer _hudTexture; }; diff --git a/plugins/oculus/src/OculusDebugDisplayPlugin.h b/plugins/oculus/src/OculusDebugDisplayPlugin.h index ec05cd92e2..690a488b34 100644 --- a/plugins/oculus/src/OculusDebugDisplayPlugin.h +++ b/plugins/oculus/src/OculusDebugDisplayPlugin.h @@ -16,7 +16,7 @@ public: bool isSupported() const override; protected: - void hmdPresent() override {} + void hmdPresent(const gpu::FramebufferPointer&) override {} bool isHmdMounted() const override { return true; } private: diff --git a/plugins/oculus/src/OculusDisplayPlugin.cpp b/plugins/oculus/src/OculusDisplayPlugin.cpp index df01591639..48440ac80f 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusDisplayPlugin.cpp @@ -108,13 +108,16 @@ void OculusDisplayPlugin::customizeContext() { } void OculusDisplayPlugin::uncustomizeContext() { + #if 0 - // Present a final black frame to the HMD - _compositeFramebuffer->Bound(FramebufferTarget::Draw, [] { - Context::ClearColor(0, 0, 0, 1); - Context::Clear().ColorBuffer(); - }); - hmdPresent(); + if (_currentFrame && _currentFrame->framebuffer) { + // Present a final black frame to the HMD + _currentFrame->framebuffer->Bound(FramebufferTarget::Draw, [] { + Context::ClearColor(0, 0, 0, 1); + Context::Clear().ColorBuffer(); + }); + hmdPresent(); + } #endif ovr_DestroyTextureSwapChain(_session, _textureSwapChain); @@ -127,7 +130,7 @@ void OculusDisplayPlugin::uncustomizeContext() { static const uint64_t FRAME_BUDGET = (11 * USECS_PER_MSEC); static const uint64_t FRAME_OVER_BUDGET = (15 * USECS_PER_MSEC); -void OculusDisplayPlugin::hmdPresent() { +void OculusDisplayPlugin::hmdPresent(const gpu::FramebufferPointer& compositeFramebuffer) { static uint64_t lastSubmitEnd = 0; if (!_customized) { @@ -157,15 +160,8 @@ void OculusDisplayPlugin::hmdPresent() { auto fbo = getGLBackend()->getFramebufferID(_outputFramebuffer); glNamedFramebufferTexture(fbo, GL_COLOR_ATTACHMENT0, curTexId, 0); render([&](gpu::Batch& batch) { - batch.enableStereo(false); - batch.setFramebuffer(_outputFramebuffer); - batch.setViewportTransform(ivec4(uvec2(), _outputFramebuffer->getSize())); - batch.setStateScissorRect(ivec4(uvec2(), _outputFramebuffer->getSize())); - batch.resetViewTransform(); - batch.setProjectionTransform(mat4()); - batch.setPipeline(_presentPipeline); - batch.setResourceTexture(0, _compositeFramebuffer->getRenderBuffer(0)); - batch.draw(gpu::TRIANGLE_STRIP, 4); + auto viewport = ivec4(uvec2(), _outputFramebuffer->getSize()); + renderFromTexture(batch, compositeFramebuffer->getRenderBuffer(0), viewport, viewport, _outputFramebuffer); }); glNamedFramebufferTexture(fbo, GL_COLOR_ATTACHMENT0, 0, 0); } diff --git a/plugins/oculus/src/OculusDisplayPlugin.h b/plugins/oculus/src/OculusDisplayPlugin.h index 9209fd373e..a0126d2e58 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.h +++ b/plugins/oculus/src/OculusDisplayPlugin.h @@ -28,7 +28,7 @@ protected: QThread::Priority getPresentPriority() override { return QThread::TimeCriticalPriority; } bool internalActivate() override; - void hmdPresent() override; + void hmdPresent(const gpu::FramebufferPointer&) override; bool isHmdMounted() const override; void customizeContext() override; void uncustomizeContext() override; diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp index e6b555443f..a928887866 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp @@ -237,7 +237,7 @@ void OculusLegacyDisplayPlugin::uncustomizeContext() { Parent::uncustomizeContext(); } -void OculusLegacyDisplayPlugin::hmdPresent() { +void OculusLegacyDisplayPlugin::hmdPresent(const gpu::FramebufferPointer& compositeFramebuffer) { if (!_hswDismissed) { ovrHSWDisplayState hswState; ovrHmd_GetHSWDisplayState(_hmd, &hswState); @@ -252,7 +252,7 @@ void OculusLegacyDisplayPlugin::hmdPresent() { memset(eyePoses, 0, sizeof(ovrPosef) * 2); eyePoses[0].Orientation = eyePoses[1].Orientation = ovrRotation; - GLint texture = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0)); + GLint texture = getGLBackend()->getTextureID(compositeFramebuffer->getRenderBuffer(0)); auto sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); glFlush(); if (_hmdWindow->makeCurrent()) { diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h index 36bdd1c792..241d626f0c 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h @@ -39,7 +39,7 @@ protected: void customizeContext() override; void uncustomizeContext() override; - void hmdPresent() override; + void hmdPresent(const gpu::FramebufferPointer&) override; bool isHmdMounted() const override { return true; } private: diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 11d941dcd0..3d22268472 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -511,13 +511,13 @@ void OpenVrDisplayPlugin::customizeContext() { Parent::customizeContext(); if (_threadedSubmit) { - _compositeInfos[0].texture = _compositeFramebuffer->getRenderBuffer(0); +// _compositeInfos[0].texture = _compositeFramebuffer->getRenderBuffer(0); for (size_t i = 0; i < COMPOSITING_BUFFER_SIZE; ++i) { - if (0 != i) { +// if (0 != i) { _compositeInfos[i].texture = gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, _renderTargetSize.x, _renderTargetSize.y, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT)); - } +// } _compositeInfos[i].textureID = getGLBackend()->getTextureID(_compositeInfos[i].texture); } _submitThread->_canvas = _submitCanvas; @@ -613,17 +613,17 @@ bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { return Parent::beginFrameRender(frameIndex); } -void OpenVrDisplayPlugin::compositeLayers() { +void OpenVrDisplayPlugin::compositeLayers(const gpu::FramebufferPointer& compositeFramebuffer) { if (_threadedSubmit) { ++_renderingIndex; _renderingIndex %= COMPOSITING_BUFFER_SIZE; auto& newComposite = _compositeInfos[_renderingIndex]; newComposite.pose = _currentPresentFrameInfo.presentPose; - _compositeFramebuffer->setRenderBuffer(0, newComposite.texture); + compositeFramebuffer->setRenderBuffer(0, newComposite.texture); } - Parent::compositeLayers(); + Parent::compositeLayers(compositeFramebuffer); if (_threadedSubmit) { auto& newComposite = _compositeInfos[_renderingIndex]; @@ -645,13 +645,13 @@ void OpenVrDisplayPlugin::compositeLayers() { } } -void OpenVrDisplayPlugin::hmdPresent() { +void OpenVrDisplayPlugin::hmdPresent(const gpu::FramebufferPointer& compositeFramebuffer) { PROFILE_RANGE_EX(render, __FUNCTION__, 0xff00ff00, (uint64_t)_currentFrame->frameIndex) if (_threadedSubmit) { _submitThread->waitForPresent(); } else { - GLuint glTexId = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0)); + GLuint glTexId = getGLBackend()->getTextureID(compositeFramebuffer->getRenderBuffer(0)); vr::Texture_t vrTexture{ (void*)(uintptr_t)glTexId, vr::TextureType_OpenGL, vr::ColorSpace_Auto }; vr::VRCompositor()->Submit(vr::Eye_Left, &vrTexture, &OPENVR_TEXTURE_BOUNDS_LEFT); vr::VRCompositor()->Submit(vr::Eye_Right, &vrTexture, &OPENVR_TEXTURE_BOUNDS_RIGHT); diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.h b/plugins/openvr/src/OpenVrDisplayPlugin.h index 265f328920..923a0f7a8f 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.h +++ b/plugins/openvr/src/OpenVrDisplayPlugin.h @@ -72,8 +72,8 @@ protected: void internalDeactivate() override; void updatePresentPose() override; - void compositeLayers() override; - void hmdPresent() override; + void compositeLayers(const gpu::FramebufferPointer&) override; + void hmdPresent(const gpu::FramebufferPointer&) override; bool isHmdMounted() const override; void postPreview() override; From 858d80073faa16dac126566cfebb7f3a2bc43428 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Tue, 12 Mar 2019 13:27:36 -0700 Subject: [PATCH 133/446] Change default snapshots folder. --- tools/nitpick/src/TestRunnerMobile.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index eaebb6ca5a..ed48c37290 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -43,7 +43,7 @@ TestRunnerMobile::TestRunnerMobile( _installAPKPushbutton = installAPKPushbutton; _runInterfacePushbutton = runInterfacePushbutton; - folderLineEdit->setText("/sdcard/DCIM/TEST"); + folderLineEdit->setText("/sdcard/snapshots"); modelNames["SM_G955U1"] = "Samsung S8+ unlocked"; modelNames["SM_N960U1"] = "Samsung Note 9 unlocked"; From 643fbfd80538a95e3fc880c27f7b7877f7bd2829 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Tue, 12 Mar 2019 14:06:45 -0700 Subject: [PATCH 134/446] TEST TEST TEST --- interface/src/Application.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index de4a6bb167..7dddaecadb 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1804,7 +1804,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo }); connect(offscreenUi.data(), &OffscreenUi::keyboardFocusActive, [this]() { -#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML) // Do not show login dialog if requested not to on the command line QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY); int index = arguments().indexOf(hifiNoLoginCommandLineKey); @@ -1814,9 +1813,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo } showLoginScreen(); -#else - resumeAfterLoginDialogActionTaken(); -#endif }); // Make sure we don't time out during slow operations at startup From 6303f61cc32b010e3a73278e73ccd37cfcb39b64 Mon Sep 17 00:00:00 2001 From: danteruiz Date: Tue, 12 Mar 2019 14:26:59 -0700 Subject: [PATCH 135/446] fix lasers scale issue --- interface/src/avatar/MyAvatar.h | 1 + libraries/shared/src/NestableTransformNode.h | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index e516364f61..917da1a852 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1122,6 +1122,7 @@ public: float getUserEyeHeight() const; virtual SpatialParentTree* getParentTree() const override; + virtual glm::vec3 scaleForChildren() const override { return glm::vec3(getSensorToWorldScale()); } const QUuid& getSelfID() const { return AVATAR_SELF_ID; } diff --git a/libraries/shared/src/NestableTransformNode.h b/libraries/shared/src/NestableTransformNode.h index a584bcd308..f70d158c91 100644 --- a/libraries/shared/src/NestableTransformNode.h +++ b/libraries/shared/src/NestableTransformNode.h @@ -20,8 +20,10 @@ public: _jointIndex(jointIndex) { auto nestablePointer = _spatiallyNestable.lock(); if (nestablePointer) { - glm::vec3 nestableDimensions = getActualScale(nestablePointer); - _baseScale = glm::max(glm::vec3(0.001f), nestableDimensions); + if (nestablePointer->getNestableType() != NestableType::Avatar) { + glm::vec3 nestableDimensions = getActualScale(nestablePointer); + _baseScale = glm::max(glm::vec3(0.001f), nestableDimensions); + } } } From 647b508b9d005584cd2806f87d69b7ff42955b7f Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Tue, 12 Mar 2019 14:53:19 -0700 Subject: [PATCH 136/446] TEST TEST TEST --- interface/src/Application - Copy.cpp | 9191 -------------------------- 1 file changed, 9191 deletions(-) delete mode 100644 interface/src/Application - Copy.cpp diff --git a/interface/src/Application - Copy.cpp b/interface/src/Application - Copy.cpp deleted file mode 100644 index ca8883f660..0000000000 --- a/interface/src/Application - Copy.cpp +++ /dev/null @@ -1,9191 +0,0 @@ -// -// Application.cpp -// interface/src -// -// Created by Andrzej Kapolka on 5/10/13. -// Copyright 2013 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 -// - - -#include "Application.h" - -#include -#include - -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include -#include - -#include - -#include -#include -#include - - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "ui/overlays/ContextOverlayInterface.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "LocationBookmarks.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "recording/ClipCache.h" - -#include "AudioClient.h" -#include "audio/AudioScope.h" -#include "avatar/AvatarManager.h" -#include "avatar/MyHead.h" -#include "avatar/AvatarPackager.h" -#include "avatar/MyCharacterController.h" -#include "CrashRecoveryHandler.h" -#include "CrashHandler.h" -#include "devices/DdeFaceTracker.h" -#include "DiscoverabilityManager.h" -#include "GLCanvas.h" -#include "InterfaceDynamicFactory.h" -#include "InterfaceLogging.h" -#include "LODManager.h" -#include "ModelPackager.h" -#include "scripting/Audio.h" -#include "networking/CloseEventSender.h" -#include "scripting/TestScriptingInterface.h" -#include "scripting/PlatformInfoScriptingInterface.h" -#include "scripting/AssetMappingsScriptingInterface.h" -#include "scripting/ClipboardScriptingInterface.h" -#include "scripting/DesktopScriptingInterface.h" -#include "scripting/AccountServicesScriptingInterface.h" -#include "scripting/HMDScriptingInterface.h" -#include "scripting/MenuScriptingInterface.h" -#include "graphics-scripting/GraphicsScriptingInterface.h" -#include "scripting/SettingsScriptingInterface.h" -#include "scripting/WindowScriptingInterface.h" -#include "scripting/ControllerScriptingInterface.h" -#include "scripting/RatesScriptingInterface.h" -#include "scripting/SelectionScriptingInterface.h" -#include "scripting/WalletScriptingInterface.h" -#include "scripting/TTSScriptingInterface.h" -#include "scripting/KeyboardScriptingInterface.h" - - - -#if defined(Q_OS_MAC) || defined(Q_OS_WIN) -#include "SpeechRecognizer.h" -#endif -#include "ui/ResourceImageItem.h" -#include "ui/AddressBarDialog.h" -#include "ui/AvatarInputs.h" -#include "ui/DialogsManager.h" -#include "ui/LoginDialog.h" -#include "ui/Snapshot.h" -#include "ui/SnapshotAnimated.h" -#include "ui/StandAloneJSConsole.h" -#include "ui/Stats.h" -#include "ui/AnimStats.h" -#include "ui/UpdateDialog.h" -#include "ui/DomainConnectionModel.h" -#include "ui/Keyboard.h" -#include "Util.h" -#include "InterfaceParentFinder.h" -#include "ui/OctreeStatsProvider.h" - -#include "avatar/GrabManager.h" - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "commerce/Ledger.h" -#include "commerce/Wallet.h" -#include "commerce/QmlCommerce.h" -#include "commerce/QmlMarketplace.h" -#include "ResourceRequestObserver.h" - -#include "webbrowser/WebBrowserSuggestionsEngine.h" -#include - - -#include "AboutUtil.h" - -#if defined(Q_OS_WIN) -#include - -#ifdef DEBUG_EVENT_QUEUE -// This is a HACK that uses private headers included with the qt source distrubution. -// To use this feature you need to add these directores to your include path: -// E:/Qt/5.10.1/Src/qtbase/include/QtCore/5.10.1/QtCore -// E:/Qt/5.10.1/Src/qtbase/include/QtCore/5.10.1 -#define QT_BOOTSTRAPPED -#include -#include -#undef QT_BOOTSTRAPPED -#endif - -// On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU -// FIXME seems to be broken. -extern "C" { - _declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001; -} -#endif - -#if defined(Q_OS_ANDROID) -#include -#include "AndroidHelper.h" -#endif - -#include "graphics/RenderEventHandler.h" - -Q_LOGGING_CATEGORY(trace_app_input_mouse, "trace.app.input.mouse") - -using namespace std; - -static QTimer locationUpdateTimer; -static QTimer identityPacketTimer; -static QTimer pingTimer; - -#if defined(Q_OS_ANDROID) -static bool DISABLE_WATCHDOG = true; -#else -static const QString DISABLE_WATCHDOG_FLAG{ "HIFI_DISABLE_WATCHDOG" }; -static bool DISABLE_WATCHDOG = nsightActive() || QProcessEnvironment::systemEnvironment().contains(DISABLE_WATCHDOG_FLAG); -#endif - -#if defined(USE_GLES) -static bool DISABLE_DEFERRED = true; -#else -static const QString RENDER_FORWARD{ "HIFI_RENDER_FORWARD" }; -static bool DISABLE_DEFERRED = QProcessEnvironment::systemEnvironment().contains(RENDER_FORWARD); -#endif - -#if !defined(Q_OS_ANDROID) -static const uint32_t MAX_CONCURRENT_RESOURCE_DOWNLOADS = 16; -#else -static const uint32_t MAX_CONCURRENT_RESOURCE_DOWNLOADS = 4; -#endif - -// For processing on QThreadPool, we target a number of threads after reserving some -// based on how many are being consumed by the application and the display plugin. However, -// we will never drop below the 'min' value -static const int MIN_PROCESSING_THREAD_POOL_SIZE = 1; - -static const QString SNAPSHOT_EXTENSION = ".jpg"; -static const QString JPG_EXTENSION = ".jpg"; -static const QString PNG_EXTENSION = ".png"; -static const QString SVO_EXTENSION = ".svo"; -static const QString SVO_JSON_EXTENSION = ".svo.json"; -static const QString JSON_GZ_EXTENSION = ".json.gz"; -static const QString JSON_EXTENSION = ".json"; -static const QString JS_EXTENSION = ".js"; -static const QString FST_EXTENSION = ".fst"; -static const QString FBX_EXTENSION = ".fbx"; -static const QString OBJ_EXTENSION = ".obj"; -static const QString AVA_JSON_EXTENSION = ".ava.json"; -static const QString WEB_VIEW_TAG = "noDownload=true"; -static const QString ZIP_EXTENSION = ".zip"; -static const QString CONTENT_ZIP_EXTENSION = ".content.zip"; - -static const float MIRROR_FULLSCREEN_DISTANCE = 0.789f; - -static const quint64 TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS = 1 * USECS_PER_SECOND; - -static const QString INFO_EDIT_ENTITIES_PATH = "html/edit-commands.html"; -static const QString INFO_HELP_PATH = "html/tabletHelp.html"; - -static const unsigned int THROTTLED_SIM_FRAMERATE = 15; -static const int THROTTLED_SIM_FRAME_PERIOD_MS = MSECS_PER_SECOND / THROTTLED_SIM_FRAMERATE; -static const int ENTITY_SERVER_ADDED_TIMEOUT = 5000; -static const int ENTITY_SERVER_CONNECTION_TIMEOUT = 5000; - -static const float INITIAL_QUERY_RADIUS = 10.0f; // priority radius for entities before physics enabled - -static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); - -Setting::Handle maxOctreePacketsPerSecond{"maxOctreePPS", DEFAULT_MAX_OCTREE_PPS}; - -Setting::Handle loginDialogPoppedUp{"loginDialogPoppedUp", false}; - -static const QString STANDARD_TO_ACTION_MAPPING_NAME = "Standard to Action"; -static const QString NO_MOVEMENT_MAPPING_NAME = "Standard to Action (No Movement)"; -static const QString NO_MOVEMENT_MAPPING_JSON = PathUtils::resourcesPath() + "/controllers/standard_nomovement.json"; - -static const QString MARKETPLACE_CDN_HOSTNAME = "mpassets.highfidelity.com"; -static const int INTERVAL_TO_CHECK_HMD_WORN_STATUS = 500; // milliseconds -static const QString DESKTOP_DISPLAY_PLUGIN_NAME = "Desktop"; -static const QString ACTIVE_DISPLAY_PLUGIN_SETTING_NAME = "activeDisplayPlugin"; -static const QString SYSTEM_TABLET = "com.highfidelity.interface.tablet.system"; -static const QString KEEP_ME_LOGGED_IN_SETTING_NAME = "keepMeLoggedIn"; - -static const float FOCUS_HIGHLIGHT_EXPANSION_FACTOR = 1.05f; - -#if defined(Q_OS_ANDROID) -static const QString TESTER_FILE = "/sdcard/_hifi_test_device.txt"; -#endif -const std::vector> Application::_acceptedExtensions { - { SVO_EXTENSION, &Application::importSVOFromURL }, - { SVO_JSON_EXTENSION, &Application::importSVOFromURL }, - { AVA_JSON_EXTENSION, &Application::askToWearAvatarAttachmentUrl }, - { JSON_EXTENSION, &Application::importJSONFromURL }, - { JS_EXTENSION, &Application::askToLoadScript }, - { FST_EXTENSION, &Application::askToSetAvatarUrl }, - { JSON_GZ_EXTENSION, &Application::askToReplaceDomainContent }, - { CONTENT_ZIP_EXTENSION, &Application::askToReplaceDomainContent }, - { ZIP_EXTENSION, &Application::importFromZIP }, - { JPG_EXTENSION, &Application::importImage }, - { PNG_EXTENSION, &Application::importImage } -}; - -class DeadlockWatchdogThread : public QThread { -public: - static const unsigned long HEARTBEAT_UPDATE_INTERVAL_SECS = 1; - static const unsigned long MAX_HEARTBEAT_AGE_USECS = 120 * USECS_PER_SECOND; // 2 mins with no checkin probably a deadlock - static const int WARNING_ELAPSED_HEARTBEAT = 500 * USECS_PER_MSEC; // warn if elapsed heartbeat average is large - static const int HEARTBEAT_SAMPLES = 100000; // ~5 seconds worth of samples - - // Set the heartbeat on launch - DeadlockWatchdogThread() { - setObjectName("Deadlock Watchdog"); - // Give the heartbeat an initial value - _heartbeat = usecTimestampNow(); - _paused = false; - connect(qApp, &QCoreApplication::aboutToQuit, [this] { - _quit = true; - }); - } - - void setMainThreadID(Qt::HANDLE threadID) { - _mainThreadID = threadID; - } - - static void updateHeartbeat() { - auto now = usecTimestampNow(); - auto elapsed = now - _heartbeat; - _movingAverage.addSample(elapsed); - _heartbeat = now; - } - - void deadlockDetectionCrash() { - setCrashAnnotation("_mod_faulting_tid", std::to_string((uint64_t)_mainThreadID)); - setCrashAnnotation("deadlock", "1"); - uint32_t* crashTrigger = nullptr; - *crashTrigger = 0xDEAD10CC; - } - - static void withPause(const std::function& lambda) { - pause(); - lambda(); - resume(); - } - static void pause() { - _paused = true; - } - - static void resume() { - // Update the heartbeat BEFORE resuming the checks - updateHeartbeat(); - _paused = false; - } - - void run() override { - while (!_quit) { - QThread::sleep(HEARTBEAT_UPDATE_INTERVAL_SECS); - // Don't do heartbeat detection under nsight - if (_paused) { - continue; - } - uint64_t lastHeartbeat = _heartbeat; // sample atomic _heartbeat, because we could context switch away and have it updated on us - uint64_t now = usecTimestampNow(); - auto lastHeartbeatAge = (now > lastHeartbeat) ? now - lastHeartbeat : 0; - auto elapsedMovingAverage = _movingAverage.getAverage(); - - if (elapsedMovingAverage > _maxElapsedAverage) { - qCDebug(interfaceapp_deadlock) << "DEADLOCK WATCHDOG WARNING:" - << "lastHeartbeatAge:" << lastHeartbeatAge - << "elapsedMovingAverage:" << elapsedMovingAverage - << "maxElapsed:" << _maxElapsed - << "PREVIOUS maxElapsedAverage:" << _maxElapsedAverage - << "NEW maxElapsedAverage:" << elapsedMovingAverage << "** NEW MAX ELAPSED AVERAGE **" - << "samples:" << _movingAverage.getSamples(); - _maxElapsedAverage = elapsedMovingAverage; - } - if (lastHeartbeatAge > _maxElapsed) { - qCDebug(interfaceapp_deadlock) << "DEADLOCK WATCHDOG WARNING:" - << "lastHeartbeatAge:" << lastHeartbeatAge - << "elapsedMovingAverage:" << elapsedMovingAverage - << "PREVIOUS maxElapsed:" << _maxElapsed - << "NEW maxElapsed:" << lastHeartbeatAge << "** NEW MAX ELAPSED **" - << "maxElapsedAverage:" << _maxElapsedAverage - << "samples:" << _movingAverage.getSamples(); - _maxElapsed = lastHeartbeatAge; - } - if (elapsedMovingAverage > WARNING_ELAPSED_HEARTBEAT) { - qCDebug(interfaceapp_deadlock) << "DEADLOCK WATCHDOG WARNING:" - << "lastHeartbeatAge:" << lastHeartbeatAge - << "elapsedMovingAverage:" << elapsedMovingAverage << "** OVER EXPECTED VALUE **" - << "maxElapsed:" << _maxElapsed - << "maxElapsedAverage:" << _maxElapsedAverage - << "samples:" << _movingAverage.getSamples(); - } - - if (lastHeartbeatAge > MAX_HEARTBEAT_AGE_USECS) { - qCDebug(interfaceapp_deadlock) << "DEADLOCK DETECTED -- " - << "lastHeartbeatAge:" << lastHeartbeatAge - << "[ lastHeartbeat :" << lastHeartbeat - << "now:" << now << " ]" - << "elapsedMovingAverage:" << elapsedMovingAverage - << "maxElapsed:" << _maxElapsed - << "maxElapsedAverage:" << _maxElapsedAverage - << "samples:" << _movingAverage.getSamples(); - - // Don't actually crash in debug builds, in case this apparent deadlock is simply from - // the developer actively debugging code - #ifdef NDEBUG - deadlockDetectionCrash(); - #endif - } - } - } - - static std::atomic _paused; - static std::atomic _heartbeat; - static std::atomic _maxElapsed; - static std::atomic _maxElapsedAverage; - static ThreadSafeMovingAverage _movingAverage; - - bool _quit { false }; - - Qt::HANDLE _mainThreadID = nullptr; -}; - -std::atomic DeadlockWatchdogThread::_paused; -std::atomic DeadlockWatchdogThread::_heartbeat; -std::atomic DeadlockWatchdogThread::_maxElapsed; -std::atomic DeadlockWatchdogThread::_maxElapsedAverage; -ThreadSafeMovingAverage DeadlockWatchdogThread::_movingAverage; - -bool isDomainURL(QUrl url) { - if (!url.isValid()) { - return false; - } - if (url.scheme() == URL_SCHEME_HIFI) { - return true; - } - if (url.scheme() != HIFI_URL_SCHEME_FILE) { - // TODO -- once Octree::readFromURL no-longer takes over the main event-loop, serverless-domain urls can - // be loaded over http(s) - // && url.scheme() != HIFI_URL_SCHEME_HTTP && - // url.scheme() != HIFI_URL_SCHEME_HTTPS - return false; - } - if (url.path().endsWith(".json", Qt::CaseInsensitive) || - url.path().endsWith(".json.gz", Qt::CaseInsensitive)) { - return true; - } - return false; -} - -#ifdef Q_OS_WIN -class MyNativeEventFilter : public QAbstractNativeEventFilter { -public: - static MyNativeEventFilter& getInstance() { - static MyNativeEventFilter staticInstance; - return staticInstance; - } - - bool nativeEventFilter(const QByteArray &eventType, void* msg, long* result) Q_DECL_OVERRIDE { - if (eventType == "windows_generic_MSG") { - MSG* message = (MSG*)msg; - - if (message->message == UWM_IDENTIFY_INSTANCES) { - *result = UWM_IDENTIFY_INSTANCES; - return true; - } - - if (message->message == UWM_SHOW_APPLICATION) { - MainWindow* applicationWindow = qApp->getWindow(); - if (applicationWindow->isMinimized()) { - applicationWindow->showNormal(); // Restores to windowed or maximized state appropriately. - } - qApp->setActiveWindow(applicationWindow); // Flashes the taskbar icon if not focus. - return true; - } - - if (message->message == WM_COPYDATA) { - COPYDATASTRUCT* pcds = (COPYDATASTRUCT*)(message->lParam); - QUrl url = QUrl((const char*)(pcds->lpData)); - if (isDomainURL(url)) { - DependencyManager::get()->handleLookupString(url.toString()); - return true; - } - } - - if (message->message == WM_DEVICECHANGE) { - const float MIN_DELTA_SECONDS = 2.0f; // de-bounce signal - static float lastTriggerTime = 0.0f; - const float deltaSeconds = secTimestampNow() - lastTriggerTime; - lastTriggerTime = secTimestampNow(); - if (deltaSeconds > MIN_DELTA_SECONDS) { - Midi::USBchanged(); // re-scan the MIDI bus - } - } - } - return false; - } -}; -#endif - -class LambdaEvent : public QEvent { - std::function _fun; -public: - LambdaEvent(const std::function & fun) : - QEvent(static_cast(ApplicationEvent::Lambda)), _fun(fun) { - } - LambdaEvent(std::function && fun) : - QEvent(static_cast(ApplicationEvent::Lambda)), _fun(fun) { - } - void call() const { _fun(); } -}; - -void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { - QString logMessage = LogHandler::getInstance().printMessage((LogMsgType) type, context, message); - - if (!logMessage.isEmpty()) { -#ifdef Q_OS_ANDROID - const char * local=logMessage.toStdString().c_str(); - switch (type) { - case QtDebugMsg: - __android_log_write(ANDROID_LOG_DEBUG,"Interface",local); - break; - case QtInfoMsg: - __android_log_write(ANDROID_LOG_INFO,"Interface",local); - break; - case QtWarningMsg: - __android_log_write(ANDROID_LOG_WARN,"Interface",local); - break; - case QtCriticalMsg: - __android_log_write(ANDROID_LOG_ERROR,"Interface",local); - break; - case QtFatalMsg: - default: - __android_log_write(ANDROID_LOG_FATAL,"Interface",local); - abort(); - } -#else - qApp->getLogger()->addMessage(qPrintable(logMessage)); -#endif - } -} - - -class ApplicationMeshProvider : public scriptable::ModelProviderFactory { -public: - virtual scriptable::ModelProviderPointer lookupModelProvider(const QUuid& uuid) override { - bool success; - if (auto nestable = DependencyManager::get()->find(uuid, success).lock()) { - auto type = nestable->getNestableType(); -#ifdef SCRIPTABLE_MESH_DEBUG - qCDebug(interfaceapp) << "ApplicationMeshProvider::lookupModelProvider" << uuid << SpatiallyNestable::nestableTypeToString(type); -#endif - switch (type) { - case NestableType::Entity: - return getEntityModelProvider(static_cast(uuid)); - case NestableType::Avatar: - return getAvatarModelProvider(uuid); - } - } - return nullptr; - } - -private: - scriptable::ModelProviderPointer getEntityModelProvider(EntityItemID entityID) { - scriptable::ModelProviderPointer provider; - auto entityTreeRenderer = qApp->getEntities(); - auto entityTree = entityTreeRenderer->getTree(); - if (auto entity = entityTree->findEntityByID(entityID)) { - if (auto renderer = entityTreeRenderer->renderableForEntityId(entityID)) { - provider = std::dynamic_pointer_cast(renderer); - provider->modelProviderType = NestableType::Entity; - } else { - qCWarning(interfaceapp) << "no renderer for entity ID" << entityID.toString(); - } - } - return provider; - } - - scriptable::ModelProviderPointer getAvatarModelProvider(QUuid sessionUUID) { - scriptable::ModelProviderPointer provider; - auto avatarManager = DependencyManager::get(); - if (auto avatar = avatarManager->getAvatarBySessionID(sessionUUID)) { - provider = std::dynamic_pointer_cast(avatar); - provider->modelProviderType = NestableType::Avatar; - } - return provider; - } -}; - -/**jsdoc - *

The Controller.Hardware.Application object has properties representing Interface's state. The property - * values are integer IDs, uniquely identifying each output. Read-only. These can be mapped to actions or functions or - * Controller.Standard items in a {@link RouteObject} mapping (e.g., using the {@link RouteObject#when} method). - * Each data value is either 1.0 for "true" or 0.0 for "false".

- * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
PropertyTypeDataDescription
CameraFirstPersonnumbernumberThe camera is in first-person mode. - *
CameraThirdPersonnumbernumberThe camera is in third-person mode. - *
CameraFSMnumbernumberThe camera is in full screen mirror mode.
CameraIndependentnumbernumberThe camera is in independent mode.
CameraEntitynumbernumberThe camera is in entity mode.
InHMDnumbernumberThe user is in HMD mode.
AdvancedMovementnumbernumberAdvanced movement controls are enabled. - *
SnapTurnnumbernumberSnap turn is enabled.
GroundednumbernumberThe user's avatar is on the ground.
NavigationFocusednumbernumberNot used.
- * @typedef {object} Controller.Hardware-Application - */ - -static const QString STATE_IN_HMD = "InHMD"; -static const QString STATE_CAMERA_FULL_SCREEN_MIRROR = "CameraFSM"; -static const QString STATE_CAMERA_FIRST_PERSON = "CameraFirstPerson"; -static const QString STATE_CAMERA_THIRD_PERSON = "CameraThirdPerson"; -static const QString STATE_CAMERA_ENTITY = "CameraEntity"; -static const QString STATE_CAMERA_INDEPENDENT = "CameraIndependent"; -static const QString STATE_SNAP_TURN = "SnapTurn"; -static const QString STATE_ADVANCED_MOVEMENT_CONTROLS = "AdvancedMovement"; -static const QString STATE_GROUNDED = "Grounded"; -static const QString STATE_NAV_FOCUSED = "NavigationFocused"; -static const QString STATE_PLATFORM_WINDOWS = "PlatformWindows"; -static const QString STATE_PLATFORM_MAC = "PlatformMac"; -static const QString STATE_PLATFORM_ANDROID = "PlatformAndroid"; - -// Statically provided display and input plugins -extern DisplayPluginList getDisplayPlugins(); -extern InputPluginList getInputPlugins(); -extern void saveInputPluginSettings(const InputPluginList& plugins); - -// Parameters used for running tests from teh command line -const QString TEST_SCRIPT_COMMAND{ "--testScript" }; -const QString TEST_QUIT_WHEN_FINISHED_OPTION{ "quitWhenFinished" }; -const QString TEST_RESULTS_LOCATION_COMMAND{ "--testResultsLocation" }; - -bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { - const char** constArgv = const_cast(argv); - - qInstallMessageHandler(messageHandler); - - // HRS: I could not figure out how to move these any earlier in startup, so when using this option, be sure to also supply - // --allowMultipleInstances - auto reportAndQuit = [&](const char* commandSwitch, std::function report) { - const char* reportfile = getCmdOption(argc, constArgv, commandSwitch); - // Reports to the specified file, because stdout is set up to be captured for logging. - if (reportfile) { - FILE* fp = fopen(reportfile, "w"); - if (fp) { - report(fp); - fclose(fp); - if (!runningMarkerExisted) { // don't leave ours around - RunningMarker runingMarker(RUNNING_MARKER_FILENAME); - runingMarker.deleteRunningMarkerFile(); // happens in deleter, but making the side-effect explicit. - } - _exit(0); - } - } - }; - reportAndQuit("--protocolVersion", [&](FILE* fp) { - auto version = protocolVersionsSignatureBase64(); - fputs(version.toLatin1().data(), fp); - }); - reportAndQuit("--version", [&](FILE* fp) { - fputs(BuildInfo::VERSION.toLatin1().data(), fp); - }); - - const char* portStr = getCmdOption(argc, constArgv, "--listenPort"); - const int listenPort = portStr ? atoi(portStr) : INVALID_PORT; - - static const auto SUPPRESS_SETTINGS_RESET = "--suppress-settings-reset"; - bool suppressPrompt = cmdOptionExists(argc, const_cast(argv), SUPPRESS_SETTINGS_RESET); - - // set the OCULUS_STORE property so the oculus plugin can know if we ran from the Oculus Store - static const auto OCULUS_STORE_ARG = "--oculus-store"; - bool isStore = cmdOptionExists(argc, const_cast(argv), OCULUS_STORE_ARG); - qApp->setProperty(hifi::properties::OCULUS_STORE, isStore); - - // Ignore any previous crashes if running from command line with a test script. - bool inTestMode { false }; - for (int i = 0; i < argc; ++i) { - QString parameter(argv[i]); - if (parameter == TEST_SCRIPT_COMMAND) { - inTestMode = true; - break; - } - } - - bool previousSessionCrashed { false }; - if (!inTestMode) { - previousSessionCrashed = CrashRecoveryHandler::checkForResetSettings(runningMarkerExisted, suppressPrompt); - } - - // get dir to use for cache - static const auto CACHE_SWITCH = "--cache"; - QString cacheDir = getCmdOption(argc, const_cast(argv), CACHE_SWITCH); - if (!cacheDir.isEmpty()) { - qApp->setProperty(hifi::properties::APP_LOCAL_DATA_PATH, cacheDir); - } - - { - const QString resourcesBinaryFile = PathUtils::getRccPath(); - if (!QFile::exists(resourcesBinaryFile)) { - throw std::runtime_error("Unable to find primary resources"); - } - if (!QResource::registerResource(resourcesBinaryFile)) { - throw std::runtime_error("Unable to load primary resources"); - } - } - - // Tell the plugin manager about our statically linked plugins - DependencyManager::set(); - auto pluginManager = PluginManager::getInstance(); - pluginManager->setInputPluginProvider([] { return getInputPlugins(); }); - pluginManager->setDisplayPluginProvider([] { return getDisplayPlugins(); }); - pluginManager->setInputPluginSettingsPersister([](const InputPluginList& plugins) { saveInputPluginSettings(plugins); }); - if (auto steamClient = pluginManager->getSteamClientPlugin()) { - steamClient->init(); - } - if (auto oculusPlatform = pluginManager->getOculusPlatformPlugin()) { - oculusPlatform->init(); - } - - PROFILE_SET_THREAD_NAME("Main Thread"); - -#if defined(Q_OS_WIN) - // Select appropriate audio DLL - QString audioDLLPath = QCoreApplication::applicationDirPath(); - if (IsWindows8OrGreater()) { - audioDLLPath += "/audioWin8"; - } else { - audioDLLPath += "/audioWin7"; - } - QCoreApplication::addLibraryPath(audioDLLPath); -#endif - - DependencyManager::registerInheritance(); - DependencyManager::registerInheritance(); - DependencyManager::registerInheritance(); - DependencyManager::registerInheritance(); - - // Set dependencies - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); -#if defined(Q_OS_ANDROID) - DependencyManager::set(); // use the default user agent getter -#else - DependencyManager::set(std::bind(&Application::getUserAgent, qApp)); -#endif - DependencyManager::set(); - DependencyManager::set(ScriptEngine::CLIENT_SCRIPT); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(NodeType::Agent, listenPort); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); // ModelFormatRegistry must be defined before ModelCache. See the ModelCache constructor. - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(true); - DependencyManager::set(); - DependencyManager::registerInheritance(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - -#if defined(Q_OS_MAC) || defined(Q_OS_WIN) - DependencyManager::set(); -#endif - DependencyManager::set(); - DependencyManager::set(); -#if !defined(DISABLE_QML) - DependencyManager::set(); -#endif - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - controller::StateController::setStateVariables({ { STATE_IN_HMD, STATE_CAMERA_FULL_SCREEN_MIRROR, - STATE_CAMERA_FIRST_PERSON, STATE_CAMERA_THIRD_PERSON, STATE_CAMERA_ENTITY, STATE_CAMERA_INDEPENDENT, - STATE_SNAP_TURN, STATE_ADVANCED_MOVEMENT_CONTROLS, STATE_GROUNDED, STATE_NAV_FOCUSED, - STATE_PLATFORM_WINDOWS, STATE_PLATFORM_MAC, STATE_PLATFORM_ANDROID } }); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(true, qApp, qApp); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(nullptr, qApp->getOcteeSceneStats()); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - - return previousSessionCrashed; -} - -// FIXME move to header, or better yet, design some kind of UI manager -// to take care of highlighting keyboard focused items, rather than -// continuing to overburden Application.cpp -QUuid _keyboardFocusHighlightID; - -OffscreenGLCanvas* _qmlShareContext { nullptr }; - -// FIXME hack access to the internal share context for the Chromium helper -// Normally we'd want to use QWebEngine::initialize(), but we can't because -// our primary context is a QGLWidget, which can't easily be initialized to share -// from a QOpenGLContext. -// -// So instead we create a new offscreen context to share with the QGLWidget, -// and manually set THAT to be the shared context for the Chromium helper -#if !defined(DISABLE_QML) -OffscreenGLCanvas* _chromiumShareContext { nullptr }; -#endif - -Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); -Q_GUI_EXPORT QOpenGLContext *qt_gl_global_share_context(); - -Setting::Handle sessionRunTime{ "sessionRunTime", 0 }; - -const float DEFAULT_HMD_TABLET_SCALE_PERCENT = 60.0f; -const float DEFAULT_DESKTOP_TABLET_SCALE_PERCENT = 75.0f; -const bool DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR = true; -const bool DEFAULT_HMD_TABLET_BECOMES_TOOLBAR = false; -const bool DEFAULT_PREFER_STYLUS_OVER_LASER = false; -const bool DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS = false; -const QString DEFAULT_CURSOR_NAME = "DEFAULT"; -const bool DEFAULT_MINI_TABLET_ENABLED = true; - -QSharedPointer getOffscreenUI() { -#if !defined(DISABLE_QML) - return DependencyManager::get(); -#else - return nullptr; -#endif -} - -Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bool runningMarkerExisted) : - QApplication(argc, argv), - _window(new MainWindow(desktop())), - _sessionRunTimer(startupTimer), -#ifndef Q_OS_ANDROID - _logger(new FileLogger(this)), -#endif - _previousSessionCrashed(setupEssentials(argc, argv, runningMarkerExisted)), - _entitySimulation(new PhysicalEntitySimulation()), - _physicsEngine(new PhysicsEngine(Vectors::ZERO)), - _entityClipboard(new EntityTree()), - _previousScriptLocation("LastScriptLocation", DESKTOP_LOCATION), - _fieldOfView("fieldOfView", DEFAULT_FIELD_OF_VIEW_DEGREES), - _hmdTabletScale("hmdTabletScale", DEFAULT_HMD_TABLET_SCALE_PERCENT), - _desktopTabletScale("desktopTabletScale", DEFAULT_DESKTOP_TABLET_SCALE_PERCENT), - _firstRun(Settings::firstRun, true), - _desktopTabletBecomesToolbarSetting("desktopTabletBecomesToolbar", DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR), - _hmdTabletBecomesToolbarSetting("hmdTabletBecomesToolbar", DEFAULT_HMD_TABLET_BECOMES_TOOLBAR), - _preferStylusOverLaserSetting("preferStylusOverLaser", DEFAULT_PREFER_STYLUS_OVER_LASER), - _preferAvatarFingerOverStylusSetting("preferAvatarFingerOverStylus", DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS), - _constrainToolbarPosition("toolbar/constrainToolbarToCenterX", true), - _preferredCursor("preferredCursor", DEFAULT_CURSOR_NAME), - _miniTabletEnabledSetting("miniTabletEnabled", DEFAULT_MINI_TABLET_ENABLED), - _scaleMirror(1.0f), - _mirrorYawOffset(0.0f), - _raiseMirror(0.0f), - _enableProcessOctreeThread(true), - _lastNackTime(usecTimestampNow()), - _lastSendDownstreamAudioStats(usecTimestampNow()), - _notifiedPacketVersionMismatchThisDomain(false), - _maxOctreePPS(maxOctreePacketsPerSecond.get()), - _lastFaceTrackerUpdate(0), - _snapshotSound(nullptr), - _sampleSound(nullptr) -{ - - auto steamClient = PluginManager::getInstance()->getSteamClientPlugin(); - setProperty(hifi::properties::STEAM, (steamClient && steamClient->isRunning())); - setProperty(hifi::properties::CRASHED, _previousSessionCrashed); - - { - const QStringList args = arguments(); - - for (int i = 0; i < args.size() - 1; ++i) { - if (args.at(i) == TEST_SCRIPT_COMMAND && (i + 1) < args.size()) { - QString testScriptPath = args.at(i + 1); - - // If the URL scheme is http(s) or ftp, then use as is, else - treat it as a local file - // This is done so as not break previous command line scripts - if (testScriptPath.left(HIFI_URL_SCHEME_HTTP.length()) == HIFI_URL_SCHEME_HTTP || - testScriptPath.left(HIFI_URL_SCHEME_FTP.length()) == HIFI_URL_SCHEME_FTP) { - - setProperty(hifi::properties::TEST, QUrl::fromUserInput(testScriptPath)); - } else if (QFileInfo(testScriptPath).exists()) { - setProperty(hifi::properties::TEST, QUrl::fromLocalFile(testScriptPath)); - } - - // quite when finished parameter must directly follow the test script - if ((i + 2) < args.size() && args.at(i + 2) == TEST_QUIT_WHEN_FINISHED_OPTION) { - quitWhenFinished = true; - } - } else if (args.at(i) == TEST_RESULTS_LOCATION_COMMAND) { - // Set test snapshot location only if it is a writeable directory - QString path(args.at(i + 1)); - - QFileInfo fileInfo(path); - if (fileInfo.isDir() && fileInfo.isWritable()) { - TestScriptingInterface::getInstance()->setTestResultsLocation(path); - } - } - } - } - - // make sure the debug draw singleton is initialized on the main thread. - DebugDraw::getInstance().removeMarker(""); - - PluginContainer* pluginContainer = dynamic_cast(this); // set the container for any plugins that care - PluginManager::getInstance()->setContainer(pluginContainer); - - QThreadPool::globalInstance()->setMaxThreadCount(MIN_PROCESSING_THREAD_POOL_SIZE); - thread()->setPriority(QThread::HighPriority); - thread()->setObjectName("Main Thread"); - - setInstance(this); - - auto controllerScriptingInterface = DependencyManager::get().data(); - _controllerScriptingInterface = dynamic_cast(controllerScriptingInterface); - connect(PluginManager::getInstance().data(), &PluginManager::inputDeviceRunningChanged, - controllerScriptingInterface, &controller::ScriptingInterface::updateRunningInputDevices); - - EntityTree::setEntityClicksCapturedOperator([this] { - return _controllerScriptingInterface->areEntityClicksCaptured(); - }); - - _entityClipboard->createRootElement(); - -#ifdef Q_OS_WIN - installNativeEventFilter(&MyNativeEventFilter::getInstance()); -#endif - - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "styles/Inconsolata.otf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/fontawesome-webfont.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/hifi-glyphs.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/AnonymousPro-Regular.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/FiraSans-Regular.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/FiraSans-SemiBold.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Light.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Regular.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/rawline-500.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Bold.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-SemiBold.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Cairo-SemiBold.ttf"); - _window->setWindowTitle("High Fidelity Interface"); - - Model::setAbstractViewStateInterface(this); // The model class will sometimes need to know view state details from us - - auto nodeList = DependencyManager::get(); - nodeList->startThread(); - nodeList->setFlagTimeForConnectionStep(true); - - // move the AddressManager to the NodeList thread so that domain resets due to domain changes always occur - // before we tell MyAvatar to go to a new location in the new domain - auto addressManager = DependencyManager::get(); - addressManager->moveToThread(nodeList->thread()); - - const char** constArgv = const_cast(argv); - if (cmdOptionExists(argc, constArgv, "--disableWatchdog")) { - DISABLE_WATCHDOG = true; - } - // Set up a watchdog thread to intentionally crash the application on deadlocks - if (!DISABLE_WATCHDOG) { - auto deadlockWatchdogThread = new DeadlockWatchdogThread(); - deadlockWatchdogThread->setMainThreadID(QThread::currentThreadId()); - deadlockWatchdogThread->start(); - } - - // Set File Logger Session UUID - auto avatarManager = DependencyManager::get(); - auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr; - if (avatarManager) { - workload::SpacePointer space = getEntities()->getWorkloadSpace(); - avatarManager->setSpace(space); - } - auto accountManager = DependencyManager::get(); - -#ifndef Q_OS_ANDROID - _logger->setSessionID(accountManager->getSessionID()); -#endif - - setCrashAnnotation("metaverse_session_id", accountManager->getSessionID().toString().toStdString()); - setCrashAnnotation("main_thread_id", std::to_string((size_t)QThread::currentThreadId())); - - if (steamClient) { - qCDebug(interfaceapp) << "[VERSION] SteamVR buildID:" << steamClient->getSteamVRBuildID(); - } - setCrashAnnotation("steam", property(hifi::properties::STEAM).toBool() ? "1" : "0"); - - qCDebug(interfaceapp) << "[VERSION] Build sequence:" << qPrintable(applicationVersion()); - qCDebug(interfaceapp) << "[VERSION] MODIFIED_ORGANIZATION:" << BuildInfo::MODIFIED_ORGANIZATION; - qCDebug(interfaceapp) << "[VERSION] VERSION:" << BuildInfo::VERSION; - qCDebug(interfaceapp) << "[VERSION] BUILD_TYPE_STRING:" << BuildInfo::BUILD_TYPE_STRING; - qCDebug(interfaceapp) << "[VERSION] BUILD_GLOBAL_SERVICES:" << BuildInfo::BUILD_GLOBAL_SERVICES; -#if USE_STABLE_GLOBAL_SERVICES - qCDebug(interfaceapp) << "[VERSION] We will use STABLE global services."; -#else - qCDebug(interfaceapp) << "[VERSION] We will use DEVELOPMENT global services."; -#endif - - bool isStore = property(hifi::properties::OCULUS_STORE).toBool(); - - DependencyManager::get()->setLimitedCommerce(isStore); // Or we could make it a separate arg, or if either arg is set, etc. And should this instead by a hifi::properties? - - updateHeartbeat(); - - // setup a timer for domain-server check ins - QTimer* domainCheckInTimer = new QTimer(this); - QWeakPointer nodeListWeak = nodeList; - connect(domainCheckInTimer, &QTimer::timeout, [this, nodeListWeak] { - auto nodeList = nodeListWeak.lock(); - if (!isServerlessMode() && nodeList) { - nodeList->sendDomainServerCheckIn(); - } - }); - domainCheckInTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS); - connect(this, &QCoreApplication::aboutToQuit, [domainCheckInTimer] { - domainCheckInTimer->stop(); - domainCheckInTimer->deleteLater(); - }); - - { - auto audioIO = DependencyManager::get().data(); - audioIO->setPositionGetter([] { - auto avatarManager = DependencyManager::get(); - auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr; - - return myAvatar ? myAvatar->getPositionForAudio() : Vectors::ZERO; - }); - audioIO->setOrientationGetter([] { - auto avatarManager = DependencyManager::get(); - auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr; - - return myAvatar ? myAvatar->getOrientationForAudio() : Quaternions::IDENTITY; - }); - - recording::Frame::registerFrameHandler(AudioConstants::getAudioFrameName(), [&audioIO](recording::Frame::ConstPointer frame) { - audioIO->handleRecordedAudioInput(frame->data); - }); - - connect(audioIO, &AudioClient::inputReceived, [](const QByteArray& audio) { - static auto recorder = DependencyManager::get(); - if (recorder->isRecording()) { - static const recording::FrameType AUDIO_FRAME_TYPE = recording::Frame::registerFrameType(AudioConstants::getAudioFrameName()); - recorder->recordFrame(AUDIO_FRAME_TYPE, audio); - } - }); - audioIO->startThread(); - } - - // Make sure we don't time out during slow operations at startup - updateHeartbeat(); - - // Setup MessagesClient - DependencyManager::get()->startThread(); - - const DomainHandler& domainHandler = nodeList->getDomainHandler(); - - connect(&domainHandler, SIGNAL(domainURLChanged(QUrl)), SLOT(domainURLChanged(QUrl))); - connect(&domainHandler, SIGNAL(redirectToErrorDomainURL(QUrl)), SLOT(goToErrorDomainURL(QUrl))); - connect(&domainHandler, &DomainHandler::domainURLChanged, [](QUrl domainURL){ - setCrashAnnotation("domain", domainURL.toString().toStdString()); - }); - connect(&domainHandler, SIGNAL(resetting()), SLOT(resettingDomain())); - connect(&domainHandler, SIGNAL(connectedToDomain(QUrl)), SLOT(updateWindowTitle())); - connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle())); - connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, [this]() { - auto tabletScriptingInterface = DependencyManager::get(); - if (tabletScriptingInterface) { - tabletScriptingInterface->setQmlTabletRoot(SYSTEM_TABLET, nullptr); - } - auto entityScriptingInterface = DependencyManager::get(); - entityScriptingInterface->deleteEntity(getTabletScreenID()); - entityScriptingInterface->deleteEntity(getTabletHomeButtonID()); - entityScriptingInterface->deleteEntity(getTabletFrameID()); - _failedToConnectToEntityServer = false; - }); - - _entityServerConnectionTimer.setSingleShot(true); - connect(&_entityServerConnectionTimer, &QTimer::timeout, this, &Application::setFailedToConnectToEntityServer); - - connect(&domainHandler, &DomainHandler::connectedToDomain, this, [this]() { - if (!isServerlessMode()) { - _entityServerConnectionTimer.setInterval(ENTITY_SERVER_ADDED_TIMEOUT); - _entityServerConnectionTimer.start(); - _failedToConnectToEntityServer = false; - } - }); - connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &Application::domainConnectionRefused); - - nodeList->getDomainHandler().setErrorDomainURL(QUrl(REDIRECT_HIFI_ADDRESS)); - - // We could clear ATP assets only when changing domains, but it's possible that the domain you are connected - // to has gone down and switched to a new content set, so when you reconnect the cached ATP assets will no longer be valid. - connect(&domainHandler, &DomainHandler::disconnectedFromDomain, DependencyManager::get().data(), &ScriptCache::clearATPScriptsFromCache); - - // update our location every 5 seconds in the metaverse server, assuming that we are authenticated with one - const qint64 DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS = 5 * MSECS_PER_SECOND; - - auto discoverabilityManager = DependencyManager::get(); - connect(&locationUpdateTimer, &QTimer::timeout, discoverabilityManager.data(), &DiscoverabilityManager::updateLocation); - connect(&locationUpdateTimer, &QTimer::timeout, - DependencyManager::get().data(), &AddressManager::storeCurrentAddress); - locationUpdateTimer.start(DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS); - - // if we get a domain change, immediately attempt update location in metaverse server - connect(&nodeList->getDomainHandler(), &DomainHandler::connectedToDomain, - discoverabilityManager.data(), &DiscoverabilityManager::updateLocation); - - // send a location update immediately - discoverabilityManager->updateLocation(); - - connect(nodeList.data(), &NodeList::nodeAdded, this, &Application::nodeAdded); - connect(nodeList.data(), &NodeList::nodeKilled, this, &Application::nodeKilled); - connect(nodeList.data(), &NodeList::nodeActivated, this, &Application::nodeActivated); - connect(nodeList.data(), &NodeList::uuidChanged, myAvatar.get(), &MyAvatar::setSessionUUID); - connect(nodeList.data(), &NodeList::uuidChanged, this, &Application::setSessionUUID); - connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &Application::notifyPacketVersionMismatch); - - // you might think we could just do this in NodeList but we only want this connection for Interface - connect(&nodeList->getDomainHandler(), SIGNAL(limitOfSilentDomainCheckInsReached()), - nodeList.data(), SLOT(reset())); - - auto dialogsManager = DependencyManager::get(); -#if defined(Q_OS_ANDROID) - connect(accountManager.data(), &AccountManager::authRequired, this, []() { - auto addressManager = DependencyManager::get(); - AndroidHelper::instance().showLoginDialog(addressManager->currentAddress()); - }); -#else - connect(accountManager.data(), &AccountManager::authRequired, dialogsManager.data(), &DialogsManager::showLoginDialog); -#endif - connect(accountManager.data(), &AccountManager::usernameChanged, this, &Application::updateWindowTitle); - - // set the account manager's root URL and trigger a login request if we don't have the access token - accountManager->setIsAgent(true); - accountManager->setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL()); - - // use our MyAvatar position and quat for address manager path - addressManager->setPositionGetter([this]{ return getMyAvatar()->getWorldFeetPosition(); }); - addressManager->setOrientationGetter([this]{ return getMyAvatar()->getWorldOrientation(); }); - - connect(addressManager.data(), &AddressManager::hostChanged, this, &Application::updateWindowTitle); - connect(this, &QCoreApplication::aboutToQuit, addressManager.data(), &AddressManager::storeCurrentAddress); - - connect(this, &Application::activeDisplayPluginChanged, this, &Application::updateThreadPoolCount); - connect(this, &Application::activeDisplayPluginChanged, this, [](){ - qApp->setProperty(hifi::properties::HMD, qApp->isHMDMode()); - auto displayPlugin = qApp->getActiveDisplayPlugin(); - setCrashAnnotation("display_plugin", displayPlugin->getName().toStdString()); - setCrashAnnotation("hmd", displayPlugin->isHmd() ? "1" : "0"); - }); - connect(this, &Application::activeDisplayPluginChanged, this, &Application::updateSystemTabletMode); - connect(this, &Application::activeDisplayPluginChanged, this, [&](){ - if (getLoginDialogPoppedUp()) { - auto dialogsManager = DependencyManager::get(); - auto keyboard = DependencyManager::get(); - if (_firstRun.get()) { - // display mode changed. Don't allow auto-switch to work after this session. - _firstRun.set(false); - } - if (isHMDMode()) { - emit loginDialogFocusDisabled(); - dialogsManager->hideLoginDialog(); - createLoginDialog(); - } else { - DependencyManager::get()->deleteEntity(_loginDialogID); - _loginDialogID = QUuid(); - _loginStateManager.tearDown(); - dialogsManager->showLoginDialog(); - emit loginDialogFocusEnabled(); - } - } - }); - - // Save avatar location immediately after a teleport. - connect(myAvatar.get(), &MyAvatar::positionGoneTo, - DependencyManager::get().data(), &AddressManager::storeCurrentAddress); - - connect(myAvatar.get(), &MyAvatar::skeletonModelURLChanged, [](){ - QUrl avatarURL = qApp->getMyAvatar()->getSkeletonModelURL(); - setCrashAnnotation("avatar", avatarURL.toString().toStdString()); - }); - - - // Inititalize sample before registering - _sampleSound = DependencyManager::get()->getSound(PathUtils::resourcesUrl("sounds/sample.wav")); - - { - auto scriptEngines = DependencyManager::get().data(); - scriptEngines->registerScriptInitializer([this](ScriptEnginePointer engine) { - registerScriptEngineWithApplicationServices(engine); - }); - - connect(scriptEngines, &ScriptEngines::scriptCountChanged, this, [this] { - auto scriptEngines = DependencyManager::get(); - if (scriptEngines->getRunningScripts().isEmpty()) { - getMyAvatar()->clearScriptableSettings(); - } - }, Qt::QueuedConnection); - - connect(scriptEngines, &ScriptEngines::scriptsReloading, this, [this] { - getEntities()->reloadEntityScripts(); - loadAvatarScripts(getMyAvatar()->getScriptUrls()); - }, Qt::QueuedConnection); - - connect(scriptEngines, &ScriptEngines::scriptLoadError, - this, [](const QString& filename, const QString& error) { - OffscreenUi::asyncWarning(nullptr, "Error Loading Script", filename + " failed to load."); - }, Qt::QueuedConnection); - } - -#ifdef _WIN32 - WSADATA WsaData; - int wsaresult = WSAStartup(MAKEWORD(2, 2), &WsaData); -#endif - - // tell the NodeList instance who to tell the domain server we care about - nodeList->addSetOfNodeTypesToNodeInterestSet(NodeSet() << NodeType::AudioMixer << NodeType::AvatarMixer - << NodeType::EntityServer << NodeType::AssetServer << NodeType::MessagesMixer << NodeType::EntityScriptServer); - - // connect to the packet sent signal of the _entityEditSender - connect(&_entityEditSender, &EntityEditPacketSender::packetSent, this, &Application::packetSent); - connect(&_entityEditSender, &EntityEditPacketSender::addingEntityWithCertificate, this, &Application::addingEntityWithCertificate); - - QString concurrentDownloadsStr = getCmdOption(argc, constArgv, "--concurrent-downloads"); - bool success; - uint32_t concurrentDownloads = concurrentDownloadsStr.toUInt(&success); - if (!success) { - concurrentDownloads = MAX_CONCURRENT_RESOURCE_DOWNLOADS; - } - ResourceCache::setRequestLimit(concurrentDownloads); - - // perhaps override the avatar url. Since we will test later for validity - // we don't need to do so here. - QString avatarURL = getCmdOption(argc, constArgv, "--avatarURL"); - _avatarOverrideUrl = QUrl::fromUserInput(avatarURL); - - // If someone specifies both --avatarURL and --replaceAvatarURL, - // the replaceAvatarURL wins. So only set the _overrideUrl if this - // does have a non-empty string. - QString replaceURL = getCmdOption(argc, constArgv, "--replaceAvatarURL"); - if (!replaceURL.isEmpty()) { - _avatarOverrideUrl = QUrl::fromUserInput(replaceURL); - _saveAvatarOverrideUrl = true; - } - - _glWidget = new GLCanvas(); - getApplicationCompositor().setRenderingWidget(_glWidget); - _window->setCentralWidget(_glWidget); - - _window->restoreGeometry(); - _window->setVisible(true); - - _glWidget->setFocusPolicy(Qt::StrongFocus); - _glWidget->setFocus(); - - if (cmdOptionExists(argc, constArgv, "--system-cursor")) { - _preferredCursor.set(Cursor::Manager::getIconName(Cursor::Icon::SYSTEM)); - } - showCursor(Cursor::Manager::lookupIcon(_preferredCursor.get())); - - // enable mouse tracking; otherwise, we only get drag events - _glWidget->setMouseTracking(true); - // Make sure the window is set to the correct size by processing the pending events - QCoreApplication::processEvents(); - - // Create the main thread context, the GPU backend - initializeGL(); - qCDebug(interfaceapp, "Initialized GL"); - - // Initialize the display plugin architecture - initializeDisplayPlugins(); - qCDebug(interfaceapp, "Initialized Display"); - - // An audio device changed signal received before the display plugins are set up will cause a crash, - // so we defer the setup of the `scripting::Audio` class until this point - { - auto audioScriptingInterface = DependencyManager::set(); - auto audioIO = DependencyManager::get().data(); - connect(audioIO, &AudioClient::mutedByMixer, audioScriptingInterface.data(), &AudioScriptingInterface::mutedByMixer); - connect(audioIO, &AudioClient::receivedFirstPacket, audioScriptingInterface.data(), &AudioScriptingInterface::receivedFirstPacket); - connect(audioIO, &AudioClient::disconnected, audioScriptingInterface.data(), &AudioScriptingInterface::disconnected); - connect(audioIO, &AudioClient::muteEnvironmentRequested, [](glm::vec3 position, float radius) { - auto audioClient = DependencyManager::get(); - auto audioScriptingInterface = DependencyManager::get(); - auto myAvatarPosition = DependencyManager::get()->getMyAvatar()->getWorldPosition(); - float distance = glm::distance(myAvatarPosition, position); - - if (distance < radius) { - audioClient->setMuted(true); - audioScriptingInterface->environmentMuted(); - } - }); - connect(this, &Application::activeDisplayPluginChanged, - reinterpret_cast(audioScriptingInterface.data()), &scripting::Audio::onContextChanged); - } - - // Create the rendering engine. This can be slow on some machines due to lots of - // GPU pipeline creation. - initializeRenderEngine(); - qCDebug(interfaceapp, "Initialized Render Engine."); - - // Overlays need to exist before we set the ContextOverlayInterface dependency - _overlays.init(); // do this before scripts load - DependencyManager::set(); - - // Initialize the user interface and menu system - // Needs to happen AFTER the render engine initialization to access its configuration - initializeUi(); - - init(); - qCDebug(interfaceapp, "init() complete."); - - // create thread for parsing of octree data independent of the main network and rendering threads - _octreeProcessor.initialize(_enableProcessOctreeThread); - connect(&_octreeProcessor, &OctreePacketProcessor::packetVersionMismatch, this, &Application::notifyPacketVersionMismatch); - _entityEditSender.initialize(_enableProcessOctreeThread); - - _idleLoopStdev.reset(); - - // update before the first render - update(0); - - // Make sure we don't time out during slow operations at startup - updateHeartbeat(); - - static const QString TESTER = "HIFI_TESTER"; - bool isTester = false; -#if defined (Q_OS_ANDROID) - // Since we cannot set environment variables in Android we use a file presence - // to denote that this is a testing device - QFileInfo check_tester_file(TESTER_FILE); - isTester = check_tester_file.exists() && check_tester_file.isFile(); -#endif - - constexpr auto INSTALLER_INI_NAME = "installer.ini"; - auto iniPath = QDir(applicationDirPath()).filePath(INSTALLER_INI_NAME); - QFile installerFile { iniPath }; - std::unordered_map installerKeyValues; - if (installerFile.open(QIODevice::ReadOnly)) { - while (!installerFile.atEnd()) { - auto line = installerFile.readLine(); - if (!line.isEmpty()) { - auto index = line.indexOf("="); - if (index >= 0) { - installerKeyValues[line.mid(0, index).trimmed()] = line.mid(index + 1).trimmed(); - } - } - } - } - - // In practice we shouldn't run across installs that don't have a known installer type. - // Client or Client+Server installs should always have the installer.ini next to their - // respective interface.exe, and Steam installs will be detected as such. If a user were - // to delete the installer.ini, though, and as an example, we won't know the context of the - // original install. - constexpr auto INSTALLER_KEY_TYPE = "type"; - constexpr auto INSTALLER_KEY_CAMPAIGN = "campaign"; - constexpr auto INSTALLER_TYPE_UNKNOWN = "unknown"; - constexpr auto INSTALLER_TYPE_STEAM = "steam"; - - auto typeIt = installerKeyValues.find(INSTALLER_KEY_TYPE); - QString installerType = INSTALLER_TYPE_UNKNOWN; - if (typeIt == installerKeyValues.end()) { - if (property(hifi::properties::STEAM).toBool()) { - installerType = INSTALLER_TYPE_STEAM; - } - } else { - installerType = typeIt->second; - } - - auto campaignIt = installerKeyValues.find(INSTALLER_KEY_CAMPAIGN); - QString installerCampaign = campaignIt != installerKeyValues.end() ? campaignIt->second : ""; - - qDebug() << "Detected installer type:" << installerType; - qDebug() << "Detected installer campaign:" << installerCampaign; - - auto& userActivityLogger = UserActivityLogger::getInstance(); - if (userActivityLogger.isEnabled()) { - // sessionRunTime will be reset soon by loadSettings. Grab it now to get previous session value. - // The value will be 0 if the user blew away settings this session, which is both a feature and a bug. - static const QString TESTER = "HIFI_TESTER"; - auto gpuIdent = GPUIdent::getInstance(); - auto glContextData = getGLContextData(); - QJsonObject properties = { - { "version", applicationVersion() }, - { "tester", QProcessEnvironment::systemEnvironment().contains(TESTER) || isTester }, - { "installer_campaign", installerCampaign }, - { "installer_type", installerType }, - { "build_type", BuildInfo::BUILD_TYPE_STRING }, - { "previousSessionCrashed", _previousSessionCrashed }, - { "previousSessionRuntime", sessionRunTime.get() }, - { "cpu_architecture", QSysInfo::currentCpuArchitecture() }, - { "kernel_type", QSysInfo::kernelType() }, - { "kernel_version", QSysInfo::kernelVersion() }, - { "os_type", QSysInfo::productType() }, - { "os_version", QSysInfo::productVersion() }, - { "gpu_name", gpuIdent->getName() }, - { "gpu_driver", gpuIdent->getDriver() }, - { "gpu_memory", static_cast(gpuIdent->getMemory()) }, - { "gl_version_int", glVersionToInteger(glContextData.value("version").toString()) }, - { "gl_version", glContextData["version"] }, - { "gl_vender", glContextData["vendor"] }, - { "gl_sl_version", glContextData["sl_version"] }, - { "gl_renderer", glContextData["renderer"] }, - { "ideal_thread_count", QThread::idealThreadCount() } - }; - auto macVersion = QSysInfo::macVersion(); - if (macVersion != QSysInfo::MV_None) { - properties["os_osx_version"] = QSysInfo::macVersion(); - } - auto windowsVersion = QSysInfo::windowsVersion(); - if (windowsVersion != QSysInfo::WV_None) { - properties["os_win_version"] = QSysInfo::windowsVersion(); - } - - ProcessorInfo procInfo; - if (getProcessorInfo(procInfo)) { - properties["processor_core_count"] = procInfo.numProcessorCores; - properties["logical_processor_count"] = procInfo.numLogicalProcessors; - properties["processor_l1_cache_count"] = procInfo.numProcessorCachesL1; - properties["processor_l2_cache_count"] = procInfo.numProcessorCachesL2; - properties["processor_l3_cache_count"] = procInfo.numProcessorCachesL3; - } - - properties["first_run"] = _firstRun.get(); - - // add the user's machine ID to the launch event - QString machineFingerPrint = uuidStringWithoutCurlyBraces(FingerprintUtils::getMachineFingerprint()); - properties["machine_fingerprint"] = machineFingerPrint; - - userActivityLogger.logAction("launch", properties); - } - - _entityEditSender.setMyAvatar(myAvatar.get()); - - // The entity octree will have to know about MyAvatar for the parentJointName import - getEntities()->getTree()->setMyAvatar(myAvatar); - _entityClipboard->setMyAvatar(myAvatar); - - // For now we're going to set the PPS for outbound packets to be super high, this is - // probably not the right long term solution. But for now, we're going to do this to - // allow you to move an entity around in your hand - _entityEditSender.setPacketsPerSecond(3000); // super high!! - - // Make sure we don't time out during slow operations at startup - updateHeartbeat(); - - connect(this, SIGNAL(aboutToQuit()), this, SLOT(onAboutToQuit())); - - // FIXME -- I'm a little concerned about this. - connect(myAvatar->getSkeletonModel().get(), &SkeletonModel::skeletonLoaded, - this, &Application::checkSkeleton, Qt::QueuedConnection); - - // Setup the userInputMapper with the actions - auto userInputMapper = DependencyManager::get(); - connect(userInputMapper.data(), &UserInputMapper::actionEvent, [this](int action, float state) { - using namespace controller; - auto tabletScriptingInterface = DependencyManager::get(); - { - auto actionEnum = static_cast(action); - int key = Qt::Key_unknown; - static int lastKey = Qt::Key_unknown; - bool navAxis = false; - switch (actionEnum) { - case Action::UI_NAV_VERTICAL: - navAxis = true; - if (state > 0.0f) { - key = Qt::Key_Up; - } else if (state < 0.0f) { - key = Qt::Key_Down; - } - break; - - case Action::UI_NAV_LATERAL: - navAxis = true; - if (state > 0.0f) { - key = Qt::Key_Right; - } else if (state < 0.0f) { - key = Qt::Key_Left; - } - break; - - case Action::UI_NAV_GROUP: - navAxis = true; - if (state > 0.0f) { - key = Qt::Key_Tab; - } else if (state < 0.0f) { - key = Qt::Key_Backtab; - } - break; - - case Action::UI_NAV_BACK: - key = Qt::Key_Escape; - break; - - case Action::UI_NAV_SELECT: - key = Qt::Key_Return; - break; - default: - break; - } - - auto window = tabletScriptingInterface->getTabletWindow(); - if (navAxis && window) { - if (lastKey != Qt::Key_unknown) { - QKeyEvent event(QEvent::KeyRelease, lastKey, Qt::NoModifier); - sendEvent(window, &event); - lastKey = Qt::Key_unknown; - } - - if (key != Qt::Key_unknown) { - QKeyEvent event(QEvent::KeyPress, key, Qt::NoModifier); - sendEvent(window, &event); - tabletScriptingInterface->processEvent(&event); - lastKey = key; - } - } else if (key != Qt::Key_unknown && window) { - if (state) { - QKeyEvent event(QEvent::KeyPress, key, Qt::NoModifier); - sendEvent(window, &event); - tabletScriptingInterface->processEvent(&event); - } else { - QKeyEvent event(QEvent::KeyRelease, key, Qt::NoModifier); - sendEvent(window, &event); - } - return; - } - } - - if (action == controller::toInt(controller::Action::RETICLE_CLICK)) { - auto reticlePos = getApplicationCompositor().getReticlePosition(); - QPoint localPos(reticlePos.x, reticlePos.y); // both hmd and desktop already handle this in our coordinates. - if (state) { - QMouseEvent mousePress(QEvent::MouseButtonPress, localPos, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); - sendEvent(_glWidget, &mousePress); - _reticleClickPressed = true; - } else { - QMouseEvent mouseRelease(QEvent::MouseButtonRelease, localPos, Qt::LeftButton, Qt::NoButton, Qt::NoModifier); - sendEvent(_glWidget, &mouseRelease); - _reticleClickPressed = false; - } - return; // nothing else to do - } - - if (state) { - if (action == controller::toInt(controller::Action::TOGGLE_MUTE)) { - auto audioClient = DependencyManager::get(); - audioClient->setMuted(!audioClient->isMuted()); - } else if (action == controller::toInt(controller::Action::CYCLE_CAMERA)) { - cycleCamera(); - } else if (action == controller::toInt(controller::Action::CONTEXT_MENU) && !isInterstitialMode()) { - toggleTabletUI(); - } else if (action == controller::toInt(controller::Action::RETICLE_X)) { - auto oldPos = getApplicationCompositor().getReticlePosition(); - getApplicationCompositor().setReticlePosition({ oldPos.x + state, oldPos.y }); - } else if (action == controller::toInt(controller::Action::RETICLE_Y)) { - auto oldPos = getApplicationCompositor().getReticlePosition(); - getApplicationCompositor().setReticlePosition({ oldPos.x, oldPos.y + state }); - } else if (action == controller::toInt(controller::Action::TOGGLE_OVERLAY)) { - toggleOverlays(); - } - } - }); - - _applicationStateDevice = userInputMapper->getStateDevice(); - - _applicationStateDevice->setInputVariant(STATE_IN_HMD, []() -> float { - return qApp->isHMDMode() ? 1 : 0; - }); - _applicationStateDevice->setInputVariant(STATE_CAMERA_FULL_SCREEN_MIRROR, []() -> float { - return qApp->getCamera().getMode() == CAMERA_MODE_MIRROR ? 1 : 0; - }); - _applicationStateDevice->setInputVariant(STATE_CAMERA_FIRST_PERSON, []() -> float { - return qApp->getCamera().getMode() == CAMERA_MODE_FIRST_PERSON ? 1 : 0; - }); - _applicationStateDevice->setInputVariant(STATE_CAMERA_THIRD_PERSON, []() -> float { - return qApp->getCamera().getMode() == CAMERA_MODE_THIRD_PERSON ? 1 : 0; - }); - _applicationStateDevice->setInputVariant(STATE_CAMERA_ENTITY, []() -> float { - return qApp->getCamera().getMode() == CAMERA_MODE_ENTITY ? 1 : 0; - }); - _applicationStateDevice->setInputVariant(STATE_CAMERA_INDEPENDENT, []() -> float { - return qApp->getCamera().getMode() == CAMERA_MODE_INDEPENDENT ? 1 : 0; - }); - _applicationStateDevice->setInputVariant(STATE_SNAP_TURN, []() -> float { - return qApp->getMyAvatar()->getSnapTurn() ? 1 : 0; - }); - _applicationStateDevice->setInputVariant(STATE_ADVANCED_MOVEMENT_CONTROLS, []() -> float { - return qApp->getMyAvatar()->useAdvancedMovementControls() ? 1 : 0; - }); - - _applicationStateDevice->setInputVariant(STATE_GROUNDED, []() -> float { - return qApp->getMyAvatar()->getCharacterController()->onGround() ? 1 : 0; - }); - _applicationStateDevice->setInputVariant(STATE_NAV_FOCUSED, []() -> float { - auto offscreenUi = getOffscreenUI(); - return offscreenUi ? (offscreenUi->navigationFocused() ? 1 : 0) : 0; - }); - _applicationStateDevice->setInputVariant(STATE_PLATFORM_WINDOWS, []() -> float { -#if defined(Q_OS_WIN) - return 1; -#else - return 0; -#endif - }); - _applicationStateDevice->setInputVariant(STATE_PLATFORM_MAC, []() -> float { -#if defined(Q_OS_MAC) - return 1; -#else - return 0; -#endif - }); - _applicationStateDevice->setInputVariant(STATE_PLATFORM_ANDROID, []() -> float { -#if defined(Q_OS_ANDROID) - return 1 ; -#else - return 0; -#endif - }); - - - // Setup the _keyboardMouseDevice, _touchscreenDevice, _touchscreenVirtualPadDevice and the user input mapper with the default bindings - userInputMapper->registerDevice(_keyboardMouseDevice->getInputDevice()); - // if the _touchscreenDevice is not supported it will not be registered - if (_touchscreenDevice) { - userInputMapper->registerDevice(_touchscreenDevice->getInputDevice()); - } - if (_touchscreenVirtualPadDevice) { - userInputMapper->registerDevice(_touchscreenVirtualPadDevice->getInputDevice()); - } - - QString scriptsSwitch = QString("--").append(SCRIPTS_SWITCH); - _defaultScriptsLocation = getCmdOption(argc, constArgv, scriptsSwitch.toStdString().c_str()); - - // Make sure we don't time out during slow operations at startup - updateHeartbeat(); - - loadSettings(); - - updateVerboseLogging(); - - // Now that we've loaded the menu and thus switched to the previous display plugin - // we can unlock the desktop repositioning code, since all the positions will be - // relative to the desktop size for this plugin - auto offscreenUi = getOffscreenUI(); - connect(offscreenUi.data(), &OffscreenUi::desktopReady, []() { - auto offscreenUi = getOffscreenUI(); - auto desktop = offscreenUi->getDesktop(); - if (desktop) { - desktop->setProperty("repositionLocked", false); - } - }); - - connect(offscreenUi.data(), &OffscreenUi::keyboardFocusActive, [this]() { -#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML) - // Do not show login dialog if requested not to on the command line - QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY); - int index = arguments().indexOf(hifiNoLoginCommandLineKey); - if (index != -1) { - resumeAfterLoginDialogActionTaken(); - return; - } - - showLoginScreen(); -#else - resumeAfterLoginDialogActionTaken(); -#endif - }); - - // Make sure we don't time out during slow operations at startup - updateHeartbeat(); - QTimer* settingsTimer = new QTimer(); - moveToNewNamedThread(settingsTimer, "Settings Thread", [this, settingsTimer]{ - // This needs to run on the settings thread, so we need to pass the `settingsTimer` as the - // receiver object, otherwise it will run on the application thread and trigger a warning - // about trying to kill the timer on the main thread. - connect(qApp, &Application::beforeAboutToQuit, settingsTimer, [this, settingsTimer]{ - // Disconnect the signal from the save settings - QObject::disconnect(settingsTimer, &QTimer::timeout, this, &Application::saveSettings); - // Stop the settings timer - settingsTimer->stop(); - // Delete it (this will trigger the thread destruction - settingsTimer->deleteLater(); - // Mark the settings thread as finished, so we know we can safely save in the main application - // shutdown code - _settingsGuard.trigger(); - }); - - int SAVE_SETTINGS_INTERVAL = 10 * MSECS_PER_SECOND; // Let's save every seconds for now - settingsTimer->setSingleShot(false); - settingsTimer->setInterval(SAVE_SETTINGS_INTERVAL); // 10s, Qt::CoarseTimer acceptable - QObject::connect(settingsTimer, &QTimer::timeout, this, &Application::saveSettings); - settingsTimer->start(); - }, QThread::LowestPriority); - - if (Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson)) { - getMyAvatar()->setBoomLength(MyAvatar::ZOOM_MIN); // So that camera doesn't auto-switch to third person. - } else if (Menu::getInstance()->isOptionChecked(MenuOption::IndependentMode)) { - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); - cameraMenuChanged(); - } else if (Menu::getInstance()->isOptionChecked(MenuOption::CameraEntityMode)) { - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); - cameraMenuChanged(); - } - - { - auto audioIO = DependencyManager::get().data(); - // set the local loopback interface for local sounds - AudioInjector::setLocalAudioInterface(audioIO); - auto audioScriptingInterface = DependencyManager::get(); - audioScriptingInterface->setLocalAudioInterface(audioIO); - connect(audioIO, &AudioClient::noiseGateOpened, audioScriptingInterface.data(), &AudioScriptingInterface::noiseGateOpened); - connect(audioIO, &AudioClient::noiseGateClosed, audioScriptingInterface.data(), &AudioScriptingInterface::noiseGateClosed); - connect(audioIO, &AudioClient::inputReceived, audioScriptingInterface.data(), &AudioScriptingInterface::inputReceived); - } - - this->installEventFilter(this); - - - -#ifdef HAVE_DDE - auto ddeTracker = DependencyManager::get(); - ddeTracker->init(); - connect(ddeTracker.data(), &FaceTracker::muteToggled, this, &Application::faceTrackerMuteToggled); -#endif - -#ifdef HAVE_IVIEWHMD - auto eyeTracker = DependencyManager::get(); - eyeTracker->init(); - setActiveEyeTracker(); -#endif - - // If launched from Steam, let it handle updates - const QString HIFI_NO_UPDATER_COMMAND_LINE_KEY = "--no-updater"; - bool noUpdater = arguments().indexOf(HIFI_NO_UPDATER_COMMAND_LINE_KEY) != -1; - bool buildCanUpdate = BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Stable - || BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Master; - if (!noUpdater && buildCanUpdate) { - constexpr auto INSTALLER_TYPE_CLIENT_ONLY = "client_only"; - - auto applicationUpdater = DependencyManager::set(); - - AutoUpdater::InstallerType type = installerType == INSTALLER_TYPE_CLIENT_ONLY - ? AutoUpdater::InstallerType::CLIENT_ONLY : AutoUpdater::InstallerType::FULL; - - applicationUpdater->setInstallerType(type); - applicationUpdater->setInstallerCampaign(installerCampaign); - connect(applicationUpdater.data(), &AutoUpdater::newVersionIsAvailable, dialogsManager.data(), &DialogsManager::showUpdateDialog); - applicationUpdater->checkForUpdate(); - } - - Menu::getInstance()->setIsOptionChecked(MenuOption::ActionMotorControl, true); - -// FIXME spacemouse code still needs cleanup -#if 0 - // the 3Dconnexion device wants to be initialized after a window is displayed. - SpacemouseManager::getInstance().init(); -#endif - - // If the user clicks on an object, we will check that it's a web surface, and if so, set the focus to it - auto pointerManager = DependencyManager::get(); - auto keyboardFocusOperator = [this](const QUuid& id, const PointerEvent& event) { - if (event.shouldFocus()) { - auto keyboard = DependencyManager::get(); - if (getEntities()->wantsKeyboardFocus(id)) { - setKeyboardFocusEntity(id); - } else if (!keyboard->containsID(id)) { // FIXME: this is a hack to make the keyboard work for now, since the keys would otherwise steal focus - setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); - } - } - }; - connect(pointerManager.data(), &PointerManager::triggerBeginEntity, keyboardFocusOperator); - connect(pointerManager.data(), &PointerManager::triggerBeginOverlay, keyboardFocusOperator); - - auto entityScriptingInterface = DependencyManager::get(); - connect(entityScriptingInterface.data(), &EntityScriptingInterface::deletingEntity, this, [this](const EntityItemID& entityItemID) { - if (entityItemID == _keyboardFocusedEntity.get()) { - setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); - } - }, Qt::QueuedConnection); - - EntityTreeRenderer::setAddMaterialToEntityOperator([this](const QUuid& entityID, graphics::MaterialLayer material, const std::string& parentMaterialName) { - if (_aboutToQuit) { - return false; - } - - auto renderable = getEntities()->renderableForEntityId(entityID); - if (renderable) { - renderable->addMaterial(material, parentMaterialName); - return true; - } - - return false; - }); - EntityTreeRenderer::setRemoveMaterialFromEntityOperator([this](const QUuid& entityID, graphics::MaterialPointer material, const std::string& parentMaterialName) { - if (_aboutToQuit) { - return false; - } - - auto renderable = getEntities()->renderableForEntityId(entityID); - if (renderable) { - renderable->removeMaterial(material, parentMaterialName); - return true; - } - - return false; - }); - - EntityTreeRenderer::setAddMaterialToAvatarOperator([](const QUuid& avatarID, graphics::MaterialLayer material, const std::string& parentMaterialName) { - auto avatarManager = DependencyManager::get(); - auto avatar = avatarManager->getAvatarBySessionID(avatarID); - if (avatar) { - avatar->addMaterial(material, parentMaterialName); - return true; - } - return false; - }); - EntityTreeRenderer::setRemoveMaterialFromAvatarOperator([](const QUuid& avatarID, graphics::MaterialPointer material, const std::string& parentMaterialName) { - auto avatarManager = DependencyManager::get(); - auto avatar = avatarManager->getAvatarBySessionID(avatarID); - if (avatar) { - avatar->removeMaterial(material, parentMaterialName); - return true; - } - return false; - }); - - EntityTree::setGetEntityObjectOperator([this](const QUuid& id) -> QObject* { - auto entities = getEntities(); - if (auto entity = entities->renderableForEntityId(id)) { - return qobject_cast(entity.get()); - } - return nullptr; - }); - - EntityTree::setTextSizeOperator([this](const QUuid& id, const QString& text) { - auto entities = getEntities(); - if (auto entity = entities->renderableForEntityId(id)) { - if (auto renderable = std::dynamic_pointer_cast(entity)) { - return renderable->textSize(text); - } - } - return QSizeF(0.0f, 0.0f); - }); - - connect(this, &Application::aboutToQuit, [this]() { - setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); - }); - - // Add periodic checks to send user activity data - static int CHECK_NEARBY_AVATARS_INTERVAL_MS = 10000; - static int NEARBY_AVATAR_RADIUS_METERS = 10; - - // setup the stats interval depending on if the 1s faster hearbeat was requested - static const QString FAST_STATS_ARG = "--fast-heartbeat"; - static int SEND_STATS_INTERVAL_MS = arguments().indexOf(FAST_STATS_ARG) != -1 ? 1000 : 10000; - - static glm::vec3 lastAvatarPosition = myAvatar->getWorldPosition(); - static glm::mat4 lastHMDHeadPose = getHMDSensorPose(); - static controller::Pose lastLeftHandPose = myAvatar->getLeftHandPose(); - static controller::Pose lastRightHandPose = myAvatar->getRightHandPose(); - - // Periodically send fps as a user activity event - QTimer* sendStatsTimer = new QTimer(this); - sendStatsTimer->setInterval(SEND_STATS_INTERVAL_MS); // 10s, Qt::CoarseTimer acceptable - connect(sendStatsTimer, &QTimer::timeout, this, [this]() { - - QJsonObject properties = {}; - MemoryInfo memInfo; - if (getMemoryInfo(memInfo)) { - properties["system_memory_total"] = static_cast(memInfo.totalMemoryBytes); - properties["system_memory_used"] = static_cast(memInfo.usedMemoryBytes); - properties["process_memory_used"] = static_cast(memInfo.processUsedMemoryBytes); - } - - // content location and build info - useful for filtering stats - auto addressManager = DependencyManager::get(); - auto currentDomain = addressManager->currentShareableAddress(true).toString(); // domain only - auto currentPath = addressManager->currentPath(true); // with orientation - properties["current_domain"] = currentDomain; - properties["current_path"] = currentPath; - properties["build_version"] = BuildInfo::VERSION; - - auto displayPlugin = qApp->getActiveDisplayPlugin(); - - properties["render_rate"] = getRenderLoopRate(); - properties["target_render_rate"] = getTargetRenderFrameRate(); - properties["present_rate"] = displayPlugin->presentRate(); - properties["new_frame_present_rate"] = displayPlugin->newFramePresentRate(); - properties["dropped_frame_rate"] = displayPlugin->droppedFrameRate(); - properties["stutter_rate"] = displayPlugin->stutterRate(); - properties["game_rate"] = getGameLoopRate(); - properties["has_async_reprojection"] = displayPlugin->hasAsyncReprojection(); - properties["hardware_stats"] = displayPlugin->getHardwareStats(); - - // deadlock watchdog related stats - properties["deadlock_watchdog_maxElapsed"] = (int)DeadlockWatchdogThread::_maxElapsed; - properties["deadlock_watchdog_maxElapsedAverage"] = (int)DeadlockWatchdogThread::_maxElapsedAverage; - - auto nodeList = DependencyManager::get(); - properties["packet_rate_in"] = nodeList->getInboundPPS(); - properties["packet_rate_out"] = nodeList->getOutboundPPS(); - properties["kbps_in"] = nodeList->getInboundKbps(); - properties["kbps_out"] = nodeList->getOutboundKbps(); - - SharedNodePointer entityServerNode = nodeList->soloNodeOfType(NodeType::EntityServer); - SharedNodePointer audioMixerNode = nodeList->soloNodeOfType(NodeType::AudioMixer); - SharedNodePointer avatarMixerNode = nodeList->soloNodeOfType(NodeType::AvatarMixer); - SharedNodePointer assetServerNode = nodeList->soloNodeOfType(NodeType::AssetServer); - SharedNodePointer messagesMixerNode = nodeList->soloNodeOfType(NodeType::MessagesMixer); - properties["entity_ping"] = entityServerNode ? entityServerNode->getPingMs() : -1; - properties["audio_ping"] = audioMixerNode ? audioMixerNode->getPingMs() : -1; - properties["avatar_ping"] = avatarMixerNode ? avatarMixerNode->getPingMs() : -1; - properties["asset_ping"] = assetServerNode ? assetServerNode->getPingMs() : -1; - properties["messages_ping"] = messagesMixerNode ? messagesMixerNode->getPingMs() : -1; - properties["atp_in_kbps"] = assetServerNode ? assetServerNode->getInboundKbps() : 0.0f; - - auto loadingRequests = ResourceCache::getLoadingRequests(); - - QJsonArray loadingRequestsStats; - for (const auto& request : loadingRequests) { - QJsonObject requestStats; - requestStats["filename"] = request->getURL().fileName(); - requestStats["received"] = request->getBytesReceived(); - requestStats["total"] = request->getBytesTotal(); - requestStats["attempts"] = (int)request->getDownloadAttempts(); - loadingRequestsStats.append(requestStats); - } - - properties["active_downloads"] = loadingRequests.size(); - properties["pending_downloads"] = (int)ResourceCache::getPendingRequestCount(); - properties["active_downloads_details"] = loadingRequestsStats; - - auto statTracker = DependencyManager::get(); - - properties["processing_resources"] = statTracker->getStat("Processing").toInt(); - properties["pending_processing_resources"] = statTracker->getStat("PendingProcessing").toInt(); - - QJsonObject startedRequests; - startedRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_STARTED).toInt(); - startedRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_STARTED).toInt(); - startedRequests["file"] = statTracker->getStat(STAT_FILE_REQUEST_STARTED).toInt(); - startedRequests["total"] = startedRequests["atp"].toInt() + startedRequests["http"].toInt() - + startedRequests["file"].toInt(); - properties["started_requests"] = startedRequests; - - QJsonObject successfulRequests; - successfulRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_SUCCESS).toInt(); - successfulRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_SUCCESS).toInt(); - successfulRequests["file"] = statTracker->getStat(STAT_FILE_REQUEST_SUCCESS).toInt(); - successfulRequests["total"] = successfulRequests["atp"].toInt() + successfulRequests["http"].toInt() - + successfulRequests["file"].toInt(); - properties["successful_requests"] = successfulRequests; - - QJsonObject failedRequests; - failedRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_FAILED).toInt(); - failedRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_FAILED).toInt(); - failedRequests["file"] = statTracker->getStat(STAT_FILE_REQUEST_FAILED).toInt(); - failedRequests["total"] = failedRequests["atp"].toInt() + failedRequests["http"].toInt() - + failedRequests["file"].toInt(); - properties["failed_requests"] = failedRequests; - - QJsonObject cacheRequests; - cacheRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_CACHE).toInt(); - cacheRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_CACHE).toInt(); - cacheRequests["total"] = cacheRequests["atp"].toInt() + cacheRequests["http"].toInt(); - properties["cache_requests"] = cacheRequests; - - QJsonObject atpMappingRequests; - atpMappingRequests["started"] = statTracker->getStat(STAT_ATP_MAPPING_REQUEST_STARTED).toInt(); - atpMappingRequests["failed"] = statTracker->getStat(STAT_ATP_MAPPING_REQUEST_FAILED).toInt(); - atpMappingRequests["successful"] = statTracker->getStat(STAT_ATP_MAPPING_REQUEST_SUCCESS).toInt(); - properties["atp_mapping_requests"] = atpMappingRequests; - - properties["throttled"] = _displayPlugin ? _displayPlugin->isThrottled() : false; - - QJsonObject bytesDownloaded; - auto atpBytes = statTracker->getStat(STAT_ATP_RESOURCE_TOTAL_BYTES).toLongLong(); - auto httpBytes = statTracker->getStat(STAT_HTTP_RESOURCE_TOTAL_BYTES).toLongLong(); - auto fileBytes = statTracker->getStat(STAT_FILE_RESOURCE_TOTAL_BYTES).toLongLong(); - bytesDownloaded["atp"] = atpBytes; - bytesDownloaded["http"] = httpBytes; - bytesDownloaded["file"] = fileBytes; - bytesDownloaded["total"] = atpBytes + httpBytes + fileBytes; - properties["bytes_downloaded"] = bytesDownloaded; - - auto myAvatar = getMyAvatar(); - glm::vec3 avatarPosition = myAvatar->getWorldPosition(); - properties["avatar_has_moved"] = lastAvatarPosition != avatarPosition; - lastAvatarPosition = avatarPosition; - - auto entityScriptingInterface = DependencyManager::get(); - auto entityActivityTracking = entityScriptingInterface->getActivityTracking(); - entityScriptingInterface->resetActivityTracking(); - properties["added_entity_cnt"] = entityActivityTracking.addedEntityCount; - properties["deleted_entity_cnt"] = entityActivityTracking.deletedEntityCount; - properties["edited_entity_cnt"] = entityActivityTracking.editedEntityCount; - - NodeToOctreeSceneStats* octreeServerSceneStats = getOcteeSceneStats(); - unsigned long totalServerOctreeElements = 0; - for (NodeToOctreeSceneStatsIterator i = octreeServerSceneStats->begin(); i != octreeServerSceneStats->end(); i++) { - totalServerOctreeElements += i->second.getTotalElements(); - } - - properties["local_octree_elements"] = (qint64) OctreeElement::getInternalNodeCount(); - properties["server_octree_elements"] = (qint64) totalServerOctreeElements; - - properties["active_display_plugin"] = getActiveDisplayPlugin()->getName(); - properties["using_hmd"] = isHMDMode(); - - _autoSwitchDisplayModeSupportedHMDPlugin = nullptr; - foreach(DisplayPluginPointer displayPlugin, PluginManager::getInstance()->getDisplayPlugins()) { - if (displayPlugin->isHmd() && - displayPlugin->getSupportsAutoSwitch()) { - _autoSwitchDisplayModeSupportedHMDPlugin = displayPlugin; - _autoSwitchDisplayModeSupportedHMDPluginName = - _autoSwitchDisplayModeSupportedHMDPlugin->getName(); - _previousHMDWornStatus = - _autoSwitchDisplayModeSupportedHMDPlugin->isDisplayVisible(); - break; - } - } - - if (_autoSwitchDisplayModeSupportedHMDPlugin) { - if (getActiveDisplayPlugin() != _autoSwitchDisplayModeSupportedHMDPlugin && - !_autoSwitchDisplayModeSupportedHMDPlugin->isSessionActive()) { - startHMDStandBySession(); - } - // Poll periodically to check whether the user has worn HMD or not. Switch Display mode accordingly. - // If the user wears HMD then switch to VR mode. If the user removes HMD then switch to Desktop mode. - QTimer* autoSwitchDisplayModeTimer = new QTimer(this); - connect(autoSwitchDisplayModeTimer, SIGNAL(timeout()), this, SLOT(switchDisplayMode())); - autoSwitchDisplayModeTimer->start(INTERVAL_TO_CHECK_HMD_WORN_STATUS); - } - - auto glInfo = getGLContextData(); - properties["gl_info"] = glInfo; - properties["gpu_used_memory"] = (int)BYTES_TO_MB(gpu::Context::getUsedGPUMemSize()); - properties["gpu_free_memory"] = (int)BYTES_TO_MB(gpu::Context::getFreeGPUMemSize()); - properties["gpu_frame_time"] = (float)(qApp->getGPUContext()->getFrameTimerGPUAverage()); - properties["batch_frame_time"] = (float)(qApp->getGPUContext()->getFrameTimerBatchAverage()); - properties["ideal_thread_count"] = QThread::idealThreadCount(); - - auto hmdHeadPose = getHMDSensorPose(); - properties["hmd_head_pose_changed"] = isHMDMode() && (hmdHeadPose != lastHMDHeadPose); - lastHMDHeadPose = hmdHeadPose; - - auto leftHandPose = myAvatar->getLeftHandPose(); - auto rightHandPose = myAvatar->getRightHandPose(); - // controller::Pose considers two poses to be different if either are invalid. In our case, we actually - // want to consider the pose to be unchanged if it was invalid and still is invalid, so we check that first. - properties["hand_pose_changed"] = - ((leftHandPose.valid || lastLeftHandPose.valid) && (leftHandPose != lastLeftHandPose)) - || ((rightHandPose.valid || lastRightHandPose.valid) && (rightHandPose != lastRightHandPose)); - lastLeftHandPose = leftHandPose; - lastRightHandPose = rightHandPose; - - UserActivityLogger::getInstance().logAction("stats", properties); - }); - sendStatsTimer->start(); - - // Periodically check for count of nearby avatars - static int lastCountOfNearbyAvatars = -1; - QTimer* checkNearbyAvatarsTimer = new QTimer(this); - checkNearbyAvatarsTimer->setInterval(CHECK_NEARBY_AVATARS_INTERVAL_MS); // 10 seconds, Qt::CoarseTimer ok - connect(checkNearbyAvatarsTimer, &QTimer::timeout, this, []() { - auto avatarManager = DependencyManager::get(); - int nearbyAvatars = avatarManager->numberOfAvatarsInRange(avatarManager->getMyAvatar()->getWorldPosition(), - NEARBY_AVATAR_RADIUS_METERS) - 1; - if (nearbyAvatars != lastCountOfNearbyAvatars) { - lastCountOfNearbyAvatars = nearbyAvatars; - UserActivityLogger::getInstance().logAction("nearby_avatars", { { "count", nearbyAvatars } }); - } - }); - checkNearbyAvatarsTimer->start(); - - // Track user activity event when we receive a mute packet - auto onMutedByMixer = []() { - UserActivityLogger::getInstance().logAction("received_mute_packet"); - }; - connect(DependencyManager::get().data(), &AudioClient::mutedByMixer, this, onMutedByMixer); - - // Track when the address bar is opened - auto onAddressBarShown = [this]() { - // Record time - UserActivityLogger::getInstance().logAction("opened_address_bar", { { "uptime_ms", _sessionRunTimer.elapsed() } }); - }; - connect(DependencyManager::get().data(), &DialogsManager::addressBarShown, this, onAddressBarShown); - - // Make sure we don't time out during slow operations at startup - updateHeartbeat(); - - OctreeEditPacketSender* packetSender = entityScriptingInterface->getPacketSender(); - EntityEditPacketSender* entityPacketSender = static_cast(packetSender); - entityPacketSender->setMyAvatar(myAvatar.get()); - - connect(this, &Application::applicationStateChanged, this, &Application::activeChanged); - connect(_window, SIGNAL(windowMinimizedChanged(bool)), this, SLOT(windowMinimizedChanged(bool))); - qCDebug(interfaceapp, "Startup time: %4.2f seconds.", (double)startupTimer.elapsed() / 1000.0); - - EntityTreeRenderer::setEntitiesShouldFadeFunction([this]() { - SharedNodePointer entityServerNode = DependencyManager::get()->soloNodeOfType(NodeType::EntityServer); - return entityServerNode && !isPhysicsEnabled(); - }); - - _snapshotSound = DependencyManager::get()->getSound(PathUtils::resourcesUrl("sounds/snapshot/snap.wav")); - - // Monitor model assets (e.g., from Clara.io) added to the world that may need resizing. - static const int ADD_ASSET_TO_WORLD_TIMER_INTERVAL_MS = 1000; - _addAssetToWorldResizeTimer.setInterval(ADD_ASSET_TO_WORLD_TIMER_INTERVAL_MS); // 1s, Qt::CoarseTimer acceptable - connect(&_addAssetToWorldResizeTimer, &QTimer::timeout, this, &Application::addAssetToWorldCheckModelSize); - - // Auto-update and close adding asset to world info message box. - static const int ADD_ASSET_TO_WORLD_INFO_TIMEOUT_MS = 5000; - _addAssetToWorldInfoTimer.setInterval(ADD_ASSET_TO_WORLD_INFO_TIMEOUT_MS); // 5s, Qt::CoarseTimer acceptable - _addAssetToWorldInfoTimer.setSingleShot(true); - connect(&_addAssetToWorldInfoTimer, &QTimer::timeout, this, &Application::addAssetToWorldInfoTimeout); - static const int ADD_ASSET_TO_WORLD_ERROR_TIMEOUT_MS = 8000; - _addAssetToWorldErrorTimer.setInterval(ADD_ASSET_TO_WORLD_ERROR_TIMEOUT_MS); // 8s, Qt::CoarseTimer acceptable - _addAssetToWorldErrorTimer.setSingleShot(true); - connect(&_addAssetToWorldErrorTimer, &QTimer::timeout, this, &Application::addAssetToWorldErrorTimeout); - - connect(this, &QCoreApplication::aboutToQuit, this, &Application::addAssetToWorldMessageClose); - connect(&domainHandler, &DomainHandler::domainURLChanged, this, &Application::addAssetToWorldMessageClose); - connect(&domainHandler, &DomainHandler::redirectToErrorDomainURL, this, &Application::addAssetToWorldMessageClose); - - updateSystemTabletMode(); - - connect(&_myCamera, &Camera::modeUpdated, this, &Application::cameraModeChanged); - - DependencyManager::get()->setShouldPickHUDOperator([]() { return DependencyManager::get()->isHMDMode(); }); - DependencyManager::get()->setCalculatePos2DFromHUDOperator([this](const glm::vec3& intersection) { - const glm::vec2 MARGIN(25.0f); - glm::vec2 maxPos = _controllerScriptingInterface->getViewportDimensions() - MARGIN; - glm::vec2 pos2D = DependencyManager::get()->overlayFromWorldPoint(intersection); - return glm::max(MARGIN, glm::min(pos2D, maxPos)); - }); - - // Setup the mouse ray pick and related operators - { - auto mouseRayPick = std::make_shared(Vectors::ZERO, Vectors::UP, PickFilter(PickScriptingInterface::PICK_ENTITIES() | PickScriptingInterface::PICK_LOCAL_ENTITIES()), 0.0f, true); - mouseRayPick->parentTransform = std::make_shared(); - mouseRayPick->setJointState(PickQuery::JOINT_STATE_MOUSE); - auto mouseRayPickID = DependencyManager::get()->addPick(PickQuery::Ray, mouseRayPick); - DependencyManager::get()->setMouseRayPickID(mouseRayPickID); - } - DependencyManager::get()->setMouseRayPickResultOperator([](unsigned int rayPickID) { - RayToEntityIntersectionResult entityResult; - entityResult.intersects = false; - auto pickResult = DependencyManager::get()->getPrevPickResultTyped(rayPickID); - if (pickResult) { - entityResult.intersects = pickResult->type != IntersectionType::NONE; - if (entityResult.intersects) { - entityResult.intersection = pickResult->intersection; - entityResult.distance = pickResult->distance; - entityResult.surfaceNormal = pickResult->surfaceNormal; - entityResult.entityID = pickResult->objectID; - entityResult.extraInfo = pickResult->extraInfo; - } - } - return entityResult; - }); - DependencyManager::get()->setSetPrecisionPickingOperator([](unsigned int rayPickID, bool value) { - DependencyManager::get()->setPrecisionPicking(rayPickID, value); - }); - - EntityItem::setBillboardRotationOperator([this](const glm::vec3& position, const glm::quat& rotation, BillboardMode billboardMode, const glm::vec3& frustumPos) { - if (billboardMode == BillboardMode::YAW) { - //rotate about vertical to face the camera - glm::vec3 dPosition = frustumPos - position; - // If x and z are 0, atan(x, z) is undefined, so default to 0 degrees - float yawRotation = dPosition.x == 0.0f && dPosition.z == 0.0f ? 0.0f : glm::atan(dPosition.x, dPosition.z); - return glm::quat(glm::vec3(0.0f, yawRotation, 0.0f)); - } else if (billboardMode == BillboardMode::FULL) { - // use the referencial from the avatar, y isn't always up - glm::vec3 avatarUP = DependencyManager::get()->getMyAvatar()->getWorldOrientation() * Vectors::UP; - // check to see if glm::lookAt will work / using glm::lookAt variable name - glm::highp_vec3 s(glm::cross(position - frustumPos, avatarUP)); - - // make sure s is not NaN for any component - if (glm::length2(s) > 0.0f) { - return glm::conjugate(glm::toQuat(glm::lookAt(frustumPos, position, avatarUP))); - } - } - return rotation; - }); - EntityItem::setPrimaryViewFrustumPositionOperator([this]() { - ViewFrustum viewFrustum; - copyViewFrustum(viewFrustum); - return viewFrustum.getPosition(); - }); - - render::entities::WebEntityRenderer::setAcquireWebSurfaceOperator([this](const QString& url, bool htmlContent, QSharedPointer& webSurface, bool& cachedWebSurface) { - bool isTablet = url == TabletScriptingInterface::QML; - if (htmlContent) { - webSurface = DependencyManager::get()->acquire(render::entities::WebEntityRenderer::QML); - cachedWebSurface = true; - auto rootItemLoadedFunctor = [url, webSurface] { - webSurface->getRootItem()->setProperty(render::entities::WebEntityRenderer::URL_PROPERTY, url); - }; - if (webSurface->getRootItem()) { - rootItemLoadedFunctor(); - } else { - QObject::connect(webSurface.data(), &hifi::qml::OffscreenSurface::rootContextCreated, rootItemLoadedFunctor); - } - auto surfaceContext = webSurface->getSurfaceContext(); - surfaceContext->setContextProperty("KeyboardScriptingInterface", DependencyManager::get().data()); - } else { - // FIXME: the tablet should use the OffscreenQmlSurfaceCache - webSurface = QSharedPointer(new OffscreenQmlSurface(), [](OffscreenQmlSurface* webSurface) { - AbstractViewStateInterface::instance()->sendLambdaEvent([webSurface] { - // WebEngineView may run other threads (wasapi), so they must be deleted for a clean shutdown - // if the application has already stopped its event loop, delete must be explicit - delete webSurface; - }); - }); - auto rootItemLoadedFunctor = [webSurface, url, isTablet] { - Application::setupQmlSurface(webSurface->getSurfaceContext(), isTablet || url == LOGIN_DIALOG.toString()); - }; - if (webSurface->getRootItem()) { - rootItemLoadedFunctor(); - } else { - QObject::connect(webSurface.data(), &hifi::qml::OffscreenSurface::rootContextCreated, rootItemLoadedFunctor); - } - webSurface->load(url); - cachedWebSurface = false; - } - const uint8_t DEFAULT_MAX_FPS = 10; - const uint8_t TABLET_FPS = 90; - webSurface->setMaxFps(isTablet ? TABLET_FPS : DEFAULT_MAX_FPS); - }); - render::entities::WebEntityRenderer::setReleaseWebSurfaceOperator([this](QSharedPointer& webSurface, bool& cachedWebSurface, std::vector& connections) { - QQuickItem* rootItem = webSurface->getRootItem(); - - // Fix for crash in QtWebEngineCore when rapidly switching domains - // Call stop on the QWebEngineView before destroying OffscreenQMLSurface. - if (rootItem && !cachedWebSurface) { - // stop loading - QMetaObject::invokeMethod(rootItem, "stop"); - } - - webSurface->pause(); - - for (auto& connection : connections) { - QObject::disconnect(connection); - } - connections.clear(); - - // If the web surface was fetched out of the cache, release it back into the cache - if (cachedWebSurface) { - // If it's going back into the cache make sure to explicitly set the URL to a blank page - // in order to stop any resource consumption or audio related to the page. - if (rootItem) { - rootItem->setProperty("url", "about:blank"); - } - auto offscreenCache = DependencyManager::get(); - if (offscreenCache) { - offscreenCache->release(render::entities::WebEntityRenderer::QML, webSurface); - } - cachedWebSurface = false; - } - webSurface.reset(); - }); - - // Preload Tablet sounds - DependencyManager::get()->setEntityTree(qApp->getEntities()->getTree()); - DependencyManager::get()->preloadSounds(); - DependencyManager::get()->createKeyboard(); - - _pendingIdleEvent = false; - _graphicsEngine.startup(); - - qCDebug(interfaceapp) << "Metaverse session ID is" << uuidStringWithoutCurlyBraces(accountManager->getSessionID()); - -#if defined(Q_OS_ANDROID) - connect(&AndroidHelper::instance(), &AndroidHelper::beforeEnterBackground, this, &Application::beforeEnterBackground); - connect(&AndroidHelper::instance(), &AndroidHelper::enterBackground, this, &Application::enterBackground); - connect(&AndroidHelper::instance(), &AndroidHelper::enterForeground, this, &Application::enterForeground); - connect(&AndroidHelper::instance(), &AndroidHelper::toggleAwayMode, this, &Application::toggleAwayMode); - AndroidHelper::instance().notifyLoadComplete(); -#endif - pauseUntilLoginDetermined(); -} - -void Application::updateVerboseLogging() { - auto menu = Menu::getInstance(); - if (!menu) { - return; - } - bool enable = menu->isOptionChecked(MenuOption::VerboseLogging); - - QString rules = - "hifi.*.info=%1\n" - "hifi.audio-stream.debug=false\n" - "hifi.audio-stream.info=false"; - rules = rules.arg(enable ? "true" : "false"); - QLoggingCategory::setFilterRules(rules); -} - -void Application::domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo) { - DomainHandler::ConnectionRefusedReason reasonCode = static_cast(reasonCodeInt); - - if (reasonCode == DomainHandler::ConnectionRefusedReason::TooManyUsers && !extraInfo.isEmpty()) { - DependencyManager::get()->handleLookupString(extraInfo); - return; - } - - switch (reasonCode) { - case DomainHandler::ConnectionRefusedReason::ProtocolMismatch: - case DomainHandler::ConnectionRefusedReason::TooManyUsers: - case DomainHandler::ConnectionRefusedReason::Unknown: { - QString message = "Unable to connect to the location you are visiting.\n"; - message += reasonMessage; - OffscreenUi::asyncWarning("", message); - getMyAvatar()->setWorldVelocity(glm::vec3(0.0f)); - break; - } - default: - // nothing to do. - break; - } -} - -QString Application::getUserAgent() { - if (QThread::currentThread() != thread()) { - QString userAgent; - - BLOCKING_INVOKE_METHOD(this, "getUserAgent", Q_RETURN_ARG(QString, userAgent)); - - return userAgent; - } - - QString userAgent = "Mozilla/5.0 (HighFidelityInterface/" + BuildInfo::VERSION + "; " - + QSysInfo::productType() + " " + QSysInfo::productVersion() + ")"; - - auto formatPluginName = [](QString name) -> QString { return name.trimmed().replace(" ", "-"); }; - - // For each plugin, add to userAgent - auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); - for (auto& dp : displayPlugins) { - if (dp->isActive() && dp->isHmd()) { - userAgent += " " + formatPluginName(dp->getName()); - } - } - auto inputPlugins= PluginManager::getInstance()->getInputPlugins(); - for (auto& ip : inputPlugins) { - if (ip->isActive()) { - userAgent += " " + formatPluginName(ip->getName()); - } - } - // for codecs, we include all of them, even if not active - auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); - for (auto& cp : codecPlugins) { - userAgent += " " + formatPluginName(cp->getName()); - } - - return userAgent; -} - -void Application::toggleTabletUI(bool shouldOpen) const { - auto hmd = DependencyManager::get(); - if (!(shouldOpen && hmd->getShouldShowTablet())) { - auto HMD = DependencyManager::get(); - HMD->toggleShouldShowTablet(); - - if (!HMD->getShouldShowTablet()) { - DependencyManager::get()->setRaised(false); - _window->activateWindow(); - auto tablet = DependencyManager::get()->getTablet(SYSTEM_TABLET); - tablet->unfocus(); - } - } -} - -void Application::checkChangeCursor() { - QMutexLocker locker(&_changeCursorLock); - if (_cursorNeedsChanging) { -#ifdef Q_OS_MAC - auto cursorTarget = _window; // OSX doesn't seem to provide for hiding the cursor only on the GL widget -#else - // On windows and linux, hiding the top level cursor also means it's invisible when hovering over the - // window menu, which is a pain, so only hide it for the GL surface - auto cursorTarget = _glWidget; -#endif - cursorTarget->setCursor(_desiredCursor); - - _cursorNeedsChanging = false; - } -} - -void Application::showCursor(const Cursor::Icon& cursor) { - QMutexLocker locker(&_changeCursorLock); - - auto managedCursor = Cursor::Manager::instance().getCursor(); - auto curIcon = managedCursor->getIcon(); - if (curIcon != cursor) { - managedCursor->setIcon(cursor); - curIcon = cursor; - } - _desiredCursor = cursor == Cursor::Icon::SYSTEM ? Qt::ArrowCursor : Qt::BlankCursor; - _cursorNeedsChanging = true; -} - -void Application::updateHeartbeat() const { - DeadlockWatchdogThread::updateHeartbeat(); -} - -void Application::onAboutToQuit() { - // quickly save AvatarEntityData before the EntityTree is dismantled - getMyAvatar()->saveAvatarEntityDataToSettings(); - - emit beforeAboutToQuit(); - - if (getLoginDialogPoppedUp() && _firstRun.get()) { - _firstRun.set(false); - } - - foreach(auto inputPlugin, PluginManager::getInstance()->getInputPlugins()) { - if (inputPlugin->isActive()) { - inputPlugin->deactivate(); - } - } - - // The active display plugin needs to be loaded before the menu system is active, - // so its persisted explicitly here - Setting::Handle{ ACTIVE_DISPLAY_PLUGIN_SETTING_NAME }.set(getActiveDisplayPlugin()->getName()); - - loginDialogPoppedUp.set(false); - - getActiveDisplayPlugin()->deactivate(); - if (_autoSwitchDisplayModeSupportedHMDPlugin - && _autoSwitchDisplayModeSupportedHMDPlugin->isSessionActive()) { - _autoSwitchDisplayModeSupportedHMDPlugin->endSession(); - } - // use the CloseEventSender via a QThread to send an event that says the user asked for the app to close - DependencyManager::get()->startThread(); - - // Hide Running Scripts dialog so that it gets destroyed in an orderly manner; prevents warnings at shutdown. -#if !defined(DISABLE_QML) - getOffscreenUI()->hide("RunningScripts"); -#endif - - _aboutToQuit = true; - - cleanupBeforeQuit(); -} - -void Application::cleanupBeforeQuit() { - // add a logline indicating if QTWEBENGINE_REMOTE_DEBUGGING is set or not - QString webengineRemoteDebugging = QProcessEnvironment::systemEnvironment().value("QTWEBENGINE_REMOTE_DEBUGGING", "false"); - qCDebug(interfaceapp) << "QTWEBENGINE_REMOTE_DEBUGGING =" << webengineRemoteDebugging; - - DependencyManager::prepareToExit(); - - if (tracing::enabled()) { - auto tracer = DependencyManager::get(); - tracer->stopTracing(); - auto outputFile = property(hifi::properties::TRACING).toString(); - tracer->serialize(outputFile); - } - - // Stop third party processes so that they're not left running in the event of a subsequent shutdown crash. -#ifdef HAVE_DDE - DependencyManager::get()->setEnabled(false); -#endif -#ifdef HAVE_IVIEWHMD - DependencyManager::get()->setEnabled(false, true); -#endif - AnimDebugDraw::getInstance().shutdown(); - - // FIXME: once we move to shared pointer for the INputDevice we shoud remove this naked delete: - _applicationStateDevice.reset(); - - { - if (_keyboardFocusHighlightID != UNKNOWN_ENTITY_ID) { - DependencyManager::get()->deleteEntity(_keyboardFocusHighlightID); - _keyboardFocusHighlightID = UNKNOWN_ENTITY_ID; - } - } - - { - auto nodeList = DependencyManager::get(); - - // send the domain a disconnect packet, force stoppage of domain-server check-ins - nodeList->getDomainHandler().disconnect(); - nodeList->setIsShuttingDown(true); - - // tell the packet receiver we're shutting down, so it can drop packets - nodeList->getPacketReceiver().setShouldDropPackets(true); - } - - getEntities()->shutdown(); // tell the entities system we're shutting down, so it will stop running scripts - - // Clear any queued processing (I/O, FBX/OBJ/Texture parsing) - QThreadPool::globalInstance()->clear(); - - DependencyManager::destroy(); - - // FIXME: Something is still holding on to the ScriptEnginePointers contained in ScriptEngines, and they hold backpointers to ScriptEngines, - // so this doesn't shut down properly - DependencyManager::get()->shutdownScripting(); // stop all currently running global scripts - // These classes hold ScriptEnginePointers, so they must be destroyed before ScriptEngines - // Must be done after shutdownScripting in case any scripts try to access these things - { - DependencyManager::destroy(); - EntityTreePointer tree = getEntities()->getTree(); - tree->setSimulation(nullptr); - DependencyManager::destroy(); - } - DependencyManager::destroy(); - - bool keepMeLoggedIn = Setting::Handle(KEEP_ME_LOGGED_IN_SETTING_NAME, false).get(); - if (!keepMeLoggedIn) { - DependencyManager::get()->removeAccountFromFile(); - } - - _displayPlugin.reset(); - PluginManager::getInstance()->shutdown(); - - // Cleanup all overlays after the scripts, as scripts might add more - _overlays.cleanupAllOverlays(); - - // first stop all timers directly or by invokeMethod - // depending on what thread they run in - locationUpdateTimer.stop(); - identityPacketTimer.stop(); - pingTimer.stop(); - - // Wait for the settings thread to shut down, and save the settings one last time when it's safe - if (_settingsGuard.wait()) { - // save state - saveSettings(); - } - - _window->saveGeometry(); - - // Destroy third party processes after scripts have finished using them. -#ifdef HAVE_DDE - DependencyManager::destroy(); -#endif -#ifdef HAVE_IVIEWHMD - DependencyManager::destroy(); -#endif - - DependencyManager::destroy(); // Must be destroyed before TabletScriptingInterface - - // stop QML - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - - DependencyManager::destroy(); - - if (_snapshotSoundInjector != nullptr) { - _snapshotSoundInjector->stop(); - } - - // destroy Audio so it and its threads have a chance to go down safely - // this must happen after QML, as there are unexplained audio crashes originating in qtwebengine - QMetaObject::invokeMethod(DependencyManager::get().data(), "stop"); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - - // The PointerManager must be destroyed before the PickManager because when a Pointer is deleted, - // it accesses the PickManager to delete its associated Pick - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - - qCDebug(interfaceapp) << "Application::cleanupBeforeQuit() complete"; -} - -Application::~Application() { - // remove avatars from physics engine - auto avatarManager = DependencyManager::get(); - avatarManager->clearOtherAvatars(); - - PhysicsEngine::Transaction transaction; - avatarManager->buildPhysicsTransaction(transaction); - _physicsEngine->processTransaction(transaction); - avatarManager->handleProcessedPhysicsTransaction(transaction); - - avatarManager->deleteAllAvatars(); - - auto myCharacterController = getMyAvatar()->getCharacterController(); - myCharacterController->clearDetailedMotionStates(); - - myCharacterController->buildPhysicsTransaction(transaction); - _physicsEngine->processTransaction(transaction); - myCharacterController->handleProcessedPhysicsTransaction(transaction); - - _physicsEngine->setCharacterController(nullptr); - - // the _shapeManager should have zero references - _shapeManager.collectGarbage(); - assert(_shapeManager.getNumShapes() == 0); - - // shutdown graphics engine - _graphicsEngine.shutdown(); - - _gameWorkload.shutdown(); - - DependencyManager::destroy(); - - _entityClipboard->eraseAllOctreeElements(); - _entityClipboard.reset(); - - _octreeProcessor.terminate(); - _entityEditSender.terminate(); - - if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { - steamClient->shutdown(); - } - - if (auto oculusPlatform = PluginManager::getInstance()->getOculusPlatformPlugin()) { - oculusPlatform->shutdown(); - } - - DependencyManager::destroy(); - - DependencyManager::destroy(); // must be destroyed before the FramebufferCache - - DependencyManager::destroy(); - - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - - DependencyManager::get()->cleanup(); - - // remove the NodeList from the DependencyManager - DependencyManager::destroy(); - -#if 0 - ConnexionClient::getInstance().destroy(); -#endif - // The window takes ownership of the menu, so this has the side effect of destroying it. - _window->setMenuBar(nullptr); - - _window->deleteLater(); - - // make sure that the quit event has finished sending before we take the application down - auto closeEventSender = DependencyManager::get(); - while (!closeEventSender->hasFinishedQuitEvent() && !closeEventSender->hasTimedOutQuitEvent()) { - // sleep a little so we're not spinning at 100% - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - // quit the thread used by the closure event sender - closeEventSender->thread()->quit(); - - // Can't log to file past this point, FileLogger about to be deleted - qInstallMessageHandler(LogHandler::verboseMessageHandler); -} - -void Application::initializeGL() { - qCDebug(interfaceapp) << "Created Display Window."; - -#ifdef DISABLE_QML - setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity); -#endif - - // initialize glut for shape drawing; Qt apparently initializes it on OS X - if (_isGLInitialized) { - return; - } else { - _isGLInitialized = true; - } - - _glWidget->windowHandle()->setFormat(getDefaultOpenGLSurfaceFormat()); - - // When loading QtWebEngineWidgets, it creates a global share context on startup. - // We have to account for this possibility by checking here for an existing - // global share context - auto globalShareContext = qt_gl_global_share_context(); - -#if !defined(DISABLE_QML) - // Build a shared canvas / context for the Chromium processes - if (!globalShareContext) { - // Chromium rendering uses some GL functions that prevent nSight from capturing - // frames, so we only create the shared context if nsight is NOT active. - if (!nsightActive()) { - _chromiumShareContext = new OffscreenGLCanvas(); - _chromiumShareContext->setObjectName("ChromiumShareContext"); - auto format =QSurfaceFormat::defaultFormat(); -#ifdef Q_OS_MAC - // On mac, the primary shared OpenGL context must be a 3.2 core context, - // or chromium flips out and spews error spam (but renders fine) - format.setMajorVersion(3); - format.setMinorVersion(2); -#endif - _chromiumShareContext->setFormat(format); - _chromiumShareContext->create(); - if (!_chromiumShareContext->makeCurrent()) { - qCWarning(interfaceapp, "Unable to make chromium shared context current"); - } - globalShareContext = _chromiumShareContext->getContext(); - qt_gl_set_global_share_context(globalShareContext); - _chromiumShareContext->doneCurrent(); - } - } -#endif - - - _glWidget->createContext(globalShareContext); - - if (!_glWidget->makeCurrent()) { - qCWarning(interfaceapp, "Unable to make window context current"); - } - -#if !defined(DISABLE_QML) - // Disable signed distance field font rendering on ATI/AMD GPUs, due to - // https://highfidelity.manuscript.com/f/cases/13677/Text-showing-up-white-on-Marketplace-app - std::string vendor{ (const char*)glGetString(GL_VENDOR) }; - if ((vendor.find("AMD") != std::string::npos) || (vendor.find("ATI") != std::string::npos)) { - qputenv("QTWEBENGINE_CHROMIUM_FLAGS", QByteArray("--disable-distance-field-text")); - } -#endif - - if (!globalShareContext) { - globalShareContext = _glWidget->qglContext(); - qt_gl_set_global_share_context(globalShareContext); - } - - // Build a shared canvas / context for the QML rendering -#if !defined(DISABLE_QML) - { - _qmlShareContext = new OffscreenGLCanvas(); - _qmlShareContext->setObjectName("QmlShareContext"); - _qmlShareContext->create(globalShareContext); - if (!_qmlShareContext->makeCurrent()) { - qCWarning(interfaceapp, "Unable to make QML shared context current"); - } - OffscreenQmlSurface::setSharedContext(_qmlShareContext->getContext()); - _qmlShareContext->doneCurrent(); - if (!_glWidget->makeCurrent()) { - qCWarning(interfaceapp, "Unable to make window context current"); - } - } -#endif - - - // Build an offscreen GL context for the main thread. - _glWidget->makeCurrent(); - glClearColor(0.2f, 0.2f, 0.2f, 1); - glClear(GL_COLOR_BUFFER_BIT); - _glWidget->swapBuffers(); - - _graphicsEngine.initializeGPU(_glWidget); -} - -void Application::initializeDisplayPlugins() { - auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); - Setting::Handle activeDisplayPluginSetting{ ACTIVE_DISPLAY_PLUGIN_SETTING_NAME, displayPlugins.at(0)->getName() }; - auto lastActiveDisplayPluginName = activeDisplayPluginSetting.get(); - - auto defaultDisplayPlugin = displayPlugins.at(0); - // Once time initialization code - DisplayPluginPointer targetDisplayPlugin; - foreach(auto displayPlugin, displayPlugins) { - displayPlugin->setContext(_graphicsEngine.getGPUContext()); - if (displayPlugin->getName() == lastActiveDisplayPluginName) { - targetDisplayPlugin = displayPlugin; - } - QObject::connect(displayPlugin.get(), &DisplayPlugin::recommendedFramebufferSizeChanged, - [this](const QSize& size) { resizeGL(); }); - QObject::connect(displayPlugin.get(), &DisplayPlugin::resetSensorsRequested, this, &Application::requestReset); - if (displayPlugin->isHmd()) { - auto hmdDisplayPlugin = dynamic_cast(displayPlugin.get()); - QObject::connect(hmdDisplayPlugin, &HmdDisplayPlugin::hmdMountedChanged, - DependencyManager::get().data(), &HMDScriptingInterface::mountedChanged); - QObject::connect(hmdDisplayPlugin, &HmdDisplayPlugin::hmdVisibleChanged, this, &Application::hmdVisibleChanged); - } - } - - // The default display plugin needs to be activated first, otherwise the display plugin thread - // may be launched by an external plugin, which is bad - setDisplayPlugin(defaultDisplayPlugin); - - // Now set the desired plugin if it's not the same as the default plugin - if (targetDisplayPlugin && (targetDisplayPlugin != defaultDisplayPlugin)) { - setDisplayPlugin(targetDisplayPlugin); - } - - // Submit a default frame to render until the engine starts up - updateRenderArgs(0.0f); -} - -void Application::initializeRenderEngine() { - // FIXME: on low end systems os the shaders take up to 1 minute to compile, so we pause the deadlock watchdog thread. - DeadlockWatchdogThread::withPause([&] { - _graphicsEngine.initializeRender(DISABLE_DEFERRED); - DependencyManager::get()->registerKeyboardHighlighting(); - }); -} - -extern void setupPreferences(); -#if !defined(DISABLE_QML) -static void addDisplayPluginToMenu(const DisplayPluginPointer& displayPlugin, int index, bool active = false); -#endif - -void Application::showLoginScreen() { -#if !defined(DISABLE_QML) - auto accountManager = DependencyManager::get(); - auto dialogsManager = DependencyManager::get(); - if (!accountManager->isLoggedIn()) { - if (!isHMDMode()) { - auto toolbar = DependencyManager::get()->getToolbar("com.highfidelity.interface.toolbar.system"); - toolbar->writeProperty("visible", false); - } - _loginDialogPoppedUp = true; - dialogsManager->showLoginDialog(); - emit loginDialogFocusEnabled(); - QJsonObject loginData = {}; - loginData["action"] = "login dialog popped up"; - UserActivityLogger::getInstance().logAction("encourageLoginDialog", loginData); - _window->setWindowTitle("High Fidelity Interface"); - } else { - resumeAfterLoginDialogActionTaken(); - } - _loginDialogPoppedUp = !accountManager->isLoggedIn(); - loginDialogPoppedUp.set(_loginDialogPoppedUp); -#else - resumeAfterLoginDialogActionTaken(); -#endif -} - -void Application::initializeUi() { - AddressBarDialog::registerType(); - ErrorDialog::registerType(); - LoginDialog::registerType(); - Tooltip::registerType(); - UpdateDialog::registerType(); - QmlContextCallback commerceCallback = [](QQmlContext* context) { - context->setContextProperty("Commerce", DependencyManager::get().data()); - }; - OffscreenQmlSurface::addWhitelistContextHandler({ - QUrl{ "hifi/commerce/checkout/Checkout.qml" }, - QUrl{ "hifi/commerce/common/CommerceLightbox.qml" }, - QUrl{ "hifi/commerce/common/EmulatedMarketplaceHeader.qml" }, - QUrl{ "hifi/commerce/common/FirstUseTutorial.qml" }, - QUrl{ "hifi/commerce/common/sendAsset/SendAsset.qml" }, - QUrl{ "hifi/commerce/common/SortableListModel.qml" }, - QUrl{ "hifi/commerce/inspectionCertificate/InspectionCertificate.qml" }, - QUrl{ "hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml"}, - QUrl{ "hifi/commerce/purchases/PurchasedItem.qml" }, - QUrl{ "hifi/commerce/purchases/Purchases.qml" }, - QUrl{ "hifi/commerce/wallet/Help.qml" }, - QUrl{ "hifi/commerce/wallet/NeedsLogIn.qml" }, - QUrl{ "hifi/commerce/wallet/PassphraseChange.qml" }, - QUrl{ "hifi/commerce/wallet/PassphraseModal.qml" }, - QUrl{ "hifi/commerce/wallet/PassphraseSelection.qml" }, - QUrl{ "hifi/commerce/wallet/Wallet.qml" }, - QUrl{ "hifi/commerce/wallet/WalletHome.qml" }, - QUrl{ "hifi/commerce/wallet/WalletSetup.qml" }, - QUrl{ "hifi/dialogs/security/Security.qml" }, - QUrl{ "hifi/dialogs/security/SecurityImageChange.qml" }, - QUrl{ "hifi/dialogs/security/SecurityImageModel.qml" }, - QUrl{ "hifi/dialogs/security/SecurityImageSelection.qml" }, - QUrl{ "hifi/tablet/TabletMenu.qml" }, - QUrl{ "hifi/commerce/marketplace/Marketplace.qml" }, - }, commerceCallback); - - QmlContextCallback marketplaceCallback = [](QQmlContext* context) { - context->setContextProperty("MarketplaceScriptingInterface", new QmlMarketplace()); - }; - OffscreenQmlSurface::addWhitelistContextHandler({ - QUrl{ "hifi/commerce/marketplace/Marketplace.qml" }, - }, marketplaceCallback); - - QmlContextCallback platformInfoCallback = [](QQmlContext* context) { - context->setContextProperty("PlatformInfo", new PlatformInfoScriptingInterface()); - }; - OffscreenQmlSurface::addWhitelistContextHandler({ - QUrl{ "hifi/commerce/marketplace/Marketplace.qml" }, - }, platformInfoCallback); - - QmlContextCallback ttsCallback = [](QQmlContext* context) { - context->setContextProperty("TextToSpeech", DependencyManager::get().data()); - }; - OffscreenQmlSurface::addWhitelistContextHandler({ - QUrl{ "hifi/tts/TTS.qml" } - }, ttsCallback); - qmlRegisterType("Hifi", 1, 0, "ResourceImageItem"); - qmlRegisterType("Hifi", 1, 0, "Preference"); - qmlRegisterType("HifiWeb", 1, 0, "WebBrowserSuggestionsEngine"); - - { - auto tabletScriptingInterface = DependencyManager::get(); - tabletScriptingInterface->getTablet(SYSTEM_TABLET); - } - - auto offscreenUi = getOffscreenUI(); - connect(offscreenUi.data(), &hifi::qml::OffscreenSurface::rootContextCreated, - this, &Application::onDesktopRootContextCreated); - connect(offscreenUi.data(), &hifi::qml::OffscreenSurface::rootItemCreated, - this, &Application::onDesktopRootItemCreated); - -#if !defined(DISABLE_QML) - offscreenUi->setProxyWindow(_window->windowHandle()); - // OffscreenUi is a subclass of OffscreenQmlSurface specifically designed to - // support the window management and scripting proxies for VR use - DeadlockWatchdogThread::withPause([&] { - offscreenUi->createDesktop(PathUtils::qmlUrl("hifi/Desktop.qml")); - }); - // FIXME either expose so that dialogs can set this themselves or - // do better detection in the offscreen UI of what has focus - offscreenUi->setNavigationFocused(false); -#else - _window->setMenuBar(new Menu()); -#endif - - setupPreferences(); - -#if !defined(DISABLE_QML) - _glWidget->installEventFilter(offscreenUi.data()); - offscreenUi->setMouseTranslator([=](const QPointF& pt) { - QPointF result = pt; - auto displayPlugin = getActiveDisplayPlugin(); - if (displayPlugin->isHmd()) { - getApplicationCompositor().handleRealMouseMoveEvent(false); - auto resultVec = getApplicationCompositor().getReticlePosition(); - result = QPointF(resultVec.x, resultVec.y); - } - return result.toPoint(); - }); - offscreenUi->resume(); -#endif - connect(_window, &MainWindow::windowGeometryChanged, [this](const QRect& r){ - resizeGL(); - if (_touchscreenVirtualPadDevice) { - _touchscreenVirtualPadDevice->resize(); - } - }); - - // This will set up the input plugins UI - _activeInputPlugins.clear(); - foreach(auto inputPlugin, PluginManager::getInstance()->getInputPlugins()) { - if (KeyboardMouseDevice::NAME == inputPlugin->getName()) { - _keyboardMouseDevice = std::dynamic_pointer_cast(inputPlugin); - } - if (TouchscreenDevice::NAME == inputPlugin->getName()) { - _touchscreenDevice = std::dynamic_pointer_cast(inputPlugin); - } - if (TouchscreenVirtualPadDevice::NAME == inputPlugin->getName()) { - _touchscreenVirtualPadDevice = std::dynamic_pointer_cast(inputPlugin); -#if defined(ANDROID_APP_INTERFACE) - auto& virtualPadManager = VirtualPad::Manager::instance(); - connect(&virtualPadManager, &VirtualPad::Manager::hapticFeedbackRequested, - this, [](int duration) { - AndroidHelper::instance().performHapticFeedback(duration); - }); -#endif - } - } - - auto compositorHelper = DependencyManager::get(); - connect(compositorHelper.data(), &CompositorHelper::allowMouseCaptureChanged, this, [=] { - if (isHMDMode()) { - auto compositorHelper = DependencyManager::get(); // don't capture outer smartpointer - showCursor(compositorHelper->getAllowMouseCapture() ? - Cursor::Manager::lookupIcon(_preferredCursor.get()) : - Cursor::Icon::SYSTEM); - } - }); - -#if !defined(DISABLE_QML) - // Pre-create a couple of offscreen surfaces to speed up tablet UI - auto offscreenSurfaceCache = DependencyManager::get(); - offscreenSurfaceCache->setOnRootContextCreated([&](const QString& rootObject, QQmlContext* surfaceContext) { - if (rootObject == TabletScriptingInterface::QML) { - // in Qt 5.10.0 there is already an "Audio" object in the QML context - // though I failed to find it (from QtMultimedia??). So.. let it be "AudioScriptingInterface" - surfaceContext->setContextProperty("AudioScriptingInterface", DependencyManager::get().data()); - surfaceContext->setContextProperty("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED - } - }); - - offscreenSurfaceCache->reserve(TabletScriptingInterface::QML, 1); - offscreenSurfaceCache->reserve(render::entities::WebEntityRenderer::QML, 2); -#endif - - flushMenuUpdates(); - -#if !defined(DISABLE_QML) - // Now that the menu is instantiated, ensure the display plugin menu is properly updated - { - auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); - // first sort the plugins into groupings: standard, advanced, developer - std::stable_sort(displayPlugins.begin(), displayPlugins.end(), - [](const DisplayPluginPointer& a, const DisplayPluginPointer& b) -> bool { return a->getGrouping() < b->getGrouping(); }); - int dpIndex = 1; - // concatenate the groupings into a single list in the order: standard, advanced, developer - for(const auto& displayPlugin : displayPlugins) { - addDisplayPluginToMenu(displayPlugin, dpIndex, _displayPlugin == displayPlugin); - dpIndex++; - } - - // after all plugins have been added to the menu, add a separator to the menu - auto parent = getPrimaryMenu()->getMenu(MenuOption::OutputMenu); - parent->addSeparator(); - } -#endif - - - // The display plugins are created before the menu now, so we need to do this here to hide the menu bar - // now that it exists - if (_window && _window->isFullScreen()) { - setFullscreen(nullptr, true); - } - - - setIsInterstitialMode(true); -} - - -void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) { - auto engine = surfaceContext->engine(); - // in Qt 5.10.0 there is already an "Audio" object in the QML context - // though I failed to find it (from QtMultimedia??). So.. let it be "AudioScriptingInterface" - surfaceContext->setContextProperty("AudioScriptingInterface", DependencyManager::get().data()); - - surfaceContext->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); - surfaceContext->setContextProperty("AudioScope", DependencyManager::get().data()); - - surfaceContext->setContextProperty("Controller", DependencyManager::get().data()); - surfaceContext->setContextProperty("Entities", DependencyManager::get().data()); - _fileDownload = new FileScriptingInterface(engine); - surfaceContext->setContextProperty("File", _fileDownload); - connect(_fileDownload, &FileScriptingInterface::unzipResult, this, &Application::handleUnzip); - surfaceContext->setContextProperty("MyAvatar", getMyAvatar().get()); - surfaceContext->setContextProperty("Messages", DependencyManager::get().data()); - surfaceContext->setContextProperty("Recording", DependencyManager::get().data()); - surfaceContext->setContextProperty("Preferences", DependencyManager::get().data()); - surfaceContext->setContextProperty("AddressManager", DependencyManager::get().data()); - surfaceContext->setContextProperty("FrameTimings", &_graphicsEngine._frameTimingsScriptingInterface); - surfaceContext->setContextProperty("Rates", new RatesScriptingInterface(this)); - - surfaceContext->setContextProperty("TREE_SCALE", TREE_SCALE); - // FIXME Quat and Vec3 won't work with QJSEngine used by QML - surfaceContext->setContextProperty("Quat", new Quat()); - surfaceContext->setContextProperty("Vec3", new Vec3()); - surfaceContext->setContextProperty("Uuid", new ScriptUUID()); - surfaceContext->setContextProperty("Assets", DependencyManager::get().data()); - surfaceContext->setContextProperty("Keyboard", DependencyManager::get().data()); - - surfaceContext->setContextProperty("AvatarList", DependencyManager::get().data()); - surfaceContext->setContextProperty("Users", DependencyManager::get().data()); - - surfaceContext->setContextProperty("UserActivityLogger", DependencyManager::get().data()); - - surfaceContext->setContextProperty("Camera", &_myCamera); - -#if defined(Q_OS_MAC) || defined(Q_OS_WIN) - surfaceContext->setContextProperty("SpeechRecognizer", DependencyManager::get().data()); -#endif - - surfaceContext->setContextProperty("Overlays", &_overlays); - surfaceContext->setContextProperty("Window", DependencyManager::get().data()); - surfaceContext->setContextProperty("Desktop", DependencyManager::get().data()); - surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); - surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); - surfaceContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); - surfaceContext->setContextProperty("AvatarBookmarks", DependencyManager::get().data()); - surfaceContext->setContextProperty("LocationBookmarks", DependencyManager::get().data()); - - // Caches - surfaceContext->setContextProperty("AnimationCache", DependencyManager::get().data()); - surfaceContext->setContextProperty("TextureCache", DependencyManager::get().data()); - surfaceContext->setContextProperty("ModelCache", DependencyManager::get().data()); - surfaceContext->setContextProperty("SoundCache", DependencyManager::get().data()); - - surfaceContext->setContextProperty("InputConfiguration", DependencyManager::get().data()); - - surfaceContext->setContextProperty("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED - surfaceContext->setContextProperty("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED - surfaceContext->setContextProperty("AccountServices", AccountServicesScriptingInterface::getInstance()); - - surfaceContext->setContextProperty("DialogsManager", _dialogsManagerScriptingInterface); - surfaceContext->setContextProperty("FaceTracker", DependencyManager::get().data()); - surfaceContext->setContextProperty("AvatarManager", DependencyManager::get().data()); - surfaceContext->setContextProperty("LODManager", DependencyManager::get().data()); - surfaceContext->setContextProperty("HMD", DependencyManager::get().data()); - surfaceContext->setContextProperty("Scene", DependencyManager::get().data()); - surfaceContext->setContextProperty("Render", _graphicsEngine.getRenderEngine()->getConfiguration().get()); - surfaceContext->setContextProperty("Workload", _gameWorkload._engine->getConfiguration().get()); - surfaceContext->setContextProperty("Reticle", getApplicationCompositor().getReticleInterface()); - surfaceContext->setContextProperty("Snapshot", DependencyManager::get().data()); - - surfaceContext->setContextProperty("ApplicationCompositor", &getApplicationCompositor()); - - surfaceContext->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); - surfaceContext->setContextProperty("Selection", DependencyManager::get().data()); - surfaceContext->setContextProperty("ContextOverlay", DependencyManager::get().data()); - surfaceContext->setContextProperty("WalletScriptingInterface", DependencyManager::get().data()); - surfaceContext->setContextProperty("HiFiAbout", AboutUtil::getInstance()); - surfaceContext->setContextProperty("ResourceRequestObserver", DependencyManager::get().data()); - - if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { - surfaceContext->setContextProperty("Steam", new SteamScriptingInterface(engine, steamClient.get())); - } - - _window->setMenuBar(new Menu()); -} - -void Application::onDesktopRootItemCreated(QQuickItem* rootItem) { - Stats::show(); - AnimStats::show(); - auto surfaceContext = getOffscreenUI()->getSurfaceContext(); - surfaceContext->setContextProperty("Stats", Stats::getInstance()); - surfaceContext->setContextProperty("AnimStats", AnimStats::getInstance()); - -#if !defined(Q_OS_ANDROID) - auto offscreenUi = getOffscreenUI(); - auto qml = PathUtils::qmlUrl("AvatarInputsBar.qml"); - offscreenUi->show(qml, "AvatarInputsBar"); -#endif -} - -void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditionalContextProperties) { - surfaceContext->setContextProperty("Users", DependencyManager::get().data()); - surfaceContext->setContextProperty("HMD", DependencyManager::get().data()); - surfaceContext->setContextProperty("UserActivityLogger", DependencyManager::get().data()); - surfaceContext->setContextProperty("Preferences", DependencyManager::get().data()); - surfaceContext->setContextProperty("Vec3", new Vec3()); - surfaceContext->setContextProperty("Quat", new Quat()); - surfaceContext->setContextProperty("MyAvatar", DependencyManager::get()->getMyAvatar().get()); - surfaceContext->setContextProperty("Entities", DependencyManager::get().data()); - surfaceContext->setContextProperty("Snapshot", DependencyManager::get().data()); - surfaceContext->setContextProperty("KeyboardScriptingInterface", DependencyManager::get().data()); - - if (setAdditionalContextProperties) { - auto tabletScriptingInterface = DependencyManager::get(); - auto flags = tabletScriptingInterface->getFlags(); - - surfaceContext->setContextProperty("offscreenFlags", flags); - surfaceContext->setContextProperty("AddressManager", DependencyManager::get().data()); - - surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); - surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); - - surfaceContext->setContextProperty("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED - surfaceContext->setContextProperty("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED - surfaceContext->setContextProperty("AccountServices", AccountServicesScriptingInterface::getInstance()); - - // in Qt 5.10.0 there is already an "Audio" object in the QML context - // though I failed to find it (from QtMultimedia??). So.. let it be "AudioScriptingInterface" - surfaceContext->setContextProperty("AudioScriptingInterface", DependencyManager::get().data()); - - surfaceContext->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); - surfaceContext->setContextProperty("fileDialogHelper", new FileDialogHelper()); - surfaceContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); - surfaceContext->setContextProperty("Assets", DependencyManager::get().data()); - surfaceContext->setContextProperty("LODManager", DependencyManager::get().data()); - surfaceContext->setContextProperty("OctreeStats", DependencyManager::get().data()); - surfaceContext->setContextProperty("DCModel", DependencyManager::get().data()); - surfaceContext->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); - surfaceContext->setContextProperty("AvatarList", DependencyManager::get().data()); - surfaceContext->setContextProperty("DialogsManager", DialogsManagerScriptingInterface::getInstance()); - surfaceContext->setContextProperty("InputConfiguration", DependencyManager::get().data()); - surfaceContext->setContextProperty("SoundCache", DependencyManager::get().data()); - surfaceContext->setContextProperty("AvatarBookmarks", DependencyManager::get().data()); - surfaceContext->setContextProperty("Render", AbstractViewStateInterface::instance()->getRenderEngine()->getConfiguration().get()); - surfaceContext->setContextProperty("Workload", qApp->getGameWorkload()._engine->getConfiguration().get()); - surfaceContext->setContextProperty("Controller", DependencyManager::get().data()); - surfaceContext->setContextProperty("Pointers", DependencyManager::get().data()); - surfaceContext->setContextProperty("Window", DependencyManager::get().data()); - surfaceContext->setContextProperty("Reticle", qApp->getApplicationCompositor().getReticleInterface()); - surfaceContext->setContextProperty("HiFiAbout", AboutUtil::getInstance()); - surfaceContext->setContextProperty("WalletScriptingInterface", DependencyManager::get().data()); - surfaceContext->setContextProperty("ResourceRequestObserver", DependencyManager::get().data()); - } -} - -void Application::updateCamera(RenderArgs& renderArgs, float deltaTime) { - PROFILE_RANGE(render, __FUNCTION__); - PerformanceTimer perfTimer("updateCamera"); - - glm::vec3 boomOffset; - auto myAvatar = getMyAvatar(); - boomOffset = myAvatar->getModelScale() * myAvatar->getBoomLength() * -IDENTITY_FORWARD; - - // The render mode is default or mirror if the camera is in mirror mode, assigned further below - renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; - - // Always use the default eye position, not the actual head eye position. - // Using the latter will cause the camera to wobble with idle animations, - // or with changes from the face tracker - if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON) { - _thirdPersonHMDCameraBoomValid= false; - if (isHMDMode()) { - mat4 camMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); - _myCamera.setPosition(extractTranslation(camMat)); - _myCamera.setOrientation(glmExtractRotation(camMat)); - } - else { - _myCamera.setPosition(myAvatar->getDefaultEyePosition()); - _myCamera.setOrientation(myAvatar->getMyHead()->getHeadOrientation()); - } - } - else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { - if (isHMDMode()) { - - if (!_thirdPersonHMDCameraBoomValid) { - const glm::vec3 CAMERA_OFFSET = glm::vec3(0.0f, 0.0f, 0.7f); - _thirdPersonHMDCameraBoom = cancelOutRollAndPitch(myAvatar->getHMDSensorOrientation()) * CAMERA_OFFSET; - _thirdPersonHMDCameraBoomValid = true; - } - - glm::mat4 thirdPersonCameraSensorToWorldMatrix = myAvatar->getSensorToWorldMatrix(); - - const glm::vec3 cameraPos = myAvatar->getHMDSensorPosition() + _thirdPersonHMDCameraBoom * myAvatar->getBoomLength(); - glm::mat4 sensorCameraMat = createMatFromQuatAndPos(myAvatar->getHMDSensorOrientation(), cameraPos); - glm::mat4 worldCameraMat = thirdPersonCameraSensorToWorldMatrix * sensorCameraMat; - - _myCamera.setOrientation(glm::normalize(glmExtractRotation(worldCameraMat))); - _myCamera.setPosition(extractTranslation(worldCameraMat)); - } - else { - _thirdPersonHMDCameraBoomValid = false; - - _myCamera.setOrientation(myAvatar->getHead()->getOrientation()); - if (isOptionChecked(MenuOption::CenterPlayerInView)) { - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + _myCamera.getOrientation() * boomOffset); - } - else { - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + myAvatar->getWorldOrientation() * boomOffset); - } - } - } - else if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { - _thirdPersonHMDCameraBoomValid= false; - - if (isHMDMode()) { - auto mirrorBodyOrientation = myAvatar->getWorldOrientation() * glm::quat(glm::vec3(0.0f, PI + _mirrorYawOffset, 0.0f)); - - glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); - // Mirror HMD yaw and roll - glm::vec3 mirrorHmdEulers = glm::eulerAngles(hmdRotation); - mirrorHmdEulers.y = -mirrorHmdEulers.y; - mirrorHmdEulers.z = -mirrorHmdEulers.z; - glm::quat mirrorHmdRotation = glm::quat(mirrorHmdEulers); - - glm::quat worldMirrorRotation = mirrorBodyOrientation * mirrorHmdRotation; - - _myCamera.setOrientation(worldMirrorRotation); - - glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); - // Mirror HMD lateral offsets - hmdOffset.x = -hmdOffset.x; - - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + glm::vec3(0, _raiseMirror * myAvatar->getModelScale(), 0) - + mirrorBodyOrientation * glm::vec3(0.0f, 0.0f, 1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror - + mirrorBodyOrientation * hmdOffset); - } - else { - auto userInputMapper = DependencyManager::get(); - const float YAW_SPEED = TWO_PI / 5.0f; - float deltaYaw = userInputMapper->getActionState(controller::Action::YAW) * YAW_SPEED * deltaTime; - _mirrorYawOffset += deltaYaw; - _myCamera.setOrientation(myAvatar->getWorldOrientation() * glm::quat(glm::vec3(0.0f, PI + _mirrorYawOffset, 0.0f))); - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + glm::vec3(0, _raiseMirror * myAvatar->getModelScale(), 0) - + (myAvatar->getWorldOrientation() * glm::quat(glm::vec3(0.0f, _mirrorYawOffset, 0.0f))) * - glm::vec3(0.0f, 0.0f, -1.0f) * myAvatar->getBoomLength() * _scaleMirror); - } - renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; - } - else if (_myCamera.getMode() == CAMERA_MODE_ENTITY) { - _thirdPersonHMDCameraBoomValid= false; - EntityItemPointer cameraEntity = _myCamera.getCameraEntityPointer(); - if (cameraEntity != nullptr) { - if (isHMDMode()) { - glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); - _myCamera.setOrientation(cameraEntity->getWorldOrientation() * hmdRotation); - glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); - _myCamera.setPosition(cameraEntity->getWorldPosition() + (hmdRotation * hmdOffset)); - } - else { - _myCamera.setOrientation(cameraEntity->getWorldOrientation()); - _myCamera.setPosition(cameraEntity->getWorldPosition()); - } - } - } - // Update camera position - if (!isHMDMode()) { - _myCamera.update(); - } - - renderArgs._cameraMode = (int8_t)_myCamera.getMode(); -} - -void Application::runTests() { - runTimingTests(); - runUnitTests(); -} - -void Application::faceTrackerMuteToggled() { - - QAction* muteAction = Menu::getInstance()->getActionForOption(MenuOption::MuteFaceTracking); - Q_CHECK_PTR(muteAction); - bool isMuted = getSelectedFaceTracker()->isMuted(); - muteAction->setChecked(isMuted); - getSelectedFaceTracker()->setEnabled(!isMuted); - Menu::getInstance()->getActionForOption(MenuOption::CalibrateCamera)->setEnabled(!isMuted); -} - -void Application::setFieldOfView(float fov) { - if (fov != _fieldOfView.get()) { - _fieldOfView.set(fov); - resizeGL(); - } -} - -void Application::setHMDTabletScale(float hmdTabletScale) { - _hmdTabletScale.set(hmdTabletScale); -} - -void Application::setDesktopTabletScale(float desktopTabletScale) { - _desktopTabletScale.set(desktopTabletScale); -} - -void Application::setDesktopTabletBecomesToolbarSetting(bool value) { - _desktopTabletBecomesToolbarSetting.set(value); - updateSystemTabletMode(); -} - -void Application::setHmdTabletBecomesToolbarSetting(bool value) { - _hmdTabletBecomesToolbarSetting.set(value); - updateSystemTabletMode(); -} - -void Application::setPreferStylusOverLaser(bool value) { - _preferStylusOverLaserSetting.set(value); -} - -void Application::setPreferAvatarFingerOverStylus(bool value) { - _preferAvatarFingerOverStylusSetting.set(value); -} - -void Application::setPreferredCursor(const QString& cursorName) { - qCDebug(interfaceapp) << "setPreferredCursor" << cursorName; - _preferredCursor.set(cursorName.isEmpty() ? DEFAULT_CURSOR_NAME : cursorName); - showCursor(Cursor::Manager::lookupIcon(_preferredCursor.get())); -} - -void Application::setSettingConstrainToolbarPosition(bool setting) { - _constrainToolbarPosition.set(setting); - getOffscreenUI()->setConstrainToolbarToCenterX(setting); -} - -void Application::setMiniTabletEnabled(bool enabled) { - _miniTabletEnabledSetting.set(enabled); - emit miniTabletEnabledChanged(enabled); -} - -void Application::showHelp() { - static const QString HAND_CONTROLLER_NAME_VIVE = "vive"; - static const QString HAND_CONTROLLER_NAME_OCULUS_TOUCH = "oculus"; - static const QString HAND_CONTROLLER_NAME_WINDOWS_MR = "windowsMR"; - - static const QString VIVE_PLUGIN_NAME = "HTC Vive"; - static const QString OCULUS_RIFT_PLUGIN_NAME = "Oculus Rift"; - static const QString WINDOWS_MR_PLUGIN_NAME = "WindowsMR"; - - static const QString TAB_KEYBOARD_MOUSE = "kbm"; - static const QString TAB_GAMEPAD = "gamepad"; - static const QString TAB_HAND_CONTROLLERS = "handControllers"; - - QString handControllerName; - QString defaultTab = TAB_KEYBOARD_MOUSE; - - if (PluginUtils::isHMDAvailable(WINDOWS_MR_PLUGIN_NAME)) { - defaultTab = TAB_HAND_CONTROLLERS; - handControllerName = HAND_CONTROLLER_NAME_WINDOWS_MR; - } else if (PluginUtils::isHMDAvailable(VIVE_PLUGIN_NAME)) { - defaultTab = TAB_HAND_CONTROLLERS; - handControllerName = HAND_CONTROLLER_NAME_VIVE; - } else if (PluginUtils::isHMDAvailable(OCULUS_RIFT_PLUGIN_NAME)) { - if (PluginUtils::isOculusTouchControllerAvailable()) { - defaultTab = TAB_HAND_CONTROLLERS; - handControllerName = HAND_CONTROLLER_NAME_OCULUS_TOUCH; - } else if (PluginUtils::isXboxControllerAvailable()) { - defaultTab = TAB_GAMEPAD; - } else { - defaultTab = TAB_KEYBOARD_MOUSE; - } - } else if (PluginUtils::isXboxControllerAvailable()) { - defaultTab = TAB_GAMEPAD; - } else { - defaultTab = TAB_KEYBOARD_MOUSE; - } - - QUrlQuery queryString; - queryString.addQueryItem("handControllerName", handControllerName); - queryString.addQueryItem("defaultTab", defaultTab); - TabletProxy* tablet = dynamic_cast(DependencyManager::get()->getTablet(SYSTEM_TABLET)); - tablet->gotoWebScreen(PathUtils::resourcesUrl() + INFO_HELP_PATH + "?" + queryString.toString()); - DependencyManager::get()->openTablet(); - //InfoView::show(INFO_HELP_PATH, false, queryString.toString()); -} - -void Application::resizeEvent(QResizeEvent* event) { - resizeGL(); -} - -void Application::resizeGL() { - PROFILE_RANGE(render, __FUNCTION__); - if (nullptr == _displayPlugin) { - return; - } - - auto displayPlugin = getActiveDisplayPlugin(); - // Set the desired FBO texture size. If it hasn't changed, this does nothing. - // Otherwise, it must rebuild the FBOs - uvec2 framebufferSize = displayPlugin->getRecommendedRenderSize(); - uvec2 renderSize = uvec2(framebufferSize); - if (_renderResolution != renderSize) { - _renderResolution = renderSize; - DependencyManager::get()->setFrameBufferSize(fromGlm(renderSize)); - } - - auto renderResolutionScale = getRenderResolutionScale(); - if (displayPlugin->getRenderResolutionScale() != renderResolutionScale) { - auto renderConfig = _graphicsEngine.getRenderEngine()->getConfiguration(); - assert(renderConfig); - auto mainView = renderConfig->getConfig("RenderMainView.RenderDeferredTask"); - // mainView can be null if we're rendering in forward mode - if (mainView) { - mainView->setProperty("resolutionScale", renderResolutionScale); - } - displayPlugin->setRenderResolutionScale(renderResolutionScale); - } - - // FIXME the aspect ratio for stereo displays is incorrect based on this. - float aspectRatio = displayPlugin->getRecommendedAspectRatio(); - _myCamera.setProjection(glm::perspective(glm::radians(_fieldOfView.get()), aspectRatio, - DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP)); - // Possible change in aspect ratio - { - QMutexLocker viewLocker(&_viewMutex); - _myCamera.loadViewFrustum(_viewFrustum); - } - -#if !defined(DISABLE_QML) - getOffscreenUI()->resize(fromGlm(displayPlugin->getRecommendedUiSize())); -#endif -} - -void Application::handleSandboxStatus(QNetworkReply* reply) { - PROFILE_RANGE(render, __FUNCTION__); - - bool sandboxIsRunning = SandboxUtils::readStatus(reply->readAll()); - - enum HandControllerType { - Vive, - Oculus - }; - static const std::map MIN_CONTENT_VERSION = { - { Vive, 1 }, - { Oculus, 27 } - }; - - // Get sandbox content set version - auto acDirPath = PathUtils::getAppDataPath() + "../../" + BuildInfo::MODIFIED_ORGANIZATION + "/assignment-client/"; - auto contentVersionPath = acDirPath + "content-version.txt"; - qCDebug(interfaceapp) << "Checking " << contentVersionPath << " for content version"; - int contentVersion = 0; - QFile contentVersionFile(contentVersionPath); - if (contentVersionFile.open(QIODevice::ReadOnly | QIODevice::Text)) { - QString line = contentVersionFile.readAll(); - contentVersion = line.toInt(); // returns 0 if conversion fails - } - - // Get controller availability -#ifdef ANDROID_APP_QUEST_INTERFACE - bool hasHandControllers = true; -#else - bool hasHandControllers = false; - if (PluginUtils::isViveControllerAvailable() || PluginUtils::isOculusTouchControllerAvailable()) { - hasHandControllers = true; - } -#endif - - // Check HMD use (may be technically available without being in use) - bool hasHMD = PluginUtils::isHMDAvailable(); - bool isUsingHMD = _displayPlugin->isHmd(); - bool isUsingHMDAndHandControllers = hasHMD && hasHandControllers && isUsingHMD; - - qCDebug(interfaceapp) << "HMD:" << hasHMD << ", Hand Controllers: " << hasHandControllers << ", Using HMD: " << isUsingHMDAndHandControllers; - - // when --url in command line, teleport to location - const QString HIFI_URL_COMMAND_LINE_KEY = "--url"; - int urlIndex = arguments().indexOf(HIFI_URL_COMMAND_LINE_KEY); - QString addressLookupString; - if (urlIndex != -1) { - QUrl url(arguments().value(urlIndex + 1)); - if (url.scheme() == URL_SCHEME_HIFIAPP) { - Setting::Handle("startUpApp").set(url.path()); - } else { - addressLookupString = url.toString(); - } - } - - static const QString SENT_TO_PREVIOUS_LOCATION = "previous_location"; - static const QString SENT_TO_ENTRY = "entry"; - - QString sentTo; - - // If this is a first run we short-circuit the address passed in - if (_firstRun.get()) { -#if !defined(Q_OS_ANDROID) - DependencyManager::get()->goToEntry(); - sentTo = SENT_TO_ENTRY; -#endif - _firstRun.set(false); - - } else { -#if !defined(Q_OS_ANDROID) - QString goingTo = ""; - if (addressLookupString.isEmpty()) { - if (Menu::getInstance()->isOptionChecked(MenuOption::HomeLocation)) { - auto locationBookmarks = DependencyManager::get(); - addressLookupString = locationBookmarks->addressForBookmark(LocationBookmarks::HOME_BOOKMARK); - goingTo = "home location"; - } else { - goingTo = "previous location"; - } - } - qCDebug(interfaceapp) << "Not first run... going to" << qPrintable(!goingTo.isEmpty() ? goingTo : addressLookupString); - DependencyManager::get()->loadSettings(addressLookupString); - sentTo = SENT_TO_PREVIOUS_LOCATION; -#endif - } - - UserActivityLogger::getInstance().logAction("startup_sent_to", { - { "sent_to", sentTo }, - { "sandbox_is_running", sandboxIsRunning }, - { "has_hmd", hasHMD }, - { "has_hand_controllers", hasHandControllers }, - { "is_using_hmd", isUsingHMD }, - { "is_using_hmd_and_hand_controllers", isUsingHMDAndHandControllers }, - { "content_version", contentVersion } - }); - - _connectionMonitor.init(); -} - -bool Application::importJSONFromURL(const QString& urlString) { - // we only load files that terminate in just .json (not .svo.json and not .ava.json) - QUrl jsonURL { urlString }; - - emit svoImportRequested(urlString); - return true; -} - -bool Application::importSVOFromURL(const QString& urlString) { - emit svoImportRequested(urlString); - return true; -} - -bool Application::importFromZIP(const QString& filePath) { - qDebug() << "A zip file has been dropped in: " << filePath; - QUrl empty; - // handle Blocks download from Marketplace - if (filePath.contains("poly.google.com/downloads")) { - addAssetToWorldFromURL(filePath); - } else { - qApp->getFileDownloadInterface()->runUnzip(filePath, empty, true, true, false); - } - return true; -} - -bool Application::isServerlessMode() const { - auto tree = getEntities()->getTree(); - if (tree) { - return tree->isServerlessMode(); - } - return false; -} - -void Application::setIsInterstitialMode(bool interstitialMode) { - bool enableInterstitial = DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled(); - if (enableInterstitial) { - if (_interstitialMode != interstitialMode) { - _interstitialMode = interstitialMode; - emit interstitialModeChanged(_interstitialMode); - - DependencyManager::get()->setAudioPaused(_interstitialMode); - DependencyManager::get()->setMyAvatarDataPacketsPaused(_interstitialMode); - } - } -} - -void Application::setIsServerlessMode(bool serverlessDomain) { - auto tree = getEntities()->getTree(); - if (tree) { - tree->setIsServerlessMode(serverlessDomain); - } -} - -std::map Application::prepareServerlessDomainContents(QUrl domainURL) { - QUuid serverlessSessionID = QUuid::createUuid(); - getMyAvatar()->setSessionUUID(serverlessSessionID); - auto nodeList = DependencyManager::get(); - nodeList->setSessionUUID(serverlessSessionID); - - // there is no domain-server to tell us our permissions, so enable all - NodePermissions permissions; - permissions.setAll(true); - nodeList->setPermissions(permissions); - - // we can't import directly into the main tree because we would need to lock it, and - // Octree::readFromURL calls loop.exec which can run code which will also attempt to lock the tree. - EntityTreePointer tmpTree(new EntityTree()); - tmpTree->setIsServerlessMode(true); - tmpTree->createRootElement(); - auto myAvatar = getMyAvatar(); - tmpTree->setMyAvatar(myAvatar); - bool success = tmpTree->readFromURL(domainURL.toString()); - if (success) { - tmpTree->reaverageOctreeElements(); - tmpTree->sendEntities(&_entityEditSender, getEntities()->getTree(), 0, 0, 0); - } - std::map namedPaths = tmpTree->getNamedPaths(); - - // we must manually eraseAllOctreeElements(false) else the tmpTree will mem-leak - tmpTree->eraseAllOctreeElements(false); - - return namedPaths; -} - -void Application::loadServerlessDomain(QUrl domainURL) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "loadServerlessDomain", Q_ARG(QUrl, domainURL)); - return; - } - - if (domainURL.isEmpty()) { - return; - } - - auto namedPaths = prepareServerlessDomainContents(domainURL); - auto nodeList = DependencyManager::get(); - - nodeList->getDomainHandler().connectedToServerless(namedPaths); - - _fullSceneReceivedCounter++; -} - -void Application::loadErrorDomain(QUrl domainURL) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "loadErrorDomain", Q_ARG(QUrl, domainURL)); - return; - } - - if (domainURL.isEmpty()) { - return; - } - - auto namedPaths = prepareServerlessDomainContents(domainURL); - auto nodeList = DependencyManager::get(); - - nodeList->getDomainHandler().loadedErrorDomain(namedPaths); - - _fullSceneReceivedCounter++; -} - -bool Application::importImage(const QString& urlString) { - qCDebug(interfaceapp) << "An image file has been dropped in"; - QString filepath(urlString); - filepath.remove("file:///"); - addAssetToWorld(filepath, "", false, false); - return true; -} - -// thread-safe -void Application::onPresent(quint32 frameCount) { - bool expected = false; - if (_pendingIdleEvent.compare_exchange_strong(expected, true)) { - postEvent(this, new QEvent((QEvent::Type)ApplicationEvent::Idle), Qt::HighEventPriority); - } - expected = false; - if (_graphicsEngine.checkPendingRenderEvent() && !isAboutToQuit()) { - postEvent(_graphicsEngine._renderEventHandler, new QEvent((QEvent::Type)ApplicationEvent::Render)); - } -} - -static inline bool isKeyEvent(QEvent::Type type) { - return type == QEvent::KeyPress || type == QEvent::KeyRelease; -} - -bool Application::handleKeyEventForFocusedEntity(QEvent* event) { - if (_keyboardFocusedEntity.get() != UNKNOWN_ENTITY_ID) { - switch (event->type()) { - case QEvent::KeyPress: - case QEvent::KeyRelease: - { - auto eventHandler = getEntities()->getEventHandler(_keyboardFocusedEntity.get()); - if (eventHandler) { - event->setAccepted(false); - QCoreApplication::sendEvent(eventHandler, event); - if (event->isAccepted()) { - _lastAcceptedKeyPress = usecTimestampNow(); - return true; - } - } - break; - } - default: - break; - } - } - - return false; -} - -bool Application::handleFileOpenEvent(QFileOpenEvent* fileEvent) { - QUrl url = fileEvent->url(); - if (!url.isEmpty()) { - QString urlString = url.toString(); - if (canAcceptURL(urlString)) { - return acceptURL(urlString); - } - } - return false; -} - -#ifdef DEBUG_EVENT_QUEUE -static int getEventQueueSize(QThread* thread) { - auto threadData = QThreadData::get2(thread); - QMutexLocker locker(&threadData->postEventList.mutex); - return threadData->postEventList.size(); -} - -static void dumpEventQueue(QThread* thread) { - auto threadData = QThreadData::get2(thread); - QMutexLocker locker(&threadData->postEventList.mutex); - qDebug() << "Event list, size =" << threadData->postEventList.size(); - for (auto& postEvent : threadData->postEventList) { - QEvent::Type type = (postEvent.event ? postEvent.event->type() : QEvent::None); - qDebug() << " " << type; - } -} -#endif // DEBUG_EVENT_QUEUE - -bool Application::event(QEvent* event) { - - if (_aboutToQuit) { - return false; - } - - if (!Menu::getInstance()) { - return false; - } - - // Allow focused Entities to handle keyboard input - if (isKeyEvent(event->type()) && handleKeyEventForFocusedEntity(event)) { - return true; - } - - int type = event->type(); - switch (type) { - case ApplicationEvent::Lambda: - static_cast(event)->call(); - return true; - - // Explicit idle keeps the idle running at a lower interval, but without any rendering - // see (windowMinimizedChanged) - case ApplicationEvent::Idle: - idle(); - -#ifdef DEBUG_EVENT_QUEUE - { - int count = getEventQueueSize(QThread::currentThread()); - if (count > 400) { - dumpEventQueue(QThread::currentThread()); - } - } -#endif // DEBUG_EVENT_QUEUE - - _pendingIdleEvent.store(false); - - return true; - - case QEvent::MouseMove: - mouseMoveEvent(static_cast(event)); - return true; - case QEvent::MouseButtonPress: - mousePressEvent(static_cast(event)); - return true; - case QEvent::MouseButtonDblClick: - mouseDoublePressEvent(static_cast(event)); - return true; - case QEvent::MouseButtonRelease: - mouseReleaseEvent(static_cast(event)); - return true; - case QEvent::KeyPress: - keyPressEvent(static_cast(event)); - return true; - case QEvent::KeyRelease: - keyReleaseEvent(static_cast(event)); - return true; - case QEvent::FocusOut: - focusOutEvent(static_cast(event)); - return true; - case QEvent::TouchBegin: - touchBeginEvent(static_cast(event)); - event->accept(); - return true; - case QEvent::TouchEnd: - touchEndEvent(static_cast(event)); - return true; - case QEvent::TouchUpdate: - touchUpdateEvent(static_cast(event)); - return true; - case QEvent::Gesture: - touchGestureEvent((QGestureEvent*)event); - return true; - case QEvent::Wheel: - wheelEvent(static_cast(event)); - return true; - case QEvent::Drop: - dropEvent(static_cast(event)); - return true; - - case QEvent::FileOpen: - if (handleFileOpenEvent(static_cast(event))) { - return true; - } - break; - - default: - break; - } - - return QApplication::event(event); -} - -bool Application::eventFilter(QObject* object, QEvent* event) { - - if (_aboutToQuit && event->type() != QEvent::DeferredDelete && event->type() != QEvent::Destroy) { - return true; - } - - if (event->type() == QEvent::Leave) { - getApplicationCompositor().handleLeaveEvent(); - } - - if (event->type() == QEvent::ShortcutOverride) { -#if !defined(DISABLE_QML) - if (getOffscreenUI()->shouldSwallowShortcut(event)) { - event->accept(); - return true; - } -#endif - - // Filter out captured keys before they're used for shortcut actions. - if (_controllerScriptingInterface->isKeyCaptured(static_cast(event))) { - event->accept(); - return true; - } - } - - return false; -} - -static bool _altPressed{ false }; - -void Application::keyPressEvent(QKeyEvent* event) { - _altPressed = event->key() == Qt::Key_Alt; - - if (!event->isAutoRepeat()) { - _keysPressed.insert(event->key(), *event); - } - - _controllerScriptingInterface->emitKeyPressEvent(event); // send events to any registered scripts - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isKeyCaptured(event) || isInterstitialMode()) { - return; - } - - if (hasFocus() && getLoginDialogPoppedUp()) { - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->keyReleaseEvent(event); - } - - bool isMeta = event->modifiers().testFlag(Qt::ControlModifier); - bool isOption = event->modifiers().testFlag(Qt::AltModifier); - switch (event->key()) { - case Qt::Key_4: - case Qt::Key_5: - case Qt::Key_6: - case Qt::Key_7: - if (isMeta || isOption) { - unsigned int index = static_cast(event->key() - Qt::Key_1); - auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); - if (index < displayPlugins.size()) { - auto targetPlugin = displayPlugins.at(index); - QString targetName = targetPlugin->getName(); - auto menu = Menu::getInstance(); - QAction* action = menu->getActionForOption(targetName); - if (action && !action->isChecked()) { - action->trigger(); - } - } - } - break; - } - } else if (hasFocus()) { - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->keyPressEvent(event); - } - - bool isShifted = event->modifiers().testFlag(Qt::ShiftModifier); - bool isMeta = event->modifiers().testFlag(Qt::ControlModifier); - bool isOption = event->modifiers().testFlag(Qt::AltModifier); - switch (event->key()) { - case Qt::Key_Enter: - case Qt::Key_Return: - if (isOption) { - if (_window->isFullScreen()) { - unsetFullscreen(); - } else { - setFullscreen(nullptr); - } - } - break; - - case Qt::Key_1: { - Menu* menu = Menu::getInstance(); - menu->triggerOption(MenuOption::FirstPerson); - break; - } - case Qt::Key_2: { - Menu* menu = Menu::getInstance(); - menu->triggerOption(MenuOption::FullscreenMirror); - break; - } - case Qt::Key_3: { - Menu* menu = Menu::getInstance(); - menu->triggerOption(MenuOption::ThirdPerson); - break; - } - case Qt::Key_4: - case Qt::Key_5: - case Qt::Key_6: - case Qt::Key_7: - if (isMeta || isOption) { - unsigned int index = static_cast(event->key() - Qt::Key_1); - auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); - if (index < displayPlugins.size()) { - auto targetPlugin = displayPlugins.at(index); - QString targetName = targetPlugin->getName(); - auto menu = Menu::getInstance(); - QAction* action = menu->getActionForOption(targetName); - if (action && !action->isChecked()) { - action->trigger(); - } - } - } - break; - - case Qt::Key_G: - if (isShifted && isMeta && Menu::getInstance() && Menu::getInstance()->getMenu("Developer")->isVisible()) { - static const QString HIFI_FRAMES_FOLDER_VAR = "HIFI_FRAMES_FOLDER"; - static const QString GPU_FRAME_FOLDER = QProcessEnvironment::systemEnvironment().contains(HIFI_FRAMES_FOLDER_VAR) - ? QProcessEnvironment::systemEnvironment().value(HIFI_FRAMES_FOLDER_VAR) - : "hifiFrames"; - static QString GPU_FRAME_TEMPLATE = GPU_FRAME_FOLDER + "/{DATE}_{TIME}"; - QString fullPath = FileUtils::computeDocumentPath(FileUtils::replaceDateTimeTokens(GPU_FRAME_TEMPLATE)); - if (FileUtils::canCreateFile(fullPath)) { - getActiveDisplayPlugin()->captureFrame(fullPath.toStdString()); - } - } - break; - case Qt::Key_X: - if (isShifted && isMeta) { - auto offscreenUi = getOffscreenUI(); - offscreenUi->togglePinned(); - //offscreenUi->getSurfaceContext()->engine()->clearComponentCache(); - //OffscreenUi::information("Debugging", "Component cache cleared"); - // placeholder for dialogs being converted to QML. - } - break; - - case Qt::Key_Y: - if (isShifted && isMeta) { - getActiveDisplayPlugin()->cycleDebugOutput(); - } - break; - - case Qt::Key_B: - if (isMeta) { - auto offscreenUi = getOffscreenUI(); - offscreenUi->load("Browser.qml"); - } else if (isOption) { - controller::InputRecorder* inputRecorder = controller::InputRecorder::getInstance(); - inputRecorder->stopPlayback(); - } - break; - - case Qt::Key_L: - if (isShifted && isMeta) { - Menu::getInstance()->triggerOption(MenuOption::Log); - } else if (isMeta) { - auto dialogsManager = DependencyManager::get(); - dialogsManager->toggleAddressBar(); - } else if (isShifted) { - Menu::getInstance()->triggerOption(MenuOption::LodTools); - } - break; - - case Qt::Key_R: - if (isMeta && !event->isAutoRepeat()) { - DependencyManager::get()->reloadAllScripts(); - getOffscreenUI()->clearCache(); - } - break; - - case Qt::Key_Asterisk: - Menu::getInstance()->triggerOption(MenuOption::DefaultSkybox); - break; - - case Qt::Key_M: - if (isMeta) { - auto audioClient = DependencyManager::get(); - audioClient->setMuted(!audioClient->isMuted()); - } - break; - - case Qt::Key_N: - if (!isOption && !isShifted && isMeta) { - DependencyManager::get()->toggleIgnoreRadius(); - } - break; - - case Qt::Key_S: - if (isShifted && isMeta && !isOption) { - Menu::getInstance()->triggerOption(MenuOption::SuppressShortTimings); - } - break; - - case Qt::Key_P: { - if (!isShifted && !isMeta && !isOption && !event->isAutoRepeat()) { - AudioInjectorOptions options; - options.localOnly = true; - options.positionSet = false; // system sound - options.stereo = true; - - Setting::Handle notificationSounds{ MenuOption::NotificationSounds, true }; - Setting::Handle notificationSoundSnapshot{ MenuOption::NotificationSoundsSnapshot, true }; - if (notificationSounds.get() && notificationSoundSnapshot.get()) { - if (_snapshotSoundInjector) { - _snapshotSoundInjector->setOptions(options); - _snapshotSoundInjector->restart(); - } else { - _snapshotSoundInjector = AudioInjector::playSound(_snapshotSound, options); - } - } - takeSnapshot(true); - } - break; - } - - case Qt::Key_Apostrophe: { - if (isMeta) { - auto cursor = Cursor::Manager::instance().getCursor(); - auto curIcon = cursor->getIcon(); - if (curIcon == Cursor::Icon::DEFAULT) { - showCursor(Cursor::Icon::RETICLE); - } else if (curIcon == Cursor::Icon::RETICLE) { - showCursor(Cursor::Icon::SYSTEM); - } else if (curIcon == Cursor::Icon::SYSTEM) { - showCursor(Cursor::Icon::LINK); - } else { - showCursor(Cursor::Icon::DEFAULT); - } - } else if (!event->isAutoRepeat()){ - resetSensors(true); - } - break; - } - - case Qt::Key_Backslash: - Menu::getInstance()->triggerOption(MenuOption::Chat); - break; - - case Qt::Key_Slash: - Menu::getInstance()->triggerOption(MenuOption::Stats); - break; - - case Qt::Key_Plus: { - if (isMeta && event->modifiers().testFlag(Qt::KeypadModifier)) { - auto& cursorManager = Cursor::Manager::instance(); - cursorManager.setScale(cursorManager.getScale() * 1.1f); - } else { - getMyAvatar()->increaseSize(); - } - break; - } - - case Qt::Key_Minus: { - if (isMeta && event->modifiers().testFlag(Qt::KeypadModifier)) { - auto& cursorManager = Cursor::Manager::instance(); - cursorManager.setScale(cursorManager.getScale() / 1.1f); - } else { - getMyAvatar()->decreaseSize(); - } - break; - } - - case Qt::Key_Equal: - getMyAvatar()->resetSize(); - break; - case Qt::Key_Escape: { - getActiveDisplayPlugin()->abandonCalibration(); - break; - } - - default: - event->ignore(); - break; - } - } -} - -void Application::keyReleaseEvent(QKeyEvent* event) { - if (!event->isAutoRepeat()) { - _keysPressed.remove(event->key()); - } - -#if defined(Q_OS_ANDROID) - if (event->key() == Qt::Key_Back) { - event->accept(); - AndroidHelper::instance().requestActivity("Home", false); - } -#endif - _controllerScriptingInterface->emitKeyReleaseEvent(event); // send events to any registered scripts - - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isKeyCaptured(event)) { - return; - } - - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->keyReleaseEvent(event); - } -} - -void Application::focusOutEvent(QFocusEvent* event) { - auto inputPlugins = PluginManager::getInstance()->getInputPlugins(); - foreach(auto inputPlugin, inputPlugins) { - if (inputPlugin->isActive()) { - inputPlugin->pluginFocusOutEvent(); - } - } - -// FIXME spacemouse code still needs cleanup -#if 0 - //SpacemouseDevice::getInstance().focusOutEvent(); - //SpacemouseManager::getInstance().getDevice()->focusOutEvent(); - SpacemouseManager::getInstance().ManagerFocusOutEvent(); -#endif - - synthesizeKeyReleasEvents(); -} - -void Application::synthesizeKeyReleasEvents() { - // synthesize events for keys currently pressed, since we may not get their release events - // Because our key event handlers may manipulate _keysPressed, lets swap the keys pressed into a local copy, - // clearing the existing list. - QHash keysPressed; - std::swap(keysPressed, _keysPressed); - for (auto& ev : keysPressed) { - QKeyEvent synthesizedEvent { QKeyEvent::KeyRelease, ev.key(), Qt::NoModifier, ev.text() }; - keyReleaseEvent(&synthesizedEvent); - } -} - -void Application::maybeToggleMenuVisible(QMouseEvent* event) const { -#ifndef Q_OS_MAC - // If in full screen, and our main windows menu bar is hidden, and we're close to the top of the QMainWindow - // then show the menubar. - if (_window->isFullScreen()) { - QMenuBar* menuBar = _window->menuBar(); - if (menuBar) { - static const int MENU_TOGGLE_AREA = 10; - if (!menuBar->isVisible()) { - if (event->pos().y() <= MENU_TOGGLE_AREA) { - menuBar->setVisible(true); - } - } else { - if (event->pos().y() > MENU_TOGGLE_AREA) { - menuBar->setVisible(false); - } - } - } - } -#endif -} - -void Application::mouseMoveEvent(QMouseEvent* event) { - PROFILE_RANGE(app_input_mouse, __FUNCTION__); - - maybeToggleMenuVisible(event); - - auto& compositor = getApplicationCompositor(); - // if this is a real mouse event, and we're in HMD mode, then we should use it to move the - // compositor reticle - // handleRealMouseMoveEvent() will return true, if we shouldn't process the event further - if (!compositor.fakeEventActive() && compositor.handleRealMouseMoveEvent()) { - return; // bail - } - -#if !defined(DISABLE_QML) - auto offscreenUi = getOffscreenUI(); - auto eventPosition = compositor.getMouseEventPosition(event); - QPointF transformedPos = offscreenUi ? offscreenUi->mapToVirtualScreen(eventPosition) : QPointF(); -#else - QPointF transformedPos; -#endif - auto button = event->button(); - auto buttons = event->buttons(); - // Determine if the ReticleClick Action is 1 and if so, fake include the LeftMouseButton - if (_reticleClickPressed) { - if (button == Qt::NoButton) { - button = Qt::LeftButton; - } - buttons |= Qt::LeftButton; - } - - QMouseEvent mappedEvent(event->type(), - transformedPos, - event->screenPos(), button, - buttons, event->modifiers()); - - if (compositor.getReticleVisible() || !isHMDMode() || !compositor.getReticleOverDesktop() || - getOverlays().getOverlayAtPoint(glm::vec2(transformedPos.x(), transformedPos.y())) != UNKNOWN_ENTITY_ID) { - getEntities()->mouseMoveEvent(&mappedEvent); - } - - _controllerScriptingInterface->emitMouseMoveEvent(&mappedEvent); // send events to any registered scripts - - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isMouseCaptured()) { - return; - } - - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->mouseMoveEvent(event); - } -} - -void Application::mousePressEvent(QMouseEvent* event) { - // Inhibit the menu if the user is using alt-mouse dragging - _altPressed = false; - -#if !defined(DISABLE_QML) - auto offscreenUi = getOffscreenUI(); - // If we get a mouse press event it means it wasn't consumed by the offscreen UI, - // hence, we should defocus all of the offscreen UI windows, in order to allow - // keyboard shortcuts not to be swallowed by them. In particular, WebEngineViews - // will consume all keyboard events. - offscreenUi->unfocusWindows(); - - auto eventPosition = getApplicationCompositor().getMouseEventPosition(event); - QPointF transformedPos = offscreenUi->mapToVirtualScreen(eventPosition); -#else - QPointF transformedPos; -#endif - - QMouseEvent mappedEvent(event->type(), transformedPos, event->screenPos(), event->button(), event->buttons(), event->modifiers()); - QUuid result = getEntities()->mousePressEvent(&mappedEvent); - setKeyboardFocusEntity(getEntities()->wantsKeyboardFocus(result) ? result : UNKNOWN_ENTITY_ID); - - _controllerScriptingInterface->emitMousePressEvent(&mappedEvent); // send events to any registered scripts - - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isMouseCaptured()) { - return; - } - -#if defined(Q_OS_MAC) - // Fix for OSX right click dragging on window when coming from a native window - bool isFocussed = hasFocus(); - if (!isFocussed && event->button() == Qt::MouseButton::RightButton) { - setFocus(); - isFocussed = true; - } - - if (isFocussed) { -#else - if (hasFocus()) { -#endif - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->mousePressEvent(event); - } - } -} - -void Application::mouseDoublePressEvent(QMouseEvent* event) { -#if !defined(DISABLE_QML) - auto offscreenUi = getOffscreenUI(); - auto eventPosition = getApplicationCompositor().getMouseEventPosition(event); - QPointF transformedPos = offscreenUi->mapToVirtualScreen(eventPosition); -#else - QPointF transformedPos; -#endif - QMouseEvent mappedEvent(event->type(), - transformedPos, - event->screenPos(), event->button(), - event->buttons(), event->modifiers()); - getEntities()->mouseDoublePressEvent(&mappedEvent); - - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isMouseCaptured()) { - return; - } - - _controllerScriptingInterface->emitMouseDoublePressEvent(event); -} - -void Application::mouseReleaseEvent(QMouseEvent* event) { - -#if !defined(DISABLE_QML) - auto offscreenUi = getOffscreenUI(); - auto eventPosition = getApplicationCompositor().getMouseEventPosition(event); - QPointF transformedPos = offscreenUi->mapToVirtualScreen(eventPosition); -#else - QPointF transformedPos; -#endif - QMouseEvent mappedEvent(event->type(), - transformedPos, - event->screenPos(), event->button(), - event->buttons(), event->modifiers()); - - getEntities()->mouseReleaseEvent(&mappedEvent); - - _controllerScriptingInterface->emitMouseReleaseEvent(&mappedEvent); // send events to any registered scripts - - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isMouseCaptured()) { - return; - } - - if (hasFocus()) { - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->mouseReleaseEvent(event); - } - } -} - -void Application::touchUpdateEvent(QTouchEvent* event) { - _altPressed = false; - - if (event->type() == QEvent::TouchUpdate) { - TouchEvent thisEvent(*event, _lastTouchEvent); - _controllerScriptingInterface->emitTouchUpdateEvent(thisEvent); // send events to any registered scripts - _lastTouchEvent = thisEvent; - } - - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isTouchCaptured()) { - return; - } - - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->touchUpdateEvent(event); - } - if (_touchscreenDevice && _touchscreenDevice->isActive()) { - _touchscreenDevice->touchUpdateEvent(event); - } - if (_touchscreenVirtualPadDevice && _touchscreenVirtualPadDevice->isActive()) { - _touchscreenVirtualPadDevice->touchUpdateEvent(event); - } -} - -void Application::touchBeginEvent(QTouchEvent* event) { - _altPressed = false; - TouchEvent thisEvent(*event); // on touch begin, we don't compare to last event - _controllerScriptingInterface->emitTouchBeginEvent(thisEvent); // send events to any registered scripts - - _lastTouchEvent = thisEvent; // and we reset our last event to this event before we call our update - touchUpdateEvent(event); - - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isTouchCaptured()) { - return; - } - - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->touchBeginEvent(event); - } - if (_touchscreenDevice && _touchscreenDevice->isActive()) { - _touchscreenDevice->touchBeginEvent(event); - } - if (_touchscreenVirtualPadDevice && _touchscreenVirtualPadDevice->isActive()) { - _touchscreenVirtualPadDevice->touchBeginEvent(event); - } - -} - -void Application::touchEndEvent(QTouchEvent* event) { - _altPressed = false; - TouchEvent thisEvent(*event, _lastTouchEvent); - _controllerScriptingInterface->emitTouchEndEvent(thisEvent); // send events to any registered scripts - _lastTouchEvent = thisEvent; - - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isTouchCaptured()) { - return; - } - - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->touchEndEvent(event); - } - if (_touchscreenDevice && _touchscreenDevice->isActive()) { - _touchscreenDevice->touchEndEvent(event); - } - if (_touchscreenVirtualPadDevice && _touchscreenVirtualPadDevice->isActive()) { - _touchscreenVirtualPadDevice->touchEndEvent(event); - } - // put any application specific touch behavior below here.. -} - -void Application::touchGestureEvent(QGestureEvent* event) { - if (_touchscreenDevice && _touchscreenDevice->isActive()) { - _touchscreenDevice->touchGestureEvent(event); - } - if (_touchscreenVirtualPadDevice && _touchscreenVirtualPadDevice->isActive()) { - _touchscreenVirtualPadDevice->touchGestureEvent(event); - } -} - -void Application::wheelEvent(QWheelEvent* event) const { - _altPressed = false; - _controllerScriptingInterface->emitWheelEvent(event); // send events to any registered scripts - - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isWheelCaptured() || getLoginDialogPoppedUp()) { - return; - } - - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->wheelEvent(event); - } -} - -void Application::dropEvent(QDropEvent *event) { - const QMimeData* mimeData = event->mimeData(); - for (auto& url : mimeData->urls()) { - QString urlString = url.toString(); - if (acceptURL(urlString, true)) { - event->acceptProposedAction(); - } - } -} - -void Application::dragEnterEvent(QDragEnterEvent* event) { - event->acceptProposedAction(); -} - -// This is currently not used, but could be invoked if the user wants to go to the place embedded in an -// Interface-taken snapshot. (It was developed for drag and drop, before we had asset-server loading or in-world browsers.) -bool Application::acceptSnapshot(const QString& urlString) { - QUrl url(urlString); - QString snapshotPath = url.toLocalFile(); - - SnapshotMetaData* snapshotData = DependencyManager::get()->parseSnapshotData(snapshotPath); - if (snapshotData) { - if (!snapshotData->getURL().toString().isEmpty()) { - DependencyManager::get()->handleLookupString(snapshotData->getURL().toString()); - } - } else { - OffscreenUi::asyncWarning("", "No location details were found in the file\n" + - snapshotPath + "\nTry dragging in an authentic Hifi snapshot."); - } - return true; -} - -#ifdef Q_OS_WIN -#include -#include -#include -#pragma comment(lib, "pdh.lib") -#pragma comment(lib, "ntdll.lib") - -extern "C" { - enum SYSTEM_INFORMATION_CLASS { - SystemBasicInformation = 0, - SystemProcessorPerformanceInformation = 8, - }; - - struct SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION { - LARGE_INTEGER IdleTime; - LARGE_INTEGER KernelTime; - LARGE_INTEGER UserTime; - LARGE_INTEGER DpcTime; - LARGE_INTEGER InterruptTime; - ULONG InterruptCount; - }; - - struct SYSTEM_BASIC_INFORMATION { - ULONG Reserved; - ULONG TimerResolution; - ULONG PageSize; - ULONG NumberOfPhysicalPages; - ULONG LowestPhysicalPageNumber; - ULONG HighestPhysicalPageNumber; - ULONG AllocationGranularity; - ULONG_PTR MinimumUserModeAddress; - ULONG_PTR MaximumUserModeAddress; - ULONG_PTR ActiveProcessorsAffinityMask; - CCHAR NumberOfProcessors; - }; - - NTSYSCALLAPI NTSTATUS NTAPI NtQuerySystemInformation( - _In_ SYSTEM_INFORMATION_CLASS SystemInformationClass, - _Out_writes_bytes_opt_(SystemInformationLength) PVOID SystemInformation, - _In_ ULONG SystemInformationLength, - _Out_opt_ PULONG ReturnLength - ); - -} -template -NTSTATUS NtQuerySystemInformation(SYSTEM_INFORMATION_CLASS SystemInformationClass, T& t) { - return NtQuerySystemInformation(SystemInformationClass, &t, (ULONG)sizeof(T), nullptr); -} - -template -NTSTATUS NtQuerySystemInformation(SYSTEM_INFORMATION_CLASS SystemInformationClass, std::vector& t) { - return NtQuerySystemInformation(SystemInformationClass, t.data(), (ULONG)(sizeof(T) * t.size()), nullptr); -} - - -template -void updateValueAndDelta(std::pair& pair, T newValue) { - auto& value = pair.first; - auto& delta = pair.second; - delta = (value != 0) ? newValue - value : 0; - value = newValue; -} - -struct MyCpuInfo { - using ValueAndDelta = std::pair; - std::string name; - ValueAndDelta kernel { 0, 0 }; - ValueAndDelta user { 0, 0 }; - ValueAndDelta idle { 0, 0 }; - float kernelUsage { 0.0f }; - float userUsage { 0.0f }; - - void update(const SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION& cpuInfo) { - updateValueAndDelta(kernel, cpuInfo.KernelTime.QuadPart); - updateValueAndDelta(user, cpuInfo.UserTime.QuadPart); - updateValueAndDelta(idle, cpuInfo.IdleTime.QuadPart); - auto totalTime = kernel.second + user.second + idle.second; - if (totalTime != 0) { - kernelUsage = (FLOAT)kernel.second / totalTime; - userUsage = (FLOAT)user.second / totalTime; - } else { - kernelUsage = userUsage = 0.0f; - } - } -}; - -void updateCpuInformation() { - static std::once_flag once; - static SYSTEM_BASIC_INFORMATION systemInfo {}; - static SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION cpuTotals; - static std::vector cpuInfos; - static std::vector myCpuInfos; - static MyCpuInfo myCpuTotals; - std::call_once(once, [&] { - NtQuerySystemInformation( SystemBasicInformation, systemInfo); - cpuInfos.resize(systemInfo.NumberOfProcessors); - myCpuInfos.resize(systemInfo.NumberOfProcessors); - for (size_t i = 0; i < systemInfo.NumberOfProcessors; ++i) { - myCpuInfos[i].name = "cpu." + std::to_string(i); - } - myCpuTotals.name = "cpu.total"; - }); - NtQuerySystemInformation(SystemProcessorPerformanceInformation, cpuInfos); - - // Zero the CPU totals. - memset(&cpuTotals, 0, sizeof(SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION)); - for (size_t i = 0; i < systemInfo.NumberOfProcessors; ++i) { - auto& cpuInfo = cpuInfos[i]; - // KernelTime includes IdleTime. - cpuInfo.KernelTime.QuadPart -= cpuInfo.IdleTime.QuadPart; - - // Update totals - cpuTotals.IdleTime.QuadPart += cpuInfo.IdleTime.QuadPart; - cpuTotals.KernelTime.QuadPart += cpuInfo.KernelTime.QuadPart; - cpuTotals.UserTime.QuadPart += cpuInfo.UserTime.QuadPart; - - // Update friendly structure - auto& myCpuInfo = myCpuInfos[i]; - myCpuInfo.update(cpuInfo); - PROFILE_COUNTER(app, myCpuInfo.name.c_str(), { - { "kernel", myCpuInfo.kernelUsage }, - { "user", myCpuInfo.userUsage } - }); - } - - myCpuTotals.update(cpuTotals); - PROFILE_COUNTER(app, myCpuTotals.name.c_str(), { - { "kernel", myCpuTotals.kernelUsage }, - { "user", myCpuTotals.userUsage } - }); -} - - -static ULARGE_INTEGER lastCPU, lastSysCPU, lastUserCPU; -static int numProcessors; -static HANDLE self; -static PDH_HQUERY cpuQuery; -static PDH_HCOUNTER cpuTotal; - -void initCpuUsage() { - SYSTEM_INFO sysInfo; - FILETIME ftime, fsys, fuser; - - GetSystemInfo(&sysInfo); - numProcessors = sysInfo.dwNumberOfProcessors; - - GetSystemTimeAsFileTime(&ftime); - memcpy(&lastCPU, &ftime, sizeof(FILETIME)); - - self = GetCurrentProcess(); - GetProcessTimes(self, &ftime, &ftime, &fsys, &fuser); - memcpy(&lastSysCPU, &fsys, sizeof(FILETIME)); - memcpy(&lastUserCPU, &fuser, sizeof(FILETIME)); - - PdhOpenQuery(NULL, NULL, &cpuQuery); - PdhAddCounter(cpuQuery, "\\Processor(_Total)\\% Processor Time", NULL, &cpuTotal); - PdhCollectQueryData(cpuQuery); -} - -void getCpuUsage(vec3& systemAndUser) { - FILETIME ftime, fsys, fuser; - ULARGE_INTEGER now, sys, user; - - GetSystemTimeAsFileTime(&ftime); - memcpy(&now, &ftime, sizeof(FILETIME)); - - GetProcessTimes(self, &ftime, &ftime, &fsys, &fuser); - memcpy(&sys, &fsys, sizeof(FILETIME)); - memcpy(&user, &fuser, sizeof(FILETIME)); - systemAndUser.x = (sys.QuadPart - lastSysCPU.QuadPart); - systemAndUser.y = (user.QuadPart - lastUserCPU.QuadPart); - systemAndUser /= (float)(now.QuadPart - lastCPU.QuadPart); - systemAndUser /= (float)numProcessors; - systemAndUser *= 100.0f; - lastCPU = now; - lastUserCPU = user; - lastSysCPU = sys; - - PDH_FMT_COUNTERVALUE counterVal; - PdhCollectQueryData(cpuQuery); - PdhGetFormattedCounterValue(cpuTotal, PDH_FMT_DOUBLE, NULL, &counterVal); - systemAndUser.z = (float)counterVal.doubleValue; -} - -void setupCpuMonitorThread() { - initCpuUsage(); - auto cpuMonitorThread = QThread::currentThread(); - - QTimer* timer = new QTimer(); - timer->setInterval(50); - QObject::connect(timer, &QTimer::timeout, [] { - updateCpuInformation(); - vec3 kernelUserAndSystem; - getCpuUsage(kernelUserAndSystem); - PROFILE_COUNTER(app, "cpuProcess", { { "system", kernelUserAndSystem.x }, { "user", kernelUserAndSystem.y } }); - PROFILE_COUNTER(app, "cpuSystem", { { "system", kernelUserAndSystem.z } }); - }); - QObject::connect(cpuMonitorThread, &QThread::finished, [=] { - timer->deleteLater(); - cpuMonitorThread->deleteLater(); - }); - timer->start(); -} - -#endif - -void Application::idle() { - PerformanceTimer perfTimer("idle"); - - // Update the deadlock watchdog - updateHeartbeat(); - -#if !defined(DISABLE_QML) - auto offscreenUi = getOffscreenUI(); - - // These tasks need to be done on our first idle, because we don't want the showing of - // overlay subwindows to do a showDesktop() until after the first time through - static bool firstIdle = true; - if (firstIdle) { - firstIdle = false; - connect(offscreenUi.data(), &OffscreenUi::showDesktop, this, &Application::showDesktop); - } -#endif - -#ifdef Q_OS_WIN - // If tracing is enabled then monitor the CPU in a separate thread - static std::once_flag once; - std::call_once(once, [&] { - if (trace_app().isDebugEnabled()) { - QThread* cpuMonitorThread = new QThread(qApp); - cpuMonitorThread->setObjectName("cpuMonitorThread"); - QObject::connect(cpuMonitorThread, &QThread::started, [this] { setupCpuMonitorThread(); }); - QObject::connect(qApp, &QCoreApplication::aboutToQuit, cpuMonitorThread, &QThread::quit); - cpuMonitorThread->start(); - } - }); -#endif - - auto displayPlugin = getActiveDisplayPlugin(); -#if !defined(DISABLE_QML) - if (displayPlugin) { - auto uiSize = displayPlugin->getRecommendedUiSize(); - // Bit of a hack since there's no device pixel ratio change event I can find. - if (offscreenUi->size() != fromGlm(uiSize)) { - qCDebug(interfaceapp) << "Device pixel ratio changed, triggering resize to " << uiSize; - offscreenUi->resize(fromGlm(uiSize)); - } - } -#endif - - if (displayPlugin) { - PROFILE_COUNTER_IF_CHANGED(app, "present", float, displayPlugin->presentRate()); - } - PROFILE_COUNTER_IF_CHANGED(app, "renderLoopRate", float, getRenderLoopRate()); - PROFILE_COUNTER_IF_CHANGED(app, "currentDownloads", uint32_t, ResourceCache::getLoadingRequests().length()); - PROFILE_COUNTER_IF_CHANGED(app, "pendingDownloads", uint32_t, ResourceCache::getPendingRequestCount()); - PROFILE_COUNTER_IF_CHANGED(app, "currentProcessing", int, DependencyManager::get()->getStat("Processing").toInt()); - PROFILE_COUNTER_IF_CHANGED(app, "pendingProcessing", int, DependencyManager::get()->getStat("PendingProcessing").toInt()); - auto renderConfig = _graphicsEngine.getRenderEngine()->getConfiguration(); - PROFILE_COUNTER_IF_CHANGED(render, "gpuTime", float, (float)_graphicsEngine.getGPUContext()->getFrameTimerGPUAverage()); - - PROFILE_RANGE(app, __FUNCTION__); - - if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { - steamClient->runCallbacks(); - } - - if (auto oculusPlugin = PluginManager::getInstance()->getOculusPlatformPlugin()) { - oculusPlugin->handleOVREvents(); - } - - float secondsSinceLastUpdate = (float)_lastTimeUpdated.nsecsElapsed() / NSECS_PER_MSEC / MSECS_PER_SECOND; - _lastTimeUpdated.start(); - -#if !defined(DISABLE_QML) - // If the offscreen Ui has something active that is NOT the root, then assume it has keyboard focus. - if (offscreenUi && offscreenUi->getWindow()) { - auto activeFocusItem = offscreenUi->getWindow()->activeFocusItem(); - if (_keyboardDeviceHasFocus && activeFocusItem != offscreenUi->getRootItem()) { - _keyboardMouseDevice->pluginFocusOutEvent(); - _keyboardDeviceHasFocus = false; - synthesizeKeyReleasEvents(); - } else if (activeFocusItem == offscreenUi->getRootItem()) { - _keyboardDeviceHasFocus = true; - } - } -#endif - - checkChangeCursor(); - -#if !defined(DISABLE_QML) - auto stats = Stats::getInstance(); - if (stats) { - stats->updateStats(); - } - auto animStats = AnimStats::getInstance(); - if (animStats) { - animStats->updateStats(); - } -#endif - - // Normally we check PipelineWarnings, but since idle will often take more than 10ms we only show these idle timing - // details if we're in ExtraDebugging mode. However, the ::update() and its subcomponents will show their timing - // details normally. -#ifdef Q_OS_ANDROID - bool showWarnings = false; -#else - bool showWarnings = getLogger()->extraDebugging(); -#endif - PerformanceWarning warn(showWarnings, "idle()"); - - { - _gameWorkload.updateViews(_viewFrustum, getMyAvatar()->getHeadPosition()); - _gameWorkload._engine->run(); - } - { - PerformanceTimer perfTimer("update"); - PerformanceWarning warn(showWarnings, "Application::idle()... update()"); - static const float BIGGEST_DELTA_TIME_SECS = 0.25f; - update(glm::clamp(secondsSinceLastUpdate, 0.0f, BIGGEST_DELTA_TIME_SECS)); - } - - { // Update keyboard focus highlight - if (!_keyboardFocusedEntity.get().isInvalidID()) { - const quint64 LOSE_FOCUS_AFTER_ELAPSED_TIME = 30 * USECS_PER_SECOND; // if idle for 30 seconds, drop focus - quint64 elapsedSinceAcceptedKeyPress = usecTimestampNow() - _lastAcceptedKeyPress; - if (elapsedSinceAcceptedKeyPress > LOSE_FOCUS_AFTER_ELAPSED_TIME) { - setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); - } else { - if (auto entity = getEntities()->getTree()->findEntityByID(_keyboardFocusedEntity.get())) { - EntityItemProperties properties; - properties.setPosition(entity->getWorldPosition()); - properties.setRotation(entity->getWorldOrientation()); - properties.setDimensions(entity->getScaledDimensions() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR); - DependencyManager::get()->editEntity(_keyboardFocusHighlightID, properties); - } - } - } - } - - { - if (_keyboardFocusWaitingOnRenderable && getEntities()->renderableForEntityId(_keyboardFocusedEntity.get())) { - QUuid entityId = _keyboardFocusedEntity.get(); - setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); - _keyboardFocusWaitingOnRenderable = false; - setKeyboardFocusEntity(entityId); - } - } - - { - PerformanceTimer perfTimer("pluginIdle"); - PerformanceWarning warn(showWarnings, "Application::idle()... pluginIdle()"); - getActiveDisplayPlugin()->idle(); - auto inputPlugins = PluginManager::getInstance()->getInputPlugins(); - foreach(auto inputPlugin, inputPlugins) { - if (inputPlugin->isActive()) { - inputPlugin->idle(); - } - } - } - { - PerformanceTimer perfTimer("rest"); - PerformanceWarning warn(showWarnings, "Application::idle()... rest of it"); - _idleLoopStdev.addValue(secondsSinceLastUpdate); - - // Record standard deviation and reset counter if needed - const int STDEV_SAMPLES = 500; - if (_idleLoopStdev.getSamples() > STDEV_SAMPLES) { - _idleLoopMeasuredJitter = _idleLoopStdev.getStDev(); - _idleLoopStdev.reset(); - } - } - - _overlayConductor.update(secondsSinceLastUpdate); - - _gameLoopCounter.increment(); -} - -ivec2 Application::getMouse() const { - return getApplicationCompositor().getReticlePosition(); -} - -FaceTracker* Application::getActiveFaceTracker() { - auto dde = DependencyManager::get(); - - return dde->isActive() ? static_cast(dde.data()) : nullptr; -} - -FaceTracker* Application::getSelectedFaceTracker() { - FaceTracker* faceTracker = nullptr; -#ifdef HAVE_DDE - if (Menu::getInstance()->isOptionChecked(MenuOption::UseCamera)) { - faceTracker = DependencyManager::get().data(); - } -#endif - return faceTracker; -} - -void Application::setActiveFaceTracker() const { -#ifdef HAVE_DDE - bool isMuted = Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking); - bool isUsingDDE = Menu::getInstance()->isOptionChecked(MenuOption::UseCamera); - Menu::getInstance()->getActionForOption(MenuOption::BinaryEyelidControl)->setVisible(isUsingDDE); - Menu::getInstance()->getActionForOption(MenuOption::CoupleEyelids)->setVisible(isUsingDDE); - Menu::getInstance()->getActionForOption(MenuOption::UseAudioForMouth)->setVisible(isUsingDDE); - Menu::getInstance()->getActionForOption(MenuOption::VelocityFilter)->setVisible(isUsingDDE); - Menu::getInstance()->getActionForOption(MenuOption::CalibrateCamera)->setVisible(isUsingDDE); - auto ddeTracker = DependencyManager::get(); - ddeTracker->setIsMuted(isMuted); - ddeTracker->setEnabled(isUsingDDE && !isMuted); -#endif -} - -#ifdef HAVE_IVIEWHMD -void Application::setActiveEyeTracker() { - auto eyeTracker = DependencyManager::get(); - if (!eyeTracker->isInitialized()) { - return; - } - - bool isEyeTracking = Menu::getInstance()->isOptionChecked(MenuOption::SMIEyeTracking); - bool isSimulating = Menu::getInstance()->isOptionChecked(MenuOption::SimulateEyeTracking); - eyeTracker->setEnabled(isEyeTracking, isSimulating); - - Menu::getInstance()->getActionForOption(MenuOption::OnePointCalibration)->setEnabled(isEyeTracking && !isSimulating); - Menu::getInstance()->getActionForOption(MenuOption::ThreePointCalibration)->setEnabled(isEyeTracking && !isSimulating); - Menu::getInstance()->getActionForOption(MenuOption::FivePointCalibration)->setEnabled(isEyeTracking && !isSimulating); -} - -void Application::calibrateEyeTracker1Point() { - DependencyManager::get()->calibrate(1); -} - -void Application::calibrateEyeTracker3Points() { - DependencyManager::get()->calibrate(3); -} - -void Application::calibrateEyeTracker5Points() { - DependencyManager::get()->calibrate(5); -} -#endif - -bool Application::exportEntities(const QString& filename, - const QVector& entityIDs, - const glm::vec3* givenOffset) { - QHash entities; - - auto nodeList = DependencyManager::get(); - const QUuid myAvatarID = nodeList->getSessionUUID(); - - auto entityTree = getEntities()->getTree(); - auto exportTree = std::make_shared(); - exportTree->setMyAvatar(getMyAvatar()); - exportTree->createRootElement(); - glm::vec3 root(TREE_SCALE, TREE_SCALE, TREE_SCALE); - bool success = true; - entityTree->withReadLock([entityIDs, entityTree, givenOffset, myAvatarID, &root, &entities, &success, &exportTree] { - for (auto entityID : entityIDs) { // Gather entities and properties. - auto entityItem = entityTree->findEntityByEntityItemID(entityID); - if (!entityItem) { - qCWarning(interfaceapp) << "Skipping export of" << entityID << "that is not in scene."; - continue; - } - - if (!givenOffset) { - EntityItemID parentID = entityItem->getParentID(); - bool parentIsAvatar = (parentID == AVATAR_SELF_ID || parentID == myAvatarID); - if (!parentIsAvatar && (parentID.isInvalidID() || - !entityIDs.contains(parentID) || - !entityTree->findEntityByEntityItemID(parentID))) { - // If parent wasn't selected, we want absolute position, which isn't in properties. - auto position = entityItem->getWorldPosition(); - root.x = glm::min(root.x, position.x); - root.y = glm::min(root.y, position.y); - root.z = glm::min(root.z, position.z); - } - } - entities[entityID] = entityItem; - } - - if (entities.size() == 0) { - success = false; - return; - } - - if (givenOffset) { - root = *givenOffset; - } - for (EntityItemPointer& entityDatum : entities) { - auto properties = entityDatum->getProperties(); - EntityItemID parentID = properties.getParentID(); - bool parentIsAvatar = (parentID == AVATAR_SELF_ID || parentID == myAvatarID); - if (parentIsAvatar) { - properties.setParentID(AVATAR_SELF_ID); - } else { - if (parentID.isInvalidID()) { - properties.setPosition(properties.getPosition() - root); - } else if (!entities.contains(parentID)) { - entityDatum->globalizeProperties(properties, "Parent %3 of %2 %1 is not selected for export.", -root); - } // else valid parent -- don't offset - } - exportTree->addEntity(entityDatum->getEntityItemID(), properties); - } - }); - if (success) { - success = exportTree->writeToJSONFile(filename.toLocal8Bit().constData()); - - // restore the main window's active state - _window->activateWindow(); - } - return success; -} - -bool Application::exportEntities(const QString& filename, float x, float y, float z, float scale) { - glm::vec3 center(x, y, z); - glm::vec3 minCorner = center - vec3(scale); - float cubeSize = scale * 2; - AACube boundingCube(minCorner, cubeSize); - QVector entities; - auto entityTree = getEntities()->getTree(); - entityTree->withReadLock([&] { - entityTree->evalEntitiesInCube(boundingCube, PickFilter(), entities); - }); - return exportEntities(filename, entities, ¢er); -} - -void Application::loadSettings() { - - sessionRunTime.set(0); // Just clean living. We're about to saveSettings, which will update value. - DependencyManager::get()->loadSettings(); - DependencyManager::get()->loadSettings(); - - // DONT CHECK IN - //DependencyManager::get()->setAutomaticLODAdjust(false); - - auto menu = Menu::getInstance(); - menu->loadSettings(); - - // override the menu option show overlays to always be true on startup - menu->setIsOptionChecked(MenuOption::Overlays, true); - - // If there is a preferred plugin, we probably messed it up with the menu settings, so fix it. - auto pluginManager = PluginManager::getInstance(); - auto plugins = pluginManager->getPreferredDisplayPlugins(); - if (plugins.size() > 0) { - for (auto plugin : plugins) { - if (auto action = menu->getActionForOption(plugin->getName())) { - action->setChecked(true); - action->trigger(); - // Find and activated highest priority plugin, bail for the rest - break; - } - } - } - - bool isFirstPerson = false; - if (_firstRun.get()) { - // If this is our first run, and no preferred devices were set, default to - // an HMD device if available. - auto displayPlugins = pluginManager->getDisplayPlugins(); - for (auto& plugin : displayPlugins) { - if (plugin->isHmd()) { - if (auto action = menu->getActionForOption(plugin->getName())) { - action->setChecked(true); - action->trigger(); - break; - } - } - } - - isFirstPerson = (qApp->isHMDMode()); - - } else { - // if this is not the first run, the camera will be initialized differently depending on user settings - - if (qApp->isHMDMode()) { - // if the HMD is active, use first-person camera, unless the appropriate setting is checked - isFirstPerson = menu->isOptionChecked(MenuOption::FirstPersonHMD); - } else { - // if HMD is not active, only use first person if the menu option is checked - isFirstPerson = menu->isOptionChecked(MenuOption::FirstPerson); - } - } - - // finish initializing the camera, based on everything we checked above. Third person camera will be used if no settings - // dictated that we should be in first person - Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, isFirstPerson); - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, !isFirstPerson); - _myCamera.setMode((isFirstPerson) ? CAMERA_MODE_FIRST_PERSON : CAMERA_MODE_THIRD_PERSON); - cameraMenuChanged(); - - auto inputs = pluginManager->getInputPlugins(); - for (auto plugin : inputs) { - if (!plugin->isActive()) { - plugin->activate(); - } - } - - getMyAvatar()->loadData(); - _settingsLoaded = true; -} - -void Application::saveSettings() const { - sessionRunTime.set(_sessionRunTimer.elapsed() / MSECS_PER_SECOND); - DependencyManager::get()->saveSettings(); - DependencyManager::get()->saveSettings(); - - Menu::getInstance()->saveSettings(); - getMyAvatar()->saveData(); - PluginManager::getInstance()->saveSettings(); -} - -bool Application::importEntities(const QString& urlOrFilename, const bool isObservable, const qint64 callerId) { - bool success = false; - _entityClipboard->withWriteLock([&] { - _entityClipboard->eraseAllOctreeElements(); - - success = _entityClipboard->readFromURL(urlOrFilename, isObservable, callerId); - if (success) { - _entityClipboard->reaverageOctreeElements(); - } - }); - return success; -} - -QVector Application::pasteEntities(float x, float y, float z) { - return _entityClipboard->sendEntities(&_entityEditSender, getEntities()->getTree(), x, y, z); -} - -void Application::init() { - // Make sure Login state is up to date -#if !defined(DISABLE_QML) - DependencyManager::get()->toggleLoginDialog(); -#endif - DependencyManager::get()->init(); - - _timerStart.start(); - _lastTimeUpdated.start(); - - if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { - // when +connect_lobby in command line, join steam lobby - const QString STEAM_LOBBY_COMMAND_LINE_KEY = "+connect_lobby"; - int lobbyIndex = arguments().indexOf(STEAM_LOBBY_COMMAND_LINE_KEY); - if (lobbyIndex != -1) { - QString lobbyId = arguments().value(lobbyIndex + 1); - steamClient->joinLobby(lobbyId); - } - } - - - qCDebug(interfaceapp) << "Loaded settings"; - - // fire off an immediate domain-server check in now that settings are loaded - if (!isServerlessMode()) { - DependencyManager::get()->sendDomainServerCheckIn(); - } - - // This allows collision to be set up properly for shape entities supported by GeometryCache. - // This is before entity setup to ensure that it's ready for whenever instance collision is initialized. - ShapeEntityItem::setShapeInfoCalulator(ShapeEntityItem::ShapeInfoCalculator(&shapeInfoCalculator)); - - getEntities()->init(); - getEntities()->setEntityLoadingPriorityFunction([this](const EntityItem& item) { - auto dims = item.getScaledDimensions(); - auto maxSize = glm::compMax(dims); - - if (maxSize <= 0.0f) { - return 0.0f; - } - - auto distance = glm::distance(getMyAvatar()->getWorldPosition(), item.getWorldPosition()); - return atan2(maxSize, distance); - }); - - ObjectMotionState::setShapeManager(&_shapeManager); - _physicsEngine->init(); - - EntityTreePointer tree = getEntities()->getTree(); - _entitySimulation->init(tree, _physicsEngine, &_entityEditSender); - tree->setSimulation(_entitySimulation); - - auto entityScriptingInterface = DependencyManager::get(); - - // connect the _entityCollisionSystem to our EntityTreeRenderer since that's what handles running entity scripts - connect(_entitySimulation.get(), &PhysicalEntitySimulation::entityCollisionWithEntity, - getEntities().data(), &EntityTreeRenderer::entityCollisionWithEntity); - - // connect the _entities (EntityTreeRenderer) to our script engine's EntityScriptingInterface for firing - // of events related clicking, hovering over, and entering entities - getEntities()->connectSignalsToSlots(entityScriptingInterface.data()); - - // Make sure any new sounds are loaded as soon as know about them. - connect(tree.get(), &EntityTree::newCollisionSoundURL, this, [this](QUrl newURL, EntityItemID id) { - getEntities()->setCollisionSound(id, DependencyManager::get()->getSound(newURL)); - }, Qt::QueuedConnection); - connect(getMyAvatar().get(), &MyAvatar::newCollisionSoundURL, this, [this](QUrl newURL) { - if (auto avatar = getMyAvatar()) { - auto sound = DependencyManager::get()->getSound(newURL); - avatar->setCollisionSound(sound); - } - }, Qt::QueuedConnection); - - _gameWorkload.startup(getEntities()->getWorkloadSpace(), _graphicsEngine.getRenderScene(), _entitySimulation); - _entitySimulation->setWorkloadSpace(getEntities()->getWorkloadSpace()); -} - -void Application::pauseUntilLoginDetermined() { - if (QThread::currentThread() != qApp->thread()) { - QMetaObject::invokeMethod(this, "pauseUntilLoginDetermined"); - return; - } - - auto myAvatar = getMyAvatar(); - _previousAvatarTargetScale = myAvatar->getTargetScale(); - _previousAvatarSkeletonModel = myAvatar->getSkeletonModelURL().toString(); - myAvatar->setTargetScale(1.0f); - myAvatar->setSkeletonModelURLFromScript(myAvatar->defaultFullAvatarModelUrl().toString()); - myAvatar->setEnableMeshVisible(false); - - _controllerScriptingInterface->disableMapping(STANDARD_TO_ACTION_MAPPING_NAME); - - { - auto userInputMapper = DependencyManager::get(); - if (userInputMapper->loadMapping(NO_MOVEMENT_MAPPING_JSON)) { - _controllerScriptingInterface->enableMapping(NO_MOVEMENT_MAPPING_NAME); - } - } - - const auto& nodeList = DependencyManager::get(); - // save interstitial mode setting until resuming. - _interstitialModeEnabled = nodeList->getDomainHandler().getInterstitialModeEnabled(); - nodeList->getDomainHandler().setInterstitialModeEnabled(false); - - auto menu = Menu::getInstance(); - menu->getMenu("Edit")->setVisible(false); - menu->getMenu("View")->setVisible(false); - menu->getMenu("Navigate")->setVisible(false); - menu->getMenu("Settings")->setVisible(false); - _developerMenuVisible = menu->getMenu("Developer")->isVisible(); - menu->setIsOptionChecked(MenuOption::Stats, false); - if (_developerMenuVisible) { - menu->getMenu("Developer")->setVisible(false); - } - _previousCameraMode = _myCamera.getMode(); - _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); - cameraModeChanged(); - - // disconnect domain handler. - nodeList->getDomainHandler().disconnect(); - -} - -void Application::resumeAfterLoginDialogActionTaken() { - if (QThread::currentThread() != qApp->thread()) { - QMetaObject::invokeMethod(this, "resumeAfterLoginDialogActionTaken"); - return; - } - - if (!isHMDMode() && getDesktopTabletBecomesToolbarSetting()) { - auto toolbar = DependencyManager::get()->getToolbar("com.highfidelity.interface.toolbar.system"); - toolbar->writeProperty("visible", true); - } else { - getApplicationCompositor().getReticleInterface()->setAllowMouseCapture(true); - getApplicationCompositor().getReticleInterface()->setVisible(true); - } - - updateSystemTabletMode(); - - { - auto userInputMapper = DependencyManager::get(); - userInputMapper->unloadMapping(NO_MOVEMENT_MAPPING_JSON); - _controllerScriptingInterface->disableMapping(NO_MOVEMENT_MAPPING_NAME); - } - - auto myAvatar = getMyAvatar(); - myAvatar->setTargetScale(_previousAvatarTargetScale); - myAvatar->setSkeletonModelURLFromScript(_previousAvatarSkeletonModel); - myAvatar->setEnableMeshVisible(true); - - _controllerScriptingInterface->enableMapping(STANDARD_TO_ACTION_MAPPING_NAME); - - const auto& nodeList = DependencyManager::get(); - nodeList->getDomainHandler().setInterstitialModeEnabled(_interstitialModeEnabled); - { - auto scriptEngines = DependencyManager::get().data(); - // this will force the model the look at the correct directory (weird order of operations issue) - scriptEngines->reloadLocalFiles(); - - // if the --scripts command-line argument was used. - if (!_defaultScriptsLocation.exists() && (arguments().indexOf(QString("--").append(SCRIPTS_SWITCH))) != -1) { - scriptEngines->loadDefaultScripts(); - scriptEngines->defaultScriptsLocationOverridden(true); - } else { - scriptEngines->loadScripts(); - } - } - - auto accountManager = DependencyManager::get(); - auto addressManager = DependencyManager::get(); - - // restart domain handler. - nodeList->getDomainHandler().resetting(); - - QVariant testProperty = property(hifi::properties::TEST); - if (testProperty.isValid()) { - const auto testScript = property(hifi::properties::TEST).toUrl(); - // Set last parameter to exit interface when the test script finishes, if so requested - DependencyManager::get()->loadScript(testScript, false, false, false, false, quitWhenFinished); - // This is done so we don't get a "connection time-out" message when we haven't passed in a URL. - if (arguments().contains("--url")) { - auto reply = SandboxUtils::getStatus(); - connect(reply, &QNetworkReply::finished, this, [this, reply] { handleSandboxStatus(reply); }); - } - } else { - auto reply = SandboxUtils::getStatus(); - connect(reply, &QNetworkReply::finished, this, [this, reply] { handleSandboxStatus(reply); }); - } - - auto menu = Menu::getInstance(); - menu->getMenu("Edit")->setVisible(true); - menu->getMenu("View")->setVisible(true); - menu->getMenu("Navigate")->setVisible(true); - menu->getMenu("Settings")->setVisible(true); - menu->getMenu("Developer")->setVisible(_developerMenuVisible); - _myCamera.setMode(_previousCameraMode); - cameraModeChanged(); -} - -void Application::loadAvatarScripts(const QVector& urls) { - auto scriptEngines = DependencyManager::get(); - auto runningScripts = scriptEngines->getRunningScripts(); - for (auto url : urls) { - int index = runningScripts.indexOf(url); - if (index < 0) { - auto scriptEnginePointer = scriptEngines->loadScript(url, false); - if (scriptEnginePointer) { - scriptEnginePointer->setType(ScriptEngine::Type::AVATAR); - } - } - } -} - -void Application::unloadAvatarScripts() { - auto scriptEngines = DependencyManager::get(); - auto urls = scriptEngines->getRunningScripts(); - for (auto url : urls) { - auto scriptEngine = scriptEngines->getScriptEngine(url); - if (scriptEngine->getType() == ScriptEngine::Type::AVATAR) { - scriptEngines->stopScript(url, false); - } - } -} - -void Application::updateLOD(float deltaTime) const { - PerformanceTimer perfTimer("LOD"); - // adjust it unless we were asked to disable this feature, or if we're currently in throttleRendering mode - if (!isThrottleRendering()) { - float presentTime = getActiveDisplayPlugin()->getAveragePresentTime(); - float engineRunTime = (float)(_graphicsEngine.getRenderEngine()->getConfiguration().get()->getCPURunTime()); - float gpuTime = getGPUContext()->getFrameTimerGPUAverage(); - float batchTime = getGPUContext()->getFrameTimerBatchAverage(); - auto lodManager = DependencyManager::get(); - lodManager->setRenderTimes(presentTime, engineRunTime, batchTime, gpuTime); - lodManager->autoAdjustLOD(deltaTime); - } else { - DependencyManager::get()->resetLODAdjust(); - } -} - -void Application::pushPostUpdateLambda(void* key, const std::function& func) { - std::unique_lock guard(_postUpdateLambdasLock); - _postUpdateLambdas[key] = func; -} - -// Called during Application::update immediately before AvatarManager::updateMyAvatar, updating my data that is then sent to everyone. -// (Maybe this code should be moved there?) -// The principal result is to call updateLookAtTargetAvatar() and then setLookAtPosition(). -// Note that it is called BEFORE we update position or joints based on sensors, etc. -void Application::updateMyAvatarLookAtPosition() { - PerformanceTimer perfTimer("lookAt"); - bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); - PerformanceWarning warn(showWarnings, "Application::updateMyAvatarLookAtPosition()"); - - auto myAvatar = getMyAvatar(); - myAvatar->updateLookAtTargetAvatar(); - FaceTracker* faceTracker = getActiveFaceTracker(); - auto eyeTracker = DependencyManager::get(); - - bool isLookingAtSomeone = false; - bool isHMD = qApp->isHMDMode(); - glm::vec3 lookAtSpot; - if (eyeTracker->isTracking() && (isHMD || eyeTracker->isSimulating())) { - // Look at the point that the user is looking at. - glm::vec3 lookAtPosition = eyeTracker->getLookAtPosition(); - if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { - lookAtPosition.x = -lookAtPosition.x; - } - if (isHMD) { - // TODO -- this code is probably wrong, getHeadPose() returns something in sensor frame, not avatar - glm::mat4 headPose = getActiveDisplayPlugin()->getHeadPose(); - glm::quat hmdRotation = glm::quat_cast(headPose); - lookAtSpot = _myCamera.getPosition() + myAvatar->getWorldOrientation() * (hmdRotation * lookAtPosition); - } else { - lookAtSpot = myAvatar->getHead()->getEyePosition() - + (myAvatar->getHead()->getFinalOrientationInWorldFrame() * lookAtPosition); - } - } else { - AvatarSharedPointer lookingAt = myAvatar->getLookAtTargetAvatar().lock(); - bool haveLookAtCandidate = lookingAt && myAvatar.get() != lookingAt.get(); - auto avatar = static_pointer_cast(lookingAt); - bool mutualLookAtSnappingEnabled = avatar && avatar->getLookAtSnappingEnabled() && myAvatar->getLookAtSnappingEnabled(); - if (haveLookAtCandidate && mutualLookAtSnappingEnabled) { - // If I am looking at someone else, look directly at one of their eyes - isLookingAtSomeone = true; - auto lookingAtHead = avatar->getHead(); - - const float MAXIMUM_FACE_ANGLE = 65.0f * RADIANS_PER_DEGREE; - glm::vec3 lookingAtFaceOrientation = lookingAtHead->getFinalOrientationInWorldFrame() * IDENTITY_FORWARD; - glm::vec3 fromLookingAtToMe = glm::normalize(myAvatar->getHead()->getEyePosition() - - lookingAtHead->getEyePosition()); - float faceAngle = glm::angle(lookingAtFaceOrientation, fromLookingAtToMe); - - if (faceAngle < MAXIMUM_FACE_ANGLE) { - // Randomly look back and forth between look targets - eyeContactTarget target = Menu::getInstance()->isOptionChecked(MenuOption::FixGaze) ? - LEFT_EYE : myAvatar->getEyeContactTarget(); - switch (target) { - case LEFT_EYE: - lookAtSpot = lookingAtHead->getLeftEyePosition(); - break; - case RIGHT_EYE: - lookAtSpot = lookingAtHead->getRightEyePosition(); - break; - case MOUTH: - lookAtSpot = lookingAtHead->getMouthPosition(); - break; - } - } else { - // Just look at their head (mid point between eyes) - lookAtSpot = lookingAtHead->getEyePosition(); - } - } else { - // I am not looking at anyone else, so just look forward - auto headPose = myAvatar->getControllerPoseInWorldFrame(controller::Action::HEAD); - if (headPose.isValid()) { - lookAtSpot = transformPoint(headPose.getMatrix(), glm::vec3(0.0f, 0.0f, TREE_SCALE)); - } else { - lookAtSpot = myAvatar->getHead()->getEyePosition() + - (myAvatar->getHead()->getFinalOrientationInWorldFrame() * glm::vec3(0.0f, 0.0f, -TREE_SCALE)); - } - } - - // Deflect the eyes a bit to match the detected gaze from the face tracker if active. - if (faceTracker && !faceTracker->isMuted()) { - float eyePitch = faceTracker->getEstimatedEyePitch(); - float eyeYaw = faceTracker->getEstimatedEyeYaw(); - const float GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT = 0.1f; - glm::vec3 origin = myAvatar->getHead()->getEyePosition(); - float deflection = faceTracker->getEyeDeflection(); - if (isLookingAtSomeone) { - deflection *= GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT; - } - lookAtSpot = origin + _myCamera.getOrientation() * glm::quat(glm::radians(glm::vec3( - eyePitch * deflection, eyeYaw * deflection, 0.0f))) * - glm::inverse(_myCamera.getOrientation()) * (lookAtSpot - origin); - } - } - - myAvatar->getHead()->setLookAtPosition(lookAtSpot); -} - -void Application::updateThreads(float deltaTime) { - PerformanceTimer perfTimer("updateThreads"); - bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); - PerformanceWarning warn(showWarnings, "Application::updateThreads()"); - - // parse voxel packets - if (!_enableProcessOctreeThread) { - _octreeProcessor.threadRoutine(); - _entityEditSender.threadRoutine(); - } -} - -void Application::toggleOverlays() { - auto menu = Menu::getInstance(); - menu->setIsOptionChecked(MenuOption::Overlays, !menu->isOptionChecked(MenuOption::Overlays)); -} - -void Application::setOverlaysVisible(bool visible) { - auto menu = Menu::getInstance(); - menu->setIsOptionChecked(MenuOption::Overlays, visible); -} - -void Application::centerUI() { - _overlayConductor.centerUI(); -} - -void Application::cycleCamera() { - auto menu = Menu::getInstance(); - if (menu->isOptionChecked(MenuOption::FullscreenMirror)) { - - menu->setIsOptionChecked(MenuOption::FullscreenMirror, false); - menu->setIsOptionChecked(MenuOption::FirstPerson, true); - - } else if (menu->isOptionChecked(MenuOption::FirstPerson)) { - - menu->setIsOptionChecked(MenuOption::FirstPerson, false); - menu->setIsOptionChecked(MenuOption::ThirdPerson, true); - - } else if (menu->isOptionChecked(MenuOption::ThirdPerson)) { - - menu->setIsOptionChecked(MenuOption::ThirdPerson, false); - menu->setIsOptionChecked(MenuOption::FullscreenMirror, true); - - } else if (menu->isOptionChecked(MenuOption::IndependentMode) || menu->isOptionChecked(MenuOption::CameraEntityMode)) { - // do nothing if in independent or camera entity modes - return; - } - cameraMenuChanged(); // handle the menu change -} - -void Application::cameraModeChanged() { - switch (_myCamera.getMode()) { - case CAMERA_MODE_FIRST_PERSON: - Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, true); - break; - case CAMERA_MODE_THIRD_PERSON: - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); - break; - case CAMERA_MODE_MIRROR: - Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, true); - break; - case CAMERA_MODE_INDEPENDENT: - Menu::getInstance()->setIsOptionChecked(MenuOption::IndependentMode, true); - break; - case CAMERA_MODE_ENTITY: - Menu::getInstance()->setIsOptionChecked(MenuOption::CameraEntityMode, true); - break; - default: - break; - } - cameraMenuChanged(); -} - -void Application::changeViewAsNeeded(float boomLength) { - // Switch between first and third person views as needed - // This is called when the boom length has changed - bool boomLengthGreaterThanMinimum = (boomLength > MyAvatar::ZOOM_MIN); - - if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON && boomLengthGreaterThanMinimum) { - Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, false); - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); - cameraMenuChanged(); - } else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON && !boomLengthGreaterThanMinimum) { - Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, true); - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, false); - cameraMenuChanged(); - } -} - -void Application::cameraMenuChanged() { - auto menu = Menu::getInstance(); - if (menu->isOptionChecked(MenuOption::FullscreenMirror)) { - if (!isHMDMode() && _myCamera.getMode() != CAMERA_MODE_MIRROR) { - _mirrorYawOffset = 0.0f; - _myCamera.setMode(CAMERA_MODE_MIRROR); - getMyAvatar()->reset(false, false, false); // to reset any active MyAvatar::FollowHelpers - getMyAvatar()->setBoomLength(MyAvatar::ZOOM_DEFAULT); - } - } else if (menu->isOptionChecked(MenuOption::FirstPerson)) { - if (_myCamera.getMode() != CAMERA_MODE_FIRST_PERSON) { - _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); - getMyAvatar()->setBoomLength(MyAvatar::ZOOM_MIN); - } - } else if (menu->isOptionChecked(MenuOption::ThirdPerson)) { - if (_myCamera.getMode() != CAMERA_MODE_THIRD_PERSON) { - _myCamera.setMode(CAMERA_MODE_THIRD_PERSON); - if (getMyAvatar()->getBoomLength() == MyAvatar::ZOOM_MIN) { - getMyAvatar()->setBoomLength(MyAvatar::ZOOM_DEFAULT); - } - } - } else if (menu->isOptionChecked(MenuOption::IndependentMode)) { - if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT) { - _myCamera.setMode(CAMERA_MODE_INDEPENDENT); - } - } else if (menu->isOptionChecked(MenuOption::CameraEntityMode)) { - if (_myCamera.getMode() != CAMERA_MODE_ENTITY) { - _myCamera.setMode(CAMERA_MODE_ENTITY); - } - } -} - -void Application::resetPhysicsReadyInformation() { - // we've changed domains or cleared out caches or something. we no longer know enough about the - // collision information of nearby entities to make running bullet be safe. - _fullSceneReceivedCounter = 0; - _fullSceneCounterAtLastPhysicsCheck = 0; - _gpuTextureMemSizeStabilityCount = 0; - _gpuTextureMemSizeAtLastCheck = 0; - _physicsEnabled = false; - _octreeProcessor.startEntitySequence(); -} - - -void Application::reloadResourceCaches() { - resetPhysicsReadyInformation(); - - // Query the octree to refresh everything in view - _queryExpiry = SteadyClock::now(); - _octreeQuery.incrementConnectionID(); - - queryOctree(NodeType::EntityServer, PacketType::EntityQuery); - - // Clear the entities and their renderables - getEntities()->clear(); - - DependencyManager::get()->clearCache(); - DependencyManager::get()->clearCache(); - - // Clear all the resource caches - DependencyManager::get()->clear(); - DependencyManager::get()->refreshAll(); - DependencyManager::get()->refreshAll(); - MaterialCache::instance().refreshAll(); - DependencyManager::get()->refreshAll(); - ShaderCache::instance().refreshAll(); - DependencyManager::get()->refreshAll(); - DependencyManager::get()->refreshAll(); - - DependencyManager::get()->reset(); // Force redownload of .fst models - - DependencyManager::get()->reloadAllScripts(); - getOffscreenUI()->clearCache(); - - DependencyManager::get()->createKeyboard(); - - getMyAvatar()->resetFullAvatarURL(); -} - -void Application::rotationModeChanged() const { - if (!Menu::getInstance()->isOptionChecked(MenuOption::CenterPlayerInView)) { - getMyAvatar()->setHeadPitch(0); - } -} - -void Application::setKeyboardFocusHighlight(const glm::vec3& position, const glm::quat& rotation, const glm::vec3& dimensions) { - if (qApp->getLoginDialogPoppedUp()) { - return; - } - - auto entityScriptingInterface = DependencyManager::get(); - if (_keyboardFocusHighlightID == UNKNOWN_ENTITY_ID || !entityScriptingInterface->isAddedEntity(_keyboardFocusHighlightID)) { - EntityItemProperties properties; - properties.setType(EntityTypes::Box); - properties.setAlpha(1.0f); - properties.setColor({ 0xFF, 0xEF, 0x00 }); - properties.setPrimitiveMode(PrimitiveMode::LINES); - properties.getPulse().setMin(0.5); - properties.getPulse().setMax(1.0f); - properties.getPulse().setColorMode(PulseMode::IN_PHASE); - properties.setIgnorePickIntersection(true); - _keyboardFocusHighlightID = entityScriptingInterface->addEntityInternal(properties, entity::HostType::LOCAL); - } - - // Position focus - EntityItemProperties properties; - properties.setPosition(position); - properties.setRotation(rotation); - properties.setDimensions(dimensions); - properties.setVisible(true); - entityScriptingInterface->editEntity(_keyboardFocusHighlightID, properties); -} - -QUuid Application::getKeyboardFocusEntity() const { - return _keyboardFocusedEntity.get(); -} - -void Application::setKeyboardFocusEntity(const QUuid& id) { - if (_keyboardFocusedEntity.get() != id) { - if (qApp->getLoginDialogPoppedUp() && !_loginDialogID.isNull()) { - if (id == _loginDialogID) { - emit loginDialogFocusEnabled(); - } else if (!_keyboardFocusWaitingOnRenderable) { - // that's the only entity we want in focus; - return; - } - } - - _keyboardFocusedEntity.set(id); - - auto entityScriptingInterface = DependencyManager::get(); - if (id != UNKNOWN_ENTITY_ID) { - EntityPropertyFlags desiredProperties; - desiredProperties += PROP_VISIBLE; - desiredProperties += PROP_SHOW_KEYBOARD_FOCUS_HIGHLIGHT; - auto properties = entityScriptingInterface->getEntityProperties(id); - if (properties.getVisible()) { - auto entities = getEntities(); - auto entityId = _keyboardFocusedEntity.get(); - auto entityItemRenderable = entities->renderableForEntityId(entityId); - if (!entityItemRenderable) { - _keyboardFocusWaitingOnRenderable = true; - } else if (entityItemRenderable->wantsKeyboardFocus()) { - entities->setProxyWindow(entityId, _window->windowHandle()); - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->pluginFocusOutEvent(); - } - _lastAcceptedKeyPress = usecTimestampNow(); - - if (properties.getShowKeyboardFocusHighlight()) { - if (auto entity = entities->getEntity(entityId)) { - setKeyboardFocusHighlight(entity->getWorldPosition(), entity->getWorldOrientation(), - entity->getScaledDimensions() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR); - return; - } - } - } - } - } - - EntityItemProperties properties; - properties.setVisible(false); - entityScriptingInterface->editEntity(_keyboardFocusHighlightID, properties); - } -} - -void Application::updateDialogs(float deltaTime) const { - PerformanceTimer perfTimer("updateDialogs"); - bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); - PerformanceWarning warn(showWarnings, "Application::updateDialogs()"); - auto dialogsManager = DependencyManager::get(); - - QPointer octreeStatsDialog = dialogsManager->getOctreeStatsDialog(); - if (octreeStatsDialog) { - octreeStatsDialog->update(); - } -} - -void Application::updateSecondaryCameraViewFrustum() { - // TODO: Fix this by modeling the way the secondary camera works on how the main camera works - // ie. Use a camera object stored in the game logic and informs the Engine on where the secondary - // camera should be. - - // Code based on SecondaryCameraJob - auto renderConfig = _graphicsEngine.getRenderEngine()->getConfiguration(); - assert(renderConfig); - auto camera = dynamic_cast(renderConfig->getConfig("SecondaryCamera")); - - if (!camera || !camera->isEnabled()) { - return; - } - - ViewFrustum secondaryViewFrustum; - if (camera->portalProjection && !camera->attachedEntityId.isNull() && !camera->portalEntranceEntityId.isNull()) { - auto entityScriptingInterface = DependencyManager::get(); - EntityItemPointer portalEntrance = qApp->getEntities()->getTree()->findEntityByID(camera->portalEntranceEntityId); - EntityItemPointer portalExit = qApp->getEntities()->getTree()->findEntityByID(camera->attachedEntityId); - - glm::vec3 portalEntrancePropertiesPosition = portalEntrance->getWorldPosition(); - glm::quat portalEntrancePropertiesRotation = portalEntrance->getWorldOrientation(); - glm::mat4 worldFromPortalEntranceRotation = glm::mat4_cast(portalEntrancePropertiesRotation); - glm::mat4 worldFromPortalEntranceTranslation = glm::translate(portalEntrancePropertiesPosition); - glm::mat4 worldFromPortalEntrance = worldFromPortalEntranceTranslation * worldFromPortalEntranceRotation; - glm::mat4 portalEntranceFromWorld = glm::inverse(worldFromPortalEntrance); - - glm::vec3 portalExitPropertiesPosition = portalExit->getWorldPosition(); - glm::quat portalExitPropertiesRotation = portalExit->getWorldOrientation(); - glm::vec3 portalExitPropertiesDimensions = portalExit->getScaledDimensions(); - glm::vec3 halfPortalExitPropertiesDimensions = 0.5f * portalExitPropertiesDimensions; - - glm::mat4 worldFromPortalExitRotation = glm::mat4_cast(portalExitPropertiesRotation); - glm::mat4 worldFromPortalExitTranslation = glm::translate(portalExitPropertiesPosition); - glm::mat4 worldFromPortalExit = worldFromPortalExitTranslation * worldFromPortalExitRotation; - - glm::vec3 mainCameraPositionWorld = getCamera().getPosition(); - glm::vec3 mainCameraPositionPortalEntrance = vec3(portalEntranceFromWorld * vec4(mainCameraPositionWorld, 1.0f)); - mainCameraPositionPortalEntrance = vec3(-mainCameraPositionPortalEntrance.x, mainCameraPositionPortalEntrance.y, - -mainCameraPositionPortalEntrance.z); - glm::vec3 portalExitCameraPositionWorld = vec3(worldFromPortalExit * vec4(mainCameraPositionPortalEntrance, 1.0f)); - - secondaryViewFrustum.setPosition(portalExitCameraPositionWorld); - secondaryViewFrustum.setOrientation(portalExitPropertiesRotation); - - float nearClip = mainCameraPositionPortalEntrance.z + portalExitPropertiesDimensions.z * 2.0f; - // `mainCameraPositionPortalEntrance` should technically be `mainCameraPositionPortalExit`, - // but the values are the same. - glm::vec3 upperRight = halfPortalExitPropertiesDimensions - mainCameraPositionPortalEntrance; - glm::vec3 bottomLeft = -halfPortalExitPropertiesDimensions - mainCameraPositionPortalEntrance; - glm::mat4 frustum = glm::frustum(bottomLeft.x, upperRight.x, bottomLeft.y, upperRight.y, nearClip, camera->farClipPlaneDistance); - secondaryViewFrustum.setProjection(frustum); - } else if (camera->mirrorProjection && !camera->attachedEntityId.isNull()) { - auto entityScriptingInterface = DependencyManager::get(); - auto entityProperties = entityScriptingInterface->getEntityProperties(camera->attachedEntityId); - glm::vec3 mirrorPropertiesPosition = entityProperties.getPosition(); - glm::quat mirrorPropertiesRotation = entityProperties.getRotation(); - glm::vec3 mirrorPropertiesDimensions = entityProperties.getDimensions(); - glm::vec3 halfMirrorPropertiesDimensions = 0.5f * mirrorPropertiesDimensions; - - // setup mirror from world as inverse of world from mirror transformation using inverted x and z for mirrored image - // TODO: we are assuming here that UP is world y-axis - glm::mat4 worldFromMirrorRotation = glm::mat4_cast(mirrorPropertiesRotation) * glm::scale(vec3(-1.0f, 1.0f, -1.0f)); - glm::mat4 worldFromMirrorTranslation = glm::translate(mirrorPropertiesPosition); - glm::mat4 worldFromMirror = worldFromMirrorTranslation * worldFromMirrorRotation; - glm::mat4 mirrorFromWorld = glm::inverse(worldFromMirror); - - // get mirror camera position by reflecting main camera position's z coordinate in mirror space - glm::vec3 mainCameraPositionWorld = getCamera().getPosition(); - glm::vec3 mainCameraPositionMirror = vec3(mirrorFromWorld * vec4(mainCameraPositionWorld, 1.0f)); - glm::vec3 mirrorCameraPositionMirror = vec3(mainCameraPositionMirror.x, mainCameraPositionMirror.y, - -mainCameraPositionMirror.z); - glm::vec3 mirrorCameraPositionWorld = vec3(worldFromMirror * vec4(mirrorCameraPositionMirror, 1.0f)); - - // set frustum position to be mirrored camera and set orientation to mirror's adjusted rotation - glm::quat mirrorCameraOrientation = glm::quat_cast(worldFromMirrorRotation); - secondaryViewFrustum.setPosition(mirrorCameraPositionWorld); - secondaryViewFrustum.setOrientation(mirrorCameraOrientation); - - // build frustum using mirror space translation of mirrored camera - float nearClip = mirrorCameraPositionMirror.z + mirrorPropertiesDimensions.z * 2.0f; - glm::vec3 upperRight = halfMirrorPropertiesDimensions - mirrorCameraPositionMirror; - glm::vec3 bottomLeft = -halfMirrorPropertiesDimensions - mirrorCameraPositionMirror; - glm::mat4 frustum = glm::frustum(bottomLeft.x, upperRight.x, bottomLeft.y, upperRight.y, nearClip, camera->farClipPlaneDistance); - secondaryViewFrustum.setProjection(frustum); - } else { - if (!camera->attachedEntityId.isNull()) { - auto entityScriptingInterface = DependencyManager::get(); - auto entityProperties = entityScriptingInterface->getEntityProperties(camera->attachedEntityId); - secondaryViewFrustum.setPosition(entityProperties.getPosition()); - secondaryViewFrustum.setOrientation(entityProperties.getRotation()); - } else { - secondaryViewFrustum.setPosition(camera->position); - secondaryViewFrustum.setOrientation(camera->orientation); - } - - float aspectRatio = (float)camera->textureWidth / (float)camera->textureHeight; - secondaryViewFrustum.setProjection(camera->vFoV, - aspectRatio, - camera->nearClipPlaneDistance, - camera->farClipPlaneDistance); - } - // Without calculating the bound planes, the secondary camera will use the same culling frustum as the main camera, - // which is not what we want here. - secondaryViewFrustum.calculate(); - - _conicalViews.push_back(secondaryViewFrustum); -} - -static bool domainLoadingInProgress = false; - -void Application::update(float deltaTime) { - PROFILE_RANGE_EX(app, __FUNCTION__, 0xffff0000, (uint64_t)_graphicsEngine._renderFrameCount + 1); - - if (_aboutToQuit) { - return; - } - - - if (!_physicsEnabled) { - if (!domainLoadingInProgress) { - PROFILE_ASYNC_BEGIN(app, "Scene Loading", ""); - domainLoadingInProgress = true; - } - - // we haven't yet enabled physics. we wait until we think we have all the collision information - // for nearby entities before starting bullet up. - quint64 now = usecTimestampNow(); - if (isServerlessMode() || _octreeProcessor.isLoadSequenceComplete()) { - bool enableInterstitial = DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled(); - - if (gpuTextureMemSizeStable() || !enableInterstitial) { - // we've received a new full-scene octree stats packet, or it's been long enough to try again anyway - _lastPhysicsCheckTime = now; - _fullSceneCounterAtLastPhysicsCheck = _fullSceneReceivedCounter; - _lastQueriedViews.clear(); // Force new view. - - // process octree stats packets are sent in between full sends of a scene (this isn't currently true). - // We keep physics disabled until we've received a full scene and everything near the avatar in that - // scene is ready to compute its collision shape. - if (getMyAvatar()->isReadyForPhysics()) { - _physicsEnabled = true; - setIsInterstitialMode(false); - getMyAvatar()->updateMotionBehaviorFromMenu(); - } - } - } - } else if (domainLoadingInProgress) { - domainLoadingInProgress = false; - PROFILE_ASYNC_END(app, "Scene Loading", ""); - } - - auto myAvatar = getMyAvatar(); - { - PerformanceTimer perfTimer("devices"); - - FaceTracker* tracker = getSelectedFaceTracker(); - if (tracker && Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking) != tracker->isMuted()) { - tracker->toggleMute(); - } - - tracker = getActiveFaceTracker(); - if (tracker && !tracker->isMuted()) { - tracker->update(deltaTime); - - // Auto-mute microphone after losing face tracking? - if (tracker->isTracking()) { - _lastFaceTrackerUpdate = usecTimestampNow(); - } else { - const quint64 MUTE_MICROPHONE_AFTER_USECS = 5000000; //5 secs - Menu* menu = Menu::getInstance(); - auto audioClient = DependencyManager::get(); - if (menu->isOptionChecked(MenuOption::AutoMuteAudio) && !audioClient->isMuted()) { - if (_lastFaceTrackerUpdate > 0 - && ((usecTimestampNow() - _lastFaceTrackerUpdate) > MUTE_MICROPHONE_AFTER_USECS)) { - audioClient->setMuted(true); - _lastFaceTrackerUpdate = 0; - } - } else { - _lastFaceTrackerUpdate = 0; - } - } - } else { - _lastFaceTrackerUpdate = 0; - } - - auto userInputMapper = DependencyManager::get(); - - controller::HmdAvatarAlignmentType hmdAvatarAlignmentType; - if (myAvatar->getHmdAvatarAlignmentType() == "eyes") { - hmdAvatarAlignmentType = controller::HmdAvatarAlignmentType::Eyes; - } else { - hmdAvatarAlignmentType = controller::HmdAvatarAlignmentType::Head; - } - - controller::InputCalibrationData calibrationData = { - myAvatar->getSensorToWorldMatrix(), - createMatFromQuatAndPos(myAvatar->getWorldOrientation(), myAvatar->getWorldPosition()), - myAvatar->getHMDSensorMatrix(), - myAvatar->getCenterEyeCalibrationMat(), - myAvatar->getHeadCalibrationMat(), - myAvatar->getSpine2CalibrationMat(), - myAvatar->getHipsCalibrationMat(), - myAvatar->getLeftFootCalibrationMat(), - myAvatar->getRightFootCalibrationMat(), - myAvatar->getRightArmCalibrationMat(), - myAvatar->getLeftArmCalibrationMat(), - myAvatar->getRightHandCalibrationMat(), - myAvatar->getLeftHandCalibrationMat(), - hmdAvatarAlignmentType - }; - - InputPluginPointer keyboardMousePlugin; - for (auto inputPlugin : PluginManager::getInstance()->getInputPlugins()) { - if (inputPlugin->getName() == KeyboardMouseDevice::NAME) { - keyboardMousePlugin = inputPlugin; - } else if (inputPlugin->isActive()) { - inputPlugin->pluginUpdate(deltaTime, calibrationData); - } - } - - userInputMapper->setInputCalibrationData(calibrationData); - userInputMapper->update(deltaTime); - - if (keyboardMousePlugin && keyboardMousePlugin->isActive()) { - keyboardMousePlugin->pluginUpdate(deltaTime, calibrationData); - } - // Transfer the user inputs to the driveKeys - // FIXME can we drop drive keys and just have the avatar read the action states directly? - myAvatar->clearDriveKeys(); - if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT && !isInterstitialMode()) { - if (!_controllerScriptingInterface->areActionsCaptured() && _myCamera.getMode() != CAMERA_MODE_MIRROR) { - myAvatar->setDriveKey(MyAvatar::TRANSLATE_Z, -1.0f * userInputMapper->getActionState(controller::Action::TRANSLATE_Z)); - myAvatar->setDriveKey(MyAvatar::TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y)); - myAvatar->setDriveKey(MyAvatar::TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X)); - if (deltaTime > FLT_EPSILON) { - myAvatar->setDriveKey(MyAvatar::PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); - myAvatar->setDriveKey(MyAvatar::YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); - myAvatar->setDriveKey(MyAvatar::DELTA_PITCH, -1.0f * userInputMapper->getActionState(controller::Action::DELTA_PITCH)); - myAvatar->setDriveKey(MyAvatar::DELTA_YAW, -1.0f * userInputMapper->getActionState(controller::Action::DELTA_YAW)); - myAvatar->setDriveKey(MyAvatar::STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW)); - } - } - myAvatar->setDriveKey(MyAvatar::ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z)); - } - - myAvatar->setSprintMode((bool)userInputMapper->getActionState(controller::Action::SPRINT)); - static const std::vector avatarControllerActions = { - controller::Action::LEFT_HAND, - controller::Action::RIGHT_HAND, - controller::Action::LEFT_FOOT, - controller::Action::RIGHT_FOOT, - controller::Action::HIPS, - controller::Action::SPINE2, - controller::Action::HEAD, - controller::Action::LEFT_HAND_THUMB1, - controller::Action::LEFT_HAND_THUMB2, - controller::Action::LEFT_HAND_THUMB3, - controller::Action::LEFT_HAND_THUMB4, - controller::Action::LEFT_HAND_INDEX1, - controller::Action::LEFT_HAND_INDEX2, - controller::Action::LEFT_HAND_INDEX3, - controller::Action::LEFT_HAND_INDEX4, - controller::Action::LEFT_HAND_MIDDLE1, - controller::Action::LEFT_HAND_MIDDLE2, - controller::Action::LEFT_HAND_MIDDLE3, - controller::Action::LEFT_HAND_MIDDLE4, - controller::Action::LEFT_HAND_RING1, - controller::Action::LEFT_HAND_RING2, - controller::Action::LEFT_HAND_RING3, - controller::Action::LEFT_HAND_RING4, - controller::Action::LEFT_HAND_PINKY1, - controller::Action::LEFT_HAND_PINKY2, - controller::Action::LEFT_HAND_PINKY3, - controller::Action::LEFT_HAND_PINKY4, - controller::Action::RIGHT_HAND_THUMB1, - controller::Action::RIGHT_HAND_THUMB2, - controller::Action::RIGHT_HAND_THUMB3, - controller::Action::RIGHT_HAND_THUMB4, - controller::Action::RIGHT_HAND_INDEX1, - controller::Action::RIGHT_HAND_INDEX2, - controller::Action::RIGHT_HAND_INDEX3, - controller::Action::RIGHT_HAND_INDEX4, - controller::Action::RIGHT_HAND_MIDDLE1, - controller::Action::RIGHT_HAND_MIDDLE2, - controller::Action::RIGHT_HAND_MIDDLE3, - controller::Action::RIGHT_HAND_MIDDLE4, - controller::Action::RIGHT_HAND_RING1, - controller::Action::RIGHT_HAND_RING2, - controller::Action::RIGHT_HAND_RING3, - controller::Action::RIGHT_HAND_RING4, - controller::Action::RIGHT_HAND_PINKY1, - controller::Action::RIGHT_HAND_PINKY2, - controller::Action::RIGHT_HAND_PINKY3, - controller::Action::RIGHT_HAND_PINKY4, - controller::Action::LEFT_ARM, - controller::Action::RIGHT_ARM, - controller::Action::LEFT_SHOULDER, - controller::Action::RIGHT_SHOULDER, - controller::Action::LEFT_FORE_ARM, - controller::Action::RIGHT_FORE_ARM, - controller::Action::LEFT_LEG, - controller::Action::RIGHT_LEG, - controller::Action::LEFT_UP_LEG, - controller::Action::RIGHT_UP_LEG, - controller::Action::LEFT_TOE_BASE, - controller::Action::RIGHT_TOE_BASE - }; - - // copy controller poses from userInputMapper to myAvatar. - glm::mat4 myAvatarMatrix = createMatFromQuatAndPos(myAvatar->getWorldOrientation(), myAvatar->getWorldPosition()); - glm::mat4 worldToSensorMatrix = glm::inverse(myAvatar->getSensorToWorldMatrix()); - glm::mat4 avatarToSensorMatrix = worldToSensorMatrix * myAvatarMatrix; - for (auto& action : avatarControllerActions) { - controller::Pose pose = userInputMapper->getPoseState(action); - myAvatar->setControllerPoseInSensorFrame(action, pose.transform(avatarToSensorMatrix)); - } - - static const std::vector trackedObjectStringLiterals = { - QStringLiteral("_TrackedObject00"), QStringLiteral("_TrackedObject01"), QStringLiteral("_TrackedObject02"), QStringLiteral("_TrackedObject03"), - QStringLiteral("_TrackedObject04"), QStringLiteral("_TrackedObject05"), QStringLiteral("_TrackedObject06"), QStringLiteral("_TrackedObject07"), - QStringLiteral("_TrackedObject08"), QStringLiteral("_TrackedObject09"), QStringLiteral("_TrackedObject10"), QStringLiteral("_TrackedObject11"), - QStringLiteral("_TrackedObject12"), QStringLiteral("_TrackedObject13"), QStringLiteral("_TrackedObject14"), QStringLiteral("_TrackedObject15") - }; - - // Controlled by the Developer > Avatar > Show Tracked Objects menu. - if (_showTrackedObjects) { - static const std::vector trackedObjectActions = { - controller::Action::TRACKED_OBJECT_00, controller::Action::TRACKED_OBJECT_01, controller::Action::TRACKED_OBJECT_02, controller::Action::TRACKED_OBJECT_03, - controller::Action::TRACKED_OBJECT_04, controller::Action::TRACKED_OBJECT_05, controller::Action::TRACKED_OBJECT_06, controller::Action::TRACKED_OBJECT_07, - controller::Action::TRACKED_OBJECT_08, controller::Action::TRACKED_OBJECT_09, controller::Action::TRACKED_OBJECT_10, controller::Action::TRACKED_OBJECT_11, - controller::Action::TRACKED_OBJECT_12, controller::Action::TRACKED_OBJECT_13, controller::Action::TRACKED_OBJECT_14, controller::Action::TRACKED_OBJECT_15 - }; - - int i = 0; - glm::vec4 BLUE(0.0f, 0.0f, 1.0f, 1.0f); - for (auto& action : trackedObjectActions) { - controller::Pose pose = userInputMapper->getPoseState(action); - if (pose.valid) { - glm::vec3 pos = transformPoint(myAvatarMatrix, pose.translation); - glm::quat rot = glmExtractRotation(myAvatarMatrix) * pose.rotation; - DebugDraw::getInstance().addMarker(trackedObjectStringLiterals[i], rot, pos, BLUE); - } else { - DebugDraw::getInstance().removeMarker(trackedObjectStringLiterals[i]); - } - i++; - } - } else if (_prevShowTrackedObjects) { - for (auto& key : trackedObjectStringLiterals) { - DebugDraw::getInstance().removeMarker(key); - } - } - _prevShowTrackedObjects = _showTrackedObjects; - } - - updateThreads(deltaTime); // If running non-threaded, then give the threads some time to process... - updateDialogs(deltaTime); // update various stats dialogs if present - - auto grabManager = DependencyManager::get(); - grabManager->simulateGrabs(); - - // TODO: break these out into distinct perfTimers when they prove interesting - { - PROFILE_RANGE(app, "PickManager"); - PerformanceTimer perfTimer("pickManager"); - DependencyManager::get()->update(); - } - - { - PROFILE_RANGE(app, "PointerManager"); - PerformanceTimer perfTimer("pointerManager"); - DependencyManager::get()->update(); - } - - QSharedPointer avatarManager = DependencyManager::get(); - - { - PROFILE_RANGE(simulation_physics, "Simulation"); - PerformanceTimer perfTimer("simulation"); - - if (_physicsEnabled) { - auto t0 = std::chrono::high_resolution_clock::now(); - auto t1 = t0; - { - PROFILE_RANGE(simulation_physics, "PrePhysics"); - PerformanceTimer perfTimer("prePhysics)"); - { - PROFILE_RANGE(simulation_physics, "RemoveEntities"); - const VectorOfMotionStates& motionStates = _entitySimulation->getObjectsToRemoveFromPhysics(); - { - PROFILE_RANGE_EX(simulation_physics, "NumObjs", 0xffff0000, (uint64_t)motionStates.size()); - _physicsEngine->removeObjects(motionStates); - } - _entitySimulation->deleteObjectsRemovedFromPhysics(); - } - - { - PROFILE_RANGE(simulation_physics, "AddEntities"); - VectorOfMotionStates motionStates; - getEntities()->getTree()->withReadLock([&] { - _entitySimulation->getObjectsToAddToPhysics(motionStates); - PROFILE_RANGE_EX(simulation_physics, "NumObjs", 0xffff0000, (uint64_t)motionStates.size()); - _physicsEngine->addObjects(motionStates); - }); - } - { - VectorOfMotionStates motionStates; - PROFILE_RANGE(simulation_physics, "ChangeEntities"); - getEntities()->getTree()->withReadLock([&] { - _entitySimulation->getObjectsToChange(motionStates); - VectorOfMotionStates stillNeedChange = _physicsEngine->changeObjects(motionStates); - _entitySimulation->setObjectsToChange(stillNeedChange); - }); - } - - _entitySimulation->applyDynamicChanges(); - - t1 = std::chrono::high_resolution_clock::now(); - - { - PROFILE_RANGE(simulation_physics, "Avatars"); - PhysicsEngine::Transaction transaction; - avatarManager->buildPhysicsTransaction(transaction); - _physicsEngine->processTransaction(transaction); - avatarManager->handleProcessedPhysicsTransaction(transaction); - myAvatar->getCharacterController()->buildPhysicsTransaction(transaction); - _physicsEngine->processTransaction(transaction); - myAvatar->getCharacterController()->handleProcessedPhysicsTransaction(transaction); - myAvatar->prepareForPhysicsSimulation(); - _physicsEngine->enableGlobalContactAddedCallback(myAvatar->isFlying()); - } - - { - PROFILE_RANGE(simulation_physics, "PrepareActions"); - _physicsEngine->forEachDynamic([&](EntityDynamicPointer dynamic) { - dynamic->prepareForPhysicsSimulation(); - }); - } - } - auto t2 = std::chrono::high_resolution_clock::now(); - { - PROFILE_RANGE(simulation_physics, "StepPhysics"); - PerformanceTimer perfTimer("stepPhysics"); - getEntities()->getTree()->withWriteLock([&] { - _physicsEngine->stepSimulation(); - }); - } - auto t3 = std::chrono::high_resolution_clock::now(); - { - if (_physicsEngine->hasOutgoingChanges()) { - { - PROFILE_RANGE(simulation_physics, "PostPhysics"); - PerformanceTimer perfTimer("postPhysics"); - // grab the collision events BEFORE handleChangedMotionStates() because at this point - // we have a better idea of which objects we own or should own. - auto& collisionEvents = _physicsEngine->getCollisionEvents(); - - getEntities()->getTree()->withWriteLock([&] { - PROFILE_RANGE(simulation_physics, "HandleChanges"); - PerformanceTimer perfTimer("handleChanges"); - - const VectorOfMotionStates& outgoingChanges = _physicsEngine->getChangedMotionStates(); - _entitySimulation->handleChangedMotionStates(outgoingChanges); - avatarManager->handleChangedMotionStates(outgoingChanges); - - const VectorOfMotionStates& deactivations = _physicsEngine->getDeactivatedMotionStates(); - _entitySimulation->handleDeactivatedMotionStates(deactivations); - }); - - // handleCollisionEvents() AFTER handleChangedMotionStates() - { - PROFILE_RANGE(simulation_physics, "CollisionEvents"); - avatarManager->handleCollisionEvents(collisionEvents); - // Collision events (and their scripts) must not be handled when we're locked, above. (That would risk - // deadlock.) - _entitySimulation->handleCollisionEvents(collisionEvents); - } - - { - PROFILE_RANGE(simulation_physics, "MyAvatar"); - myAvatar->harvestResultsFromPhysicsSimulation(deltaTime); - } - - if (PerformanceTimer::isActive() && - Menu::getInstance()->isOptionChecked(MenuOption::DisplayDebugTimingDetails) && - Menu::getInstance()->isOptionChecked(MenuOption::ExpandPhysicsTiming)) { - _physicsEngine->harvestPerformanceStats(); - } - // NOTE: the PhysicsEngine stats are written to stdout NOT to Qt log framework - _physicsEngine->dumpStatsIfNecessary(); - } - auto t4 = std::chrono::high_resolution_clock::now(); - - // NOTE: the getEntities()->update() call below will wait for lock - // and will provide non-physical entity motion - getEntities()->update(true); // update the models... - - auto t5 = std::chrono::high_resolution_clock::now(); - - workload::Timings timings(6); - timings[0] = t1 - t0; // prePhysics entities - timings[1] = t2 - t1; // prePhysics avatars - timings[2] = t3 - t2; // stepPhysics - timings[3] = t4 - t3; // postPhysics - timings[4] = t5 - t4; // non-physical kinematics - timings[5] = workload::Timing_ns((int32_t)(NSECS_PER_SECOND * deltaTime)); // game loop duration - _gameWorkload.updateSimulationTimings(timings); - } - } - } else { - // update the rendering without any simulation - getEntities()->update(false); - } - // remove recently dead avatarEntities - SetOfEntities deadAvatarEntities; - _entitySimulation->takeDeadAvatarEntities(deadAvatarEntities); - avatarManager->removeDeadAvatarEntities(deadAvatarEntities); - } - - // AvatarManager update - { - { - PROFILE_RANGE(simulation, "OtherAvatars"); - PerformanceTimer perfTimer("otherAvatars"); - avatarManager->updateOtherAvatars(deltaTime); - } - - { - PROFILE_RANGE(simulation, "MyAvatar"); - PerformanceTimer perfTimer("MyAvatar"); - qApp->updateMyAvatarLookAtPosition(); - avatarManager->updateMyAvatar(deltaTime); - } - } - - bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); - PerformanceWarning warn(showWarnings, "Application::update()"); - - updateLOD(deltaTime); - - if (!_loginDialogID.isNull()) { - _loginStateManager.update(getMyAvatar()->getDominantHand(), _loginDialogID); - updateLoginDialogPosition(); - } - - { - PROFILE_RANGE_EX(app, "Overlays", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount()); - PerformanceTimer perfTimer("overlays"); - _overlays.update(deltaTime); - } - - // Update _viewFrustum with latest camera and view frustum data... - // NOTE: we get this from the view frustum, to make it simpler, since the - // loadViewFrumstum() method will get the correct details from the camera - // We could optimize this to not actually load the viewFrustum, since we don't - // actually need to calculate the view frustum planes to send these details - // to the server. - { - QMutexLocker viewLocker(&_viewMutex); - _myCamera.loadViewFrustum(_viewFrustum); - - _conicalViews.clear(); - _conicalViews.push_back(_viewFrustum); - // TODO: Fix this by modeling the way the secondary camera works on how the main camera works - // ie. Use a camera object stored in the game logic and informs the Engine on where the secondary - // camera should be. - updateSecondaryCameraViewFrustum(); - } - - quint64 now = usecTimestampNow(); - - // Update my voxel servers with my current voxel query... - { - PROFILE_RANGE_EX(app, "QueryOctree", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount()); - PerformanceTimer perfTimer("queryOctree"); - QMutexLocker viewLocker(&_viewMutex); - - bool viewIsDifferentEnough = false; - if (_conicalViews.size() == _lastQueriedViews.size()) { - for (size_t i = 0; i < _conicalViews.size(); ++i) { - if (!_conicalViews[i].isVerySimilar(_lastQueriedViews[i])) { - viewIsDifferentEnough = true; - break; - } - } - } else { - viewIsDifferentEnough = true; - } - - - // if it's been a while since our last query or the view has significantly changed then send a query, otherwise suppress it - static const std::chrono::seconds MIN_PERIOD_BETWEEN_QUERIES { 3 }; - auto now = SteadyClock::now(); - if (now > _queryExpiry || viewIsDifferentEnough) { - if (DependencyManager::get()->shouldRenderEntities()) { - queryOctree(NodeType::EntityServer, PacketType::EntityQuery); - } - queryAvatars(); - - _lastQueriedViews = _conicalViews; - _queryExpiry = now + MIN_PERIOD_BETWEEN_QUERIES; - } - } - - // sent nack packets containing missing sequence numbers of received packets from nodes - { - quint64 sinceLastNack = now - _lastNackTime; - const quint64 TOO_LONG_SINCE_LAST_NACK = 1 * USECS_PER_SECOND; - if (sinceLastNack > TOO_LONG_SINCE_LAST_NACK) { - _lastNackTime = now; - sendNackPackets(); - } - } - - // send packet containing downstream audio stats to the AudioMixer - { - quint64 sinceLastNack = now - _lastSendDownstreamAudioStats; - if (sinceLastNack > TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS && !isInterstitialMode()) { - _lastSendDownstreamAudioStats = now; - - QMetaObject::invokeMethod(DependencyManager::get().data(), "sendDownstreamAudioStatsPacket", Qt::QueuedConnection); - } - } - - { - PerformanceTimer perfTimer("avatarManager/postUpdate"); - avatarManager->postUpdate(deltaTime, getMain3DScene()); - } - - { - PROFILE_RANGE_EX(app, "PostUpdateLambdas", 0xffff0000, (uint64_t)0); - PerformanceTimer perfTimer("postUpdateLambdas"); - std::unique_lock guard(_postUpdateLambdasLock); - for (auto& iter : _postUpdateLambdas) { - iter.second(); - } - _postUpdateLambdas.clear(); - } - - - updateRenderArgs(deltaTime); - - { - PerformanceTimer perfTimer("AnimDebugDraw"); - AnimDebugDraw::getInstance().update(); - } - - - { // Game loop is done, mark the end of the frame for the scene transactions and the render loop to take over - PerformanceTimer perfTimer("enqueueFrame"); - getMain3DScene()->enqueueFrame(); - } - - // If the display plugin is inactive then the frames won't be processed so process them here. - if (!getActiveDisplayPlugin()->isActive()) { - getMain3DScene()->processTransactionQueue(); - } -} - -void Application::updateRenderArgs(float deltaTime) { - _graphicsEngine.editRenderArgs([this, deltaTime](AppRenderArgs& appRenderArgs) { - PerformanceTimer perfTimer("editRenderArgs"); - appRenderArgs._headPose = getHMDSensorPose(); - - auto myAvatar = getMyAvatar(); - - // update the avatar with a fresh HMD pose - { - PROFILE_RANGE(render, "/updateAvatar"); - myAvatar->updateFromHMDSensorMatrix(appRenderArgs._headPose); - } - - auto lodManager = DependencyManager::get(); - - float sensorToWorldScale = getMyAvatar()->getSensorToWorldScale(); - appRenderArgs._sensorToWorldScale = sensorToWorldScale; - appRenderArgs._sensorToWorld = getMyAvatar()->getSensorToWorldMatrix(); - { - PROFILE_RANGE(render, "/buildFrustrumAndArgs"); - { - QMutexLocker viewLocker(&_viewMutex); - // adjust near clip plane to account for sensor scaling. - auto adjustedProjection = glm::perspective(glm::radians(_fieldOfView.get()), - getActiveDisplayPlugin()->getRecommendedAspectRatio(), - DEFAULT_NEAR_CLIP * sensorToWorldScale, - DEFAULT_FAR_CLIP); - _viewFrustum.setProjection(adjustedProjection); - _viewFrustum.calculate(); - } - appRenderArgs._renderArgs = RenderArgs(_graphicsEngine.getGPUContext(), lodManager->getOctreeSizeScale(), - lodManager->getBoundaryLevelAdjust(), lodManager->getLODAngleHalfTan(), RenderArgs::DEFAULT_RENDER_MODE, - RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); - appRenderArgs._renderArgs._scene = getMain3DScene(); - - { - QMutexLocker viewLocker(&_viewMutex); - appRenderArgs._renderArgs.setViewFrustum(_viewFrustum); - } - } - { - PROFILE_RANGE(render, "/resizeGL"); - bool showWarnings = false; - bool suppressShortTimings = false; - auto menu = Menu::getInstance(); - if (menu) { - suppressShortTimings = menu->isOptionChecked(MenuOption::SuppressShortTimings); - showWarnings = menu->isOptionChecked(MenuOption::PipelineWarnings); - } - PerformanceWarning::setSuppressShortTimings(suppressShortTimings); - PerformanceWarning warn(showWarnings, "Application::paintGL()"); - resizeGL(); - } - - this->updateCamera(appRenderArgs._renderArgs, deltaTime); - appRenderArgs._eyeToWorld = _myCamera.getTransform(); - appRenderArgs._isStereo = false; - - { - auto hmdInterface = DependencyManager::get(); - float ipdScale = hmdInterface->getIPDScale(); - - // scale IPD by sensorToWorldScale, to make the world seem larger or smaller accordingly. - ipdScale *= sensorToWorldScale; - - auto baseProjection = appRenderArgs._renderArgs.getViewFrustum().getProjection(); - - if (getActiveDisplayPlugin()->isStereo()) { - // Stereo modes will typically have a larger projection matrix overall, - // so we ask for the 'mono' projection matrix, which for stereo and HMD - // plugins will imply the combined projection for both eyes. - // - // This is properly implemented for the Oculus plugins, but for OpenVR - // and Stereo displays I'm not sure how to get / calculate it, so we're - // just relying on the left FOV in each case and hoping that the - // overall culling margin of error doesn't cause popping in the - // right eye. There are FIXMEs in the relevant plugins - _myCamera.setProjection(getActiveDisplayPlugin()->getCullingProjection(baseProjection)); - appRenderArgs._isStereo = true; - - auto& eyeOffsets = appRenderArgs._eyeOffsets; - auto& eyeProjections = appRenderArgs._eyeProjections; - - // FIXME we probably don't need to set the projection matrix every frame, - // only when the display plugin changes (or in non-HMD modes when the user - // changes the FOV manually, which right now I don't think they can. - for_each_eye([&](Eye eye) { - // For providing the stereo eye views, the HMD head pose has already been - // applied to the avatar, so we need to get the difference between the head - // pose applied to the avatar and the per eye pose, and use THAT as - // the per-eye stereo matrix adjustment. - mat4 eyeToHead = getActiveDisplayPlugin()->getEyeToHeadTransform(eye); - // Grab the translation - vec3 eyeOffset = glm::vec3(eyeToHead[3]); - // Apply IPD scaling - mat4 eyeOffsetTransform = glm::translate(mat4(), eyeOffset * -1.0f * ipdScale); - eyeOffsets[eye] = eyeOffsetTransform; - eyeProjections[eye] = getActiveDisplayPlugin()->getEyeProjection(eye, baseProjection); - }); - - // Configure the type of display / stereo - appRenderArgs._renderArgs._displayMode = (isHMDMode() ? RenderArgs::STEREO_HMD : RenderArgs::STEREO_MONITOR); - } - } - - { - QMutexLocker viewLocker(&_viewMutex); - _myCamera.loadViewFrustum(_displayViewFrustum); - appRenderArgs._view = glm::inverse(_displayViewFrustum.getView()); - } - - { - QMutexLocker viewLocker(&_viewMutex); - appRenderArgs._renderArgs.setViewFrustum(_displayViewFrustum); - } - - - // HACK - // load the view frustum - // FIXME: This preDisplayRender call is temporary until we create a separate render::scene for the mirror rendering. - // Then we can move this logic into the Avatar::simulate call. - myAvatar->preDisplaySide(&appRenderArgs._renderArgs); - }); -} - -void Application::queryAvatars() { - if (!isInterstitialMode()) { - auto avatarPacket = NLPacket::create(PacketType::AvatarQuery); - auto destinationBuffer = reinterpret_cast(avatarPacket->getPayload()); - unsigned char* bufferStart = destinationBuffer; - - uint8_t numFrustums = (uint8_t)_conicalViews.size(); - memcpy(destinationBuffer, &numFrustums, sizeof(numFrustums)); - destinationBuffer += sizeof(numFrustums); - - for (const auto& view : _conicalViews) { - destinationBuffer += view.serialize(destinationBuffer); - } - - avatarPacket->setPayloadSize(destinationBuffer - bufferStart); - - DependencyManager::get()->broadcastToNodes(std::move(avatarPacket), NodeSet() << NodeType::AvatarMixer); - } -} - - -int Application::sendNackPackets() { - - // iterates through all nodes in NodeList - auto nodeList = DependencyManager::get(); - - int packetsSent = 0; - - nodeList->eachNode([&](const SharedNodePointer& node){ - - if (node->getActiveSocket() && node->getType() == NodeType::EntityServer) { - - auto nackPacketList = NLPacketList::create(PacketType::OctreeDataNack); - - QUuid nodeUUID = node->getUUID(); - - // if there are octree packets from this node that are waiting to be processed, - // don't send a NACK since the missing packets may be among those waiting packets. - if (_octreeProcessor.hasPacketsToProcessFrom(nodeUUID)) { - return; - } - - QSet missingSequenceNumbers; - _octreeServerSceneStats.withReadLock([&] { - // retrieve octree scene stats of this node - if (_octreeServerSceneStats.find(nodeUUID) == _octreeServerSceneStats.end()) { - return; - } - // get sequence number stats of node, prune its missing set, and make a copy of the missing set - SequenceNumberStats& sequenceNumberStats = _octreeServerSceneStats[nodeUUID].getIncomingOctreeSequenceNumberStats(); - sequenceNumberStats.pruneMissingSet(); - missingSequenceNumbers = sequenceNumberStats.getMissingSet(); - }); - - _isMissingSequenceNumbers = (missingSequenceNumbers.size() != 0); - - // construct nack packet(s) for this node - foreach(const OCTREE_PACKET_SEQUENCE& missingNumber, missingSequenceNumbers) { - nackPacketList->writePrimitive(missingNumber); - } - - if (nackPacketList->getNumPackets()) { - packetsSent += (int)nackPacketList->getNumPackets(); - - // send the packet list - nodeList->sendPacketList(std::move(nackPacketList), *node); - } - } - }); - - return packetsSent; -} - -void Application::queryOctree(NodeType_t serverType, PacketType packetType) { - - if (!_settingsLoaded) { - return; // bail early if settings are not loaded - } - - const bool isModifiedQuery = !_physicsEnabled; - if (isModifiedQuery) { - // Create modified view that is a simple sphere. - bool interstitialModeEnabled = DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled(); - - ConicalViewFrustum sphericalView; - sphericalView.setSimpleRadius(INITIAL_QUERY_RADIUS); - - if (interstitialModeEnabled) { - ConicalViewFrustum farView; - farView.set(_viewFrustum); - _octreeQuery.setConicalViews({ sphericalView, farView }); - } else { - _octreeQuery.setConicalViews({ sphericalView }); - } - - _octreeQuery.setOctreeSizeScale(DEFAULT_OCTREE_SIZE_SCALE); - static constexpr float MIN_LOD_ADJUST = -20.0f; - _octreeQuery.setBoundaryLevelAdjust(MIN_LOD_ADJUST); - } else { - _octreeQuery.setConicalViews(_conicalViews); - auto lodManager = DependencyManager::get(); - _octreeQuery.setOctreeSizeScale(lodManager->getOctreeSizeScale()); - _octreeQuery.setBoundaryLevelAdjust(lodManager->getBoundaryLevelAdjust()); - } - _octreeQuery.setReportInitialCompletion(isModifiedQuery); - - - auto nodeList = DependencyManager::get(); - - auto node = nodeList->soloNodeOfType(serverType); - if (node && node->getActiveSocket()) { - _octreeQuery.setMaxQueryPacketsPerSecond(getMaxOctreePacketsPerSecond()); - - auto queryPacket = NLPacket::create(packetType); - - // encode the query data - auto packetData = reinterpret_cast(queryPacket->getPayload()); - int packetSize = _octreeQuery.getBroadcastData(packetData); - queryPacket->setPayloadSize(packetSize); - - // make sure we still have an active socket - nodeList->sendUnreliablePacket(*queryPacket, *node); - } -} - - -bool Application::isHMDMode() const { - return getActiveDisplayPlugin()->isHmd(); -} - -float Application::getNumCollisionObjects() const { - return _physicsEngine ? _physicsEngine->getNumCollisionObjects() : 0; -} - -float Application::getTargetRenderFrameRate() const { return getActiveDisplayPlugin()->getTargetFrameRate(); } - -QRect Application::getDesirableApplicationGeometry() const { - QRect applicationGeometry = getWindow()->geometry(); - - // If our parent window is on the HMD, then don't use its geometry, instead use - // the "main screen" geometry. - HMDToolsDialog* hmdTools = DependencyManager::get()->getHMDToolsDialog(); - if (hmdTools && hmdTools->hasHMDScreen()) { - QScreen* hmdScreen = hmdTools->getHMDScreen(); - QWindow* appWindow = getWindow()->windowHandle(); - QScreen* appScreen = appWindow->screen(); - - // if our app's screen is the hmd screen, we don't want to place the - // running scripts widget on it. So we need to pick a better screen. - // we will use the screen for the HMDTools since it's a guaranteed - // better screen. - if (appScreen == hmdScreen) { - QScreen* betterScreen = hmdTools->windowHandle()->screen(); - applicationGeometry = betterScreen->geometry(); - } - } - return applicationGeometry; -} - -PickRay Application::computePickRay(float x, float y) const { - vec2 pickPoint { x, y }; - PickRay result; - if (isHMDMode()) { - getApplicationCompositor().computeHmdPickRay(pickPoint, result.origin, result.direction); - } else { - pickPoint /= getCanvasSize(); - if (_myCamera.getMode() == CameraMode::CAMERA_MODE_MIRROR) { - pickPoint.x = 1.0f - pickPoint.x; - } - QMutexLocker viewLocker(&_viewMutex); - _viewFrustum.computePickRay(pickPoint.x, pickPoint.y, result.origin, result.direction); - } - return result; -} - -std::shared_ptr Application::getMyAvatar() const { - return DependencyManager::get()->getMyAvatar(); -} - -glm::vec3 Application::getAvatarPosition() const { - return getMyAvatar()->getWorldPosition(); -} - -void Application::copyViewFrustum(ViewFrustum& viewOut) const { - QMutexLocker viewLocker(&_viewMutex); - viewOut = _viewFrustum; -} - -void Application::copyDisplayViewFrustum(ViewFrustum& viewOut) const { - QMutexLocker viewLocker(&_viewMutex); - viewOut = _displayViewFrustum; -} - -// resentSensors() is a bit of vestigial feature. It used to be used for Oculus DK2 to recenter the view around -// the current head orientation. With the introduction of "room scale" tracking we no longer need that particular -// feature. However, we still use this to reset face trackers, eye trackers, audio and to optionally re-load the avatar -// rig and animations from scratch. -void Application::resetSensors(bool andReload) { - DependencyManager::get()->reset(); - DependencyManager::get()->reset(); - _overlayConductor.centerUI(); - getActiveDisplayPlugin()->resetSensors(); - getMyAvatar()->reset(true, andReload); - QMetaObject::invokeMethod(DependencyManager::get().data(), "reset", Qt::QueuedConnection); -} - -void Application::hmdVisibleChanged(bool visible) { - // TODO - // calling start and stop will change audio input and ouput to default audio devices. - // we need to add a pause/unpause functionality to AudioClient for this to work properly -#if 0 - if (visible) { - QMetaObject::invokeMethod(DependencyManager::get().data(), "start", Qt::QueuedConnection); - } else { - QMetaObject::invokeMethod(DependencyManager::get().data(), "stop", Qt::QueuedConnection); - } -#endif -} - -void Application::updateWindowTitle() const { - - auto nodeList = DependencyManager::get(); - auto accountManager = DependencyManager::get(); - auto isInErrorState = nodeList->getDomainHandler().isInErrorState(); - - QString buildVersion = " - " - + (BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Stable ? QString("Version") : QString("Build")) - + " " + applicationVersion(); - - QString loginStatus = accountManager->isLoggedIn() ? "" : " (NOT LOGGED IN)"; - - QString connectionStatus = isInErrorState ? " (ERROR CONNECTING)" : - nodeList->getDomainHandler().isConnected() ? "" : " (NOT CONNECTED)"; - QString username = accountManager->getAccountInfo().getUsername(); - - setCrashAnnotation("username", username.toStdString()); - - QString currentPlaceName; - if (isServerlessMode()) { - if (isInErrorState) { - currentPlaceName = "serverless: " + nodeList->getDomainHandler().getErrorDomainURL().toString(); - } else { - currentPlaceName = "serverless: " + DependencyManager::get()->getDomainURL().toString(); - } - } else { - currentPlaceName = DependencyManager::get()->getDomainURL().host(); - if (currentPlaceName.isEmpty()) { - currentPlaceName = nodeList->getDomainHandler().getHostname(); - } - } - - QString title = QString() + (!username.isEmpty() ? username + " @ " : QString()) - + currentPlaceName + connectionStatus + loginStatus + buildVersion; - -#ifndef WIN32 - // crashes with vs2013/win32 - qCDebug(interfaceapp, "Application title set to: %s", title.toStdString().c_str()); -#endif - _window->setWindowTitle(title); - - // updateTitleWindow gets called whenever there's a change regarding the domain, so rather - // than placing this within domainURLChanged, it's placed here to cover the other potential cases. - DependencyManager::get< MessagesClient >()->sendLocalMessage("Toolbar-DomainChanged", ""); -} - -void Application::clearDomainOctreeDetails(bool clearAll) { - // before we delete all entities get MyAvatar's AvatarEntityData ready - getMyAvatar()->prepareAvatarEntityDataForReload(); - - // if we're about to quit, we really don't need to do the rest of these things... - if (_aboutToQuit) { - return; - } - - qCDebug(interfaceapp) << "Clearing domain octree details..."; - - resetPhysicsReadyInformation(); - setIsInterstitialMode(true); - - _octreeServerSceneStats.withWriteLock([&] { - _octreeServerSceneStats.clear(); - }); - - // reset the model renderer - clearAll ? getEntities()->clear() : getEntities()->clearDomainAndNonOwnedEntities(); - - auto skyStage = DependencyManager::get()->getSkyStage(); - - skyStage->setBackgroundMode(graphics::SunSkyStage::SKY_DEFAULT); - - DependencyManager::get()->clearUnusedResources(); - DependencyManager::get()->clearUnusedResources(); - MaterialCache::instance().clearUnusedResources(); - DependencyManager::get()->clearUnusedResources(); - ShaderCache::instance().clearUnusedResources(); - DependencyManager::get()->clearUnusedResources(); - DependencyManager::get()->clearUnusedResources(); -} - -void Application::domainURLChanged(QUrl domainURL) { - // disable physics until we have enough information about our new location to not cause craziness. - resetPhysicsReadyInformation(); - setIsServerlessMode(domainURL.scheme() != URL_SCHEME_HIFI); - if (isServerlessMode()) { - loadServerlessDomain(domainURL); - } - updateWindowTitle(); -} - -void Application::goToErrorDomainURL(QUrl errorDomainURL) { - // disable physics until we have enough information about our new location to not cause craziness. - resetPhysicsReadyInformation(); - setIsServerlessMode(errorDomainURL.scheme() != URL_SCHEME_HIFI); - if (isServerlessMode()) { - loadErrorDomain(errorDomainURL); - } - updateWindowTitle(); -} - -void Application::resettingDomain() { - _notifiedPacketVersionMismatchThisDomain = false; - - clearDomainOctreeDetails(false); -} - -void Application::nodeAdded(SharedNodePointer node) const { - if (node->getType() == NodeType::EntityServer) { - if (!_failedToConnectToEntityServer) { - _entityServerConnectionTimer.stop(); - _entityServerConnectionTimer.setInterval(ENTITY_SERVER_CONNECTION_TIMEOUT); - _entityServerConnectionTimer.start(); - } - } -} - -void Application::nodeActivated(SharedNodePointer node) { - if (node->getType() == NodeType::AssetServer) { - // asset server just connected - check if we have the asset browser showing - -#if !defined(DISABLE_QML) - auto offscreenUi = getOffscreenUI(); - - if (offscreenUi) { - auto nodeList = DependencyManager::get(); - - if (nodeList->getThisNodeCanWriteAssets()) { - // call reload on the shown asset browser dialog to get the mappings (if permissions allow) - auto assetDialog = offscreenUi ? offscreenUi->getRootItem()->findChild("AssetServer") : nullptr; - if (assetDialog) { - QMetaObject::invokeMethod(assetDialog, "reload"); - } - } else { - // we switched to an Asset Server that we can't modify, hide the Asset Browser - offscreenUi->hide("AssetServer"); - } - } -#endif - } - - // If we get a new EntityServer activated, reset lastQueried time - // so we will do a proper query during update - if (node->getType() == NodeType::EntityServer) { - _queryExpiry = SteadyClock::now(); - _octreeQuery.incrementConnectionID(); - - if (!_failedToConnectToEntityServer) { - _entityServerConnectionTimer.stop(); - } - } - - if (node->getType() == NodeType::AudioMixer && !isInterstitialMode()) { - DependencyManager::get()->negotiateAudioFormat(); - } - - if (node->getType() == NodeType::AvatarMixer) { - _queryExpiry = SteadyClock::now(); - - // new avatar mixer, send off our identity packet on next update loop - // Reset skeletonModelUrl if the last server modified our choice. - // Override the avatar url (but not model name) here too. - if (_avatarOverrideUrl.isValid()) { - getMyAvatar()->useFullAvatarURL(_avatarOverrideUrl); - } - - if (getMyAvatar()->getFullAvatarURLFromPreferences() != getMyAvatar()->getSkeletonModelURL()) { - getMyAvatar()->resetFullAvatarURL(); - } - getMyAvatar()->markIdentityDataChanged(); - getMyAvatar()->resetLastSent(); - - if (!isInterstitialMode()) { - // transmit a "sendAll" packet to the AvatarMixer we just connected to. - getMyAvatar()->sendAvatarDataPacket(true); - } - } -} - -void Application::nodeKilled(SharedNodePointer node) { - // These are here because connecting NodeList::nodeKilled to OctreePacketProcessor::nodeKilled doesn't work: - // OctreePacketProcessor::nodeKilled is not being called when NodeList::nodeKilled is emitted. - // This may have to do with GenericThread::threadRoutine() blocking the QThread event loop - - _octreeProcessor.nodeKilled(node); - - _entityEditSender.nodeKilled(node); - - if (node->getType() == NodeType::AudioMixer) { - QMetaObject::invokeMethod(DependencyManager::get().data(), "audioMixerKilled"); - } else if (node->getType() == NodeType::EntityServer) { - // we lost an entity server, clear all of the domain octree details - clearDomainOctreeDetails(false); - } else if (node->getType() == NodeType::AssetServer) { - // asset server going away - check if we have the asset browser showing - -#if !defined(DISABLE_QML) - auto offscreenUi = getOffscreenUI(); - auto assetDialog = offscreenUi ? offscreenUi->getRootItem()->findChild("AssetServer") : nullptr; - - if (assetDialog) { - // call reload on the shown asset browser dialog - QMetaObject::invokeMethod(assetDialog, "clear"); - } -#endif - } -} - -void Application::trackIncomingOctreePacket(ReceivedMessage& message, SharedNodePointer sendingNode, bool wasStatsPacket) { - // Attempt to identify the sender from its address. - if (sendingNode) { - const QUuid& nodeUUID = sendingNode->getUUID(); - - // now that we know the node ID, let's add these stats to the stats for that node... - _octreeServerSceneStats.withWriteLock([&] { - if (_octreeServerSceneStats.find(nodeUUID) != _octreeServerSceneStats.end()) { - OctreeSceneStats& stats = _octreeServerSceneStats[nodeUUID]; - stats.trackIncomingOctreePacket(message, wasStatsPacket, sendingNode->getClockSkewUsec()); - } - }); - } -} - -bool Application::gpuTextureMemSizeStable() { - auto renderConfig = qApp->getRenderEngine()->getConfiguration(); - auto renderStats = renderConfig->getConfig("Stats"); - - qint64 textureResourceGPUMemSize = renderStats->textureResourceGPUMemSize; - qint64 texturePopulatedGPUMemSize = renderStats->textureResourcePopulatedGPUMemSize; - qint64 textureTransferSize = renderStats->texturePendingGPUTransferSize; - - if (_gpuTextureMemSizeAtLastCheck == textureResourceGPUMemSize) { - _gpuTextureMemSizeStabilityCount++; - } else { - _gpuTextureMemSizeStabilityCount = 0; - } - _gpuTextureMemSizeAtLastCheck = textureResourceGPUMemSize; - - if (_gpuTextureMemSizeStabilityCount >= _minimumGPUTextureMemSizeStabilityCount) { - return (textureResourceGPUMemSize == texturePopulatedGPUMemSize) && (textureTransferSize == 0); - } - return false; -} - -int Application::processOctreeStats(ReceivedMessage& message, SharedNodePointer sendingNode) { - // parse the incoming stats datas stick it in a temporary object for now, while we - // determine which server it belongs to - int statsMessageLength = 0; - - const QUuid& nodeUUID = sendingNode->getUUID(); - - // now that we know the node ID, let's add these stats to the stats for that node... - _octreeServerSceneStats.withWriteLock([&] { - OctreeSceneStats& octreeStats = _octreeServerSceneStats[nodeUUID]; - statsMessageLength = octreeStats.unpackFromPacket(message); - - if (octreeStats.isFullScene()) { - _fullSceneReceivedCounter++; - } - }); - - return statsMessageLength; -} - -void Application::packetSent(quint64 length) { -} - -void Application::addingEntityWithCertificate(const QString& certificateID, const QString& placeName) { - auto ledger = DependencyManager::get(); - ledger->updateLocation(certificateID, placeName); -} - -void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointer scriptEngine) { - - scriptEngine->setEmitScriptUpdatesFunction([this]() { - SharedNodePointer entityServerNode = DependencyManager::get()->soloNodeOfType(NodeType::EntityServer); - return !entityServerNode || isPhysicsEnabled(); - }); - - // setup the packet sender of the script engine's scripting interfaces so - // we can use the same ones from the application. - auto entityScriptingInterface = DependencyManager::get(); - entityScriptingInterface->setPacketSender(&_entityEditSender); - entityScriptingInterface->setEntityTree(getEntities()->getTree()); - - if (property(hifi::properties::TEST).isValid()) { - scriptEngine->registerGlobalObject("Test", TestScriptingInterface::getInstance()); - } - - scriptEngine->registerGlobalObject("PlatformInfo", PlatformInfoScriptingInterface::getInstance()); - scriptEngine->registerGlobalObject("Rates", new RatesScriptingInterface(this)); - - // hook our avatar and avatar hash map object into this script engine - getMyAvatar()->registerMetaTypes(scriptEngine); - - scriptEngine->registerGlobalObject("AvatarList", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("Camera", &_myCamera); - -#if defined(Q_OS_MAC) || defined(Q_OS_WIN) - scriptEngine->registerGlobalObject("SpeechRecognizer", DependencyManager::get().data()); -#endif - - ClipboardScriptingInterface* clipboardScriptable = new ClipboardScriptingInterface(); - scriptEngine->registerGlobalObject("Clipboard", clipboardScriptable); - connect(scriptEngine.data(), &ScriptEngine::finished, clipboardScriptable, &ClipboardScriptingInterface::deleteLater); - - scriptEngine->registerGlobalObject("Overlays", &_overlays); - qScriptRegisterMetaType(scriptEngine.data(), RayToOverlayIntersectionResultToScriptValue, - RayToOverlayIntersectionResultFromScriptValue); - -#if !defined(DISABLE_QML) - scriptEngine->registerGlobalObject("OffscreenFlags", getOffscreenUI()->getFlags()); - scriptEngine->registerGlobalObject("Desktop", DependencyManager::get().data()); -#endif - - qScriptRegisterMetaType(scriptEngine.data(), wrapperToScriptValue, wrapperFromScriptValue); - qScriptRegisterMetaType(scriptEngine.data(), - wrapperToScriptValue, wrapperFromScriptValue); - scriptEngine->registerGlobalObject("Toolbars", DependencyManager::get().data()); - - qScriptRegisterMetaType(scriptEngine.data(), wrapperToScriptValue, wrapperFromScriptValue); - qScriptRegisterMetaType(scriptEngine.data(), - wrapperToScriptValue, wrapperFromScriptValue); - scriptEngine->registerGlobalObject("Tablet", DependencyManager::get().data()); - // FIXME remove these deprecated names for the tablet scripting interface - scriptEngine->registerGlobalObject("tabletInterface", DependencyManager::get().data()); - - auto toolbarScriptingInterface = DependencyManager::get().data(); - DependencyManager::get().data()->setToolbarScriptingInterface(toolbarScriptingInterface); - - scriptEngine->registerGlobalObject("Window", DependencyManager::get().data()); - scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, - LocationScriptingInterface::locationSetter, "Window"); - // register `location` on the global object. - scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, - LocationScriptingInterface::locationSetter); - - bool clientScript = scriptEngine->isClientScript(); - scriptEngine->registerFunction("OverlayWindow", clientScript ? QmlWindowClass::constructor : QmlWindowClass::restricted_constructor); -#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML) - scriptEngine->registerFunction("OverlayWebWindow", clientScript ? QmlWebWindowClass::constructor : QmlWebWindowClass::restricted_constructor); -#endif - scriptEngine->registerFunction("QmlFragment", clientScript ? QmlFragmentClass::constructor : QmlFragmentClass::restricted_constructor); - - scriptEngine->registerGlobalObject("Menu", MenuScriptingInterface::getInstance()); - scriptEngine->registerGlobalObject("DesktopPreviewProvider", DependencyManager::get().data()); -#if !defined(DISABLE_QML) - scriptEngine->registerGlobalObject("Stats", Stats::getInstance()); -#endif - scriptEngine->registerGlobalObject("Settings", SettingsScriptingInterface::getInstance()); - scriptEngine->registerGlobalObject("Snapshot", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("AudioStats", DependencyManager::get()->getStats().data()); - scriptEngine->registerGlobalObject("AudioScope", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("AvatarBookmarks", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("LocationBookmarks", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("RayPick", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("LaserPointers", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("Picks", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("Pointers", DependencyManager::get().data()); - - // Caches - scriptEngine->registerGlobalObject("AnimationCache", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("TextureCache", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("ModelCache", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("SoundCache", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("DialogsManager", _dialogsManagerScriptingInterface); - - scriptEngine->registerGlobalObject("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED - scriptEngine->registerGlobalObject("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED - scriptEngine->registerGlobalObject("AccountServices", AccountServicesScriptingInterface::getInstance()); - qScriptRegisterMetaType(scriptEngine.data(), DownloadInfoResultToScriptValue, DownloadInfoResultFromScriptValue); - - scriptEngine->registerGlobalObject("FaceTracker", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("AvatarManager", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("LODManager", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("Keyboard", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("Paths", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("HMD", DependencyManager::get().data()); - scriptEngine->registerFunction("HMD", "getHUDLookAtPosition2D", HMDScriptingInterface::getHUDLookAtPosition2D, 0); - scriptEngine->registerFunction("HMD", "getHUDLookAtPosition3D", HMDScriptingInterface::getHUDLookAtPosition3D, 0); - - scriptEngine->registerGlobalObject("Scene", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("Render", _graphicsEngine.getRenderEngine()->getConfiguration().get()); - scriptEngine->registerGlobalObject("Workload", _gameWorkload._engine->getConfiguration().get()); - - GraphicsScriptingInterface::registerMetaTypes(scriptEngine.data()); - scriptEngine->registerGlobalObject("Graphics", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("ScriptDiscoveryService", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("Reticle", getApplicationCompositor().getReticleInterface()); - - scriptEngine->registerGlobalObject("UserActivityLogger", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("Users", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("GooglePoly", DependencyManager::get().data()); - - if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { - scriptEngine->registerGlobalObject("Steam", new SteamScriptingInterface(scriptEngine.data(), steamClient.get())); - } - auto scriptingInterface = DependencyManager::get(); - scriptEngine->registerGlobalObject("Controller", scriptingInterface.data()); - UserInputMapper::registerControllerTypes(scriptEngine.data()); - - auto recordingInterface = DependencyManager::get(); - scriptEngine->registerGlobalObject("Recording", recordingInterface.data()); - - auto entityScriptServerLog = DependencyManager::get(); - scriptEngine->registerGlobalObject("EntityScriptServerLog", entityScriptServerLog.data()); - scriptEngine->registerGlobalObject("AvatarInputs", AvatarInputs::getInstance()); - scriptEngine->registerGlobalObject("Selection", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("ContextOverlay", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("WalletScriptingInterface", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("AddressManager", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("HifiAbout", AboutUtil::getInstance()); - scriptEngine->registerGlobalObject("ResourceRequestObserver", DependencyManager::get().data()); - - registerInteractiveWindowMetaType(scriptEngine.data()); - - auto pickScriptingInterface = DependencyManager::get(); - pickScriptingInterface->registerMetaTypes(scriptEngine.data()); - - // connect this script engines printedMessage signal to the global ScriptEngines these various messages - auto scriptEngines = DependencyManager::get().data(); - connect(scriptEngine.data(), &ScriptEngine::printedMessage, scriptEngines, &ScriptEngines::onPrintedMessage); - connect(scriptEngine.data(), &ScriptEngine::errorMessage, scriptEngines, &ScriptEngines::onErrorMessage); - connect(scriptEngine.data(), &ScriptEngine::warningMessage, scriptEngines, &ScriptEngines::onWarningMessage); - connect(scriptEngine.data(), &ScriptEngine::infoMessage, scriptEngines, &ScriptEngines::onInfoMessage); - connect(scriptEngine.data(), &ScriptEngine::clearDebugWindow, scriptEngines, &ScriptEngines::onClearDebugWindow); - -} - -bool Application::canAcceptURL(const QString& urlString) const { - QUrl url(urlString); - if (url.query().contains(WEB_VIEW_TAG)) { - return false; - } else if (urlString.startsWith(URL_SCHEME_HIFI)) { - return true; - } - QString lowerPath = url.path().toLower(); - for (auto& pair : _acceptedExtensions) { - if (lowerPath.endsWith(pair.first, Qt::CaseInsensitive)) { - return true; - } - } - return false; -} - -bool Application::acceptURL(const QString& urlString, bool defaultUpload) { - QUrl url(urlString); - - if (url.scheme() == URL_SCHEME_HIFI) { - // this is a hifi URL - have the AddressManager handle it - QMetaObject::invokeMethod(DependencyManager::get().data(), "handleLookupString", - Qt::AutoConnection, Q_ARG(const QString&, urlString)); - return true; - } - - QString lowerPath = url.path().toLower(); - for (auto& pair : _acceptedExtensions) { - if (lowerPath.endsWith(pair.first, Qt::CaseInsensitive)) { - AcceptURLMethod method = pair.second; - return (this->*method)(urlString); - } - } - - if (defaultUpload && !url.fileName().isEmpty() && url.isLocalFile()) { - showAssetServerWidget(urlString); - } - return defaultUpload; -} - -void Application::setSessionUUID(const QUuid& sessionUUID) const { - Physics::setSessionUUID(sessionUUID); -} - -bool Application::askToSetAvatarUrl(const QString& url) { - QUrl realUrl(url); - if (realUrl.isLocalFile()) { - OffscreenUi::asyncWarning("", "You can not use local files for avatar components."); - return false; - } - - // Download the FST file, to attempt to determine its model type - QVariantHash fstMapping = FSTReader::downloadMapping(url); - - FSTReader::ModelType modelType = FSTReader::predictModelType(fstMapping); - - QString modelName = fstMapping["name"].toString(); - QString modelLicense = fstMapping["license"].toString(); - - bool agreeToLicense = true; // assume true - //create set avatar callback - auto setAvatar = [=] (QString url, QString modelName) { - ModalDialogListener* dlg = OffscreenUi::asyncQuestion("Set Avatar", - "Would you like to use '" + modelName + "' for your avatar?", - QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok); - QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { - QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); - - bool ok = (QMessageBox::Ok == static_cast(answer.toInt())); - if (ok) { - getMyAvatar()->useFullAvatarURL(url, modelName); - emit fullAvatarURLChanged(url, modelName); - } else { - qCDebug(interfaceapp) << "Declined to use the avatar"; - } - }); - }; - - if (!modelLicense.isEmpty()) { - // word wrap the license text to fit in a reasonable shaped message box. - const int MAX_CHARACTERS_PER_LINE = 90; - modelLicense = simpleWordWrap(modelLicense, MAX_CHARACTERS_PER_LINE); - - ModalDialogListener* dlg = OffscreenUi::asyncQuestion("Avatar Usage License", - modelLicense + "\nDo you agree to these terms?", - QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); - QObject::connect(dlg, &ModalDialogListener::response, this, [=, &agreeToLicense] (QVariant answer) { - QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); - - agreeToLicense = (static_cast(answer.toInt()) == QMessageBox::Yes); - if (agreeToLicense) { - switch (modelType) { - case FSTReader::HEAD_AND_BODY_MODEL: { - setAvatar(url, modelName); - break; - } - default: - OffscreenUi::asyncWarning("", modelName + "Does not support a head and body as required."); - break; - } - } else { - qCDebug(interfaceapp) << "Declined to agree to avatar license"; - } - - //auto offscreenUi = getOffscreenUI(); - }); - } else { - setAvatar(url, modelName); - } - - return true; -} - - -bool Application::askToLoadScript(const QString& scriptFilenameOrURL) { - QString shortName = scriptFilenameOrURL; - - QUrl scriptURL { scriptFilenameOrURL }; - - if (scriptURL.host().endsWith(MARKETPLACE_CDN_HOSTNAME)) { - int startIndex = shortName.lastIndexOf('/') + 1; - int endIndex = shortName.lastIndexOf('?'); - shortName = shortName.mid(startIndex, endIndex - startIndex); - } - -#ifdef DISABLE_QML - DependencyManager::get()->loadScript(scriptFilenameOrURL); -#else - QString message = "Would you like to run this script:\n" + shortName; - ModalDialogListener* dlg = OffscreenUi::asyncQuestion(getWindow(), "Run Script", message, - QMessageBox::Yes | QMessageBox::No); - - QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { - const QString& fileName = scriptFilenameOrURL; - if (static_cast(answer.toInt()) == QMessageBox::Yes) { - qCDebug(interfaceapp) << "Chose to run the script: " << fileName; - DependencyManager::get()->loadScript(fileName); - } else { - qCDebug(interfaceapp) << "Declined to run the script"; - } - QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); - }); -#endif - return true; -} - -bool Application::askToWearAvatarAttachmentUrl(const QString& url) { - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkRequest networkRequest = QNetworkRequest(url); - networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); - networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - QNetworkReply* reply = networkAccessManager.get(networkRequest); - int requestNumber = ++_avatarAttachmentRequest; - connect(reply, &QNetworkReply::finished, [this, reply, url, requestNumber]() { - - if (requestNumber != _avatarAttachmentRequest) { - // this request has been superseded by another more recent request - reply->deleteLater(); - return; - } - - QNetworkReply::NetworkError networkError = reply->error(); - if (networkError == QNetworkReply::NoError) { - // download success - QByteArray contents = reply->readAll(); - - QJsonParseError jsonError; - auto doc = QJsonDocument::fromJson(contents, &jsonError); - if (jsonError.error == QJsonParseError::NoError) { - - auto jsonObject = doc.object(); - - // retrieve optional name field from JSON - QString name = tr("Unnamed Attachment"); - auto nameValue = jsonObject.value("name"); - if (nameValue.isString()) { - name = nameValue.toString(); - } - - auto avatarAttachmentConfirmationTitle = tr("Avatar Attachment Confirmation"); - auto avatarAttachmentConfirmationMessage = tr("Would you like to wear '%1' on your avatar?").arg(name); - ModalDialogListener* dlg = OffscreenUi::asyncQuestion(avatarAttachmentConfirmationTitle, - avatarAttachmentConfirmationMessage, - QMessageBox::Ok | QMessageBox::Cancel); - QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { - QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); - if (static_cast(answer.toInt()) == QMessageBox::Yes) { - // add attachment to avatar - auto myAvatar = getMyAvatar(); - assert(myAvatar); - auto attachmentDataVec = myAvatar->getAttachmentData(); - AttachmentData attachmentData; - attachmentData.fromJson(jsonObject); - attachmentDataVec.push_back(attachmentData); - myAvatar->setAttachmentData(attachmentDataVec); - } else { - qCDebug(interfaceapp) << "User declined to wear the avatar attachment"; - } - }); - } else { - // json parse error - auto avatarAttachmentParseErrorString = tr("Error parsing attachment JSON from url: \"%1\""); - displayAvatarAttachmentWarning(avatarAttachmentParseErrorString.arg(url)); - } - } else { - // download failure - auto avatarAttachmentDownloadErrorString = tr("Error downloading attachment JSON from url: \"%1\""); - displayAvatarAttachmentWarning(avatarAttachmentDownloadErrorString.arg(url)); - } - reply->deleteLater(); - }); - return true; -} - -void Application::replaceDomainContent(const QString& url) { - qCDebug(interfaceapp) << "Attempting to replace domain content"; - QByteArray urlData(url.toUtf8()); - auto limitedNodeList = DependencyManager::get(); - const auto& domainHandler = limitedNodeList->getDomainHandler(); - - auto octreeFilePacket = NLPacket::create(PacketType::DomainContentReplacementFromUrl, urlData.size(), true); - octreeFilePacket->write(urlData); - limitedNodeList->sendPacket(std::move(octreeFilePacket), domainHandler.getSockAddr()); - - auto addressManager = DependencyManager::get(); - addressManager->handleLookupString(DOMAIN_SPAWNING_POINT); - QString newHomeAddress = addressManager->getHost() + DOMAIN_SPAWNING_POINT; - qCDebug(interfaceapp) << "Setting new home bookmark to: " << newHomeAddress; - DependencyManager::get()->setHomeLocationToAddress(newHomeAddress); -} - -bool Application::askToReplaceDomainContent(const QString& url) { - QString methodDetails; - const int MAX_CHARACTERS_PER_LINE = 90; - if (DependencyManager::get()->getThisNodeCanReplaceContent()) { - QUrl originURL { url }; - if (originURL.host().endsWith(MARKETPLACE_CDN_HOSTNAME)) { - // Create a confirmation dialog when this call is made - static const QString infoText = simpleWordWrap("Your domain's content will be replaced with a new content set. " - "If you want to save what you have now, create a backup before proceeding. For more information about backing up " - "and restoring content, visit the documentation page at: ", MAX_CHARACTERS_PER_LINE) + - "\nhttps://docs.highfidelity.com/create-and-explore/start-working-in-your-sandbox/restoring-sandbox-content"; - - ModalDialogListener* dig = OffscreenUi::asyncQuestion("Are you sure you want to replace this domain's content set?", - infoText, QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - QObject::connect(dig, &ModalDialogListener::response, this, [=] (QVariant answer) { - QString details; - if (static_cast(answer.toInt()) == QMessageBox::Yes) { - // Given confirmation, send request to domain server to replace content - replaceDomainContent(url); - details = "SuccessfulRequestToReplaceContent"; - } else { - details = "UserDeclinedToReplaceContent"; - } - QJsonObject messageProperties = { - { "status", details }, - { "content_set_url", url } - }; - UserActivityLogger::getInstance().logAction("replace_domain_content", messageProperties); - QObject::disconnect(dig, &ModalDialogListener::response, this, nullptr); - }); - } else { - methodDetails = "ContentSetDidNotOriginateFromMarketplace"; - QJsonObject messageProperties = { - { "status", methodDetails }, - { "content_set_url", url } - }; - UserActivityLogger::getInstance().logAction("replace_domain_content", messageProperties); - } - } else { - methodDetails = "UserDoesNotHavePermissionToReplaceContent"; - static const QString warningMessage = simpleWordWrap("The domain owner must enable 'Replace Content' " - "permissions for you in this domain's server settings before you can continue.", MAX_CHARACTERS_PER_LINE); - OffscreenUi::asyncWarning("You do not have permissions to replace domain content", warningMessage, - QMessageBox::Ok, QMessageBox::Ok); - - QJsonObject messageProperties = { - { "status", methodDetails }, - { "content_set_url", url } - }; - UserActivityLogger::getInstance().logAction("replace_domain_content", messageProperties); - } - return true; -} - -void Application::displayAvatarAttachmentWarning(const QString& message) const { - auto avatarAttachmentWarningTitle = tr("Avatar Attachment Failure"); - OffscreenUi::asyncWarning(avatarAttachmentWarningTitle, message); -} - -void Application::showDialog(const QUrl& widgetUrl, const QUrl& tabletUrl, const QString& name) const { - auto tablet = DependencyManager::get()->getTablet(SYSTEM_TABLET); - auto hmd = DependencyManager::get(); - bool onTablet = false; - - if (!tablet->getToolbarMode()) { - onTablet = tablet->pushOntoStack(tabletUrl); - if (onTablet) { - toggleTabletUI(true); - } - } else { -#if !defined(DISABLE_QML) - getOffscreenUI()->show(widgetUrl, name); -#endif - } -} - -void Application::showScriptLogs() { - QUrl defaultScriptsLoc = PathUtils::defaultScriptsLocation(); - defaultScriptsLoc.setPath(defaultScriptsLoc.path() + "developer/debugging/debugWindow.js"); - DependencyManager::get()->loadScript(defaultScriptsLoc.toString()); -} - -void Application::showAssetServerWidget(QString filePath) { - if (!DependencyManager::get()->getThisNodeCanWriteAssets() || getLoginDialogPoppedUp()) { - return; - } - static const QUrl url { "hifi/AssetServer.qml" }; - - auto startUpload = [=](QQmlContext* context, QObject* newObject){ - if (!filePath.isEmpty()) { - emit uploadRequest(filePath); - } - }; - auto tabletScriptingInterface = DependencyManager::get(); - auto tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); - auto hmd = DependencyManager::get(); - if (tablet->getToolbarMode()) { - getOffscreenUI()->show(url, "AssetServer", startUpload); - } else { - if (!hmd->getShouldShowTablet() && !isHMDMode()) { - getOffscreenUI()->show(url, "AssetServer", startUpload); - } else { - static const QUrl url("hifi/dialogs/TabletAssetServer.qml"); - if (!tablet->isPathLoaded(url)) { - tablet->pushOntoStack(url); - } - } - } - - startUpload(nullptr, nullptr); -} - -void Application::addAssetToWorldFromURL(QString url) { - - QString filename; - if (url.contains("filename")) { - filename = url.section("filename=", 1, 1); // Filename is in "?filename=" parameter at end of URL. - } - if (url.contains("poly.google.com/downloads")) { - filename = url.section('/', -1); - if (url.contains("noDownload")) { - filename.remove(".zip?noDownload=false"); - } else { - filename.remove(".zip"); - } - - } - - if (!DependencyManager::get()->getThisNodeCanWriteAssets()) { - QString errorInfo = "You do not have permissions to write to the Asset Server."; - qWarning(interfaceapp) << "Error downloading model: " + errorInfo; - addAssetToWorldError(filename, errorInfo); - return; - } - - addAssetToWorldInfo(filename, "Downloading model file " + filename + "."); - - auto request = DependencyManager::get()->createResourceRequest( - nullptr, QUrl(url), true, -1, "Application::addAssetToWorldFromURL"); - connect(request, &ResourceRequest::finished, this, &Application::addAssetToWorldFromURLRequestFinished); - request->send(); -} - -void Application::addAssetToWorldFromURLRequestFinished() { - auto request = qobject_cast(sender()); - auto url = request->getUrl().toString(); - auto result = request->getResult(); - - QString filename; - bool isBlocks = false; - - if (url.contains("filename")) { - filename = url.section("filename=", 1, 1); // Filename is in "?filename=" parameter at end of URL. - } - if (url.contains("poly.google.com/downloads")) { - filename = url.section('/', -1); - if (url.contains("noDownload")) { - filename.remove(".zip?noDownload=false"); - } else { - filename.remove(".zip"); - } - isBlocks = true; - } - - if (result == ResourceRequest::Success) { - QTemporaryDir temporaryDir; - temporaryDir.setAutoRemove(false); - if (temporaryDir.isValid()) { - QString temporaryDirPath = temporaryDir.path(); - QString downloadPath = temporaryDirPath + "/" + filename; - - QFile tempFile(downloadPath); - if (tempFile.open(QIODevice::WriteOnly)) { - tempFile.write(request->getData()); - addAssetToWorldInfoClear(filename); // Remove message from list; next one added will have a different key. - tempFile.close(); - qApp->getFileDownloadInterface()->runUnzip(downloadPath, url, true, false, isBlocks); - } else { - QString errorInfo = "Couldn't open temporary file for download"; - qWarning(interfaceapp) << errorInfo; - addAssetToWorldError(filename, errorInfo); - } - } else { - QString errorInfo = "Couldn't create temporary directory for download"; - qWarning(interfaceapp) << errorInfo; - addAssetToWorldError(filename, errorInfo); - } - } else { - qWarning(interfaceapp) << "Error downloading" << url << ":" << request->getResultString(); - addAssetToWorldError(filename, "Error downloading " + filename + " : " + request->getResultString()); - } - - request->deleteLater(); -} - - -QString filenameFromPath(QString filePath) { - return filePath.right(filePath.length() - filePath.lastIndexOf("/") - 1); -} - -void Application::addAssetToWorldUnzipFailure(QString filePath) { - QString filename = filenameFromPath(QUrl(filePath).toLocalFile()); - qWarning(interfaceapp) << "Couldn't unzip file" << filePath; - addAssetToWorldError(filename, "Couldn't unzip file " + filename + "."); -} - -void Application::addAssetToWorld(QString path, QString zipFile, bool isZip, bool isBlocks) { - // Automatically upload and add asset to world as an alternative manual process initiated by showAssetServerWidget(). - QString mapping; - QString filename = filenameFromPath(path); - if (isZip || isBlocks) { - QString assetName = zipFile.section("/", -1).remove(QRegExp("[.]zip(.*)$")); - QString assetFolder = path.section("model_repo/", -1); - mapping = "/" + assetName + "/" + assetFolder; - } else { - mapping = "/" + filename; - } - - // Test repeated because possibly different code paths. - if (!DependencyManager::get()->getThisNodeCanWriteAssets()) { - QString errorInfo = "You do not have permissions to write to the Asset Server."; - qWarning(interfaceapp) << "Error downloading model: " + errorInfo; - addAssetToWorldError(filename, errorInfo); - return; - } - - addAssetToWorldInfo(filename, "Adding " + mapping.mid(1) + " to the Asset Server."); - - addAssetToWorldWithNewMapping(path, mapping, 0, isZip, isBlocks); -} - -void Application::addAssetToWorldWithNewMapping(QString filePath, QString mapping, int copy, bool isZip, bool isBlocks) { - auto request = DependencyManager::get()->createGetMappingRequest(mapping); - - QObject::connect(request, &GetMappingRequest::finished, this, [=](GetMappingRequest* request) mutable { - const int MAX_COPY_COUNT = 100; // Limit number of duplicate assets; recursion guard. - auto result = request->getError(); - if (result == GetMappingRequest::NotFound) { - addAssetToWorldUpload(filePath, mapping, isZip, isBlocks); - } else if (result != GetMappingRequest::NoError) { - QString errorInfo = "Could not map asset name: " - + mapping.left(mapping.length() - QString::number(copy).length() - 1); - qWarning(interfaceapp) << "Error downloading model: " + errorInfo; - addAssetToWorldError(filenameFromPath(filePath), errorInfo); - } else if (copy < MAX_COPY_COUNT - 1) { - if (copy > 0) { - mapping = mapping.remove(mapping.lastIndexOf("-"), QString::number(copy).length() + 1); - } - copy++; - mapping = mapping.insert(mapping.lastIndexOf("."), "-" + QString::number(copy)); - addAssetToWorldWithNewMapping(filePath, mapping, copy, isZip, isBlocks); - } else { - QString errorInfo = "Too many copies of asset name: " - + mapping.left(mapping.length() - QString::number(copy).length() - 1); - qWarning(interfaceapp) << "Error downloading model: " + errorInfo; - addAssetToWorldError(filenameFromPath(filePath), errorInfo); - } - request->deleteLater(); - }); - - request->start(); -} - -void Application::addAssetToWorldUpload(QString filePath, QString mapping, bool isZip, bool isBlocks) { - qInfo(interfaceapp) << "Uploading" << filePath << "to Asset Server as" << mapping; - auto upload = DependencyManager::get()->createUpload(filePath); - QObject::connect(upload, &AssetUpload::finished, this, [=](AssetUpload* upload, const QString& hash) mutable { - if (upload->getError() != AssetUpload::NoError) { - QString errorInfo = "Could not upload model to the Asset Server."; - qWarning(interfaceapp) << "Error downloading model: " + errorInfo; - addAssetToWorldError(filenameFromPath(filePath), errorInfo); - } else { - addAssetToWorldSetMapping(filePath, mapping, hash, isZip, isBlocks); - } - - // Remove temporary directory created by Clara.io market place download. - int index = filePath.lastIndexOf("/model_repo/"); - if (index > 0) { - QString tempDir = filePath.left(index); - qCDebug(interfaceapp) << "Removing temporary directory at: " + tempDir; - QDir(tempDir).removeRecursively(); - } - - upload->deleteLater(); - }); - - upload->start(); -} - -void Application::addAssetToWorldSetMapping(QString filePath, QString mapping, QString hash, bool isZip, bool isBlocks) { - auto request = DependencyManager::get()->createSetMappingRequest(mapping, hash); - connect(request, &SetMappingRequest::finished, this, [=](SetMappingRequest* request) mutable { - if (request->getError() != SetMappingRequest::NoError) { - QString errorInfo = "Could not set asset mapping."; - qWarning(interfaceapp) << "Error downloading model: " + errorInfo; - addAssetToWorldError(filenameFromPath(filePath), errorInfo); - } else { - // to prevent files that aren't models or texture files from being loaded into world automatically - if ((filePath.toLower().endsWith(OBJ_EXTENSION) || filePath.toLower().endsWith(FBX_EXTENSION)) || - ((filePath.toLower().endsWith(JPG_EXTENSION) || filePath.toLower().endsWith(PNG_EXTENSION)) && - ((!isBlocks) && (!isZip)))) { - addAssetToWorldAddEntity(filePath, mapping); - } else { - qCDebug(interfaceapp) << "Zipped contents are not supported entity files"; - addAssetToWorldInfoDone(filenameFromPath(filePath)); - } - } - request->deleteLater(); - }); - - request->start(); -} - -void Application::addAssetToWorldAddEntity(QString filePath, QString mapping) { - EntityItemProperties properties; - properties.setName(mapping.right(mapping.length() - 1)); - if (filePath.toLower().endsWith(PNG_EXTENSION) || filePath.toLower().endsWith(JPG_EXTENSION)) { - properties.setType(EntityTypes::Image); - properties.setImageURL(QString("atp:" + mapping)); - properties.setKeepAspectRatio(false); - } else { - properties.setType(EntityTypes::Model); - properties.setModelURL("atp:" + mapping); - properties.setShapeType(SHAPE_TYPE_SIMPLE_COMPOUND); - } - properties.setCollisionless(true); // Temporarily set so that doesn't collide with avatar. - properties.setVisible(false); // Temporarily set so that don't see at large unresized dimensions. - bool grabbable = (Menu::getInstance()->isOptionChecked(MenuOption::CreateEntitiesGrabbable)); - properties.setUserData(grabbable ? GRABBABLE_USER_DATA : NOT_GRABBABLE_USER_DATA); - glm::vec3 positionOffset = getMyAvatar()->getWorldOrientation() * (getMyAvatar()->getSensorToWorldScale() * glm::vec3(0.0f, 0.0f, -2.0f)); - properties.setPosition(getMyAvatar()->getWorldPosition() + positionOffset); - properties.setRotation(getMyAvatar()->getWorldOrientation()); - properties.setGravity(glm::vec3(0.0f, 0.0f, 0.0f)); - auto entityID = DependencyManager::get()->addEntity(properties); - - // Note: Model dimensions are not available here; model is scaled per FBX mesh in RenderableModelEntityItem::update() later - // on. But FBX dimensions may be in cm, so we monitor for the dimension change and rescale again if warranted. - - if (entityID == QUuid()) { - QString errorInfo = "Could not add model " + mapping + " to world."; - qWarning(interfaceapp) << "Could not add model to world: " + errorInfo; - addAssetToWorldError(filenameFromPath(filePath), errorInfo); - } else { - // Monitor when asset is rendered in world so that can resize if necessary. - _addAssetToWorldResizeList.insert(entityID, 0); // List value is count of checks performed. - if (!_addAssetToWorldResizeTimer.isActive()) { - _addAssetToWorldResizeTimer.start(); - } - - // Close progress message box. - addAssetToWorldInfoDone(filenameFromPath(filePath)); - } -} - -void Application::addAssetToWorldCheckModelSize() { - if (_addAssetToWorldResizeList.size() == 0) { - return; - } - - auto item = _addAssetToWorldResizeList.begin(); - while (item != _addAssetToWorldResizeList.end()) { - auto entityID = item.key(); - - EntityPropertyFlags propertyFlags; - propertyFlags += PROP_NAME; - propertyFlags += PROP_DIMENSIONS; - auto entityScriptingInterface = DependencyManager::get(); - auto properties = entityScriptingInterface->getEntityProperties(entityID, propertyFlags); - auto name = properties.getName(); - auto dimensions = properties.getDimensions(); - - bool doResize = false; - - const glm::vec3 DEFAULT_DIMENSIONS = glm::vec3(0.1f, 0.1f, 0.1f); - if (dimensions != DEFAULT_DIMENSIONS) { - - // Scale model so that its maximum is exactly specific size. - const float MAXIMUM_DIMENSION = getMyAvatar()->getSensorToWorldScale(); - auto previousDimensions = dimensions; - auto scale = std::min(MAXIMUM_DIMENSION / dimensions.x, std::min(MAXIMUM_DIMENSION / dimensions.y, - MAXIMUM_DIMENSION / dimensions.z)); - dimensions *= scale; - qInfo(interfaceapp) << "Model" << name << "auto-resized from" << previousDimensions << " to " << dimensions; - doResize = true; - - item = _addAssetToWorldResizeList.erase(item); // Finished with this entity; advance to next. - } else { - // Increment count of checks done. - _addAssetToWorldResizeList[entityID]++; - - const int CHECK_MODEL_SIZE_MAX_CHECKS = 300; - if (_addAssetToWorldResizeList[entityID] > CHECK_MODEL_SIZE_MAX_CHECKS) { - // Have done enough checks; model was either the default size or something's gone wrong. - - // Rescale all dimensions. - const glm::vec3 UNIT_DIMENSIONS = glm::vec3(1.0f, 1.0f, 1.0f); - dimensions = UNIT_DIMENSIONS; - qInfo(interfaceapp) << "Model" << name << "auto-resize timed out; resized to " << dimensions; - doResize = true; - - item = _addAssetToWorldResizeList.erase(item); // Finished with this entity; advance to next. - } else { - // No action on this entity; advance to next. - ++item; - } - } - - if (doResize) { - EntityItemProperties properties; - properties.setDimensions(dimensions); - properties.setVisible(true); - if (!name.toLower().endsWith(PNG_EXTENSION) && !name.toLower().endsWith(JPG_EXTENSION)) { - properties.setCollisionless(false); - } - bool grabbable = (Menu::getInstance()->isOptionChecked(MenuOption::CreateEntitiesGrabbable)); - properties.setUserData(grabbable ? GRABBABLE_USER_DATA : NOT_GRABBABLE_USER_DATA); - properties.setLastEdited(usecTimestampNow()); - entityScriptingInterface->editEntity(entityID, properties); - } - } - - // Stop timer if nothing in list to check. - if (_addAssetToWorldResizeList.size() == 0) { - _addAssetToWorldResizeTimer.stop(); - } -} - - -void Application::addAssetToWorldInfo(QString modelName, QString infoText) { - // Displays the most recent info message, subject to being overridden by error messages. - - if (_aboutToQuit) { - return; - } - - /* - Cancel info timer if running. - If list has an entry for modelName, delete it (just one). - Append modelName, infoText to list. - Display infoText in message box unless an error is being displayed (i.e., error timer is running). - Show message box if not already visible. - */ - - _addAssetToWorldInfoTimer.stop(); - - addAssetToWorldInfoClear(modelName); - - _addAssetToWorldInfoKeys.append(modelName); - _addAssetToWorldInfoMessages.append(infoText); - - if (!_addAssetToWorldErrorTimer.isActive()) { - if (!_addAssetToWorldMessageBox) { - _addAssetToWorldMessageBox = getOffscreenUI()->createMessageBox(OffscreenUi::ICON_INFORMATION, - "Downloading Model", "", QMessageBox::NoButton, QMessageBox::NoButton); - connect(_addAssetToWorldMessageBox, SIGNAL(destroyed()), this, SLOT(onAssetToWorldMessageBoxClosed())); - } - - _addAssetToWorldMessageBox->setProperty("text", "\n" + infoText); - _addAssetToWorldMessageBox->setVisible(true); - } -} - -void Application::addAssetToWorldInfoClear(QString modelName) { - // Clears modelName entry from message list without affecting message currently displayed. - - if (_aboutToQuit) { - return; - } - - /* - Delete entry for modelName from list. - */ - - auto index = _addAssetToWorldInfoKeys.indexOf(modelName); - if (index > -1) { - _addAssetToWorldInfoKeys.removeAt(index); - _addAssetToWorldInfoMessages.removeAt(index); - } -} - -void Application::addAssetToWorldInfoDone(QString modelName) { - // Continues to display this message if the latest for a few seconds, then deletes it and displays the next latest. - - if (_aboutToQuit) { - return; - } - - /* - Delete entry for modelName from list. - (Re)start the info timer to update message box. ... onAddAssetToWorldInfoTimeout() - */ - - addAssetToWorldInfoClear(modelName); - _addAssetToWorldInfoTimer.start(); -} - -void Application::addAssetToWorldInfoTimeout() { - if (_aboutToQuit) { - return; - } - - /* - If list not empty, display last message in list (may already be displayed ) unless an error is being displayed. - If list empty, close the message box unless an error is being displayed. - */ - - if (!_addAssetToWorldErrorTimer.isActive() && _addAssetToWorldMessageBox) { - if (_addAssetToWorldInfoKeys.length() > 0) { - _addAssetToWorldMessageBox->setProperty("text", "\n" + _addAssetToWorldInfoMessages.last()); - } else { - disconnect(_addAssetToWorldMessageBox); - _addAssetToWorldMessageBox->setVisible(false); - _addAssetToWorldMessageBox->deleteLater(); - _addAssetToWorldMessageBox = nullptr; - } - } -} - -void Application::addAssetToWorldError(QString modelName, QString errorText) { - // Displays the most recent error message for a few seconds. - - if (_aboutToQuit) { - return; - } - - /* - If list has an entry for modelName, delete it. - Display errorText in message box. - Show message box if not already visible. - (Re)start error timer. ... onAddAssetToWorldErrorTimeout() - */ - - addAssetToWorldInfoClear(modelName); - - if (!_addAssetToWorldMessageBox) { - _addAssetToWorldMessageBox = getOffscreenUI()->createMessageBox(OffscreenUi::ICON_INFORMATION, - "Downloading Model", "", QMessageBox::NoButton, QMessageBox::NoButton); - connect(_addAssetToWorldMessageBox, SIGNAL(destroyed()), this, SLOT(onAssetToWorldMessageBoxClosed())); - } - - _addAssetToWorldMessageBox->setProperty("text", "\n" + errorText); - _addAssetToWorldMessageBox->setVisible(true); - - _addAssetToWorldErrorTimer.start(); -} - -void Application::addAssetToWorldErrorTimeout() { - if (_aboutToQuit) { - return; - } - - /* - If list is not empty, display message from last entry. - If list is empty, close the message box. - */ - - if (_addAssetToWorldMessageBox) { - if (_addAssetToWorldInfoKeys.length() > 0) { - _addAssetToWorldMessageBox->setProperty("text", "\n" + _addAssetToWorldInfoMessages.last()); - } else { - disconnect(_addAssetToWorldMessageBox); - _addAssetToWorldMessageBox->setVisible(false); - _addAssetToWorldMessageBox->deleteLater(); - _addAssetToWorldMessageBox = nullptr; - } - } -} - - -void Application::addAssetToWorldMessageClose() { - // Clear messages, e.g., if Interface is being closed or domain changes. - - /* - Call if user manually closes message box. - Call if domain changes. - Call if application is shutting down. - - Stop timers. - Close the message box if open. - Clear lists. - */ - - _addAssetToWorldInfoTimer.stop(); - _addAssetToWorldErrorTimer.stop(); - - if (_addAssetToWorldMessageBox) { - disconnect(_addAssetToWorldMessageBox); - _addAssetToWorldMessageBox->setVisible(false); - _addAssetToWorldMessageBox->deleteLater(); - _addAssetToWorldMessageBox = nullptr; - } - - _addAssetToWorldInfoKeys.clear(); - _addAssetToWorldInfoMessages.clear(); -} - -void Application::onAssetToWorldMessageBoxClosed() { - if (_addAssetToWorldMessageBox) { - // User manually closed message box; perhaps because it has become stuck, so reset all messages. - qInfo(interfaceapp) << "User manually closed download status message box"; - disconnect(_addAssetToWorldMessageBox); - _addAssetToWorldMessageBox = nullptr; - addAssetToWorldMessageClose(); - } -} - - -void Application::handleUnzip(QString zipFile, QStringList unzipFile, bool autoAdd, bool isZip, bool isBlocks) { - if (autoAdd) { - if (!unzipFile.isEmpty()) { - for (int i = 0; i < unzipFile.length(); i++) { - if (QFileInfo(unzipFile.at(i)).isFile()) { - qCDebug(interfaceapp) << "Preparing file for asset server: " << unzipFile.at(i); - addAssetToWorld(unzipFile.at(i), zipFile, isZip, isBlocks); - } - } - } else { - addAssetToWorldUnzipFailure(zipFile); - } - } else { - showAssetServerWidget(unzipFile.first()); - } -} - -void Application::packageModel() { - ModelPackager::package(); -} - -void Application::openUrl(const QUrl& url) const { - if (!url.isEmpty()) { - if (url.scheme() == URL_SCHEME_HIFI) { - DependencyManager::get()->handleLookupString(url.toString()); - } else if (url.scheme() == URL_SCHEME_HIFIAPP) { - DependencyManager::get()->openSystemApp(url.path()); - } else { - // address manager did not handle - ask QDesktopServices to handle - QDesktopServices::openUrl(url); - } - } -} - -void Application::loadDialog() { - ModalDialogListener* dlg = OffscreenUi::getOpenFileNameAsync(_glWidget, tr("Open Script"), - getPreviousScriptLocation(), - tr("JavaScript Files (*.js)")); - connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { - disconnect(dlg, &ModalDialogListener::response, this, nullptr); - const QString& response = answer.toString(); - if (!response.isEmpty() && QFile(response).exists()) { - setPreviousScriptLocation(QFileInfo(response).absolutePath()); - DependencyManager::get()->loadScript(response, true, false, false, true); // Don't load from cache - } - }); -} - -QString Application::getPreviousScriptLocation() { - QString result = _previousScriptLocation.get(); - return result; -} - -void Application::setPreviousScriptLocation(const QString& location) { - _previousScriptLocation.set(location); -} - -void Application::loadScriptURLDialog() const { - ModalDialogListener* dlg = OffscreenUi::getTextAsync(OffscreenUi::ICON_NONE, "Open and Run Script", "Script URL"); - connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) { - disconnect(dlg, &ModalDialogListener::response, this, nullptr); - const QString& newScript = response.toString(); - if (QUrl(newScript).scheme() == "atp") { - OffscreenUi::asyncWarning("Error Loading Script", "Cannot load client script over ATP"); - } else if (!newScript.isEmpty()) { - DependencyManager::get()->loadScript(newScript.trimmed()); - } - }); -} - -SharedSoundPointer Application::getSampleSound() const { - return _sampleSound; -} - -void Application::loadLODToolsDialog() { - auto tabletScriptingInterface = DependencyManager::get(); - auto tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); - if (tablet->getToolbarMode() || (!tablet->getTabletRoot() && !isHMDMode())) { - auto dialogsManager = DependencyManager::get(); - dialogsManager->lodTools(); - } else { - tablet->pushOntoStack("hifi/dialogs/TabletLODTools.qml"); - } -} - -void Application::loadEntityStatisticsDialog() { - auto tabletScriptingInterface = DependencyManager::get(); - auto tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); - if (tablet->getToolbarMode() || (!tablet->getTabletRoot() && !isHMDMode())) { - auto dialogsManager = DependencyManager::get(); - dialogsManager->octreeStatsDetails(); - } else { - tablet->pushOntoStack("hifi/dialogs/TabletEntityStatistics.qml"); - } -} - -void Application::loadDomainConnectionDialog() { - auto tabletScriptingInterface = DependencyManager::get(); - auto tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); - if (tablet->getToolbarMode() || (!tablet->getTabletRoot() && !isHMDMode())) { - auto dialogsManager = DependencyManager::get(); - dialogsManager->showDomainConnectionDialog(); - } else { - tablet->pushOntoStack("hifi/dialogs/TabletDCDialog.qml"); - } -} - -void Application::toggleLogDialog() { -#ifndef ANDROID_APP_QUEST_INTERFACE - if (getLoginDialogPoppedUp()) { - return; - } - if (! _logDialog) { - - bool keepOnTop =_keepLogWindowOnTop.get(); -#ifdef Q_OS_WIN - _logDialog = new LogDialog(keepOnTop ? qApp->getWindow() : nullptr, getLogger()); -#elif !defined(Q_OS_ANDROID) - _logDialog = new LogDialog(nullptr, getLogger()); - - if (keepOnTop) { - Qt::WindowFlags flags = _logDialog->windowFlags() | Qt::Tool; - _logDialog->setWindowFlags(flags); - } -#endif - } - - if (_logDialog->isVisible()) { - _logDialog->hide(); - } else { - _logDialog->show(); - } -#endif -} - - void Application::recreateLogWindow(int keepOnTop) { - _keepLogWindowOnTop.set(keepOnTop != 0); - if (_logDialog) { - bool toggle = _logDialog->isVisible(); - _logDialog->close(); - _logDialog = nullptr; - - if (toggle) { - toggleLogDialog(); - } - } - } - -void Application::toggleEntityScriptServerLogDialog() { - if (! _entityScriptServerLogDialog) { - _entityScriptServerLogDialog = new EntityScriptServerLogDialog(nullptr); - } - - if (_entityScriptServerLogDialog->isVisible()) { - _entityScriptServerLogDialog->hide(); - } else { - _entityScriptServerLogDialog->show(); - } -} - -void Application::loadAddAvatarBookmarkDialog() const { - auto avatarBookmarks = DependencyManager::get(); -} - -void Application::loadAvatarBrowser() const { - auto tablet = dynamic_cast(DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system")); - // construct the url to the marketplace item - QString url = NetworkingConstants::METAVERSE_SERVER_URL().toString() + "/marketplace?category=avatars"; - - QString MARKETPLACES_INJECT_SCRIPT_PATH = "file:///" + qApp->applicationDirPath() + "/scripts/system/html/js/marketplacesInject.js"; - tablet->gotoWebScreen(url, MARKETPLACES_INJECT_SCRIPT_PATH); - DependencyManager::get()->openTablet(); -} - -void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRatio, const QString& filename) { - postLambdaEvent([notify, includeAnimated, aspectRatio, filename, this] { - // Get a screenshot and save it - QString path = DependencyManager::get()->saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio), filename, - TestScriptingInterface::getInstance()->getTestResultsLocation()); - - // If we're not doing an animated snapshot as well... - if (!includeAnimated) { - if (!path.isEmpty()) { - // Tell the dependency manager that the capture of the still snapshot has taken place. - emit DependencyManager::get()->stillSnapshotTaken(path, notify); - } - } else if (!SnapshotAnimated::isAlreadyTakingSnapshotAnimated()) { - // Get an animated GIF snapshot and save it - SnapshotAnimated::saveSnapshotAnimated(path, aspectRatio, qApp, DependencyManager::get()); - } - }); -} - -void Application::takeSecondaryCameraSnapshot(const bool& notify, const QString& filename) { - postLambdaEvent([notify, filename, this] { - QString snapshotPath = DependencyManager::get()->saveSnapshot(getActiveDisplayPlugin()->getSecondaryCameraScreenshot(), filename, - TestScriptingInterface::getInstance()->getTestResultsLocation()); - - emit DependencyManager::get()->stillSnapshotTaken(snapshotPath, notify); - }); -} - -void Application::takeSecondaryCamera360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat, const bool& notify, const QString& filename) { - postLambdaEvent([notify, filename, cubemapOutputFormat, cameraPosition] { - DependencyManager::get()->save360Snapshot(cameraPosition, cubemapOutputFormat, notify, filename); - }); -} - -void Application::shareSnapshot(const QString& path, const QUrl& href) { - postLambdaEvent([path, href] { - // not much to do here, everything is done in snapshot code... - DependencyManager::get()->uploadSnapshot(path, href); - }); -} - -float Application::getRenderResolutionScale() const { - auto menu = Menu::getInstance(); - if (!menu) { - return 1.0f; - } - if (menu->isOptionChecked(MenuOption::RenderResolutionOne)) { - return 1.0f; - } else if (menu->isOptionChecked(MenuOption::RenderResolutionTwoThird)) { - return 0.666f; - } else if (menu->isOptionChecked(MenuOption::RenderResolutionHalf)) { - return 0.5f; - } else if (menu->isOptionChecked(MenuOption::RenderResolutionThird)) { - return 0.333f; - } else if (menu->isOptionChecked(MenuOption::RenderResolutionQuarter)) { - return 0.25f; - } else { - return 1.0f; - } -} - -void Application::notifyPacketVersionMismatch() { - if (!_notifiedPacketVersionMismatchThisDomain && !isInterstitialMode()) { - _notifiedPacketVersionMismatchThisDomain = true; - - QString message = "The location you are visiting is running an incompatible server version.\n"; - message += "Content may not display properly."; - - OffscreenUi::asyncWarning("", message); - } -} - -void Application::checkSkeleton() const { - if (getMyAvatar()->getSkeletonModel()->isActive() && !getMyAvatar()->getSkeletonModel()->hasSkeleton()) { - qCDebug(interfaceapp) << "MyAvatar model has no skeleton"; - - QString message = "Your selected avatar body has no skeleton.\n\nThe default body will be loaded..."; - OffscreenUi::asyncWarning("", message); - - getMyAvatar()->useFullAvatarURL(AvatarData::defaultFullAvatarModelUrl(), DEFAULT_FULL_AVATAR_MODEL_NAME); - } else { - _physicsEngine->setCharacterController(getMyAvatar()->getCharacterController()); - } -} - -void Application::activeChanged(Qt::ApplicationState state) { - switch (state) { - case Qt::ApplicationActive: - _isForeground = true; - break; - - case Qt::ApplicationSuspended: - case Qt::ApplicationHidden: - case Qt::ApplicationInactive: - default: - _isForeground = false; - break; - } -} - -void Application::windowMinimizedChanged(bool minimized) { - // initialize the _minimizedWindowTimer - static std::once_flag once; - std::call_once(once, [&] { - connect(&_minimizedWindowTimer, &QTimer::timeout, this, [] { - QCoreApplication::postEvent(QCoreApplication::instance(), new QEvent(static_cast(Idle)), Qt::HighEventPriority); - }); - }); - - // avoid rendering to the display plugin but continue posting Idle events, - // so that physics continues to simulate and the deadlock watchdog knows we're alive - if (!minimized && !getActiveDisplayPlugin()->isActive()) { - _minimizedWindowTimer.stop(); - getActiveDisplayPlugin()->activate(); - } else if (minimized && getActiveDisplayPlugin()->isActive()) { - getActiveDisplayPlugin()->deactivate(); - _minimizedWindowTimer.start(THROTTLED_SIM_FRAME_PERIOD_MS); - } -} - -void Application::postLambdaEvent(const std::function& f) { - if (this->thread() == QThread::currentThread()) { - f(); - } else { - QCoreApplication::postEvent(this, new LambdaEvent(f)); - } -} - -void Application::sendLambdaEvent(const std::function& f) { - if (this->thread() == QThread::currentThread()) { - f(); - } else { - LambdaEvent event(f); - QCoreApplication::sendEvent(this, &event); - } -} - -void Application::initPlugins(const QStringList& arguments) { - QCommandLineOption display("display", "Preferred displays", "displays"); - QCommandLineOption disableDisplays("disable-displays", "Displays to disable", "displays"); - QCommandLineOption disableInputs("disable-inputs", "Inputs to disable", "inputs"); - - QCommandLineParser parser; - parser.addOption(display); - parser.addOption(disableDisplays); - parser.addOption(disableInputs); - parser.parse(arguments); - - if (parser.isSet(display)) { - auto preferredDisplays = parser.value(display).split(',', QString::SkipEmptyParts); - qInfo() << "Setting prefered display plugins:" << preferredDisplays; - PluginManager::getInstance()->setPreferredDisplayPlugins(preferredDisplays); - } - - if (parser.isSet(disableDisplays)) { - auto disabledDisplays = parser.value(disableDisplays).split(',', QString::SkipEmptyParts); - qInfo() << "Disabling following display plugins:" << disabledDisplays; - PluginManager::getInstance()->disableDisplays(disabledDisplays); - } - - if (parser.isSet(disableInputs)) { - auto disabledInputs = parser.value(disableInputs).split(',', QString::SkipEmptyParts); - qInfo() << "Disabling following input plugins:" << disabledInputs; - PluginManager::getInstance()->disableInputs(disabledInputs); - } -} - -void Application::shutdownPlugins() { -} - -glm::uvec2 Application::getCanvasSize() const { - return glm::uvec2(_glWidget->width(), _glWidget->height()); -} - -QRect Application::getRenderingGeometry() const { - auto geometry = _glWidget->geometry(); - auto topLeft = geometry.topLeft(); - auto topLeftScreen = _glWidget->mapToGlobal(topLeft); - geometry.moveTopLeft(topLeftScreen); - return geometry; -} - -glm::uvec2 Application::getUiSize() const { - static const uint MIN_SIZE = 1; - glm::uvec2 result(MIN_SIZE); - if (_displayPlugin) { - result = getActiveDisplayPlugin()->getRecommendedUiSize(); - } - return result; -} - -QRect Application::getRecommendedHUDRect() const { - auto uiSize = getUiSize(); - QRect result(0, 0, uiSize.x, uiSize.y); - if (_displayPlugin) { - result = getActiveDisplayPlugin()->getRecommendedHUDRect(); - } - return result; -} - -glm::vec2 Application::getDeviceSize() const { - static const int MIN_SIZE = 1; - glm::vec2 result(MIN_SIZE); - if (_displayPlugin) { - result = getActiveDisplayPlugin()->getRecommendedRenderSize(); - } - return result; -} - -bool Application::isThrottleRendering() const { - if (_displayPlugin) { - return getActiveDisplayPlugin()->isThrottled(); - } - return false; -} - -bool Application::hasFocus() const { - bool result = (QApplication::activeWindow() != nullptr); -#if defined(Q_OS_WIN) - // On Windows, QWidget::activateWindow() - as called in setFocus() - makes the application's taskbar icon flash but doesn't - // take user focus away from their current window. So also check whether the application is the user's current foreground - // window. - result = result && (HWND)QApplication::activeWindow()->winId() == GetForegroundWindow(); -#endif - return result; -} - -void Application::setFocus() { - // Note: Windows doesn't allow a user focus to be taken away from another application. Instead, it changes the color of and - // flashes the taskbar icon. - auto window = qApp->getWindow(); - window->activateWindow(); -} - -void Application::raise() { - auto windowState = qApp->getWindow()->windowState(); - if (windowState & Qt::WindowMinimized) { - if (windowState & Qt::WindowMaximized) { - qApp->getWindow()->showMaximized(); - } else if (windowState & Qt::WindowFullScreen) { - qApp->getWindow()->showFullScreen(); - } else { - qApp->getWindow()->showNormal(); - } - } - qApp->getWindow()->raise(); -} - -void Application::setMaxOctreePacketsPerSecond(int maxOctreePPS) { - if (maxOctreePPS != _maxOctreePPS) { - _maxOctreePPS = maxOctreePPS; - maxOctreePacketsPerSecond.set(_maxOctreePPS); - } -} - -int Application::getMaxOctreePacketsPerSecond() const { - return _maxOctreePPS; -} - -qreal Application::getDevicePixelRatio() { - return (_window && _window->windowHandle()) ? _window->windowHandle()->devicePixelRatio() : 1.0; -} - -DisplayPluginPointer Application::getActiveDisplayPlugin() const { - if (QThread::currentThread() != thread()) { - std::unique_lock lock(_displayPluginLock); - return _displayPlugin; - } - - if (!_aboutToQuit && !_displayPlugin) { - const_cast(this)->updateDisplayMode(); - Q_ASSERT(_displayPlugin); - } - return _displayPlugin; -} - - -#if !defined(DISABLE_QML) -static const char* EXCLUSION_GROUP_KEY = "exclusionGroup"; - -static void addDisplayPluginToMenu(const DisplayPluginPointer& displayPlugin, int index, bool active) { - auto menu = Menu::getInstance(); - QString name = displayPlugin->getName(); - auto grouping = displayPlugin->getGrouping(); - QString groupingMenu { "" }; - Q_ASSERT(!menu->menuItemExists(MenuOption::OutputMenu, name)); - - // assign the meny grouping based on plugin grouping - switch (grouping) { - case Plugin::ADVANCED: - groupingMenu = "Advanced"; - break; - case Plugin::DEVELOPER: - groupingMenu = "Developer"; - break; - default: - groupingMenu = "Standard"; - break; - } - - static QActionGroup* displayPluginGroup = nullptr; - if (!displayPluginGroup) { - displayPluginGroup = new QActionGroup(menu); - displayPluginGroup->setExclusive(true); - } - auto parent = menu->getMenu(MenuOption::OutputMenu); - auto action = menu->addActionToQMenuAndActionHash(parent, - name, QKeySequence(Qt::CTRL + (Qt::Key_0 + index)), qApp, - SLOT(updateDisplayMode()), - QAction::NoRole, Menu::UNSPECIFIED_POSITION, groupingMenu); - - action->setCheckable(true); - action->setChecked(active); - displayPluginGroup->addAction(action); - - action->setProperty(EXCLUSION_GROUP_KEY, QVariant::fromValue(displayPluginGroup)); - Q_ASSERT(menu->menuItemExists(MenuOption::OutputMenu, name)); -} -#endif - -void Application::updateDisplayMode() { - // Unsafe to call this method from anything but the main thread - if (QThread::currentThread() != thread()) { - qFatal("Attempted to switch display plugins from a non-main thread"); - } - - // Once time initialization code that depends on the UI being available - auto displayPlugins = getDisplayPlugins(); - - // Default to the first item on the list, in case none of the menu items match - DisplayPluginPointer newDisplayPlugin = displayPlugins.at(0); - auto menu = getPrimaryMenu(); - if (menu) { - foreach(DisplayPluginPointer displayPlugin, PluginManager::getInstance()->getDisplayPlugins()) { - QString name = displayPlugin->getName(); - QAction* action = menu->getActionForOption(name); - // Menu might have been removed if the display plugin lost - if (!action) { - continue; - } - if (action->isChecked()) { - newDisplayPlugin = displayPlugin; - break; - } - } - } - - if (newDisplayPlugin == _displayPlugin) { - return; - } - - setDisplayPlugin(newDisplayPlugin); -} - -void Application::setDisplayPlugin(DisplayPluginPointer newDisplayPlugin) { - if (newDisplayPlugin == _displayPlugin) { - return; - } - - // FIXME don't have the application directly set the state of the UI, - // instead emit a signal that the display plugin is changing and let - // the desktop lock itself. Reduces coupling between the UI and display - // plugins - auto offscreenUi = getOffscreenUI(); - auto desktop = offscreenUi ? offscreenUi->getDesktop() : nullptr; - auto menu = Menu::getInstance(); - - // Make the switch atomic from the perspective of other threads - { - std::unique_lock lock(_displayPluginLock); - bool wasRepositionLocked = false; - if (desktop) { - // Tell the desktop to no reposition (which requires plugin info), until we have set the new plugin, below. - wasRepositionLocked = desktop->property("repositionLocked").toBool(); - desktop->setProperty("repositionLocked", true); - } - - if (_displayPlugin) { - disconnect(_displayPlugin.get(), &DisplayPlugin::presented, this, &Application::onPresent); - _displayPlugin->deactivate(); - } - - auto oldDisplayPlugin = _displayPlugin; - bool active = newDisplayPlugin->activate(); - - if (!active) { - auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); - - // If the new plugin fails to activate, fallback to last display - qWarning() << "Failed to activate display: " << newDisplayPlugin->getName(); - newDisplayPlugin = oldDisplayPlugin; - - if (newDisplayPlugin) { - qWarning() << "Falling back to last display: " << newDisplayPlugin->getName(); - active = newDisplayPlugin->activate(); - } - - // If there is no last display, or - // If the last display fails to activate, fallback to desktop - if (!active) { - newDisplayPlugin = displayPlugins.at(0); - qWarning() << "Falling back to display: " << newDisplayPlugin->getName(); - active = newDisplayPlugin->activate(); - } - - if (!active) { - qFatal("Failed to activate fallback plugin"); - } - } - - if (offscreenUi) { - offscreenUi->resize(fromGlm(newDisplayPlugin->getRecommendedUiSize())); - } - getApplicationCompositor().setDisplayPlugin(newDisplayPlugin); - _displayPlugin = newDisplayPlugin; - connect(_displayPlugin.get(), &DisplayPlugin::presented, this, &Application::onPresent, Qt::DirectConnection); - if (desktop) { - desktop->setProperty("repositionLocked", wasRepositionLocked); - } - } - - bool isHmd = _displayPlugin->isHmd(); - qCDebug(interfaceapp) << "Entering into" << (isHmd ? "HMD" : "Desktop") << "Mode"; - - // Only log/emit after a successful change - UserActivityLogger::getInstance().logAction("changed_display_mode", { - { "previous_display_mode", _displayPlugin ? _displayPlugin->getName() : "" }, - { "display_mode", newDisplayPlugin ? newDisplayPlugin->getName() : "" }, - { "hmd", isHmd } - }); - emit activeDisplayPluginChanged(); - - // reset the avatar, to set head and hand palms back to a reasonable default pose. - getMyAvatar()->reset(false); - - // switch to first person if entering hmd and setting is checked - if (menu) { - QAction* action = menu->getActionForOption(newDisplayPlugin->getName()); - if (action) { - action->setChecked(true); - } - - if (isHmd && menu->isOptionChecked(MenuOption::FirstPersonHMD)) { - menu->setIsOptionChecked(MenuOption::FirstPerson, true); - cameraMenuChanged(); - } - - // Remove the mirror camera option from menu if in HMD mode - auto mirrorAction = menu->getActionForOption(MenuOption::FullscreenMirror); - mirrorAction->setVisible(!isHmd); - } - - Q_ASSERT_X(_displayPlugin, "Application::updateDisplayMode", "could not find an activated display plugin"); -} - -void Application::switchDisplayMode() { - if (!_autoSwitchDisplayModeSupportedHMDPlugin) { - return; - } - bool currentHMDWornStatus = _autoSwitchDisplayModeSupportedHMDPlugin->isDisplayVisible(); - if (currentHMDWornStatus != _previousHMDWornStatus) { - // Switch to respective mode as soon as currentHMDWornStatus changes - if (currentHMDWornStatus) { - qCDebug(interfaceapp) << "Switching from Desktop to HMD mode"; - endHMDSession(); - setActiveDisplayPlugin(_autoSwitchDisplayModeSupportedHMDPluginName); - } else { - qCDebug(interfaceapp) << "Switching from HMD to desktop mode"; - setActiveDisplayPlugin(DESKTOP_DISPLAY_PLUGIN_NAME); - startHMDStandBySession(); - } - } - _previousHMDWornStatus = currentHMDWornStatus; -} - -void Application::setShowBulletWireframe(bool value) { - _physicsEngine->setShowBulletWireframe(value); -} - -void Application::setShowBulletAABBs(bool value) { - _physicsEngine->setShowBulletAABBs(value); -} - -void Application::setShowBulletContactPoints(bool value) { - _physicsEngine->setShowBulletContactPoints(value); -} - -void Application::setShowBulletConstraints(bool value) { - _physicsEngine->setShowBulletConstraints(value); -} - -void Application::setShowBulletConstraintLimits(bool value) { - _physicsEngine->setShowBulletConstraintLimits(value); -} - -void Application::createLoginDialog() { - const glm::vec3 LOGIN_DIMENSIONS { 0.89f, 0.5f, 0.01f }; - const auto OFFSET = glm::vec2(0.7f, -0.1f); - auto cameraPosition = _myCamera.getPosition(); - auto cameraOrientation = _myCamera.getOrientation(); - auto upVec = getMyAvatar()->getWorldOrientation() * Vectors::UNIT_Y; - auto headLookVec = (cameraOrientation * Vectors::FRONT); - // DEFAULT_DPI / tablet scale percentage - const float DPI = 31.0f / (75.0f / 100.0f); - auto offset = headLookVec * OFFSET.x; - auto position = (cameraPosition + offset) + (upVec * OFFSET.y); - - EntityItemProperties properties; - properties.setType(EntityTypes::Web); - properties.setName("LoginDialogEntity"); - properties.setSourceUrl(LOGIN_DIALOG.toString()); - properties.setPosition(position); - properties.setRotation(cameraOrientation); - properties.setDimensions(LOGIN_DIMENSIONS); - properties.setPrimitiveMode(PrimitiveMode::SOLID); - properties.getGrab().setGrabbable(false); - properties.setIgnorePickIntersection(false); - properties.setAlpha(1.0f); - properties.setDPI(DPI); - properties.setVisible(true); - - auto entityScriptingInterface = DependencyManager::get(); - _loginDialogID = entityScriptingInterface->addEntityInternal(properties, entity::HostType::LOCAL); - - auto keyboard = DependencyManager::get().data(); - if (!keyboard->getAnchorID().isNull() && !_loginDialogID.isNull()) { - auto keyboardLocalOffset = cameraOrientation * glm::vec3(-0.4f * getMyAvatar()->getSensorToWorldScale(), -0.3f, 0.2f); - - EntityItemProperties properties; - properties.setPosition(position + keyboardLocalOffset); - properties.setRotation(cameraOrientation * Quaternions::Y_180); - - entityScriptingInterface->editEntity(keyboard->getAnchorID(), properties); - keyboard->setResetKeyboardPositionOnRaise(false); - } - setKeyboardFocusEntity(_loginDialogID); - emit loginDialogFocusEnabled(); - getApplicationCompositor().getReticleInterface()->setAllowMouseCapture(false); - getApplicationCompositor().getReticleInterface()->setVisible(false); - if (!_loginStateManager.isSetUp()) { - _loginStateManager.setUp(); - } -} - -void Application::updateLoginDialogPosition() { - const float LOOK_AWAY_THRESHOLD_ANGLE = 70.0f; - const auto OFFSET = glm::vec2(0.7f, -0.1f); - - auto entityScriptingInterface = DependencyManager::get(); - EntityPropertyFlags desiredProperties; - desiredProperties += PROP_POSITION; - auto properties = entityScriptingInterface->getEntityProperties(_loginDialogID, desiredProperties); - auto positionVec = properties.getPosition(); - auto cameraPositionVec = _myCamera.getPosition(); - auto cameraOrientation = cancelOutRollAndPitch(_myCamera.getOrientation()); - auto headLookVec = (cameraOrientation * Vectors::FRONT); - auto entityToHeadVec = positionVec - cameraPositionVec; - auto pointAngle = (glm::acos(glm::dot(glm::normalize(entityToHeadVec), glm::normalize(headLookVec))) * 180.0f / PI); - auto upVec = getMyAvatar()->getWorldOrientation() * Vectors::UNIT_Y; - auto offset = headLookVec * OFFSET.x; - auto newPositionVec = (cameraPositionVec + offset) + (upVec * OFFSET.y); - - bool outOfBounds = glm::distance(positionVec, cameraPositionVec) > 1.0f; - - if (pointAngle > LOOK_AWAY_THRESHOLD_ANGLE || outOfBounds) { - { - EntityItemProperties properties; - properties.setPosition(newPositionVec); - properties.setRotation(cameraOrientation); - entityScriptingInterface->editEntity(_loginDialogID, properties); - } - - { - glm::vec3 keyboardLocalOffset = cameraOrientation * glm::vec3(-0.4f * getMyAvatar()->getSensorToWorldScale(), -0.3f, 0.2f); - glm::quat keyboardOrientation = cameraOrientation * glm::quat(glm::radians(glm::vec3(-30.0f, 180.0f, 0.0f))); - - EntityItemProperties properties; - properties.setPosition(newPositionVec + keyboardLocalOffset); - properties.setRotation(keyboardOrientation); - entityScriptingInterface->editEntity(DependencyManager::get()->getAnchorID(), properties); - } - } -} - -bool Application::hasRiftControllers() { - return PluginUtils::isOculusTouchControllerAvailable(); -} - -bool Application::hasViveControllers() { - return PluginUtils::isViveControllerAvailable(); -} - -void Application::onDismissedLoginDialog() { - _loginDialogPoppedUp = false; - loginDialogPoppedUp.set(false); - auto keyboard = DependencyManager::get().data(); - keyboard->setResetKeyboardPositionOnRaise(true); - if (!_loginDialogID.isNull()) { - DependencyManager::get()->deleteEntity(_loginDialogID); - _loginDialogID = QUuid(); - _loginStateManager.tearDown(); - } - resumeAfterLoginDialogActionTaken(); -} - -void Application::setShowTrackedObjects(bool value) { - _showTrackedObjects = value; -} - -void Application::startHMDStandBySession() { - _autoSwitchDisplayModeSupportedHMDPlugin->startStandBySession(); -} - -void Application::endHMDSession() { - _autoSwitchDisplayModeSupportedHMDPlugin->endSession(); -} - -mat4 Application::getEyeProjection(int eye) const { - QMutexLocker viewLocker(&_viewMutex); - if (isHMDMode()) { - return getActiveDisplayPlugin()->getEyeProjection((Eye)eye, _viewFrustum.getProjection()); - } - return _viewFrustum.getProjection(); -} - -mat4 Application::getEyeOffset(int eye) const { - // FIXME invert? - return getActiveDisplayPlugin()->getEyeToHeadTransform((Eye)eye); -} - -mat4 Application::getHMDSensorPose() const { - if (isHMDMode()) { - return getActiveDisplayPlugin()->getHeadPose(); - } - return mat4(); -} - -void Application::deadlockApplication() { - qCDebug(interfaceapp) << "Intentionally deadlocked Interface"; - // Using a loop that will *technically* eventually exit (in ~600 billion years) - // to avoid compiler warnings about a loop that will never exit - for (uint64_t i = 1; i != 0; ++i) { - QThread::sleep(1); - } -} - -// cause main thread to be unresponsive for 35 seconds -void Application::unresponsiveApplication() { - // to avoid compiler warnings about a loop that will never exit - uint64_t start = usecTimestampNow(); - uint64_t UNRESPONSIVE_FOR_SECONDS = 35; - uint64_t UNRESPONSIVE_FOR_USECS = UNRESPONSIVE_FOR_SECONDS * USECS_PER_SECOND; - qCDebug(interfaceapp) << "Intentionally cause Interface to be unresponsive for " << UNRESPONSIVE_FOR_SECONDS << " seconds"; - while (usecTimestampNow() - start < UNRESPONSIVE_FOR_USECS) { - QThread::sleep(1); - } -} - -void Application::setActiveDisplayPlugin(const QString& pluginName) { - DisplayPluginPointer newDisplayPlugin; - for (DisplayPluginPointer displayPlugin : PluginManager::getInstance()->getDisplayPlugins()) { - QString name = displayPlugin->getName(); - if (pluginName == name) { - newDisplayPlugin = displayPlugin; - break; - } - } - - if (newDisplayPlugin) { - setDisplayPlugin(newDisplayPlugin); - } -} - -void Application::handleLocalServerConnection() const { - auto server = qobject_cast(sender()); - - qCDebug(interfaceapp) << "Got connection on local server from additional instance - waiting for parameters"; - - auto socket = server->nextPendingConnection(); - - connect(socket, &QLocalSocket::readyRead, this, &Application::readArgumentsFromLocalSocket); - - qApp->getWindow()->raise(); - qApp->getWindow()->activateWindow(); -} - -void Application::readArgumentsFromLocalSocket() const { - auto socket = qobject_cast(sender()); - - auto message = socket->readAll(); - socket->deleteLater(); - - qCDebug(interfaceapp) << "Read from connection: " << message; - - // If we received a message, try to open it as a URL - if (message.length() > 0) { - qApp->openUrl(QString::fromUtf8(message)); - } -} - -void Application::showDesktop() { -} - -CompositorHelper& Application::getApplicationCompositor() const { - return *DependencyManager::get(); -} - - -// virtual functions required for PluginContainer -ui::Menu* Application::getPrimaryMenu() { - auto appMenu = _window->menuBar(); - auto uiMenu = dynamic_cast(appMenu); - return uiMenu; -} - -void Application::showDisplayPluginsTools(bool show) { - DependencyManager::get()->hmdTools(show); -} - -GLWidget* Application::getPrimaryWidget() { - return _glWidget; -} - -MainWindow* Application::getPrimaryWindow() { - return getWindow(); -} - -QOpenGLContext* Application::getPrimaryContext() { - return _glWidget->qglContext(); -} - -bool Application::makeRenderingContextCurrent() { - return true; -} - -bool Application::isForeground() const { - return _isForeground && !_window->isMinimized(); -} - -// FIXME? perhaps two, one for the main thread and one for the offscreen UI rendering thread? -static const int UI_RESERVED_THREADS = 1; -// Windows won't let you have all the cores -static const int OS_RESERVED_THREADS = 1; - -void Application::updateThreadPoolCount() const { - auto reservedThreads = UI_RESERVED_THREADS + OS_RESERVED_THREADS + _displayPlugin->getRequiredThreadCount(); - auto availableThreads = QThread::idealThreadCount() - reservedThreads; - auto threadPoolSize = std::max(MIN_PROCESSING_THREAD_POOL_SIZE, availableThreads); - qCDebug(interfaceapp) << "Ideal Thread Count " << QThread::idealThreadCount(); - qCDebug(interfaceapp) << "Reserved threads " << reservedThreads; - qCDebug(interfaceapp) << "Setting thread pool size to " << threadPoolSize; - QThreadPool::globalInstance()->setMaxThreadCount(threadPoolSize); -} - -void Application::updateSystemTabletMode() { - if (_settingsLoaded && !getLoginDialogPoppedUp()) { - qApp->setProperty(hifi::properties::HMD, isHMDMode()); - if (isHMDMode()) { - DependencyManager::get()->setToolbarMode(getHmdTabletBecomesToolbarSetting()); - } else { - DependencyManager::get()->setToolbarMode(getDesktopTabletBecomesToolbarSetting()); - } - } -} - -QUuid Application::getTabletScreenID() const { - auto HMD = DependencyManager::get(); - return HMD->getCurrentTabletScreenID(); -} - -QUuid Application::getTabletHomeButtonID() const { - auto HMD = DependencyManager::get(); - return HMD->getCurrentHomeButtonID(); -} - -QUuid Application::getTabletFrameID() const { - auto HMD = DependencyManager::get(); - return HMD->getCurrentTabletFrameID(); -} - -QVector Application::getTabletIDs() const { - // Most important first. - QVector result; - auto HMD = DependencyManager::get(); - result << HMD->getCurrentTabletScreenID(); - result << HMD->getCurrentHomeButtonID(); - result << HMD->getCurrentTabletFrameID(); - return result; -} - -void Application::setAvatarOverrideUrl(const QUrl& url, bool save) { - _avatarOverrideUrl = url; - _saveAvatarOverrideUrl = save; -} - -void Application::saveNextPhysicsStats(QString filename) { - _physicsEngine->saveNextPhysicsStats(filename); -} - -void Application::copyToClipboard(const QString& text) { - if (QThread::currentThread() != qApp->thread()) { - QMetaObject::invokeMethod(this, "copyToClipboard"); - return; - } - - // assume that the address is being copied because the user wants a shareable address - QApplication::clipboard()->setText(text); -} - -QString Application::getGraphicsCardType() { - return GPUIdent::getInstance()->getName(); -} - -#if defined(Q_OS_ANDROID) -void Application::beforeEnterBackground() { - auto nodeList = DependencyManager::get(); - nodeList->setSendDomainServerCheckInEnabled(false); - nodeList->reset(true); - clearDomainOctreeDetails(); -} - - - -void Application::enterBackground() { - QMetaObject::invokeMethod(DependencyManager::get().data(), - "stop", Qt::BlockingQueuedConnection); -// Quest only supports one plugin which can't be deactivated currently -#if !defined(ANDROID_APP_QUEST_INTERFACE) - if (getActiveDisplayPlugin()->isActive()) { - getActiveDisplayPlugin()->deactivate(); - } -#endif -} - -void Application::enterForeground() { - QMetaObject::invokeMethod(DependencyManager::get().data(), - "start", Qt::BlockingQueuedConnection); -// Quest only supports one plugin which can't be deactivated currently -#if !defined(ANDROID_APP_QUEST_INTERFACE) - if (!getActiveDisplayPlugin() || getActiveDisplayPlugin()->isActive() || !getActiveDisplayPlugin()->activate()) { - qWarning() << "Could not re-activate display plugin"; - } -#endif - auto nodeList = DependencyManager::get(); - nodeList->setSendDomainServerCheckInEnabled(true); -} - - -void Application::toggleAwayMode(){ - QKeyEvent event = QKeyEvent (QEvent::KeyPress, Qt::Key_Escape, Qt::NoModifier); - QCoreApplication::sendEvent (this, &event); -} - - -#endif - - -#include "Application.moc" From 186588ddc4974069769b082ccc55b2abdf32d165 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Tue, 12 Mar 2019 15:09:50 -0700 Subject: [PATCH 137/446] set audio client muted when loaded data --- interface/src/scripting/Audio.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index bf43db3044..b1b5077e60 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -228,6 +228,9 @@ void Audio::loadData() { _hmdMuted = _hmdMutedSetting.get(); _pttDesktop = _pttDesktopSetting.get(); _pttHMD = _pttHMDSetting.get(); + + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted()), Q_ARG(bool, false)); } bool Audio::getPTTHMD() const { From efc9f993f59db17556049aa9cdcfe40136fa2359 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Thu, 7 Mar 2019 17:39:19 -0800 Subject: [PATCH 138/446] Add FSTBaker, and make ModelBaker output an FST Restore feature to look for baked model file in other oven directory --- libraries/baking/src/Baker.h | 2 +- libraries/baking/src/FBXBaker.cpp | 11 ++ libraries/baking/src/FBXBaker.h | 3 +- libraries/baking/src/ModelBaker.cpp | 98 ++++++++++---- libraries/baking/src/ModelBaker.h | 15 ++- libraries/baking/src/baking/BakerLibrary.cpp | 15 ++- libraries/baking/src/baking/BakerLibrary.h | 9 +- libraries/baking/src/baking/FSTBaker.cpp | 128 +++++++++++++++++++ libraries/baking/src/baking/FSTBaker.h | 45 +++++++ libraries/fbx/src/FSTReader.h | 2 + tools/oven/src/DomainBaker.cpp | 20 +-- tools/oven/src/DomainBaker.h | 1 - 12 files changed, 301 insertions(+), 48 deletions(-) create mode 100644 libraries/baking/src/baking/FSTBaker.cpp create mode 100644 libraries/baking/src/baking/FSTBaker.h diff --git a/libraries/baking/src/Baker.h b/libraries/baking/src/Baker.h index c1b2ddf959..611f992c96 100644 --- a/libraries/baking/src/Baker.h +++ b/libraries/baking/src/Baker.h @@ -52,7 +52,7 @@ protected: void handleErrors(const QStringList& errors); // List of baked output files. For instance, for an FBX this would - // include the .fbx and all of its texture files. + // include the .fbx, a .fst pointing to the fbx, and all of the fbx texture files. std::vector _outputFiles; QStringList _errorList; diff --git a/libraries/baking/src/FBXBaker.cpp b/libraries/baking/src/FBXBaker.cpp index e1bb86d051..d2dc86c783 100644 --- a/libraries/baking/src/FBXBaker.cpp +++ b/libraries/baking/src/FBXBaker.cpp @@ -37,6 +37,17 @@ #include "FBXToJSON.h" #endif +FBXBaker::FBXBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, + const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) : + ModelBaker(inputModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, hasBeenBaked) { + if (hasBeenBaked) { + // Look for the original model file one directory higher. Perhaps this is an oven output directory. + QUrl originalRelativePath = QUrl("../original/" + inputModelURL.fileName().replace(BAKED_FBX_EXTENSION, FBX_EXTENSION)); + QUrl newInputModelURL = inputModelURL.adjusted(QUrl::RemoveFilename).resolved(originalRelativePath); + _modelURL = newInputModelURL; + } +} + void FBXBaker::bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) { _hfmModel = hfmModel; // Load the root node from the FBX file diff --git a/libraries/baking/src/FBXBaker.h b/libraries/baking/src/FBXBaker.h index 7770e3014d..f8a023f431 100644 --- a/libraries/baking/src/FBXBaker.h +++ b/libraries/baking/src/FBXBaker.h @@ -31,7 +31,8 @@ using TextureBakerThreadGetter = std::function; class FBXBaker : public ModelBaker { Q_OBJECT public: - using ModelBaker::ModelBaker; + FBXBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, + const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); protected: virtual void bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) override; diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index 6568850c1f..c80df2db2e 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -21,6 +21,7 @@ #include #include +#include #ifdef _WIN32 #pragma warning( push ) @@ -61,12 +62,20 @@ ModelBaker::ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter input qDebug() << "Made temporary dir " << _modelTempDir; qDebug() << "Origin file path: " << _originalModelFilePath; + { + auto bakedFilename = _modelURL.fileName(); + if (!hasBeenBaked) { + bakedFilename = bakedFilename.left(bakedFilename.lastIndexOf('.')); + bakedFilename += BAKED_FBX_EXTENSION; + } + _bakedModelURL = _bakedOutputDir + "/" + bakedFilename; + } } ModelBaker::~ModelBaker() { if (_modelTempDir.exists()) { if (!_modelTempDir.remove(_originalModelFilePath)) { - qCWarning(model_baking) << "Failed to remove temporary copy of fbx file:" << _originalModelFilePath; + qCWarning(model_baking) << "Failed to remove temporary copy of model file:" << _originalModelFilePath; } if (!_modelTempDir.rmdir(".")) { qCWarning(model_baking) << "Failed to remove temporary directory:" << _modelTempDir; @@ -74,6 +83,26 @@ ModelBaker::~ModelBaker() { } } +void ModelBaker::setOutputURLSuffix(const QUrl& outputURLSuffix) { + _outputURLSuffix = outputURLSuffix; +} + +void ModelBaker::setMappingURL(const QUrl& mappingURL) { + _mappingURL = mappingURL; +} + +void ModelBaker::setMapping(const hifi::VariantHash& mapping) { + _mapping = mapping; +} + +QUrl ModelBaker::getFullOutputMappingURL() const { + QUrl appendedURL = _outputMappingURL; + appendedURL.setFragment(_outputURLSuffix.fragment()); + appendedURL.setQuery(_outputURLSuffix.query()); + appendedURL.setUserInfo(_outputURLSuffix.userInfo()); + return appendedURL; +} + void ModelBaker::bake() { qDebug() << "ModelBaker" << _modelURL << "bake starting"; @@ -92,19 +121,24 @@ void ModelBaker::bake() { void ModelBaker::initializeOutputDirs() { // Attempt to make the output folders - // Warn if there is an output directory using the same name + // Warn if there is an output directory using the same name, unless we know a parent FST baker created them already if (QDir(_bakedOutputDir).exists()) { - qWarning() << "Output path" << _bakedOutputDir << "already exists. Continuing."; + if (_mappingURL.isEmpty()) { + qWarning() << "Output path" << _bakedOutputDir << "already exists. Continuing."; + } } else { qCDebug(model_baking) << "Creating baked output folder" << _bakedOutputDir; if (!QDir().mkpath(_bakedOutputDir)) { handleError("Failed to create baked output folder " + _bakedOutputDir); + return; } } if (QDir(_originalOutputDir).exists()) { - qWarning() << "Output path" << _originalOutputDir << "already exists. Continuing."; + if (_mappingURL.isEmpty()) { + qWarning() << "Output path" << _originalOutputDir << "already exists. Continuing."; + } } else { qCDebug(model_baking) << "Creating original output folder" << _originalOutputDir; if (!QDir().mkpath(_originalOutputDir)) { @@ -122,7 +156,7 @@ void ModelBaker::saveSourceModel() { qDebug() << "Local file url: " << _modelURL << _modelURL.toString() << _modelURL.toLocalFile() << ", copying to: " << _originalModelFilePath; if (!localModelURL.exists()) { - //QMessageBox::warning(this, "Could not find " + _fbxURL.toString(), ""); + //QMessageBox::warning(this, "Could not find " + _modelURL.toString(), ""); handleError("Could not find " + _modelURL.toString()); return; } @@ -135,7 +169,7 @@ void ModelBaker::saveSourceModel() { localModelURL.copy(_originalModelFilePath); - // emit our signal to start the import of the FBX source copy + // emit our signal to start the import of the model source copy emit modelLoaded(); } else { // remote file, kick off a download @@ -214,11 +248,11 @@ void ModelBaker::bakeSourceCopy() { handleError("Could not recognize file type of model file " + _originalModelFilePath); return; } - hifi::VariantHash mapping; - mapping["combineParts"] = true; // set true so that OBJSerializer reads material info from material library - hfm::Model::Pointer loadedModel = serializer->read(modelData, mapping, _modelURL); + hifi::VariantHash serializerMapping = _mapping; + serializerMapping["combineParts"] = true; // set true so that OBJSerializer reads material info from material library + hfm::Model::Pointer loadedModel = serializer->read(modelData, serializerMapping, _modelURL); - baker::Baker baker(loadedModel, mapping); + baker::Baker baker(loadedModel, serializerMapping); auto config = baker.getConfiguration(); // Enable compressed draco mesh generation config->getJobConfig("BuildDracoMesh")->setEnabled(true); @@ -269,6 +303,32 @@ void ModelBaker::bakeSourceCopy() { return; } + // Output FST file, copying over input mappings if available + QString outputFSTFilename = !_mappingURL.isEmpty() ? _mappingURL.fileName() : _modelURL.fileName(); + auto extensionStart = outputFSTFilename.indexOf("."); + if (extensionStart != -1) { + outputFSTFilename.resize(extensionStart); + } + outputFSTFilename += ".baked.fst"; + QString outputFSTURL = _bakedOutputDir + "/" + outputFSTFilename; + + auto outputMapping = _mapping; + outputMapping[FST_VERSION_FIELD] = FST_VERSION; + outputMapping[FILENAME_FIELD] = _bakedModelURL.fileName(); + hifi::ByteArray fstOut = FSTReader::writeMapping(outputMapping); + + QFile fstOutputFile { outputFSTURL }; + if (!fstOutputFile.open(QIODevice::WriteOnly)) { + handleError("Failed to open file '" + outputFSTURL + "' for writing"); + return; + } + if (fstOutputFile.write(fstOut) == -1) { + handleError("Failed to write to file '" + outputFSTURL + "'"); + return; + } + _outputFiles.push_back(outputFSTURL); + _outputMappingURL = outputFSTURL; + // check if we're already done with textures (in case we had none to re-write) checkIfTexturesFinished(); } @@ -657,31 +717,25 @@ void ModelBaker::embedTextureMetaData() { } void ModelBaker::exportScene() { - // save the relative path to this FBX inside our passed output folder - auto fileName = _modelURL.fileName(); - auto baseName = fileName.left(fileName.lastIndexOf('.')); - auto bakedFilename = baseName + BAKED_FBX_EXTENSION; - - _bakedModelFilePath = _bakedOutputDir + "/" + bakedFilename; - auto fbxData = FBXWriter::encodeFBX(_rootNode); - QFile bakedFile(_bakedModelFilePath); + QString bakedModelURL = _bakedModelURL.toString(); + QFile bakedFile(bakedModelURL); if (!bakedFile.open(QIODevice::WriteOnly)) { - handleError("Error opening " + _bakedModelFilePath + " for writing"); + handleError("Error opening " + bakedModelURL + " for writing"); return; } bakedFile.write(fbxData); - _outputFiles.push_back(_bakedModelFilePath); + _outputFiles.push_back(bakedModelURL); #ifdef HIFI_DUMP_FBX { FBXToJSON fbxToJSON; fbxToJSON << _rootNode; - QFileInfo modelFile(_bakedModelFilePath); + QFileInfo modelFile(_bakedModelURL.toString()); QString outFilename(modelFile.dir().absolutePath() + "/" + modelFile.completeBaseName() + "_FBX.json"); QFile jsonFile(outFilename); if (jsonFile.open(QIODevice::WriteOnly)) { @@ -691,5 +745,5 @@ void ModelBaker::exportScene() { } #endif - qCDebug(model_baking) << "Exported" << _modelURL << "with re-written paths to" << _bakedModelFilePath; + qCDebug(model_baking) << "Exported" << _modelURL << "with re-written paths to" << bakedModelURL; } diff --git a/libraries/baking/src/ModelBaker.h b/libraries/baking/src/ModelBaker.h index b0bd3798ff..f1ef6db56d 100644 --- a/libraries/baking/src/ModelBaker.h +++ b/libraries/baking/src/ModelBaker.h @@ -45,6 +45,10 @@ public: const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); virtual ~ModelBaker(); + void setOutputURLSuffix(const QUrl& urlSuffix); + void setMappingURL(const QUrl& mappingURL); + void setMapping(const hifi::VariantHash& mapping); + void initializeOutputDirs(); bool buildDracoMeshNode(FBXNode& dracoMeshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList); @@ -52,7 +56,8 @@ public: virtual void setWasAborted(bool wasAborted) override; QUrl getModelURL() const { return _modelURL; } - QString getBakedModelFilePath() const { return _bakedModelFilePath; } + virtual QUrl getFullOutputMappingURL() const; + QUrl getBakedModelURL() const { return _bakedModelURL; } signals: void modelLoaded(); @@ -72,9 +77,14 @@ protected: FBXNode _rootNode; QHash _textureContentMap; QUrl _modelURL; + QUrl _outputURLSuffix; + QUrl _mappingURL; + hifi::VariantHash _mapping; QString _bakedOutputDir; QString _originalOutputDir; - QString _bakedModelFilePath; + TextureBakerThreadGetter _textureThreadGetter; + QString _outputMappingURL; + QUrl _bakedModelURL; QDir _modelTempDir; QString _originalModelFilePath; @@ -93,7 +103,6 @@ private: const QString & bakedFilename, const QByteArray & textureContent); QString texturePathRelativeToModel(QUrl modelURL, QUrl textureURL); - TextureBakerThreadGetter _textureThreadGetter; QMultiHash> _bakingTextures; QHash _textureNameMatchCount; QHash _remappedTexturePaths; diff --git a/libraries/baking/src/baking/BakerLibrary.cpp b/libraries/baking/src/baking/BakerLibrary.cpp index 202fd4b3d8..c95745146b 100644 --- a/libraries/baking/src/baking/BakerLibrary.cpp +++ b/libraries/baking/src/baking/BakerLibrary.cpp @@ -11,6 +11,7 @@ #include "BakerLibrary.h" +#include "FSTBaker.h" #include "../FBXBaker.h" #include "../OBJBaker.h" @@ -51,22 +52,24 @@ std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, TextureB QString bakedOutputDirectory = contentOutputPath + subDirName + "/baked"; QString originalOutputDirectory = contentOutputPath + subDirName + "/original"; + return getModelBakerWithOutputDirectories(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory); +} + +std::unique_ptr getModelBakerWithOutputDirectories(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& bakedOutputDirectory, const QString& originalOutputDirectory) { + auto filename = bakeableModelURL.fileName(); + std::unique_ptr baker; if (filename.endsWith(FST_EXTENSION, Qt::CaseInsensitive)) { - //baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FST_EXTENSION, Qt::CaseInsensitive)); + baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FST_EXTENSION, Qt::CaseInsensitive)); } else if (filename.endsWith(FBX_EXTENSION, Qt::CaseInsensitive)) { baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FBX_EXTENSION, Qt::CaseInsensitive)); } else if (filename.endsWith(OBJ_EXTENSION, Qt::CaseInsensitive)) { baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory); - } else if (filename.endsWith(GLTF_EXTENSION, Qt::CaseInsensitive)) { + //} else if (filename.endsWith(GLTF_EXTENSION, Qt::CaseInsensitive)) { //baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory); } else { qDebug() << "Could not create ModelBaker for url" << bakeableModelURL; } - if (baker) { - QDir(contentOutputPath).mkpath(subDirName); - } - return baker; } diff --git a/libraries/baking/src/baking/BakerLibrary.h b/libraries/baking/src/baking/BakerLibrary.h index e77463b502..57197b53fd 100644 --- a/libraries/baking/src/baking/BakerLibrary.h +++ b/libraries/baking/src/baking/BakerLibrary.h @@ -16,13 +16,14 @@ #include "../ModelBaker.h" -// Returns either the given model URL, or, if the model is baked and shouldRebakeOriginals is true, -// the guessed location of the original model -// Returns an empty URL if no bakeable URL found +// Returns either the given model URL if valid, or an empty URL QUrl getBakeableModelURL(const QUrl& url); // Assuming the URL is valid, gets the appropriate baker for the given URL, and creates the base directory where the baker's output will later be stored // Returns an empty pointer if a baker could not be created std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& contentOutputPath); -#endif hifi_BakerLibrary_h +// Similar to getModelBaker, but gives control over where the output folders will be +std::unique_ptr getModelBakerWithOutputDirectories(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& bakedOutputDirectory, const QString& originalOutputDirectory); + +#endif // hifi_BakerLibrary_h diff --git a/libraries/baking/src/baking/FSTBaker.cpp b/libraries/baking/src/baking/FSTBaker.cpp new file mode 100644 index 0000000000..f76180bb58 --- /dev/null +++ b/libraries/baking/src/baking/FSTBaker.cpp @@ -0,0 +1,128 @@ +// +// FSTBaker.cpp +// libraries/baking/src/baking +// +// Created by Sabrina Shanman on 2019/03/06. +// Copyright 2019 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 +// + +#include "FSTBaker.h" + +#include +#include + +#include "BakerLibrary.h" + +#include + +FSTBaker::FSTBaker(const QUrl& inputMappingURL, TextureBakerThreadGetter inputTextureThreadGetter, + const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) : + ModelBaker(inputMappingURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, hasBeenBaked) { + if (hasBeenBaked) { + // Look for the original model file one directory higher. Perhaps this is an oven output directory. + QUrl originalRelativePath = QUrl("../original/" + inputMappingURL.fileName().replace(BAKED_FST_EXTENSION, FST_EXTENSION)); + QUrl newInputMappingURL = inputMappingURL.adjusted(QUrl::RemoveFilename).resolved(originalRelativePath); + _modelURL = newInputMappingURL; + } + _mappingURL = _modelURL; + + { + // Unused, but defined for consistency + auto bakedFilename = _modelURL.fileName(); + bakedFilename.replace(FST_EXTENSION, BAKED_FST_EXTENSION); + _bakedModelURL = _bakedOutputDir + "/" + bakedFilename; + } +} + +QUrl FSTBaker::getFullOutputMappingURL() const { + if (_modelBaker) { + return _modelBaker->getFullOutputMappingURL(); + } + return QUrl(); +} + +void FSTBaker::bakeSourceCopy() { + if (shouldStop()) { + return; + } + + QFile fstFile(_originalModelFilePath); + if (!fstFile.open(QIODevice::ReadOnly)) { + handleError("Error opening " + _originalModelFilePath + " for reading"); + return; + } + + hifi::ByteArray fstData = fstFile.readAll(); + _mapping = FSTReader::readMapping(fstData); + + auto filenameField = _mapping[FILENAME_FIELD].toString(); + if (filenameField.isEmpty()) { + handleError("The '" + FILENAME_FIELD + "' property in the FST file '" + _originalModelFilePath + "' could not be found"); + return; + } + auto modelURL = _mappingURL.adjusted(QUrl::RemoveFilename).resolved(filenameField); + auto bakeableModelURL = getBakeableModelURL(modelURL); + if (bakeableModelURL.isEmpty()) { + handleError("The '" + FILENAME_FIELD + "' property in the FST file '" + _originalModelFilePath + "' could not be resolved to a valid bakeable model url"); + return; + } + + auto baker = getModelBakerWithOutputDirectories(bakeableModelURL, _textureThreadGetter, _bakedOutputDir, _originalOutputDir); + _modelBaker = std::unique_ptr(dynamic_cast(baker.release())); + if (!_modelBaker) { + handleError("The model url '" + bakeableModelURL.toString() + "' from the FST file '" + _originalModelFilePath + "' (property: '" + FILENAME_FIELD + "') could not be used to initialize a valid model baker"); + return; + } + if (dynamic_cast(_modelBaker.get())) { + // Could be interesting, but for now let's just prevent infinite FST loops in the most straightforward way possible + handleError("The FST file '" + _originalModelFilePath + "' (property: '" + FILENAME_FIELD + "') references another FST file. FST chaining is not supported."); + return; + } + _modelBaker->setMappingURL(_mappingURL); + _modelBaker->setMapping(_mapping); + // Hold on to the old url userinfo/query/fragment data so ModelBaker::getFullOutputMappingURL retains that data from the original model URL + _modelBaker->setOutputURLSuffix(modelURL); + + connect(_modelBaker.get(), &ModelBaker::aborted, this, &FSTBaker::handleModelBakerAborted); + connect(_modelBaker.get(), &ModelBaker::finished, this, &FSTBaker::handleModelBakerFinished); + + // FSTBaker can't do much while waiting for the ModelBaker to finish, so start the bake on this thread. + _modelBaker->bake(); +} + +void FSTBaker::handleModelBakerEnded() { + for (auto& warning : _modelBaker->getWarnings()) { + _warningList.push_back(warning); + } + for (auto& error : _modelBaker->getErrors()) { + _errorList.push_back(error); + } + + // Get the output files, including but not limited to the FST file and the baked model file + for (auto& outputFile : _modelBaker->getOutputFiles()) { + _outputFiles.push_back(outputFile); + } + +} + +void FSTBaker::handleModelBakerAborted() { + handleModelBakerEnded(); + if (!wasAborted()) { + setWasAborted(true); + } +} + +void FSTBaker::handleModelBakerFinished() { + handleModelBakerEnded(); + setIsFinished(true); +} + +void FSTBaker::abort() { + ModelBaker::abort(); + if (_modelBaker) { + _modelBaker->abort(); + } +} diff --git a/libraries/baking/src/baking/FSTBaker.h b/libraries/baking/src/baking/FSTBaker.h new file mode 100644 index 0000000000..aeb7286af3 --- /dev/null +++ b/libraries/baking/src/baking/FSTBaker.h @@ -0,0 +1,45 @@ +// +// FSTBaker.h +// libraries/baking/src/baking +// +// Created by Sabrina Shanman on 2019/03/06. +// Copyright 2019 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 +// + +#ifndef hifi_FSTBaker_h +#define hifi_FSTBaker_h + +#include "../ModelBaker.h" + +class FSTBaker : public ModelBaker { + Q_OBJECT + +public: + FSTBaker(const QUrl& inputMappingURL, TextureBakerThreadGetter inputTextureThreadGetter, + const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); + + virtual QUrl getFullOutputMappingURL() const; + +signals: + void fstLoaded(); + +public slots: + virtual void abort() override; + +protected: + std::unique_ptr _modelBaker; + +protected slots: + virtual void bakeSourceCopy() override; + virtual void bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) override {}; + void handleModelBakerAborted(); + void handleModelBakerFinished(); + +private: + void handleModelBakerEnded(); +}; + +#endif // hifi_FSTBaker_h diff --git a/libraries/fbx/src/FSTReader.h b/libraries/fbx/src/FSTReader.h index ad952c4ed7..fade0fa5bc 100644 --- a/libraries/fbx/src/FSTReader.h +++ b/libraries/fbx/src/FSTReader.h @@ -15,6 +15,8 @@ #include #include +static const unsigned int FST_VERSION = 1; +static const QString FST_VERSION_FIELD = "version"; static const QString NAME_FIELD = "name"; static const QString TYPE_FIELD = "type"; static const QString FILENAME_FIELD = "filename"; diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 42dfe59241..15b5a1ae12 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -152,8 +152,11 @@ void DomainBaker::addModelBaker(const QString& property, const QString& url, QJs auto getWorkerThreadCallback = []() -> QThread* { return Oven::instance().getNextWorkerThread(); }; - QSharedPointer baker = QSharedPointer(getModelBaker(bakeableModelURL, getWorkerThreadCallback, _contentOutputPath).release(), &ModelBaker::deleteLater); + QSharedPointer baker = QSharedPointer(getModelBaker(bakeableModelURL, getWorkerThreadCallback, _contentOutputPath).release(), &Baker::deleteLater); if (baker) { + // Hold on to the old url userinfo/query/fragment data so ModelBaker::getFullOutputMappingURL retains that data from the original model URL + baker->setOutputURLSuffix(url); + // make sure our handler is called when the baker is done connect(baker.data(), &Baker::finished, this, &DomainBaker::handleFinishedModelBaker); @@ -332,6 +335,7 @@ void DomainBaker::enumerateEntities() { addModelBaker(MODEL_URL_KEY, entity[MODEL_URL_KEY].toString(), *it); } if (entity.contains(COMPOUND_SHAPE_URL_KEY)) { + // TODO: Do not combine mesh parts, otherwise the collision behavior will be different // TODO: this could be optimized so that we don't do the full baking pass for collision shapes, // but we have to handle the case where it's also used as a modelURL somewhere addModelBaker(COMPOUND_SHAPE_URL_KEY, entity[COMPOUND_SHAPE_URL_KEY].toString(), *it); @@ -415,11 +419,11 @@ void DomainBaker::handleFinishedModelBaker() { qDebug() << "Re-writing entity references to" << baker->getModelURL(); // setup a new URL using the prefix we were passed - auto relativeFBXFilePath = baker->getBakedModelFilePath().remove(_contentOutputPath); - if (relativeFBXFilePath.startsWith("/")) { - relativeFBXFilePath = relativeFBXFilePath.right(relativeFBXFilePath.length() - 1); + auto relativeMappingFilePath = baker->getFullOutputMappingURL().toString().remove(_contentOutputPath); + if (relativeMappingFilePath.startsWith("/")) { + relativeMappingFilePath = relativeMappingFilePath.right(relativeMappingFilePath.length() - 1); } - QUrl newURL = _destinationPath.resolved(relativeFBXFilePath); + QUrl newURL = _destinationPath.resolved(relativeMappingFilePath); // enumerate the QJsonRef values for the URL of this model from our multi hash of // entity objects needing a URL re-write @@ -432,12 +436,8 @@ void DomainBaker::handleFinishedModelBaker() { // grab the old URL QUrl oldURL = entity[property].toString(); - // copy the fragment and query, and user info from the old model URL - newURL.setQuery(oldURL.query()); - newURL.setFragment(oldURL.fragment()); - newURL.setUserInfo(oldURL.userInfo()); - // set the new URL as the value in our temp QJsonObject + // The fragment, query, and user info from the original model URL should now be present on the filename in the FST file entity[property] = newURL.toString(); } else { // Group property diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h index 4504d5b8fa..dbbf182fa7 100644 --- a/tools/oven/src/DomainBaker.h +++ b/tools/oven/src/DomainBaker.h @@ -17,7 +17,6 @@ #include #include -#include "Baker.h" #include "ModelBaker.h" #include "TextureBaker.h" #include "JSBaker.h" From 5b504c47590032a474a125c97fa7c3186fdb05db Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Mon, 4 Mar 2019 18:04:40 -0800 Subject: [PATCH 139/446] Add encode/decode speed config to BuildDracoMeshTask --- libraries/baking/src/ModelBaker.cpp | 10 ---------- .../model-baker/src/model-baker/BuildDracoMeshTask.cpp | 5 +++-- .../model-baker/src/model-baker/BuildDracoMeshTask.h | 9 +++++++++ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index c80df2db2e..f3954c98da 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -227,9 +227,6 @@ void ModelBaker::handleModelNetworkReply() { } } -// TODO: Remove after testing -#include - void ModelBaker::bakeSourceCopy() { QFile modelFile(_originalModelFilePath); if (!modelFile.open(QIODevice::ReadOnly)) { @@ -258,13 +255,6 @@ void ModelBaker::bakeSourceCopy() { config->getJobConfig("BuildDracoMesh")->setEnabled(true); // Do not permit potentially lossy modification of joint data meant for runtime ((PrepareJointsConfig*)config->getJobConfig("PrepareJoints"))->passthrough = true; - - // TODO: Remove after testing - { - auto* dracoConfig = ((BuildDracoMeshConfig*)config->getJobConfig("BuildDracoMesh")); - dracoConfig->encodeSpeed = 10; - dracoConfig->decodeSpeed = -1; - } // Begin hfm baking baker.run(); diff --git a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp index 9bfd03e218..e45b2bf584 100644 --- a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp +++ b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp @@ -193,7 +193,8 @@ std::unique_ptr createDracoMesh(const hfm::Mesh& mesh, const std::v } void BuildDracoMeshTask::configure(const Config& config) { - // Nothing to configure yet + _encodeSpeed = config.encodeSpeed; + _decodeSpeed = config.decodeSpeed; } void BuildDracoMeshTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) { @@ -222,7 +223,7 @@ void BuildDracoMeshTask::run(const baker::BakeContextPointer& context, const Inp encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 14); encoder.SetAttributeQuantization(draco::GeometryAttribute::TEX_COORD, 12); encoder.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 10); - encoder.SetSpeedOptions(0, 5); + encoder.SetSpeedOptions(_encodeSpeed, _decodeSpeed); draco::EncoderBuffer buffer; encoder.EncodeMeshToBuffer(*dracoMesh, &buffer); diff --git a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h index ab1679959a..0e33be3c41 100644 --- a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h +++ b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h @@ -21,8 +21,13 @@ // BuildDracoMeshTask is disabled by default class BuildDracoMeshConfig : public baker::JobConfig { Q_OBJECT + Q_PROPERTY(int encodeSpeed MEMBER encodeSpeed) + Q_PROPERTY(int decodeSpeed MEMBER decodeSpeed) public: BuildDracoMeshConfig() : baker::JobConfig(false) {} + + int encodeSpeed { 0 }; + int decodeSpeed { 5 }; }; class BuildDracoMeshTask { @@ -34,6 +39,10 @@ public: void configure(const Config& config); void run(const baker::BakeContextPointer& context, const Input& input, Output& output); + +protected: + int _encodeSpeed { 0 }; + int _decodeSpeed { 5 }; }; #endif // hifi_BuildDracoMeshTask_h From b42c6d1352d815699d10c9af9da0eaf6ba59a171 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Tue, 12 Mar 2019 11:24:56 -0700 Subject: [PATCH 140/446] Fix baked models not mapping to correct textures --- libraries/baking/src/ModelBaker.cpp | 5 +++++ libraries/baking/src/TextureBaker.cpp | 5 +---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index f3954c98da..ac69653e45 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -305,6 +305,8 @@ void ModelBaker::bakeSourceCopy() { auto outputMapping = _mapping; outputMapping[FST_VERSION_FIELD] = FST_VERSION; outputMapping[FILENAME_FIELD] = _bakedModelURL.fileName(); + // All textures will be found in the same directory as the model + outputMapping[TEXDIR_FIELD] = "."; hifi::ByteArray fstOut = FSTReader::writeMapping(outputMapping); QFile fstOutputFile { outputFSTURL }; @@ -403,6 +405,9 @@ QString ModelBaker::compressTexture(QString modelTextureFileName, image::Texture // ensuring that the baked texture will have a unique name // even if there was another texture with the same name at a different path baseTextureFileName = createBaseTextureFileName(modelTextureFileInfo); + // If two textures have the same URL but are used differently, we need to process them separately + QString addMapChannel = QString::fromStdString("_" + std::to_string(textureType)); + baseTextureFileName += addMapChannel; _remappedTexturePaths[urlToTexture] = baseTextureFileName; } diff --git a/libraries/baking/src/TextureBaker.cpp b/libraries/baking/src/TextureBaker.cpp index db54cbdf98..8591cbd0aa 100644 --- a/libraries/baking/src/TextureBaker.cpp +++ b/libraries/baking/src/TextureBaker.cpp @@ -128,11 +128,8 @@ void TextureBaker::processTexture() { TextureMeta meta; - // If two textures have the same URL but are used differently, we need to process them separately - QString addMapChannel = QString::fromStdString("_" + std::to_string(_textureType)); - _baseFilename += addMapChannel; - QString newFilename = _textureURL.fileName(); + QString addMapChannel = QString::fromStdString("_" + std::to_string(_textureType)); newFilename.replace(QString("."), addMapChannel + "."); QString originalCopyFilePath = _outputDirectory.absoluteFilePath(newFilename); From abbbeb11e1e85395e50f6da4c4830f030347dde0 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Tue, 12 Mar 2019 11:59:21 -0700 Subject: [PATCH 141/446] Comment out baking for collision model, with explanation --- tools/oven/src/DomainBaker.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 15b5a1ae12..28b45eccaf 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -335,10 +335,12 @@ void DomainBaker::enumerateEntities() { addModelBaker(MODEL_URL_KEY, entity[MODEL_URL_KEY].toString(), *it); } if (entity.contains(COMPOUND_SHAPE_URL_KEY)) { - // TODO: Do not combine mesh parts, otherwise the collision behavior will be different + // TODO: Support collision model baking + // Do not combine mesh parts, otherwise the collision behavior will be different + // combineParts is currently only used by OBJBaker (mesh-combining functionality ought to be moved to the asset engine at some point), and is also used by OBJBaker to determine if the material library should be loaded (should be separate flag) // TODO: this could be optimized so that we don't do the full baking pass for collision shapes, // but we have to handle the case where it's also used as a modelURL somewhere - addModelBaker(COMPOUND_SHAPE_URL_KEY, entity[COMPOUND_SHAPE_URL_KEY].toString(), *it); + //addModelBaker(COMPOUND_SHAPE_URL_KEY, entity[COMPOUND_SHAPE_URL_KEY].toString(), *it); } if (entity.contains(ANIMATION_KEY)) { auto animationObject = entity[ANIMATION_KEY].toObject(); From 09c30269d4e78ec492e8f54ed3a4cca783d7dd80 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Tue, 12 Mar 2019 12:44:21 -0700 Subject: [PATCH 142/446] Add a note about FST support for url userinfo/query/fragment --- tools/oven/src/DomainBaker.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 28b45eccaf..0f07a392b4 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -155,6 +155,10 @@ void DomainBaker::addModelBaker(const QString& property, const QString& url, QJs QSharedPointer baker = QSharedPointer(getModelBaker(bakeableModelURL, getWorkerThreadCallback, _contentOutputPath).release(), &Baker::deleteLater); if (baker) { // Hold on to the old url userinfo/query/fragment data so ModelBaker::getFullOutputMappingURL retains that data from the original model URL + // Note: The ModelBaker currently doesn't store this in the FST because the equal signs mess up FST parsing. + // There is a small chance this could break a server workflow relying on the old behavior. + // Url suffix is still propagated to the baked URL if the input URL is an FST. + // Url suffix has always been stripped from the URL when loading the original model file to be baked. baker->setOutputURLSuffix(url); // make sure our handler is called when the baker is done From 41c05943612660725bc246af63033a557cfc5e2b Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Tue, 12 Mar 2019 13:27:39 -0700 Subject: [PATCH 143/446] Make output folder cleaner for single model bake when baked model url is given as input --- libraries/baking/src/baking/BakerLibrary.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/baking/src/baking/BakerLibrary.cpp b/libraries/baking/src/baking/BakerLibrary.cpp index c95745146b..c516338ad1 100644 --- a/libraries/baking/src/baking/BakerLibrary.cpp +++ b/libraries/baking/src/baking/BakerLibrary.cpp @@ -42,7 +42,7 @@ std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, TextureB auto filename = bakeableModelURL.fileName(); // Output in a sub-folder with the name of the model, potentially suffixed by a number to make it unique - auto baseName = filename.left(filename.lastIndexOf('.')); + auto baseName = filename.left(filename.lastIndexOf('.')).left(filename.lastIndexOf(".baked")); auto subDirName = "/" + baseName; int i = 1; while (QDir(contentOutputPath + subDirName).exists()) { From f9f55f08dbd8009f81f807926a89b0d6f3525e04 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Tue, 12 Mar 2019 13:46:28 -0700 Subject: [PATCH 144/446] Fix model baking GUI being less lenient with local Windows files --- tools/oven/src/ui/ModelBakeWidget.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp index 8f8e068b50..79ab733b0c 100644 --- a/tools/oven/src/ui/ModelBakeWidget.cpp +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -176,9 +176,9 @@ void ModelBakeWidget::bakeButtonClicked() { auto fileURLStrings = _modelLineEdit->text().split(','); foreach (QString fileURLString, fileURLStrings) { // construct a URL from the path in the model file text box - QUrl modelToBakeURL(fileURLString); + QUrl modelToBakeURL = QUrl::fromUserInput(fileURLString); - QUrl bakeableModelURL = getBakeableModelURL(QUrl(modelToBakeURL)); + QUrl bakeableModelURL = getBakeableModelURL(modelToBakeURL); if (!bakeableModelURL.isEmpty()) { auto getWorkerThreadCallback = []() -> QThread* { return Oven::instance().getNextWorkerThread(); From 06d38bf8e73c2f06acb531cfbd4d7295f7377a9b Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Tue, 12 Mar 2019 15:25:12 -0700 Subject: [PATCH 145/446] Fix DomainBaker not outputting URLs relative to the Destination URL Path --- tools/oven/src/DomainBaker.cpp | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 0f07a392b4..109d2f1809 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -425,10 +425,11 @@ void DomainBaker::handleFinishedModelBaker() { qDebug() << "Re-writing entity references to" << baker->getModelURL(); // setup a new URL using the prefix we were passed - auto relativeMappingFilePath = baker->getFullOutputMappingURL().toString().remove(_contentOutputPath); + auto relativeMappingFilePath = QDir(_contentOutputPath).relativeFilePath(baker->getFullOutputMappingURL().toString()); if (relativeMappingFilePath.startsWith("/")) { relativeMappingFilePath = relativeMappingFilePath.right(relativeMappingFilePath.length() - 1); } + QUrl newURL = _destinationPath.resolved(relativeMappingFilePath); // enumerate the QJsonRef values for the URL of this model from our multi hash of @@ -494,7 +495,12 @@ void DomainBaker::handleFinishedTextureBaker() { // this TextureBaker is done and everything went according to plan qDebug() << "Re-writing entity references to" << baker->getTextureURL(); - auto newURL = _destinationPath.resolved(baker->getMetaTextureFileName()); + // setup a new URL using the prefix we were passed + auto relativeTextureFilePath = QDir(_contentOutputPath).relativeFilePath(baker->getMetaTextureFileName()); + if (relativeTextureFilePath.startsWith("/")) { + relativeTextureFilePath = relativeTextureFilePath.right(relativeTextureFilePath.length() - 1); + } + auto newURL = _destinationPath.resolved(relativeTextureFilePath); // enumerate the QJsonRef values for the URL of this texture from our multi hash of // entity objects needing a URL re-write @@ -563,7 +569,12 @@ void DomainBaker::handleFinishedScriptBaker() { // this JSBaker is done and everything went according to plan qDebug() << "Re-writing entity references to" << baker->getJSPath(); - auto newURL = _destinationPath.resolved(baker->getBakedJSFilePath()); + // setup a new URL using the prefix we were passed + auto relativeScriptFilePath = QDir(_contentOutputPath).relativeFilePath(baker->getBakedJSFilePath()); + if (relativeScriptFilePath.startsWith("/")) { + relativeScriptFilePath = relativeScriptFilePath.right(relativeScriptFilePath.length() - 1); + } + auto newURL = _destinationPath.resolved(relativeScriptFilePath); // enumerate the QJsonRef values for the URL of this script from our multi hash of // entity objects needing a URL re-write @@ -634,7 +645,12 @@ void DomainBaker::handleFinishedMaterialBaker() { QString newDataOrURL; if (baker->isURL()) { - newDataOrURL = _destinationPath.resolved(baker->getBakedMaterialData()).toDisplayString(); + // setup a new URL using the prefix we were passed + auto relativeMaterialFilePath = QDir(_contentOutputPath).relativeFilePath(baker->getBakedMaterialData()); + if (relativeMaterialFilePath.startsWith("/")) { + relativeMaterialFilePath = relativeMaterialFilePath.right(relativeMaterialFilePath.length() - 1); + } + newDataOrURL = _destinationPath.resolved(relativeMaterialFilePath).toDisplayString(); } else { newDataOrURL = baker->getBakedMaterialData(); } From 7567e0d355b8ac58ae6b40f1c9c7953465f128d0 Mon Sep 17 00:00:00 2001 From: amantley Date: Tue, 12 Mar 2019 15:48:42 -0700 Subject: [PATCH 146/446] debugging the root of the fbx, it is not 0 in some cases --- libraries/fbx/src/FBXSerializer.cpp | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index e6e3d73815..f91eeb6519 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -508,6 +508,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } } else if (child.name == "Objects") { + //qCDebug(modelformat) << " the root model id is " << getID(child.properties); foreach (const FBXNode& object, child.children) { nodeParentId++; if (object.name == "Geometry") { @@ -1169,8 +1170,19 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr counter++; } } - _connectionParentMap.insert(getID(connection.properties, 1), getID(connection.properties, 2)); - _connectionChildMap.insert(getID(connection.properties, 2), getID(connection.properties, 1)); + + if ("2830302416448" == getID(connection.properties, 1) || "2830302416448" == getID(connection.properties, 2)) { + if ("2829544143536" == getID(connection.properties, 1)) { + _connectionParentMap.insert(getID(connection.properties, 1), "0"); + _connectionChildMap.insert("0", getID(connection.properties, 1)); + } + qCDebug(modelformat) << " parent map inserted with id " << getID(connection.properties, 1) << " name " << modelIDsToNames.value(getID(connection.properties, 1)) << " id " << getID(connection.properties, 2) << " name " << modelIDsToNames.value(getID(connection.properties, 2)); + } else { + _connectionParentMap.insert(getID(connection.properties, 1), getID(connection.properties, 2)); + _connectionChildMap.insert(getID(connection.properties, 2), getID(connection.properties, 1)); + } + + // qCDebug(modelformat) << " child map inserted with id " << getID(connection.properties, 2) << " name " << modelIDsToNames.value(getID(connection.properties, 2)) << " id " << getID(connection.properties, 1) << " name " << modelIDsToNames.value(getID(connection.properties, 1)); } } } @@ -1220,13 +1232,16 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr mapping.value("tz").toFloat())) * glm::mat4_cast(offsetRotation) * glm::scale(glm::vec3(offsetScale, offsetScale, offsetScale)); + for (QHash::const_iterator modelIDPair = modelIDsToNames.constBegin(); modelIDPair != modelIDsToNames.constEnd(); modelIDPair++) { + qCDebug(modelformat) << " model ID " << modelIDPair.key() << " name " << modelIDPair.value(); + } + // get the list of models in depth-first traversal order QVector modelIDs; QSet remainingFBXModels; for (QHash::const_iterator fbxModel = fbxModels.constBegin(); fbxModel != fbxModels.constEnd(); fbxModel++) { // models with clusters must be parented to the cluster top // Unless the model is a root node. - qCDebug(modelformat) << "fbx model name " << fbxModel.key(); bool isARootNode = !modelIDs.contains(_connectionParentMap.value(fbxModel.key())); if (!isARootNode) { foreach(const QString& deformerID, _connectionChildMap.values(fbxModel.key())) { @@ -1235,6 +1250,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr continue; } QString topID = getTopModelID(_connectionParentMap, fbxModels, _connectionChildMap.value(clusterID), url); + qCDebug(modelformat) << "fbx model name " << fbxModel.value().name << " top id " << topID << " modelID " << fbxModel.key(); _connectionChildMap.remove(_connectionParentMap.take(fbxModel.key()), fbxModel.key()); _connectionParentMap.insert(fbxModel.key(), topID); goto outerBreak; @@ -1258,6 +1274,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } QString topID = getTopModelID(_connectionParentMap, fbxModels, first, url); + qCDebug(modelformat) << "topper name fbx name " << modelIDsToNames.value(first) << " top id " << topID << " top name " << modelIDsToNames.value(topID); + qCDebug(modelformat) << "parent id " << _connectionParentMap.value(topID) << " parent name " << modelIDsToNames.value(_connectionParentMap.value(topID)) << " remaining models parent value " << remainingFBXModels.contains(_connectionParentMap.value(topID)); appendModelIDs(_connectionParentMap.value(topID), _connectionChildMap, fbxModels, remainingFBXModels, modelIDs, true); } From 1e354fb280b3e42f7698edddf16ebd003139b5b6 Mon Sep 17 00:00:00 2001 From: danteruiz Date: Tue, 12 Mar 2019 17:18:15 -0700 Subject: [PATCH 147/446] fix notications scaling --- scripts/system/notifications.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index 1d6b4dada3..469f30cd23 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -203,7 +203,7 @@ // Notification plane positions noticeY = -sensorScaleFactor * (y * NOTIFICATION_3D_SCALE + 0.5 * noticeHeight); notificationPosition = { x: 0, y: noticeY, z: 0 }; - buttonPosition = { x: 0.5 * sensorScaleFactor * (noticeWidth - NOTIFICATION_3D_BUTTON_WIDTH), y: noticeY, z: 0.001 }; + buttonPosition = { x: sensorScaleFactor * (noticeWidth - NOTIFICATION_3D_BUTTON_WIDTH), y: noticeY, z: 0.001 }; // Rotate plane notificationOrientation = Quat.fromPitchYawRollDegrees(NOTIFICATIONS_3D_PITCH, @@ -241,7 +241,7 @@ noticeWidth = notice.width * NOTIFICATION_3D_SCALE + NOTIFICATION_3D_BUTTON_WIDTH; noticeHeight = notice.height * NOTIFICATION_3D_SCALE; - notice.size = { x: noticeWidth, y: noticeHeight }; + notice.size = { x: noticeWidth * sensorScaleFactor, y: noticeHeight * sensorScaleFactor }; positions = calculate3DOverlayPositions(noticeWidth, noticeHeight, notice.y); @@ -249,15 +249,15 @@ notice.parentJointIndex = -2; if (!image) { - notice.topMargin = 0.75 * notice.topMargin * NOTIFICATION_3D_SCALE; - notice.leftMargin = 2 * notice.leftMargin * NOTIFICATION_3D_SCALE; + notice.topMargin = 0.75 * notice.topMargin * NOTIFICATION_3D_SCALE * sensorScaleFactor; + notice.leftMargin = 2 * notice.leftMargin * NOTIFICATION_3D_SCALE * sensorScaleFactor; notice.bottomMargin = 0; notice.rightMargin = 0; notice.lineHeight = 10.0 * (fontSize * sensorScaleFactor / 12.0) * NOTIFICATION_3D_SCALE; notice.isFacingAvatar = false; notificationText = Overlays.addOverlay("text3d", notice); - notifications.push(notificationText); + notifications.push(notificationText); } else { notifications.push(Overlays.addOverlay("image3d", notice)); } @@ -267,14 +267,15 @@ button.isFacingAvatar = false; button.parentID = MyAvatar.sessionUUID; button.parentJointIndex = -2; + button.visible = false; buttons.push((Overlays.addOverlay("image3d", button))); overlay3DDetails.push({ notificationOrientation: positions.notificationOrientation, notificationPosition: positions.notificationPosition, buttonPosition: positions.buttonPosition, - width: noticeWidth, - height: noticeHeight + width: noticeWidth * sensorScaleFactor, + height: noticeHeight * sensorScaleFactor }); From fff0d1a80e6801ad44e90974474289fb71b311cf Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Tue, 12 Mar 2019 17:43:23 -0700 Subject: [PATCH 148/446] Rig.cpp: Fix for index out of range assert in debug builds --- libraries/animation/src/Rig.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 07bdfde189..fb55bd2c98 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1210,7 +1210,8 @@ void Rig::updateAnimations(float deltaTime, const glm::mat4& rootTransform, cons _networkAnimState.blendTime += deltaTime; alpha = _computeNetworkAnimation ? (_networkAnimState.blendTime / TOTAL_BLEND_TIME) : (1.0f - (_networkAnimState.blendTime / TOTAL_BLEND_TIME)); alpha = glm::clamp(alpha, 0.0f, 1.0f); - for (size_t i = 0; i < _networkPoseSet._relativePoses.size(); i++) { + size_t numJoints = std::min(_networkPoseSet._relativePoses.size(), _internalPoseSet._relativePoses.size()); + for (size_t i = 0; i < numJoints; i++) { _networkPoseSet._relativePoses[i].blend(_internalPoseSet._relativePoses[i], alpha); } } From ddd411e1137935ca03ea4da0f97cb21a7d86a844 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Tue, 12 Mar 2019 17:46:52 -0700 Subject: [PATCH 149/446] Removed test code. --- interface/src/Application.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 7dddaecadb..de4a6bb167 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1804,6 +1804,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo }); connect(offscreenUi.data(), &OffscreenUi::keyboardFocusActive, [this]() { +#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML) // Do not show login dialog if requested not to on the command line QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY); int index = arguments().indexOf(hifiNoLoginCommandLineKey); @@ -1813,6 +1814,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo } showLoginScreen(); +#else + resumeAfterLoginDialogActionTaken(); +#endif }); // Make sure we don't time out during slow operations at startup From 6b82b8cd7fcc0ccdf5728ad7e7b103da0273a1e7 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Tue, 12 Mar 2019 17:43:53 -0700 Subject: [PATCH 150/446] fix for steam complete profile bug --- interface/resources/qml/LoginDialog/CompleteProfileBody.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml index ebc677fb00..65f8a8c1dc 100644 --- a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml +++ b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml @@ -510,7 +510,7 @@ Item { console.log("Create Failed: " + error); if (completeProfileBody.withSteam || completeProfileBody.withOculus) { if (completeProfileBody.loginDialogPoppedUp) { - action = completeProfileBody.withSteam ? "Steam" : "Oculus"; + var action = completeProfileBody.withSteam ? "Steam" : "Oculus"; var data = { "action": "user failed to create a profile with " + action + " from the complete profile screen" } From a93825c2f96133d1ef09bbe2db8e6dab8ddf0851 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Tue, 12 Mar 2019 18:11:33 -0700 Subject: [PATCH 151/446] Fix remaining issues with merge --- libraries/baking/src/ModelBaker.cpp | 2 +- libraries/model-baker/src/model-baker/Baker.cpp | 9 ++++++--- libraries/model-baker/src/model-baker/Baker.h | 2 +- libraries/model-baker/src/model-baker/BakerTypes.h | 1 - .../src/model-baker/ParseMaterialMappingTask.cpp | 4 ++-- .../src/model-baker/ParseMaterialMappingTask.h | 4 +++- .../model-networking/src/model-networking/ModelCache.cpp | 2 +- 7 files changed, 14 insertions(+), 10 deletions(-) diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index ac69653e45..d38b965f6d 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -249,7 +249,7 @@ void ModelBaker::bakeSourceCopy() { serializerMapping["combineParts"] = true; // set true so that OBJSerializer reads material info from material library hfm::Model::Pointer loadedModel = serializer->read(modelData, serializerMapping, _modelURL); - baker::Baker baker(loadedModel, serializerMapping); + baker::Baker baker(loadedModel, serializerMapping, hifi::URL()); auto config = baker.getConfiguration(); // Enable compressed draco mesh generation config->getJobConfig("BuildDracoMesh")->setEnabled(true); diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp index 113c1a0fe5..7bb53376ed 100644 --- a/libraries/model-baker/src/model-baker/Baker.cpp +++ b/libraries/model-baker/src/model-baker/Baker.cpp @@ -119,12 +119,13 @@ namespace baker { class BakerEngineBuilder { public: - using Input = VaryingSet2; + using Input = VaryingSet3; using Output = VaryingSet4, std::vector>>; using JobModel = Task::ModelIO; void build(JobModel& model, const Varying& input, Varying& output) { const auto& hfmModelIn = input.getN(0); const auto& mapping = input.getN(1); + const auto& materialMappingBaseURL = input.getN(2); // Split up the inputs from hfm::Model const auto modelPartsIn = model.addJob("GetModelParts", hfmModelIn); @@ -157,7 +158,8 @@ namespace baker { const auto jointIndices = jointInfoOut.getN(2); // Parse material mapping - const auto materialMapping = model.addJob("ParseMaterialMapping", mapping); + const auto parseMaterialMappingInputs = ParseMaterialMappingTask::Input(mapping, materialMappingBaseURL).asVarying(); + const auto materialMapping = model.addJob("ParseMaterialMapping", parseMaterialMappingInputs); // Build Draco meshes // NOTE: This task is disabled by default and must be enabled through configuration @@ -182,10 +184,11 @@ namespace baker { } }; - Baker::Baker(const hfm::Model::Pointer& hfmModel, const hifi::VariantHash& mapping) : + Baker::Baker(const hfm::Model::Pointer& hfmModel, const hifi::VariantHash& mapping, const hifi::URL& materialMappingBaseURL) : _engine(std::make_shared(BakerEngineBuilder::JobModel::create("Baker"), std::make_shared())) { _engine->feedInput(0, hfmModel); _engine->feedInput(1, mapping); + _engine->feedInput(2, materialMappingBaseURL); } std::shared_ptr Baker::getConfiguration() { diff --git a/libraries/model-baker/src/model-baker/Baker.h b/libraries/model-baker/src/model-baker/Baker.h index 056431dc8e..6f74cb646e 100644 --- a/libraries/model-baker/src/model-baker/Baker.h +++ b/libraries/model-baker/src/model-baker/Baker.h @@ -23,7 +23,7 @@ namespace baker { class Baker { public: - Baker(const hfm::Model::Pointer& hfmModel, const hifi::VariantHash& mapping); + Baker(const hfm::Model::Pointer& hfmModel, const hifi::VariantHash& mapping, const hifi::URL& materialMappingBaseURL); std::shared_ptr getConfiguration(); diff --git a/libraries/model-baker/src/model-baker/BakerTypes.h b/libraries/model-baker/src/model-baker/BakerTypes.h index 8b80b0bde4..3d16afab2e 100644 --- a/libraries/model-baker/src/model-baker/BakerTypes.h +++ b/libraries/model-baker/src/model-baker/BakerTypes.h @@ -36,7 +36,6 @@ namespace baker { using TangentsPerBlendshape = std::vector>; using MeshIndicesToModelNames = QHash; - using GeometryMappingPair = std::pair; }; #endif // hifi_BakerTypes_h diff --git a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp index 0a1964d8cd..acb2bdc1c5 100644 --- a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp +++ b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp @@ -11,8 +11,8 @@ #include "ModelBakerLogging.h" void ParseMaterialMappingTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) { - const auto& url = input.first; - const auto& mapping = input.second; + const auto& mapping = input.get0(); + const auto& url = input.get1(); MaterialMapping materialMapping; auto mappingIter = mapping.find("materialMap"); diff --git a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h index 5f5eff327d..7c94661b28 100644 --- a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h +++ b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h @@ -13,6 +13,8 @@ #include +#include + #include "Engine.h" #include "BakerTypes.h" @@ -20,7 +22,7 @@ class ParseMaterialMappingTask { public: - using Input = baker::GeometryMappingPair; + using Input = baker::VaryingSet2; using Output = MaterialMapping; using JobModel = baker::Job::ModelIO; diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 364bf010a6..12553b42eb 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -340,7 +340,7 @@ void GeometryDefinitionResource::downloadFinished(const QByteArray& data) { void GeometryDefinitionResource::setGeometryDefinition(HFMModel::Pointer hfmModel, const GeometryMappingPair& mapping) { // Do processing on the model - baker::Baker modelBaker(hfmModel, mapping.second); + baker::Baker modelBaker(hfmModel, mapping.second, mapping.first); modelBaker.run(); // Assume ownership of the processed HFMModel From 7f77e163acd3570d4edc228d4cd613374128e785 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Tue, 12 Mar 2019 18:17:11 -0700 Subject: [PATCH 152/446] Restore 'Re-bake originals' checkbox to domain bake window --- libraries/baking/src/baking/BakerLibrary.cpp | 8 ++++++++ libraries/baking/src/baking/BakerLibrary.h | 2 ++ tools/oven/src/DomainBaker.cpp | 8 +++++--- tools/oven/src/DomainBaker.h | 5 ++++- tools/oven/src/ui/DomainBakeWidget.cpp | 7 ++++++- tools/oven/src/ui/DomainBakeWidget.h | 1 + 6 files changed, 26 insertions(+), 5 deletions(-) diff --git a/libraries/baking/src/baking/BakerLibrary.cpp b/libraries/baking/src/baking/BakerLibrary.cpp index c516338ad1..2afeef4800 100644 --- a/libraries/baking/src/baking/BakerLibrary.cpp +++ b/libraries/baking/src/baking/BakerLibrary.cpp @@ -38,6 +38,13 @@ QUrl getBakeableModelURL(const QUrl& url) { return QUrl(); } +bool isModelBaked(const QUrl& bakeableModelURL) { + auto modelString = bakeableModelURL.toString(); + auto beforeModelExtension = modelString; + beforeModelExtension.resize(modelString.lastIndexOf('.')); + return beforeModelExtension.endsWith(".baked"); +} + std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& contentOutputPath) { auto filename = bakeableModelURL.fileName(); @@ -59,6 +66,7 @@ std::unique_ptr getModelBakerWithOutputDirectories(const QUrl& bakea auto filename = bakeableModelURL.fileName(); std::unique_ptr baker; + if (filename.endsWith(FST_EXTENSION, Qt::CaseInsensitive)) { baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FST_EXTENSION, Qt::CaseInsensitive)); } else if (filename.endsWith(FBX_EXTENSION, Qt::CaseInsensitive)) { diff --git a/libraries/baking/src/baking/BakerLibrary.h b/libraries/baking/src/baking/BakerLibrary.h index 57197b53fd..a646c8d36a 100644 --- a/libraries/baking/src/baking/BakerLibrary.h +++ b/libraries/baking/src/baking/BakerLibrary.h @@ -19,6 +19,8 @@ // Returns either the given model URL if valid, or an empty URL QUrl getBakeableModelURL(const QUrl& url); +bool isModelBaked(const QUrl& bakeableModelURL); + // Assuming the URL is valid, gets the appropriate baker for the given URL, and creates the base directory where the baker's output will later be stored // Returns an empty pointer if a baker could not be created std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& contentOutputPath); diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 109d2f1809..5f8ec3a678 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -23,10 +23,12 @@ #include "baking/BakerLibrary.h" DomainBaker::DomainBaker(const QUrl& localModelFileURL, const QString& domainName, - const QString& baseOutputPath, const QUrl& destinationPath) : + const QString& baseOutputPath, const QUrl& destinationPath, + bool shouldRebakeOriginals) : _localEntitiesFileURL(localModelFileURL), _domainName(domainName), - _baseOutputPath(baseOutputPath) + _baseOutputPath(baseOutputPath), + _shouldRebakeOriginals(shouldRebakeOriginals) { // make sure the destination path has a trailing slash if (!destinationPath.toString().endsWith('/')) { @@ -146,7 +148,7 @@ void DomainBaker::loadLocalFile() { void DomainBaker::addModelBaker(const QString& property, const QString& url, QJsonValueRef& jsonRef) { // grab a QUrl for the model URL QUrl bakeableModelURL = getBakeableModelURL(url); - if (!bakeableModelURL.isEmpty()) { + if (!bakeableModelURL.isEmpty() && (_shouldRebakeOriginals || !isModelBaked(bakeableModelURL))) { // setup a ModelBaker for this URL, as long as we don't already have one if (!_modelBakers.contains(bakeableModelURL)) { auto getWorkerThreadCallback = []() -> QThread* { diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h index dbbf182fa7..c9f5a59672 100644 --- a/tools/oven/src/DomainBaker.h +++ b/tools/oven/src/DomainBaker.h @@ -29,7 +29,8 @@ public: // This means that we need to put all of the FBX importing/exporting from the same process on the same thread. // That means you must pass a usable running QThread when constructing a domain baker. DomainBaker(const QUrl& localEntitiesFileURL, const QString& domainName, - const QString& baseOutputPath, const QUrl& destinationPath); + const QString& baseOutputPath, const QUrl& destinationPath, + bool shouldRebakeOriginals); signals: void allModelsFinished(); @@ -70,6 +71,8 @@ private: int _totalNumberOfSubBakes { 0 }; int _completedSubBakes { 0 }; + bool _shouldRebakeOriginals { false }; + void addModelBaker(const QString& property, const QString& url, QJsonValueRef& jsonRef); void addTextureBaker(const QString& property, const QString& url, image::TextureUsage::Type type, QJsonValueRef& jsonRef); void addScriptBaker(const QString& property, const QString& url, QJsonValueRef& jsonRef); diff --git a/tools/oven/src/ui/DomainBakeWidget.cpp b/tools/oven/src/ui/DomainBakeWidget.cpp index 23074e775e..1121041e39 100644 --- a/tools/oven/src/ui/DomainBakeWidget.cpp +++ b/tools/oven/src/ui/DomainBakeWidget.cpp @@ -126,6 +126,10 @@ void DomainBakeWidget::setupUI() { // start a new row for the next component ++rowIndex; + // setup a checkbox to allow re-baking of original assets + _rebakeOriginalsCheckBox = new QCheckBox("Re-bake originals"); + gridLayout->addWidget(_rebakeOriginalsCheckBox, rowIndex, 0); + // add a button that will kickoff the bake QPushButton* bakeButton = new QPushButton("Bake"); connect(bakeButton, &QPushButton::clicked, this, &DomainBakeWidget::bakeButtonClicked); @@ -207,7 +211,8 @@ void DomainBakeWidget::bakeButtonClicked() { auto fileToBakeURL = QUrl::fromLocalFile(_entitiesFileLineEdit->text()); auto domainBaker = std::unique_ptr { new DomainBaker(fileToBakeURL, _domainNameLineEdit->text(), - outputDirectory.absolutePath(), _destinationPathLineEdit->text()) + outputDirectory.absolutePath(), _destinationPathLineEdit->text(), + _rebakeOriginalsCheckBox->isChecked()) }; // make sure we hear from the baker when it is done diff --git a/tools/oven/src/ui/DomainBakeWidget.h b/tools/oven/src/ui/DomainBakeWidget.h index 0a1d613912..a6f26b3731 100644 --- a/tools/oven/src/ui/DomainBakeWidget.h +++ b/tools/oven/src/ui/DomainBakeWidget.h @@ -45,6 +45,7 @@ private: QLineEdit* _entitiesFileLineEdit; QLineEdit* _outputDirLineEdit; QLineEdit* _destinationPathLineEdit; + QCheckBox* _rebakeOriginalsCheckBox; Setting::Handle _domainNameSetting; Setting::Handle _exportDirectory; From b35b7687b727d1c69cc9cc943b666cc66d9276df Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Tue, 12 Mar 2019 18:32:17 -0700 Subject: [PATCH 153/446] Ready. --- tools/nitpick/src/AWSInterface.cpp | 33 ++++++++++++++++++++++++------ tools/nitpick/src/AWSInterface.h | 11 ++++++++-- tools/nitpick/src/Nitpick.cpp | 4 ++-- tools/nitpick/src/TestCreator.cpp | 9 ++++++-- tools/nitpick/src/TestCreator.h | 5 ++++- tools/nitpick/src/common.h | 1 + 6 files changed, 50 insertions(+), 13 deletions(-) diff --git a/tools/nitpick/src/AWSInterface.cpp b/tools/nitpick/src/AWSInterface.cpp index a098d17917..16c0a220d8 100644 --- a/tools/nitpick/src/AWSInterface.cpp +++ b/tools/nitpick/src/AWSInterface.cpp @@ -8,6 +8,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "AWSInterface.h" +#include "common.h" #include #include @@ -29,7 +30,9 @@ void AWSInterface::createWebPageFromResults(const QString& testResults, QCheckBox* updateAWSCheckBox, QRadioButton* diffImageRadioButton, QRadioButton* ssimImageRadionButton, - QLineEdit* urlLineEdit + QLineEdit* urlLineEdit, + const QString& branch, + const QString& user ) { _workingDirectory = workingDirectory; @@ -53,6 +56,13 @@ void AWSInterface::createWebPageFromResults(const QString& testResults, _urlLineEdit = urlLineEdit; _urlLineEdit->setEnabled(false); + + _urlLineEdit = urlLineEdit; + _urlLineEdit->setEnabled(false); + + _branch = branch; + _user = user; + QString zipFilenameWithoutExtension = zipFilename.split('.')[0]; extractTestFailuresFromZippedFolder(_workingDirectory + "/" + zipFilenameWithoutExtension); @@ -202,13 +212,21 @@ void AWSInterface::writeTitle(QTextStream& stream, const QStringList& originalNa stream << "run on " << hostName << "\n"; - int numberOfFailures = originalNamesFailures.length(); - int numberOfSuccesses = originalNamesSuccesses.length(); + stream << "

"; + stream << "nitpick " << nitpickVersion; + stream << ", tests from GitHub: " << _user << "/" << _branch; + stream << "

"; - stream << "

" << QString::number(numberOfFailures) << " failed, out of a total of " << QString::number(numberOfSuccesses) << " tests

\n"; + _numberOfFailures = originalNamesFailures.length(); + _numberOfSuccesses = originalNamesSuccesses.length(); + + stream << "

" << QString::number(_numberOfFailures) << " failed, out of a total of " << QString::number(_numberOfFailures + _numberOfSuccesses) << " tests

\n"; stream << "\t" << "\t" << "\n"; - stream << "\t" << "\t" << "

The following tests failed:

"; + + if (_numberOfFailures > 0) { + stream << "\t" << "\t" << "

The following tests failed:

"; + } } void AWSInterface::writeTable(QTextStream& stream, const QStringList& originalNamesFailures, const QStringList& originalNamesSuccesses) { @@ -289,7 +307,10 @@ void AWSInterface::writeTable(QTextStream& stream, const QStringList& originalNa closeTable(stream); stream << "\t" << "\t" << "\n"; - stream << "\t" << "\t" << "

The following tests passed:

"; + + if (_numberOfSuccesses > 0) { + stream << "\t" << "\t" << "

The following tests passed:

"; + } // Now do the same for passes folderNames.clear(); diff --git a/tools/nitpick/src/AWSInterface.h b/tools/nitpick/src/AWSInterface.h index 77d500fa7c..a2e4e36c37 100644 --- a/tools/nitpick/src/AWSInterface.h +++ b/tools/nitpick/src/AWSInterface.h @@ -31,7 +31,10 @@ public: QCheckBox* updateAWSCheckBox, QRadioButton* diffImageRadioButton, QRadioButton* ssimImageRadionButton, - QLineEdit* urlLineEdit); + QLineEdit* urlLineEdit, + const QString& branch, + const QString& user + ); void extractTestFailuresFromZippedFolder(const QString& folderName); void createHTMLFile(); @@ -70,9 +73,13 @@ private: QString AWS_BUCKET{ "hifi-qa" }; QLineEdit* _urlLineEdit; - + QString _user; + QString _branch; QString _comparisonImageFilename; + + int _numberOfFailures; + int _numberOfSuccesses; }; #endif // hifi_AWSInterface_h \ No newline at end of file diff --git a/tools/nitpick/src/Nitpick.cpp b/tools/nitpick/src/Nitpick.cpp index cf50774617..02ed120350 100644 --- a/tools/nitpick/src/Nitpick.cpp +++ b/tools/nitpick/src/Nitpick.cpp @@ -38,7 +38,7 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { _ui.plainTextEdit->setReadOnly(true); - setWindowTitle("Nitpick - v3.1.2"); + setWindowTitle("Nitpick - " + nitpickVersion); clientProfiles << "VR-High" << "Desktop-High" << "Desktop-Low" << "Mobile-Touch" << "VR-Standalone"; _ui.clientProfileComboBox->insertItems(0, clientProfiles); @@ -266,7 +266,7 @@ void Nitpick::on_createXMLScriptRadioButton_clicked() { } void Nitpick::on_createWebPagePushbutton_clicked() { - _testCreator->createWebPage(_ui.updateAWSCheckBox, _ui.diffImageRadioButton, _ui.ssimImageRadioButton, _ui.awsURLLineEdit); + _testCreator->createWebPage(_ui.updateAWSCheckBox, _ui.diffImageRadioButton, _ui.ssimImageRadioButton, _ui.awsURLLineEdit, _ui.branchLineEdit->text(), _ui.userLineEdit->text()); } void Nitpick::about() { diff --git a/tools/nitpick/src/TestCreator.cpp b/tools/nitpick/src/TestCreator.cpp index 089e84904a..f45a23e459 100644 --- a/tools/nitpick/src/TestCreator.cpp +++ b/tools/nitpick/src/TestCreator.cpp @@ -1112,7 +1112,10 @@ void TestCreator::createWebPage( QCheckBox* updateAWSCheckBox, QRadioButton* diffImageRadioButton, QRadioButton* ssimImageRadionButton, - QLineEdit* urlLineEdit + QLineEdit* urlLineEdit, + const QString& branch, + const QString& user + ) { QString testResults = QFileDialog::getOpenFileName(nullptr, "Please select the zipped test results to update from", nullptr, "Zipped TestCreator Results (TestResults--*.zip)"); @@ -1136,6 +1139,8 @@ void TestCreator::createWebPage( updateAWSCheckBox, diffImageRadioButton, ssimImageRadionButton, - urlLineEdit + urlLineEdit, + branch, + user ); } \ No newline at end of file diff --git a/tools/nitpick/src/TestCreator.h b/tools/nitpick/src/TestCreator.h index 7cd38b42d4..50aa06e944 100644 --- a/tools/nitpick/src/TestCreator.h +++ b/tools/nitpick/src/TestCreator.h @@ -107,7 +107,10 @@ public: QCheckBox* updateAWSCheckBox, QRadioButton* diffImageRadioButton, QRadioButton* ssimImageRadionButton, - QLineEdit* urlLineEdit); + QLineEdit* urlLineEdit, + const QString& branch, + const QString& user + ); private: QProgressBar* _progressBar; diff --git a/tools/nitpick/src/common.h b/tools/nitpick/src/common.h index eb228ff2b3..b0a58747c1 100644 --- a/tools/nitpick/src/common.h +++ b/tools/nitpick/src/common.h @@ -56,4 +56,5 @@ const double R_Y = 0.212655f; const double G_Y = 0.715158f; const double B_Y = 0.072187f; +const QString nitpickVersion { "v3.1.4" }; #endif // hifi_common_h \ No newline at end of file From b855b0e2a30442a4cfab5a179949dafde32faf49 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Tue, 12 Mar 2019 18:33:41 -0700 Subject: [PATCH 154/446] Document available bake types for baker command line --- tools/oven/src/OvenCLIApplication.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/oven/src/OvenCLIApplication.cpp b/tools/oven/src/OvenCLIApplication.cpp index c405c5f4a0..b4a011291d 100644 --- a/tools/oven/src/OvenCLIApplication.cpp +++ b/tools/oven/src/OvenCLIApplication.cpp @@ -33,7 +33,7 @@ OvenCLIApplication::OvenCLIApplication(int argc, char* argv[]) : parser.addOptions({ { CLI_INPUT_PARAMETER, "Path to file that you would like to bake.", "input" }, { CLI_OUTPUT_PARAMETER, "Path to folder that will be used as output.", "output" }, - { CLI_TYPE_PARAMETER, "Type of asset.", "type" }, + { CLI_TYPE_PARAMETER, "Type of asset. [model|material|js]", "type" }, { CLI_DISABLE_TEXTURE_COMPRESSION_PARAMETER, "Disable texture compression." } }); From 609c4ab52ea5b138f0e41e99212cc63c6e9fcec2 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 12 Mar 2019 18:41:43 -0700 Subject: [PATCH 155/446] try to fix audio injector threading issues --- interface/resources/qml/Stats.qml | 4 + interface/src/Application.cpp | 9 +- interface/src/avatar/AvatarManager.cpp | 5 +- interface/src/avatar/AvatarManager.h | 4 +- .../src/scripting/TTSScriptingInterface.cpp | 10 +- interface/src/ui/Keyboard.cpp | 4 +- interface/src/ui/Keyboard.h | 2 +- interface/src/ui/Stats.cpp | 5 + interface/src/ui/Stats.h | 9 + libraries/audio-client/src/AudioClient.cpp | 43 +-- libraries/audio-client/src/AudioClient.h | 2 + libraries/audio/src/AudioInjector.cpp | 267 +++++------------- libraries/audio/src/AudioInjector.h | 47 ++- .../audio/src/AudioInjectorLocalBuffer.cpp | 7 +- .../audio/src/AudioInjectorLocalBuffer.h | 1 + libraries/audio/src/AudioInjectorManager.cpp | 229 +++++++++++++-- libraries/audio/src/AudioInjectorManager.h | 22 +- .../src/EntityTreeRenderer.cpp | 2 +- .../src/EntityTreeRenderer.h | 2 +- .../src/AudioScriptingInterface.cpp | 2 +- .../script-engine/src/ScriptAudioInjector.cpp | 19 +- .../script-engine/src/ScriptAudioInjector.h | 21 +- .../ui/src/ui/TabletScriptingInterface.cpp | 4 +- libraries/ui/src/ui/types/SoundEffect.cpp | 11 +- libraries/ui/src/ui/types/SoundEffect.h | 6 +- 25 files changed, 403 insertions(+), 334 deletions(-) diff --git a/interface/resources/qml/Stats.qml b/interface/resources/qml/Stats.qml index 6748418d19..e10f86a947 100644 --- a/interface/resources/qml/Stats.qml +++ b/interface/resources/qml/Stats.qml @@ -232,6 +232,10 @@ Item { text: "Audio Codec: " + root.audioCodec + " Noise Gate: " + root.audioNoiseGate; } + StatText { + visible: root.expanded; + text: "Injectors (Local/NonLocal): " + root.audioInjectors.x + "/" + root.audioInjectors.y; + } StatText { visible: root.expanded; text: "Entity Servers In: " + root.entityPacketsInKbps + " kbps"; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 6d9a1823a1..fd8f8dd4b0 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2695,9 +2695,7 @@ void Application::cleanupBeforeQuit() { DependencyManager::destroy(); - if (_snapshotSoundInjector != nullptr) { - _snapshotSoundInjector->stop(); - } + _snapshotSoundInjector = nullptr; // destroy Audio so it and its threads have a chance to go down safely // this must happen after QML, as there are unexplained audio crashes originating in qtwebengine @@ -4216,10 +4214,9 @@ void Application::keyPressEvent(QKeyEvent* event) { Setting::Handle notificationSoundSnapshot{ MenuOption::NotificationSoundsSnapshot, true }; if (notificationSounds.get() && notificationSoundSnapshot.get()) { if (_snapshotSoundInjector) { - _snapshotSoundInjector->setOptions(options); - _snapshotSoundInjector->restart(); + DependencyManager::get()->setOptionsAndRestart(_snapshotSoundInjector, options); } else { - _snapshotSoundInjector = AudioInjector::playSound(_snapshotSound, options); + _snapshotSoundInjector = DependencyManager::get()->playSound(_snapshotSound, options); } } takeSnapshot(true); diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index c66c0a30cb..69f7054953 100755 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -629,8 +629,7 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents // but most avatars are roughly the same size, so let's not be so fancy yet. const float AVATAR_STRETCH_FACTOR = 1.0f; - _collisionInjectors.remove_if( - [](const AudioInjectorPointer& injector) { return !injector || injector->isFinished(); }); + _collisionInjectors.remove_if([](const AudioInjectorPointer& injector) { return !injector; }); static const int MAX_INJECTOR_COUNT = 3; if (_collisionInjectors.size() < MAX_INJECTOR_COUNT) { @@ -640,7 +639,7 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents options.volume = energyFactorOfFull; options.pitch = 1.0f / AVATAR_STRETCH_FACTOR; - auto injector = AudioInjector::playSoundAndDelete(collisionSound, options); + auto injector = DependencyManager::get()->playSound(collisionSound, options, true); _collisionInjectors.emplace_back(injector); } } diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 2b58b14d11..0468fbd809 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -24,7 +24,7 @@ #include #include #include -#include +#include #include #include // for SetOfEntities @@ -239,7 +239,7 @@ private: std::shared_ptr _myAvatar; quint64 _lastSendAvatarDataTime = 0; // Controls MyAvatar send data rate. - std::list _collisionInjectors; + std::list> _collisionInjectors; RateCounter<> _myAvatarSendRate; int _numAvatarsUpdated { 0 }; diff --git a/interface/src/scripting/TTSScriptingInterface.cpp b/interface/src/scripting/TTSScriptingInterface.cpp index 6589769ece..325e1ff649 100644 --- a/interface/src/scripting/TTSScriptingInterface.cpp +++ b/interface/src/scripting/TTSScriptingInterface.cpp @@ -66,7 +66,7 @@ void TTSScriptingInterface::updateLastSoundAudioInjector() { if (_lastSoundAudioInjector) { AudioInjectorOptions options; options.position = DependencyManager::get()->getMyAvatarPosition(); - _lastSoundAudioInjector->setOptions(options); + DependencyManager::get()->setOptions(_lastSoundAudioInjector, options); _lastSoundAudioInjectorUpdateTimer.start(INJECTOR_INTERVAL_MS); } } @@ -143,7 +143,7 @@ void TTSScriptingInterface::speakText(const QString& textToSpeak) { options.position = DependencyManager::get()->getMyAvatarPosition(); if (_lastSoundAudioInjector) { - _lastSoundAudioInjector->stop(); + DependencyManager::get()->stop(_lastSoundAudioInjector); _lastSoundAudioInjectorUpdateTimer.stop(); } @@ -151,7 +151,7 @@ void TTSScriptingInterface::speakText(const QString& textToSpeak) { uint32_t numSamples = (uint32_t)_lastSoundByteArray.size() / sizeof(AudioData::AudioSample); auto samples = reinterpret_cast(_lastSoundByteArray.data()); auto newAudioData = AudioData::make(numSamples, numChannels, samples); - _lastSoundAudioInjector = AudioInjector::playSoundAndDelete(newAudioData, options); + _lastSoundAudioInjector = DependencyManager::get()->playSound(newAudioData, options, true); _lastSoundAudioInjectorUpdateTimer.start(INJECTOR_INTERVAL_MS); #else @@ -161,7 +161,7 @@ void TTSScriptingInterface::speakText(const QString& textToSpeak) { void TTSScriptingInterface::stopLastSpeech() { if (_lastSoundAudioInjector) { - _lastSoundAudioInjector->stop(); - _lastSoundAudioInjector = NULL; + DependencyManager::get()->stop(_lastSoundAudioInjector); + _lastSoundAudioInjector = nullptr; } } diff --git a/interface/src/ui/Keyboard.cpp b/interface/src/ui/Keyboard.cpp index 9b75f78e67..1cbe31f1eb 100644 --- a/interface/src/ui/Keyboard.cpp +++ b/interface/src/ui/Keyboard.cpp @@ -29,7 +29,7 @@ #include #include #include -#include +#include #include #include @@ -537,7 +537,7 @@ void Keyboard::handleTriggerBegin(const QUuid& id, const PointerEvent& event) { audioOptions.position = keyWorldPosition; audioOptions.volume = 0.05f; - AudioInjector::playSoundAndDelete(_keySound, audioOptions); + DependencyManager::get()->playSound(_keySound, audioOptions, true); int scanCode = key.getScanCode(_capsEnabled); QString keyString = key.getKeyString(_capsEnabled); diff --git a/interface/src/ui/Keyboard.h b/interface/src/ui/Keyboard.h index b3358e486d..51e5e0571f 100644 --- a/interface/src/ui/Keyboard.h +++ b/interface/src/ui/Keyboard.h @@ -19,9 +19,9 @@ #include #include #include +#include #include #include -#include #include #include diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index ecdae0b375..3c943028f5 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -266,6 +266,11 @@ void Stats::updateStats(bool force) { } STAT_UPDATE(audioCodec, audioClient->getSelectedAudioFormat()); STAT_UPDATE(audioNoiseGate, audioClient->getNoiseGateOpen() ? "Open" : "Closed"); + { + int localInjectors = audioClient->getNumLocalInjectors(); + int nonLocalInjectors = DependencyManager::get()->getNumInjectors(); + STAT_UPDATE(audioInjectors, QVector2D(localInjectors, nonLocalInjectors)); + } STAT_UPDATE(entityPacketsInKbps, octreeServerCount ? totalEntityKbps / octreeServerCount : -1); diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index 0f563a6935..3134b223d6 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -87,6 +87,7 @@ private: \ * @property {number} audioPacketLoss - Read-only. * @property {string} audioCodec - Read-only. * @property {string} audioNoiseGate - Read-only. + * @property {Vec2} audioInjectors - Read-only. * @property {number} entityPacketsInKbps - Read-only. * * @property {number} downloads - Read-only. @@ -243,6 +244,7 @@ class Stats : public QQuickItem { STATS_PROPERTY(int, audioPacketLoss, 0) STATS_PROPERTY(QString, audioCodec, QString()) STATS_PROPERTY(QString, audioNoiseGate, QString()) + STATS_PROPERTY(QVector2D, audioInjectors, QVector2D()); STATS_PROPERTY(int, entityPacketsInKbps, 0) STATS_PROPERTY(int, downloads, 0) @@ -692,6 +694,13 @@ signals: */ void audioNoiseGateChanged(); + /**jsdoc + * Triggered when the value of the audioInjectors property changes. + * @function Stats.audioInjectorsChanged + * @returns {Signal} + */ + void audioInjectorsChanged(); + /**jsdoc * Triggered when the value of the entityPacketsInKbps property changes. * @function Stats.entityPacketsInKbpsChanged diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index b2e6167ffa..afe57647f3 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1354,26 +1354,28 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { for (const AudioInjectorPointer& injector : _activeLocalAudioInjectors) { // the lock guarantees that injectorBuffer, if found, is invariant - AudioInjectorLocalBuffer* injectorBuffer = injector->getLocalBuffer(); + auto injectorBuffer = injector->getLocalBuffer(); if (injectorBuffer) { + auto options = injector->getOptions(); + static const int HRTF_DATASET_INDEX = 1; - int numChannels = injector->isAmbisonic() ? AudioConstants::AMBISONIC : (injector->isStereo() ? AudioConstants::STEREO : AudioConstants::MONO); + int numChannels = options.ambisonic ? AudioConstants::AMBISONIC : (options.stereo ? AudioConstants::STEREO : AudioConstants::MONO); size_t bytesToRead = numChannels * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; // get one frame from the injector memset(_localScratchBuffer, 0, bytesToRead); if (0 < injectorBuffer->readData((char*)_localScratchBuffer, bytesToRead)) { - float gain = injector->getVolume(); + float gain = options.volume; - if (injector->isAmbisonic()) { + if (options.ambisonic) { - if (injector->isPositionSet()) { + if (options.positionSet) { // distance attenuation - glm::vec3 relativePosition = injector->getPosition() - _positionGetter(); + glm::vec3 relativePosition = options.position - _positionGetter(); float distance = glm::max(glm::length(relativePosition), EPSILON); gain = gainForSource(distance, gain); } @@ -1382,7 +1384,7 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { // Calculate the soundfield orientation relative to the listener. // Injector orientation can be used to align a recording to our world coordinates. // - glm::quat relativeOrientation = injector->getOrientation() * glm::inverse(_orientationGetter()); + glm::quat relativeOrientation = options.orientation * glm::inverse(_orientationGetter()); // convert from Y-up (OpenGL) to Z-up (Ambisonic) coordinate system float qw = relativeOrientation.w; @@ -1394,12 +1396,12 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { injector->getLocalFOA().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX, qw, qx, qy, qz, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); - } else if (injector->isStereo()) { + } else if (options.stereo) { - if (injector->isPositionSet()) { + if (options.positionSet) { // distance attenuation - glm::vec3 relativePosition = injector->getPosition() - _positionGetter(); + glm::vec3 relativePosition = options.position - _positionGetter(); float distance = glm::max(glm::length(relativePosition), EPSILON); gain = gainForSource(distance, gain); } @@ -1412,10 +1414,10 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { } else { // injector is mono - if (injector->isPositionSet()) { + if (options.positionSet) { // distance attenuation - glm::vec3 relativePosition = injector->getPosition() - _positionGetter(); + glm::vec3 relativePosition = options.position - _positionGetter(); float distance = glm::max(glm::length(relativePosition), EPSILON); gain = gainForSource(distance, gain); @@ -1437,21 +1439,21 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { } else { - qCDebug(audioclient) << "injector has no more data, marking finished for removal"; + //qCDebug(audioclient) << "injector has no more data, marking finished for removal"; injector->finishLocalInjection(); injectorsToRemove.append(injector); } } else { - qCDebug(audioclient) << "injector has no local buffer, marking as finished for removal"; + //qCDebug(audioclient) << "injector has no local buffer, marking as finished for removal"; injector->finishLocalInjection(); injectorsToRemove.append(injector); } } for (const AudioInjectorPointer& injector : injectorsToRemove) { - qCDebug(audioclient) << "removing injector"; + //qCDebug(audioclient) << "removing injector"; _activeLocalAudioInjectors.removeOne(injector); } @@ -1562,15 +1564,13 @@ bool AudioClient::setIsStereoInput(bool isStereoInput) { } bool AudioClient::outputLocalInjector(const AudioInjectorPointer& injector) { - AudioInjectorLocalBuffer* injectorBuffer = injector->getLocalBuffer(); + auto injectorBuffer = injector->getLocalBuffer(); if (injectorBuffer) { // local injectors are on the AudioInjectorsThread, so we must guard access Lock lock(_injectorsMutex); if (!_activeLocalAudioInjectors.contains(injector)) { - qCDebug(audioclient) << "adding new injector"; + //qCDebug(audioclient) << "adding new injector"; _activeLocalAudioInjectors.append(injector); - // move local buffer to the LocalAudioThread to avoid dataraces with AudioInjector (like stop()) - injectorBuffer->setParent(nullptr); // update the flag _localInjectorsAvailable.exchange(true, std::memory_order_release); @@ -1586,6 +1586,11 @@ bool AudioClient::outputLocalInjector(const AudioInjectorPointer& injector) { } } +int AudioClient::getNumLocalInjectors() { + Lock lock(_injectorsMutex); + return _activeLocalAudioInjectors.size(); +} + void AudioClient::outputFormatChanged() { _outputFrameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * OUTPUT_CHANNEL_COUNT * _outputFormat.sampleRate()) / _desiredOutputFormat.sampleRate(); diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 87e0f68e72..2cfe83d445 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -181,6 +181,8 @@ public: bool isHeadsetPluggedIn() { return _isHeadsetPluggedIn; } #endif + int getNumLocalInjectors(); + public slots: void start(); void stop(); diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index 1581990e0c..4911917bf0 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -24,9 +24,10 @@ #include "AudioRingBuffer.h" #include "AudioLogging.h" #include "SoundCache.h" -#include "AudioSRC.h" #include "AudioHelpers.h" +int metaType = qRegisterMetaType("AudioInjectorPointer"); + AbstractAudioInterface* AudioInjector::_localAudioInterface{ nullptr }; AudioInjectorState operator& (AudioInjectorState lhs, AudioInjectorState rhs) { @@ -51,26 +52,30 @@ AudioInjector::AudioInjector(AudioDataPointer audioData, const AudioInjectorOpti { } -AudioInjector::~AudioInjector() { - deleteLocalBuffer(); -} +AudioInjector::~AudioInjector() {} bool AudioInjector::stateHas(AudioInjectorState state) const { - return (_state & state) == state; + return resultWithReadLock([&] { + return (_state & state) == state; + }); } void AudioInjector::setOptions(const AudioInjectorOptions& options) { // since options.stereo is computed from the audio stream, // we need to copy it from existing options just in case. - bool currentlyStereo = _options.stereo; - bool currentlyAmbisonic = _options.ambisonic; - _options = options; - _options.stereo = currentlyStereo; - _options.ambisonic = currentlyAmbisonic; + withWriteLock([&] { + bool currentlyStereo = _options.stereo; + bool currentlyAmbisonic = _options.ambisonic; + _options = options; + _options.stereo = currentlyStereo; + _options.ambisonic = currentlyAmbisonic; + }); } void AudioInjector::finishNetworkInjection() { - _state |= AudioInjectorState::NetworkInjectionFinished; + withWriteLock([&] { + _state |= AudioInjectorState::NetworkInjectionFinished; + }); // if we are already finished with local // injection, then we are finished @@ -80,35 +85,31 @@ void AudioInjector::finishNetworkInjection() { } void AudioInjector::finishLocalInjection() { - _state |= AudioInjectorState::LocalInjectionFinished; - if(_options.localOnly || stateHas(AudioInjectorState::NetworkInjectionFinished)) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "finishLocalInjection"); + return; + } + + bool localOnly = false; + withWriteLock([&] { + _state |= AudioInjectorState::LocalInjectionFinished; + localOnly = _options.localOnly; + }); + + if(localOnly || stateHas(AudioInjectorState::NetworkInjectionFinished)) { finish(); } } void AudioInjector::finish() { - _state |= AudioInjectorState::Finished; - + withWriteLock([&] { + _state |= AudioInjectorState::Finished; + }); emit finished(); - - deleteLocalBuffer(); + _localBuffer = nullptr; } void AudioInjector::restart() { - // grab the AudioInjectorManager - auto injectorManager = DependencyManager::get(); - - if (thread() != QThread::currentThread()) { - QMetaObject::invokeMethod(this, "restart"); - - if (!_options.localOnly) { - // notify the AudioInjectorManager to wake up in case it's waiting for new injectors - injectorManager->notifyInjectorReadyCondition(); - } - - return; - } - // reset the current send offset to zero _currentSendOffset = 0; @@ -121,19 +122,23 @@ void AudioInjector::restart() { // check our state to decide if we need extra handling for the restart request if (stateHas(AudioInjectorState::Finished)) { - if (!inject(&AudioInjectorManager::restartFinishedInjector)) { + if (!inject(&AudioInjectorManager::threadInjector)) { qWarning() << "AudioInjector::restart failed to thread injector"; } } } bool AudioInjector::inject(bool(AudioInjectorManager::*injection)(const AudioInjectorPointer&)) { - _state = AudioInjectorState::NotFinished; + AudioInjectorOptions options; + withWriteLock([&] { + _state = AudioInjectorState::NotFinished; + options = _options; + }); int byteOffset = 0; - if (_options.secondOffset > 0.0f) { - int numChannels = _options.ambisonic ? 4 : (_options.stereo ? 2 : 1); - byteOffset = (int)(AudioConstants::SAMPLE_RATE * _options.secondOffset * numChannels); + if (options.secondOffset > 0.0f) { + int numChannels = options.ambisonic ? 4 : (options.stereo ? 2 : 1); + byteOffset = (int)(AudioConstants::SAMPLE_RATE * options.secondOffset * numChannels); byteOffset *= AudioConstants::SAMPLE_SIZE; } _currentSendOffset = byteOffset; @@ -143,7 +148,7 @@ bool AudioInjector::inject(bool(AudioInjectorManager::*injection)(const AudioInj } bool success = true; - if (!_options.localOnly) { + if (!options.localOnly) { auto injectorManager = DependencyManager::get(); if (!(*injectorManager.*injection)(sharedFromThis())) { success = false; @@ -158,7 +163,8 @@ bool AudioInjector::injectLocally() { if (_localAudioInterface) { if (_audioData->getNumBytes() > 0) { - _localBuffer = new AudioInjectorLocalBuffer(_audioData); + _localBuffer = QSharedPointer(new AudioInjectorLocalBuffer(_audioData), &AudioInjectorLocalBuffer::deleteLater); + _localBuffer->moveToThread(thread()); _localBuffer->open(QIODevice::ReadOnly); _localBuffer->setShouldLoop(_options.loop); @@ -181,14 +187,6 @@ bool AudioInjector::injectLocally() { return success; } -void AudioInjector::deleteLocalBuffer() { - if (_localBuffer) { - _localBuffer->stop(); - _localBuffer->deleteLater(); - _localBuffer = nullptr; - } -} - const uchar MAX_INJECTOR_VOLUME = packFloatGainToByte(1.0f); static const int64_t NEXT_FRAME_DELTA_ERROR_OR_FINISHED = -1; static const int64_t NEXT_FRAME_DELTA_IMMEDIATELY = 0; @@ -220,6 +218,10 @@ int64_t AudioInjector::injectNextFrame() { static int volumeOptionOffset = -1; static int audioDataOffset = -1; + AudioInjectorOptions options = resultWithReadLock([&] { + return _options; + }); + if (!_currentPacket) { if (_currentSendOffset < 0 || _currentSendOffset >= (int)_audioData->getNumBytes()) { @@ -253,7 +255,7 @@ int64_t AudioInjector::injectNextFrame() { audioPacketStream << QUuid::createUuid(); // pack the stereo/mono type of the stream - audioPacketStream << _options.stereo; + audioPacketStream << options.stereo; // pack the flag for loopback, if requested loopbackOptionOffset = _currentPacket->pos(); @@ -262,15 +264,16 @@ int64_t AudioInjector::injectNextFrame() { // pack the position for injected audio positionOptionOffset = _currentPacket->pos(); - audioPacketStream.writeRawData(reinterpret_cast(&_options.position), - sizeof(_options.position)); + audioPacketStream.writeRawData(reinterpret_cast(&options.position), + sizeof(options.position)); // pack our orientation for injected audio - audioPacketStream.writeRawData(reinterpret_cast(&_options.orientation), - sizeof(_options.orientation)); + audioPacketStream.writeRawData(reinterpret_cast(&options.orientation), + sizeof(options.orientation)); + + audioPacketStream.writeRawData(reinterpret_cast(&options.position), + sizeof(options.position)); - audioPacketStream.writeRawData(reinterpret_cast(&_options.position), - sizeof(_options.position)); glm::vec3 boxCorner = glm::vec3(0); audioPacketStream.writeRawData(reinterpret_cast(&boxCorner), sizeof(glm::vec3)); @@ -283,7 +286,7 @@ int64_t AudioInjector::injectNextFrame() { volumeOptionOffset = _currentPacket->pos(); quint8 volume = MAX_INJECTOR_VOLUME; audioPacketStream << volume; - audioPacketStream << _options.ignorePenumbra; + audioPacketStream << options.ignorePenumbra; audioDataOffset = _currentPacket->pos(); @@ -313,10 +316,10 @@ int64_t AudioInjector::injectNextFrame() { _currentPacket->writePrimitive((uchar)(_localAudioInterface && _localAudioInterface->shouldLoopbackInjectors())); _currentPacket->seek(positionOptionOffset); - _currentPacket->writePrimitive(_options.position); - _currentPacket->writePrimitive(_options.orientation); + _currentPacket->writePrimitive(options.position); + _currentPacket->writePrimitive(options.orientation); - quint8 volume = packFloatGainToByte(_options.volume); + quint8 volume = packFloatGainToByte(options.volume); _currentPacket->seek(volumeOptionOffset); _currentPacket->writePrimitive(volume); @@ -326,8 +329,8 @@ int64_t AudioInjector::injectNextFrame() { // Might be a reasonable place to do the encode step here. QByteArray decodedAudio; - int totalBytesLeftToCopy = (_options.stereo ? 2 : 1) * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; - if (!_options.loop) { + int totalBytesLeftToCopy = (options.stereo ? 2 : 1) * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; + if (!options.loop) { // If we aren't looping, let's make sure we don't read past the end int bytesLeftToRead = _audioData->getNumBytes() - _currentSendOffset; totalBytesLeftToCopy = std::min(totalBytesLeftToCopy, bytesLeftToRead); @@ -342,14 +345,16 @@ int64_t AudioInjector::injectNextFrame() { auto samplesOut = reinterpret_cast(decodedAudio.data()); // Copy and Measure the loudness of this frame - _loudness = 0.0f; - for (int i = 0; i < samplesLeftToCopy; ++i) { - auto index = (currentSample + i) % _audioData->getNumSamples(); - auto sample = samples[index]; - samplesOut[i] = sample; - _loudness += abs(sample) / (AudioConstants::MAX_SAMPLE_VALUE / 2.0f); - } - _loudness /= (float)samplesLeftToCopy; + withWriteLock([&] { + _loudness = 0.0f; + for (int i = 0; i < samplesLeftToCopy; ++i) { + auto index = (currentSample + i) % _audioData->getNumSamples(); + auto sample = samples[index]; + samplesOut[i] = sample; + _loudness += abs(sample) / (AudioConstants::MAX_SAMPLE_VALUE / 2.0f); + } + _loudness /= (float)samplesLeftToCopy; + }); _currentSendOffset = (_currentSendOffset + totalBytesLeftToCopy) % _audioData->getNumBytes(); @@ -371,7 +376,7 @@ int64_t AudioInjector::injectNextFrame() { _outgoingSequenceNumber++; } - if (_currentSendOffset == 0 && !_options.loop) { + if (_currentSendOffset == 0 && !options.loop) { finishNetworkInjection(); return NEXT_FRAME_DELTA_ERROR_OR_FINISHED; } @@ -391,134 +396,10 @@ int64_t AudioInjector::injectNextFrame() { // If we are falling behind by more frames than our threshold, let's skip the frames ahead qCDebug(audio) << this << "injectNextFrame() skipping ahead, fell behind by " << (currentFrameBasedOnElapsedTime - _nextFrame) << " frames"; _nextFrame = currentFrameBasedOnElapsedTime; - _currentSendOffset = _nextFrame * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL * (_options.stereo ? 2 : 1) % _audioData->getNumBytes(); + _currentSendOffset = _nextFrame * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL * (options.stereo ? 2 : 1) % _audioData->getNumBytes(); } int64_t playNextFrameAt = ++_nextFrame * AudioConstants::NETWORK_FRAME_USECS; return std::max(INT64_C(0), playNextFrameAt - currentTime); -} - -void AudioInjector::stop() { - // trigger a call on the injector's thread to change state to finished - QMetaObject::invokeMethod(this, "finish"); -} - -void AudioInjector::triggerDeleteAfterFinish() { - // make sure this fires on the AudioInjector thread - if (thread() != QThread::currentThread()) { - QMetaObject::invokeMethod(this, "triggerDeleteAfterFinish", Qt::QueuedConnection); - return; - } - - if (stateHas(AudioInjectorState::Finished)) { - stop(); - } else { - _state |= AudioInjectorState::PendingDelete; - } -} - -AudioInjectorPointer AudioInjector::playSoundAndDelete(SharedSoundPointer sound, const AudioInjectorOptions& options) { - AudioInjectorPointer injector = playSound(sound, options); - - if (injector) { - injector->_state |= AudioInjectorState::PendingDelete; - } - - return injector; -} - - -AudioInjectorPointer AudioInjector::playSound(SharedSoundPointer sound, const AudioInjectorOptions& options) { - if (!sound || !sound->isReady()) { - return AudioInjectorPointer(); - } - - if (options.pitch == 1.0f) { - - AudioInjectorPointer injector = AudioInjectorPointer::create(sound, options); - - if (!injector->inject(&AudioInjectorManager::threadInjector)) { - qWarning() << "AudioInjector::playSound failed to thread injector"; - } - return injector; - - } else { - using AudioConstants::AudioSample; - using AudioConstants::SAMPLE_RATE; - const int standardRate = SAMPLE_RATE; - // limit pitch to 4 octaves - const float pitch = glm::clamp(options.pitch, 1 / 16.0f, 16.0f); - const int resampledRate = glm::round(SAMPLE_RATE / pitch); - - auto audioData = sound->getAudioData(); - auto numChannels = audioData->getNumChannels(); - auto numFrames = audioData->getNumFrames(); - - AudioSRC resampler(standardRate, resampledRate, numChannels); - - // create a resampled buffer that is guaranteed to be large enough - const int maxOutputFrames = resampler.getMaxOutput(numFrames); - const int maxOutputSize = maxOutputFrames * numChannels * sizeof(AudioSample); - QByteArray resampledBuffer(maxOutputSize, '\0'); - auto bufferPtr = reinterpret_cast(resampledBuffer.data()); - - resampler.render(audioData->data(), bufferPtr, numFrames); - - int numSamples = maxOutputFrames * numChannels; - auto newAudioData = AudioData::make(numSamples, numChannels, bufferPtr); - - AudioInjectorPointer injector = AudioInjectorPointer::create(newAudioData, options); - - if (!injector->inject(&AudioInjectorManager::threadInjector)) { - qWarning() << "AudioInjector::playSound failed to thread pitch-shifted injector"; - } - return injector; - } -} - -AudioInjectorPointer AudioInjector::playSoundAndDelete(AudioDataPointer audioData, const AudioInjectorOptions& options) { - AudioInjectorPointer injector = playSound(audioData, options); - - if (injector) { - injector->_state |= AudioInjectorState::PendingDelete; - } - - return injector; -} - -AudioInjectorPointer AudioInjector::playSound(AudioDataPointer audioData, const AudioInjectorOptions& options) { - if (options.pitch == 1.0f) { - AudioInjectorPointer injector = AudioInjectorPointer::create(audioData, options); - - if (!injector->inject(&AudioInjectorManager::threadInjector)) { - qWarning() << "AudioInjector::playSound failed to thread pitch-shifted injector"; - } - return injector; - } else { - using AudioConstants::AudioSample; - using AudioConstants::SAMPLE_RATE; - const int standardRate = SAMPLE_RATE; - // limit pitch to 4 octaves - const float pitch = glm::clamp(options.pitch, 1 / 16.0f, 16.0f); - const int resampledRate = glm::round(SAMPLE_RATE / pitch); - - auto numChannels = audioData->getNumChannels(); - auto numFrames = audioData->getNumFrames(); - - AudioSRC resampler(standardRate, resampledRate, numChannels); - - // create a resampled buffer that is guaranteed to be large enough - const int maxOutputFrames = resampler.getMaxOutput(numFrames); - const int maxOutputSize = maxOutputFrames * numChannels * sizeof(AudioSample); - QByteArray resampledBuffer(maxOutputSize, '\0'); - auto bufferPtr = reinterpret_cast(resampledBuffer.data()); - - resampler.render(audioData->data(), bufferPtr, numFrames); - - int numSamples = maxOutputFrames * numChannels; - auto newAudioData = AudioData::make(numSamples, numChannels, bufferPtr); - - return AudioInjector::playSound(newAudioData, options); - } } \ No newline at end of file diff --git a/libraries/audio/src/AudioInjector.h b/libraries/audio/src/AudioInjector.h index 3c21d2eccf..1d5cf50033 100644 --- a/libraries/audio/src/AudioInjector.h +++ b/libraries/audio/src/AudioInjector.h @@ -19,6 +19,8 @@ #include #include +#include + #include #include @@ -49,7 +51,7 @@ AudioInjectorState& operator|= (AudioInjectorState& lhs, AudioInjectorState rhs) // In order to make scripting cleaner for the AudioInjector, the script now holds on to the AudioInjector object // until it dies. -class AudioInjector : public QObject, public QEnableSharedFromThis { +class AudioInjector : public QObject, public QEnableSharedFromThis, public ReadWriteLockable { Q_OBJECT public: AudioInjector(SharedSoundPointer sound, const AudioInjectorOptions& injectorOptions); @@ -61,38 +63,30 @@ public: int getCurrentSendOffset() const { return _currentSendOffset; } void setCurrentSendOffset(int currentSendOffset) { _currentSendOffset = currentSendOffset; } - AudioInjectorLocalBuffer* getLocalBuffer() const { return _localBuffer; } + QSharedPointer getLocalBuffer() const { return _localBuffer; } AudioHRTF& getLocalHRTF() { return _localHRTF; } AudioFOA& getLocalFOA() { return _localFOA; } - bool isLocalOnly() const { return _options.localOnly; } - float getVolume() const { return _options.volume; } - bool isPositionSet() const { return _options.positionSet; } - glm::vec3 getPosition() const { return _options.position; } - glm::quat getOrientation() const { return _options.orientation; } - bool isStereo() const { return _options.stereo; } - bool isAmbisonic() const { return _options.ambisonic; } + float getLoudness() const { return resultWithReadLock([&] { return _loudness; }); } + bool isPlaying() const { return !stateHas(AudioInjectorState::Finished); } + + bool isLocalOnly() const { return resultWithReadLock([&] { return _options.localOnly; }); } + float getVolume() const { return resultWithReadLock([&] { return _options.volume; }); } + bool isPositionSet() const { return resultWithReadLock([&] { return _options.positionSet; }); } + glm::vec3 getPosition() const { return resultWithReadLock([&] { return _options.position; }); } + glm::quat getOrientation() const { return resultWithReadLock([&] { return _options.orientation; }); } + bool isStereo() const { return resultWithReadLock([&] { return _options.stereo; }); } + bool isAmbisonic() const { return resultWithReadLock([&] { return _options.ambisonic; }); } + + const AudioInjectorOptions& getOptions() const { return resultWithReadLock([&] { return _options; }); } + void setOptions(const AudioInjectorOptions& options); bool stateHas(AudioInjectorState state) const ; static void setLocalAudioInterface(AbstractAudioInterface* audioInterface) { _localAudioInterface = audioInterface; } - static AudioInjectorPointer playSoundAndDelete(SharedSoundPointer sound, const AudioInjectorOptions& options); - static AudioInjectorPointer playSound(SharedSoundPointer sound, const AudioInjectorOptions& options); - static AudioInjectorPointer playSoundAndDelete(AudioDataPointer audioData, const AudioInjectorOptions& options); - static AudioInjectorPointer playSound(AudioDataPointer audioData, const AudioInjectorOptions& options); - -public slots: void restart(); - - void stop(); - void triggerDeleteAfterFinish(); - - const AudioInjectorOptions& getOptions() const { return _options; } - void setOptions(const AudioInjectorOptions& options); - - float getLoudness() const { return _loudness; } - bool isPlaying() const { return !stateHas(AudioInjectorState::Finished); } void finish(); + void finishLocalInjection(); void finishNetworkInjection(); @@ -104,7 +98,6 @@ private: int64_t injectNextFrame(); bool inject(bool(AudioInjectorManager::*injection)(const AudioInjectorPointer&)); bool injectLocally(); - void deleteLocalBuffer(); static AbstractAudioInterface* _localAudioInterface; @@ -116,7 +109,7 @@ private: float _loudness { 0.0f }; int _currentSendOffset { 0 }; std::unique_ptr _currentPacket { nullptr }; - AudioInjectorLocalBuffer* _localBuffer { nullptr }; + QSharedPointer _localBuffer { nullptr }; int64_t _nextFrame { 0 }; std::unique_ptr _frameTimer { nullptr }; @@ -128,4 +121,6 @@ private: friend class AudioInjectorManager; }; +Q_DECLARE_METATYPE(AudioInjectorPointer) + #endif // hifi_AudioInjector_h diff --git a/libraries/audio/src/AudioInjectorLocalBuffer.cpp b/libraries/audio/src/AudioInjectorLocalBuffer.cpp index 015d87e03b..680513abf5 100644 --- a/libraries/audio/src/AudioInjectorLocalBuffer.cpp +++ b/libraries/audio/src/AudioInjectorLocalBuffer.cpp @@ -16,6 +16,10 @@ AudioInjectorLocalBuffer::AudioInjectorLocalBuffer(AudioDataPointer audioData) : { } +AudioInjectorLocalBuffer::~AudioInjectorLocalBuffer() { + stop(); +} + void AudioInjectorLocalBuffer::stop() { _isStopped = true; @@ -30,9 +34,8 @@ bool AudioInjectorLocalBuffer::seek(qint64 pos) { } } - qint64 AudioInjectorLocalBuffer::readData(char* data, qint64 maxSize) { - if (!_isStopped) { + if (!_isStopped && _audioData) { // first copy to the end of the raw audio int bytesToEnd = (int)_audioData->getNumBytes() - _currentOffset; diff --git a/libraries/audio/src/AudioInjectorLocalBuffer.h b/libraries/audio/src/AudioInjectorLocalBuffer.h index e0f8847883..2f73e5b313 100644 --- a/libraries/audio/src/AudioInjectorLocalBuffer.h +++ b/libraries/audio/src/AudioInjectorLocalBuffer.h @@ -22,6 +22,7 @@ class AudioInjectorLocalBuffer : public QIODevice { Q_OBJECT public: AudioInjectorLocalBuffer(AudioDataPointer audioData); + ~AudioInjectorLocalBuffer(); void stop(); diff --git a/libraries/audio/src/AudioInjectorManager.cpp b/libraries/audio/src/AudioInjectorManager.cpp index f30d3093ec..e5ffc77798 100644 --- a/libraries/audio/src/AudioInjectorManager.cpp +++ b/libraries/audio/src/AudioInjectorManager.cpp @@ -14,11 +14,14 @@ #include #include +#include #include "AudioConstants.h" #include "AudioInjector.h" #include "AudioLogging.h" +#include "AudioSRC.h" + AudioInjectorManager::~AudioInjectorManager() { _shouldStop = true; @@ -30,7 +33,7 @@ AudioInjectorManager::~AudioInjectorManager() { auto& timePointerPair = _injectors.top(); // ask it to stop and be deleted - timePointerPair.second->stop(); + timePointerPair.second->finish(); _injectors.pop(); } @@ -46,6 +49,8 @@ AudioInjectorManager::~AudioInjectorManager() { _thread->quit(); _thread->wait(); } + + moveToThread(qApp->thread()); } void AudioInjectorManager::createThread() { @@ -55,6 +60,8 @@ void AudioInjectorManager::createThread() { // when the thread is started, have it call our run to handle injection of audio connect(_thread, &QThread::started, this, &AudioInjectorManager::run, Qt::DirectConnection); + moveToThread(_thread); + // start the thread _thread->start(); } @@ -141,36 +148,7 @@ bool AudioInjectorManager::wouldExceedLimits() { // Should be called inside of a bool AudioInjectorManager::threadInjector(const AudioInjectorPointer& injector) { if (_shouldStop) { - qCDebug(audio) << "AudioInjectorManager::threadInjector asked to thread injector but is shutting down."; - return false; - } - - // guard the injectors vector with a mutex - Lock lock(_injectorsMutex); - - if (wouldExceedLimits()) { - return false; - } else { - if (!_thread) { - createThread(); - } - - // move the injector to the QThread - injector->moveToThread(_thread); - - // add the injector to the queue with a send timestamp of now - _injectors.emplace(usecTimestampNow(), injector); - - // notify our wait condition so we can inject two frames for this injector immediately - _injectorReady.notify_one(); - - return true; - } -} - -bool AudioInjectorManager::restartFinishedInjector(const AudioInjectorPointer& injector) { - if (_shouldStop) { - qCDebug(audio) << "AudioInjectorManager::threadInjector asked to thread injector but is shutting down."; + qCDebug(audio) << "AudioInjectorManager::threadInjector asked to thread injector but is shutting down."; return false; } @@ -188,3 +166,192 @@ bool AudioInjectorManager::restartFinishedInjector(const AudioInjectorPointer& i } return true; } + +AudioInjectorPointer AudioInjectorManager::playSound(const SharedSoundPointer& sound, const AudioInjectorOptions& options, bool setPendingDelete) { + if (_shouldStop) { + qCDebug(audio) << "AudioInjectorManager::threadInjector asked to thread injector but is shutting down."; + return nullptr; + } + + AudioInjectorPointer injector = nullptr; + if (sound && sound->isReady()) { + if (options.pitch == 1.0f) { + injector = QSharedPointer(new AudioInjector(sound, options), &AudioInjector::deleteLater); + } else { + using AudioConstants::AudioSample; + using AudioConstants::SAMPLE_RATE; + const int standardRate = SAMPLE_RATE; + // limit pitch to 4 octaves + const float pitch = glm::clamp(options.pitch, 1 / 16.0f, 16.0f); + const int resampledRate = glm::round(SAMPLE_RATE / pitch); + + auto audioData = sound->getAudioData(); + auto numChannels = audioData->getNumChannels(); + auto numFrames = audioData->getNumFrames(); + + AudioSRC resampler(standardRate, resampledRate, numChannels); + + // create a resampled buffer that is guaranteed to be large enough + const int maxOutputFrames = resampler.getMaxOutput(numFrames); + const int maxOutputSize = maxOutputFrames * numChannels * sizeof(AudioSample); + QByteArray resampledBuffer(maxOutputSize, '\0'); + auto bufferPtr = reinterpret_cast(resampledBuffer.data()); + + resampler.render(audioData->data(), bufferPtr, numFrames); + + int numSamples = maxOutputFrames * numChannels; + auto newAudioData = AudioData::make(numSamples, numChannels, bufferPtr); + + injector = QSharedPointer(new AudioInjector(newAudioData, options), &AudioInjector::deleteLater); + } + } + + if (!injector) { + return nullptr; + } + + if (setPendingDelete) { + injector->_state |= AudioInjectorState::PendingDelete; + } + + injector->moveToThread(_thread); + injector->inject(&AudioInjectorManager::threadInjector); + + return injector; +} + +AudioInjectorPointer AudioInjectorManager::playSound(const AudioDataPointer& audioData, const AudioInjectorOptions& options, bool setPendingDelete) { + if (_shouldStop) { + qCDebug(audio) << "AudioInjectorManager::threadInjector asked to thread injector but is shutting down."; + return nullptr; + } + + AudioInjectorPointer injector = nullptr; + if (options.pitch == 1.0f) { + injector = QSharedPointer(new AudioInjector(audioData, options), &AudioInjector::deleteLater); + } else { + using AudioConstants::AudioSample; + using AudioConstants::SAMPLE_RATE; + const int standardRate = SAMPLE_RATE; + // limit pitch to 4 octaves + const float pitch = glm::clamp(options.pitch, 1 / 16.0f, 16.0f); + const int resampledRate = glm::round(SAMPLE_RATE / pitch); + + auto numChannels = audioData->getNumChannels(); + auto numFrames = audioData->getNumFrames(); + + AudioSRC resampler(standardRate, resampledRate, numChannels); + + // create a resampled buffer that is guaranteed to be large enough + const int maxOutputFrames = resampler.getMaxOutput(numFrames); + const int maxOutputSize = maxOutputFrames * numChannels * sizeof(AudioSample); + QByteArray resampledBuffer(maxOutputSize, '\0'); + auto bufferPtr = reinterpret_cast(resampledBuffer.data()); + + resampler.render(audioData->data(), bufferPtr, numFrames); + + int numSamples = maxOutputFrames * numChannels; + auto newAudioData = AudioData::make(numSamples, numChannels, bufferPtr); + + injector = QSharedPointer(new AudioInjector(newAudioData, options), &AudioInjector::deleteLater); + } + + if (!injector) { + return nullptr; + } + + if (setPendingDelete) { + injector->_state |= AudioInjectorState::PendingDelete; + } + + injector->moveToThread(_thread); + injector->inject(&AudioInjectorManager::threadInjector); + + return injector; +} + +void AudioInjectorManager::setOptionsAndRestart(const AudioInjectorPointer& injector, const AudioInjectorOptions& options) { + if (!injector) { + return; + } + + if (QThread::currentThread() != _thread) { + QMetaObject::invokeMethod(this, "setOptionsAndRestart", Q_ARG(const AudioInjectorPointer&, injector), Q_ARG(const AudioInjectorOptions&, options)); + _injectorReady.notify_one(); + return; + } + + injector->setOptions(options); + injector->restart(); +} + +void AudioInjectorManager::restart(const AudioInjectorPointer& injector) { + if (!injector) { + return; + } + + if (QThread::currentThread() != _thread) { + QMetaObject::invokeMethod(this, "restart", Q_ARG(const AudioInjectorPointer&, injector)); + _injectorReady.notify_one(); + return; + } + + injector->restart(); +} + +void AudioInjectorManager::setOptions(const AudioInjectorPointer& injector, const AudioInjectorOptions& options) { + if (!injector) { + return; + } + + if (QThread::currentThread() != _thread) { + QMetaObject::invokeMethod(this, "setOptions", Q_ARG(const AudioInjectorPointer&, injector), Q_ARG(const AudioInjectorOptions&, options)); + _injectorReady.notify_one(); + return; + } + + injector->setOptions(options); +} + +AudioInjectorOptions AudioInjectorManager::getOptions(const AudioInjectorPointer& injector) { + if (!injector) { + return AudioInjectorOptions(); + } + + return injector->getOptions(); +} + +float AudioInjectorManager::getLoudness(const AudioInjectorPointer& injector) { + if (!injector) { + return 0.0f; + } + + return injector->getLoudness(); +} + +bool AudioInjectorManager::isPlaying(const AudioInjectorPointer& injector) { + if (!injector) { + return false; + } + + return injector->isPlaying(); +} + +void AudioInjectorManager::stop(const AudioInjectorPointer& injector) { + if (!injector) { + return; + } + + if (QThread::currentThread() != _thread) { + QMetaObject::invokeMethod(this, "stop", Q_ARG(const AudioInjectorPointer&, injector)); + _injectorReady.notify_one(); + return; + } + + injector->finish(); +} + +size_t AudioInjectorManager::getNumInjectors() { + Lock lock(_injectorsMutex); + return _injectors.size(); +} \ No newline at end of file diff --git a/libraries/audio/src/AudioInjectorManager.h b/libraries/audio/src/AudioInjectorManager.h index 9aca3014e3..cb243f23de 100644 --- a/libraries/audio/src/AudioInjectorManager.h +++ b/libraries/audio/src/AudioInjectorManager.h @@ -30,8 +30,27 @@ class AudioInjectorManager : public QObject, public Dependency { SINGLETON_DEPENDENCY public: ~AudioInjectorManager(); + + AudioInjectorPointer playSound(const SharedSoundPointer& sound, const AudioInjectorOptions& options, bool setPendingDelete = false); + AudioInjectorPointer playSound(const AudioDataPointer& audioData, const AudioInjectorOptions& options, bool setPendingDelete = false); + + size_t getNumInjectors(); + +public slots: + void setOptionsAndRestart(const AudioInjectorPointer& injector, const AudioInjectorOptions& options); + void restart(const AudioInjectorPointer& injector); + + void setOptions(const AudioInjectorPointer& injector, const AudioInjectorOptions& options); + AudioInjectorOptions getOptions(const AudioInjectorPointer& injector); + + float getLoudness(const AudioInjectorPointer& injector); + bool isPlaying(const AudioInjectorPointer& injector); + + void stop(const AudioInjectorPointer& injector); + private slots: void run(); + private: using TimeInjectorPointerPair = std::pair; @@ -49,11 +68,10 @@ private: using Lock = std::unique_lock; bool threadInjector(const AudioInjectorPointer& injector); - bool restartFinishedInjector(const AudioInjectorPointer& injector); void notifyInjectorReadyCondition() { _injectorReady.notify_one(); } bool wouldExceedLimits(); - AudioInjectorManager() {}; + AudioInjectorManager() { createThread(); } AudioInjectorManager(const AudioInjectorManager&) = delete; AudioInjectorManager& operator=(const AudioInjectorManager&) = delete; diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 143c7fa377..c235460404 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -1105,7 +1105,7 @@ void EntityTreeRenderer::playEntityCollisionSound(const EntityItemPointer& entit options.volume = volume; options.pitch = 1.0f / stretchFactor; - AudioInjector::playSoundAndDelete(collisionSound, options); + DependencyManager::get()->playSound(collisionSound, options, true); } void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index a257951ba8..a511d73210 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -16,7 +16,7 @@ #include #include -#include +#include #include // for RayToEntityIntersectionResult #include #include diff --git a/libraries/script-engine/src/AudioScriptingInterface.cpp b/libraries/script-engine/src/AudioScriptingInterface.cpp index 65d71e46e6..395571c51f 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.cpp +++ b/libraries/script-engine/src/AudioScriptingInterface.cpp @@ -63,7 +63,7 @@ ScriptAudioInjector* AudioScriptingInterface::playSound(SharedSoundPointer sound optionsCopy.ambisonic = sound->isAmbisonic(); optionsCopy.localOnly = optionsCopy.localOnly || sound->isAmbisonic(); // force localOnly when Ambisonic - auto injector = AudioInjector::playSound(sound, optionsCopy); + auto injector = DependencyManager::get()->playSound(sound, optionsCopy); if (!injector) { return nullptr; } diff --git a/libraries/script-engine/src/ScriptAudioInjector.cpp b/libraries/script-engine/src/ScriptAudioInjector.cpp index 8b51377bff..822aa0a9c1 100644 --- a/libraries/script-engine/src/ScriptAudioInjector.cpp +++ b/libraries/script-engine/src/ScriptAudioInjector.cpp @@ -20,8 +20,11 @@ QScriptValue injectorToScriptValue(QScriptEngine* engine, ScriptAudioInjector* c } // when the script goes down we want to cleanup the injector - QObject::connect(engine, &QScriptEngine::destroyed, in, &ScriptAudioInjector::stopInjectorImmediately, - Qt::DirectConnection); + QObject::connect(engine, &QScriptEngine::destroyed, DependencyManager::get().data(), [&] { + qCDebug(scriptengine) << "Script was shutdown, stopping an injector"; + // FIXME: this doesn't work and leaves the injectors lying around + //DependencyManager::get()->stop(in->_injector); + }); return engine->newQObject(in, QScriptEngine::ScriptOwnership); } @@ -37,13 +40,5 @@ ScriptAudioInjector::ScriptAudioInjector(const AudioInjectorPointer& injector) : } ScriptAudioInjector::~ScriptAudioInjector() { - if (!_injector.isNull()) { - // we've been asked to delete after finishing, trigger a queued deleteLater here - _injector->triggerDeleteAfterFinish(); - } -} - -void ScriptAudioInjector::stopInjectorImmediately() { - qCDebug(scriptengine) << "ScriptAudioInjector::stopInjectorImmediately called to stop audio injector immediately."; - _injector->stop(); -} + DependencyManager::get()->stop(_injector); +} \ No newline at end of file diff --git a/libraries/script-engine/src/ScriptAudioInjector.h b/libraries/script-engine/src/ScriptAudioInjector.h index c7fb2f8a9a..d77291b92c 100644 --- a/libraries/script-engine/src/ScriptAudioInjector.h +++ b/libraries/script-engine/src/ScriptAudioInjector.h @@ -14,7 +14,7 @@ #include -#include +#include /**jsdoc * Plays — "injects" — the content of an audio file. Used in the {@link Audio} API. @@ -48,7 +48,7 @@ public slots: * Stop current playback, if any, and start playing from the beginning. * @function AudioInjector.restart */ - void restart() { _injector->restart(); } + void restart() { DependencyManager::get()->restart(_injector); } /**jsdoc * Stop audio playback. @@ -68,28 +68,28 @@ public slots: * injector.stop(); * }, 2000); */ - void stop() { _injector->stop(); } + void stop() { DependencyManager::get()->stop(_injector); } /**jsdoc * Get the current configuration of the audio injector. * @function AudioInjector.getOptions * @returns {AudioInjector.AudioInjectorOptions} Configuration of how the injector plays the audio. */ - const AudioInjectorOptions& getOptions() const { return _injector->getOptions(); } + AudioInjectorOptions getOptions() const { return DependencyManager::get()->getOptions(_injector); } /**jsdoc * Configure how the injector plays the audio. * @function AudioInjector.setOptions * @param {AudioInjector.AudioInjectorOptions} options - Configuration of how the injector plays the audio. */ - void setOptions(const AudioInjectorOptions& options) { _injector->setOptions(options); } + void setOptions(const AudioInjectorOptions& options) { DependencyManager::get()->setOptions(_injector, options); } /**jsdoc * Get the loudness of the most recent frame of audio played. * @function AudioInjector.getLoudness * @returns {number} The loudness of the most recent frame of audio played, range 0.01.0. */ - float getLoudness() const { return _injector->getLoudness(); } + float getLoudness() const { return DependencyManager::get()->getLoudness(_injector); } /**jsdoc * Get whether or not the audio is currently playing. @@ -110,7 +110,7 @@ public slots: * print("Sound is playing: " + injector.isPlaying()); * }, 2000); */ - bool isPlaying() const { return _injector->isPlaying(); } + bool isPlaying() const { return DependencyManager::get()->isPlaying(_injector); } signals: @@ -134,13 +134,6 @@ signals: */ void finished(); -protected slots: - - /**jsdoc - * Stop audio playback. (Synonym of {@link AudioInjector.stop|stop}.) - * @function AudioInjector.stopInjectorImmediately - */ - void stopInjectorImmediately(); private: AudioInjectorPointer _injector; diff --git a/libraries/ui/src/ui/TabletScriptingInterface.cpp b/libraries/ui/src/ui/TabletScriptingInterface.cpp index 7a1c37af33..963e0d87c1 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.cpp +++ b/libraries/ui/src/ui/TabletScriptingInterface.cpp @@ -23,7 +23,7 @@ #include "ToolbarScriptingInterface.h" #include "Logging.h" -#include +#include #include "SettingHandle.h" @@ -212,7 +212,7 @@ void TabletScriptingInterface::playSound(TabletAudioEvents aEvent) { options.localOnly = true; options.positionSet = false; // system sound - AudioInjectorPointer injector = AudioInjector::playSoundAndDelete(sound, options); + DependencyManager::get()->playSound(sound, options, true); } } diff --git a/libraries/ui/src/ui/types/SoundEffect.cpp b/libraries/ui/src/ui/types/SoundEffect.cpp index dc2328b33e..38114ecef1 100644 --- a/libraries/ui/src/ui/types/SoundEffect.cpp +++ b/libraries/ui/src/ui/types/SoundEffect.cpp @@ -2,12 +2,10 @@ #include "SoundEffect.h" #include -#include SoundEffect::~SoundEffect() { if (_injector) { - // stop will cause the AudioInjector to delete itself. - _injector->stop(); + DependencyManager::get()->stop(_injector); } } @@ -28,15 +26,14 @@ void SoundEffect::setVolume(float volume) { _volume = volume; } -void SoundEffect::play(QVariant position) { +void SoundEffect::play(const QVariant& position) { AudioInjectorOptions options; options.position = vec3FromVariant(position); options.localOnly = true; options.volume = _volume; if (_injector) { - _injector->setOptions(options); - _injector->restart(); + DependencyManager::get()->setOptionsAndRestart(_injector, options); } else { - _injector = AudioInjector::playSound(_sound, options); + _injector = DependencyManager::get()->playSound(_sound, options); } } diff --git a/libraries/ui/src/ui/types/SoundEffect.h b/libraries/ui/src/ui/types/SoundEffect.h index a7e29d86f9..cb8a5cd67f 100644 --- a/libraries/ui/src/ui/types/SoundEffect.h +++ b/libraries/ui/src/ui/types/SoundEffect.h @@ -13,9 +13,7 @@ #include #include - -class AudioInjector; -using AudioInjectorPointer = QSharedPointer; +#include // SoundEffect object, exposed to qml only, not interface JavaScript. // This is used to play spatial sound effects on tablets/web entities from within QML. @@ -34,7 +32,7 @@ public: float getVolume() const; void setVolume(float volume); - Q_INVOKABLE void play(QVariant position); + Q_INVOKABLE void play(const QVariant& position); protected: QUrl _url; float _volume { 1.0f }; From ea84847950e61db2534e86fd7487004b45e78559 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 13 Mar 2019 16:20:38 +1300 Subject: [PATCH 156/446] Update AnimStateDictionary JSDoc per feedback --- libraries/animation/src/Rig.cpp | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 74fd5d600b..82ab067472 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -91,6 +91,7 @@ static const QString MAIN_STATE_MACHINE_RIGHT_HAND_POSITION("mainStateMachineRig /**jsdoc *

An AnimStateDictionary object may have the following properties. It may also have other properties, set by * scripts.

+ *

Warning: These properties are subject to change. * * * @@ -171,11 +172,6 @@ static const QString MAIN_STATE_MACHINE_RIGHT_HAND_POSITION("mainStateMachineRig * * - * - * - * * * * - * - * * * * From f013b9af2b6691a85da36f153be482e74b6574e9 Mon Sep 17 00:00:00 2001 From: Sam Gondelman Date: Wed, 13 Mar 2019 00:24:19 -0700 Subject: [PATCH 157/446] fix warnings --- libraries/audio/src/AudioInjector.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/audio/src/AudioInjector.h b/libraries/audio/src/AudioInjector.h index 1d5cf50033..94527cfdd0 100644 --- a/libraries/audio/src/AudioInjector.h +++ b/libraries/audio/src/AudioInjector.h @@ -78,7 +78,7 @@ public: bool isStereo() const { return resultWithReadLock([&] { return _options.stereo; }); } bool isAmbisonic() const { return resultWithReadLock([&] { return _options.ambisonic; }); } - const AudioInjectorOptions& getOptions() const { return resultWithReadLock([&] { return _options; }); } + AudioInjectorOptions getOptions() const { return resultWithReadLock([&] { return _options; }); } void setOptions(const AudioInjectorOptions& options); bool stateHas(AudioInjectorState state) const ; From d8139af42960d3f522f52058f43f89911f462f11 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 13 Mar 2019 08:51:19 -0700 Subject: [PATCH 158/446] Remove .vs folder from version control --- .gitignore | 3 +++ .vs/slnx.sqlite | Bin 77824 -> 0 bytes 2 files changed, 3 insertions(+) delete mode 100644 .vs/slnx.sqlite diff --git a/.gitignore b/.gitignore index 747f613a4b..4b0251156e 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,9 @@ android/**/src/main/assets android/**/gradle* *.class +# Visual Studio +/.vs + # VSCode # List taken from Github Global Ignores master@435c4d92 # https://github.com/github/gitignore/commits/master/Global/VisualStudioCode.gitignore diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite deleted file mode 100644 index 4227190a376249e4edb90c9be12165dbacd46945..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 77824 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 Date: Wed, 13 Mar 2019 10:17:39 -0700 Subject: [PATCH 159/446] Case 20393 - Display item counts in categories dropdown in Marketplace --- .../hifi/commerce/marketplace/Marketplace.qml | 68 ++++++++++++------- .../src/avatar/MarketplaceItemUploader.cpp | 2 +- 2 files changed, 44 insertions(+), 26 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index 07ded49956..601a04080a 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -87,22 +87,11 @@ Rectangle { console.log("Failed to get Marketplace Categories", result.data.message); } else { categoriesModel.clear(); - categoriesModel.append({ - id: -1, - name: "Everything" - }); - categoriesModel.append({ - id: -1, - name: "Stand-alone Optimized" - }); - categoriesModel.append({ - id: -1, - name: "Stand-alone Compatible" - }); - result.data.items.forEach(function(category) { + result.data.categories.forEach(function(category) { categoriesModel.append({ id: category.id, - name: category.name + name: category.name, + count: category.count }); }); } @@ -396,12 +385,12 @@ Rectangle { Rectangle { anchors { - left: parent.left; - bottom: parent.bottom; - top: parent.top; - topMargin: 100; + left: parent.left + bottom: parent.bottom + top: parent.top + topMargin: 100 } - width: parent.width/3 + width: parent.width/2 color: hifi.colors.white @@ -432,20 +421,49 @@ Rectangle { color: hifi.colors.white visible: true - RalewayRegular { + RalewaySemiBold { id: categoriesItemText anchors.leftMargin: 15 - anchors.fill:parent + anchors.top:parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + elide: Text.ElideRight text: model.name color: ListView.isCurrentItem ? hifi.colors.lightBlueHighlight : hifi.colors.baseGray horizontalAlignment: Text.AlignLeft verticalAlignment: Text.AlignVCenter size: 14 } + Rectangle { + id: categoryItemCount + anchors { + top: parent.top + bottom: parent.bottom + topMargin: 5 + bottomMargin: 5 + leftMargin: 10 + rightMargin: 10 + left: categoriesItemText.right + } + width: childrenRect.width + color: hifi.colors.blueHighlight + radius: height/2 + + RalewaySemiBold { + anchors.top: parent.top + anchors.bottom: parent.bottom + width: paintedWidth+30 + + text: model.count + color: hifi.colors.white + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + size: 16 + } + } } - MouseArea { anchors.fill: parent z: 10 @@ -476,9 +494,9 @@ Rectangle { parent: categoriesListView.parent anchors { - top: categoriesListView.top; - bottom: categoriesListView.bottom; - left: categoriesListView.right; + top: categoriesListView.top + bottom: categoriesListView.bottom + left: categoriesListView.right } contentItem.opacity: 1 diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 53b37eba4f..28b07780b0 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -87,7 +87,7 @@ void MarketplaceItemUploader::doGetCategories() { if (error == QNetworkReply::NoError) { auto doc = QJsonDocument::fromJson(reply->readAll()); auto extractCategoryID = [&doc]() -> std::pair { - auto items = doc.object()["data"].toObject()["items"]; + auto items = doc.object()["data"].toObject()["categories"]; if (!items.isArray()) { qWarning() << "Categories parse error: data.items is not an array"; return { false, 0 }; From 93d7a4ae3b4ade0ac97017ee4737622cad825f5c Mon Sep 17 00:00:00 2001 From: amantley Date: Wed, 13 Mar 2019 11:14:15 -0700 Subject: [PATCH 160/446] will no longer allow a non-zero parent of the root of an fbx model --- libraries/animation/src/AnimClip.cpp | 2 +- libraries/fbx/src/FBXSerializer.cpp | 39 +++------------------------- 2 files changed, 5 insertions(+), 36 deletions(-) diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index 5d846a8f84..4fe02e9307 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -154,7 +154,7 @@ void AnimClip::copyFromNetworkAnim() { const float avatarToAnimationHeightRatio = avatarHeightInMeters / animHeightInMeters; const float unitsRatio = 1.0f / (avatarUnitScale / animationUnitScale); const float parentScaleRatio = 1.0f / (avatarHipsParentScale / animHipsParentScale); - qCDebug(animation) << " avatar unit scale " << avatarUnitScale << " animation unit scale " << animationUnitScale << " avatar height " << avatarHeightInMeters << " animation height " << animHeightInMeters << " avatar scale " << avatarHipsParentScale << " animation scale " << animHipsParentScale; + boneLengthScale = avatarToAnimationHeightRatio * unitsRatio * parentScaleRatio; } diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index c542eef6a6..4bb499bd84 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -142,7 +142,6 @@ glm::mat4 getGlobalTransform(const QMultiMap& _connectionParen visitedNodes.append(nodeID); // Append each node we visit const FBXModel& fbxModel = fbxModels.value(nodeID); - qCDebug(modelformat) << "this fbx model name is " << fbxModel.name; globalTransform = glm::translate(fbxModel.translation) * fbxModel.preTransform * glm::mat4_cast(fbxModel.preRotation * fbxModel.rotation * fbxModel.postRotation) * fbxModel.postTransform * globalTransform; if (fbxModel.hasGeometricOffset) { @@ -202,23 +201,13 @@ public: void appendModelIDs(const QString& parentID, const QMultiMap& connectionChildMap, QHash& fbxModels, QSet& remainingModels, QVector& modelIDs, bool isRootNode = false) { if (remainingModels.contains(parentID)) { - qCDebug(modelformat) << " remaining models contains parent " << parentID; modelIDs.append(parentID); remainingModels.remove(parentID); } - int parentIndex = 1000; - if (isRootNode) { - qCDebug(modelformat) << " found a root node " << parentID; - parentIndex = -1; - } else { - parentIndex = modelIDs.size() - 1; - } - //int parentIndex = isRootNode ? -1 : modelIDs.size() - 1; + int parentIndex = isRootNode ? -1 : modelIDs.size() - 1; foreach (const QString& childID, connectionChildMap.values(parentID)) { - qCDebug(modelformat) << " searching children, parent id " << parentID; if (remainingModels.contains(childID)) { FBXModel& fbxModel = fbxModels[childID]; - qCDebug(modelformat) << " child id " << fbxModel.name; if (fbxModel.parentIndex == -1) { fbxModel.parentIndex = parentIndex; appendModelIDs(childID, connectionChildMap, fbxModels, remainingModels, modelIDs); @@ -452,7 +441,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr QString hifiGlobalNodeID; unsigned int meshIndex = 0; haveReportedUnhandledRotationOrder = false; - int nodeParentId = -1; foreach (const FBXNode& child, node.children) { if (child.name == "FBXHeaderExtension") { @@ -508,9 +496,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } } else if (child.name == "Objects") { - //qCDebug(modelformat) << " the root model id is " << getID(child.properties); foreach (const FBXNode& object, child.children) { - nodeParentId++; if (object.name == "Geometry") { if (object.properties.at(2) == "Mesh") { meshes.insert(getID(object.properties), extractMesh(object, meshIndex)); @@ -519,7 +505,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr blendshapes.append(extracted); } } else if (object.name == "Model") { - qCDebug(modelformat) << "model name from object properties " << getName(object.properties) << " node parentID " << nodeParentId; QString name = getName(object.properties); QString id = getID(object.properties); modelIDsToNames.insert(id, name); @@ -1174,19 +1159,13 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr counter++; } } - - if ("2830302416448" == getID(connection.properties, 1) || "2830302416448" == getID(connection.properties, 2)) { - if ("2829544143536" == getID(connection.properties, 1)) { - _connectionParentMap.insert(getID(connection.properties, 1), "0"); - _connectionChildMap.insert("0", getID(connection.properties, 1)); - } - qCDebug(modelformat) << " parent map inserted with id " << getID(connection.properties, 1) << " name " << modelIDsToNames.value(getID(connection.properties, 1)) << " id " << getID(connection.properties, 2) << " name " << modelIDsToNames.value(getID(connection.properties, 2)); + if (_connectionParentMap.value(getID(connection.properties, 1)) == "0") { + // don't assign the new parent + qCDebug(modelformat) << "root node " << getID(connection.properties, 1) << " has discarded parent " << getID(connection.properties, 2); } else { _connectionParentMap.insert(getID(connection.properties, 1), getID(connection.properties, 2)); _connectionChildMap.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } - - // qCDebug(modelformat) << " child map inserted with id " << getID(connection.properties, 2) << " name " << modelIDsToNames.value(getID(connection.properties, 2)) << " id " << getID(connection.properties, 1) << " name " << modelIDsToNames.value(getID(connection.properties, 1)); } } } @@ -1211,7 +1190,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr #endif } - // TODO: check if is code is needed if (!lights.empty()) { if (hifiGlobalNodeID.isEmpty()) { @@ -1236,10 +1214,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr mapping.value("tz").toFloat())) * glm::mat4_cast(offsetRotation) * glm::scale(glm::vec3(offsetScale, offsetScale, offsetScale)); - for (QHash::const_iterator modelIDPair = modelIDsToNames.constBegin(); modelIDPair != modelIDsToNames.constEnd(); modelIDPair++) { - qCDebug(modelformat) << " model ID " << modelIDPair.key() << " name " << modelIDPair.value(); - } - // get the list of models in depth-first traversal order QVector modelIDs; QSet remainingFBXModels; @@ -1254,7 +1228,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr continue; } QString topID = getTopModelID(_connectionParentMap, fbxModels, _connectionChildMap.value(clusterID), url); - qCDebug(modelformat) << "fbx model name " << fbxModel.value().name << " top id " << topID << " modelID " << fbxModel.key(); _connectionChildMap.remove(_connectionParentMap.take(fbxModel.key()), fbxModel.key()); _connectionParentMap.insert(fbxModel.key(), topID); goto outerBreak; @@ -1278,8 +1251,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } QString topID = getTopModelID(_connectionParentMap, fbxModels, first, url); - qCDebug(modelformat) << "topper name fbx name " << modelIDsToNames.value(first) << " top id " << topID << " top name " << modelIDsToNames.value(topID); - qCDebug(modelformat) << "parent id " << _connectionParentMap.value(topID) << " parent name " << modelIDsToNames.value(_connectionParentMap.value(topID)) << " remaining models parent value " << remainingFBXModels.contains(_connectionParentMap.value(topID)); appendModelIDs(_connectionParentMap.value(topID), _connectionChildMap, fbxModels, remainingFBXModels, modelIDs, true); } @@ -1304,8 +1275,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr joint.parentIndex = fbxModel.parentIndex; int jointIndex = hfmModel.joints.size(); - qCDebug(modelformat) << "fbx joint name " << fbxModel.name << " joint index " << jointIndex << " parent index " << joint.parentIndex; - joint.translation = fbxModel.translation; // these are usually in centimeters joint.preTransform = fbxModel.preTransform; joint.preRotation = fbxModel.preRotation; From 300dd39abf76459eddb7b7da2f58e9bb665df433 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 13 Mar 2019 12:23:31 -0700 Subject: [PATCH 161/446] fix script engine shutdown --- interface/src/ui/Stats.cpp | 2 +- libraries/audio/src/AudioInjector.h | 4 +++- .../script-engine/src/AudioScriptingInterface.cpp | 10 ---------- libraries/script-engine/src/ScriptAudioInjector.cpp | 7 ------- libraries/script-engine/src/ScriptEngine.cpp | 7 +------ libraries/script-engine/src/ScriptEngines.cpp | 2 ++ 6 files changed, 7 insertions(+), 25 deletions(-) diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index 3c943028f5..022b57c0d9 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -268,7 +268,7 @@ void Stats::updateStats(bool force) { STAT_UPDATE(audioNoiseGate, audioClient->getNoiseGateOpen() ? "Open" : "Closed"); { int localInjectors = audioClient->getNumLocalInjectors(); - int nonLocalInjectors = DependencyManager::get()->getNumInjectors(); + size_t nonLocalInjectors = DependencyManager::get()->getNumInjectors(); STAT_UPDATE(audioInjectors, QVector2D(localInjectors, nonLocalInjectors)); } diff --git a/libraries/audio/src/AudioInjector.h b/libraries/audio/src/AudioInjector.h index 94527cfdd0..555af84025 100644 --- a/libraries/audio/src/AudioInjector.h +++ b/libraries/audio/src/AudioInjector.h @@ -87,9 +87,11 @@ public: void restart(); void finish(); - void finishLocalInjection(); void finishNetworkInjection(); +public slots: + void finishLocalInjection(); + signals: void finished(); void restarting(); diff --git a/libraries/script-engine/src/AudioScriptingInterface.cpp b/libraries/script-engine/src/AudioScriptingInterface.cpp index 395571c51f..a55cac292f 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.cpp +++ b/libraries/script-engine/src/AudioScriptingInterface.cpp @@ -46,16 +46,6 @@ ScriptAudioInjector* AudioScriptingInterface::playSystemSound(SharedSoundPointer } ScriptAudioInjector* AudioScriptingInterface::playSound(SharedSoundPointer sound, const AudioInjectorOptions& injectorOptions) { - if (QThread::currentThread() != thread()) { - ScriptAudioInjector* injector = NULL; - - BLOCKING_INVOKE_METHOD(this, "playSound", - Q_RETURN_ARG(ScriptAudioInjector*, injector), - Q_ARG(SharedSoundPointer, sound), - Q_ARG(const AudioInjectorOptions&, injectorOptions)); - return injector; - } - if (sound) { // stereo option isn't set from script, this comes from sound metadata or filename AudioInjectorOptions optionsCopy = injectorOptions; diff --git a/libraries/script-engine/src/ScriptAudioInjector.cpp b/libraries/script-engine/src/ScriptAudioInjector.cpp index 822aa0a9c1..267ac3339d 100644 --- a/libraries/script-engine/src/ScriptAudioInjector.cpp +++ b/libraries/script-engine/src/ScriptAudioInjector.cpp @@ -19,13 +19,6 @@ QScriptValue injectorToScriptValue(QScriptEngine* engine, ScriptAudioInjector* c return QScriptValue(QScriptValue::NullValue); } - // when the script goes down we want to cleanup the injector - QObject::connect(engine, &QScriptEngine::destroyed, DependencyManager::get().data(), [&] { - qCDebug(scriptengine) << "Script was shutdown, stopping an injector"; - // FIXME: this doesn't work and leaves the injectors lying around - //DependencyManager::get()->stop(in->_injector); - }); - return engine->newQObject(in, QScriptEngine::ScriptOwnership); } diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 825017b1fe..a4fd2540d4 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -260,12 +260,7 @@ bool ScriptEngine::isDebugMode() const { #endif } -ScriptEngine::~ScriptEngine() { - QSharedPointer scriptEngines(_scriptEngines); - if (scriptEngines) { - scriptEngines->removeScriptEngine(qSharedPointerCast(sharedFromThis())); - } -} +ScriptEngine::~ScriptEngine() {} void ScriptEngine::disconnectNonEssentialSignals() { disconnect(); diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index 3963ad5593..25c330e3fe 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -591,6 +591,8 @@ void ScriptEngines::onScriptFinished(const QString& rawScriptURL, ScriptEnginePo } } + removeScriptEngine(engine); + if (removed && !_isReloading) { // Update settings with removed script saveScripts(); From 822e5ceb98409e299e6500ca7b60aee167ef6691 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Wed, 13 Mar 2019 13:42:55 -0700 Subject: [PATCH 162/446] Case 21703 - Disable SUBMIT before balance is retrieved to prevent sending a send-money request with too large of a balance --- .../resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml index 68d437a346..626ac4da65 100644 --- a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml +++ b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml @@ -71,6 +71,7 @@ Item { onBalanceResult : { balanceText.text = result.data.balance; + sendButton.enabled = true; } onTransferAssetToNodeResult: { @@ -1371,6 +1372,7 @@ Item { height: 40; width: 100; text: "SUBMIT"; + enabled: false; onClicked: { if (root.assetCertID === "" && parseInt(amountTextField.text) > parseInt(balanceText.text)) { amountTextField.focus = true; From 277ef56f4941e9d7d9742be7a23406b3b05fd516 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 14 Mar 2019 10:24:19 +1300 Subject: [PATCH 163/446] Fill in JSDoc for new flow functions --- interface/src/avatar/MyAvatar.cpp | 33 +++++++++++++++++++++++++++++++ interface/src/avatar/MyAvatar.h | 15 +++++++------- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index e0353da1b4..e0e9b5b648 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -5443,6 +5443,39 @@ void MyAvatar::useFlow(bool isActive, bool isCollidable, const QVariantMap& phys } } +/**jsdoc + * Flow options currently used in flow simulation. + * @typedef {object} MyAvatar.FlowData + * @property {boolean} initialized - true if flow has been initialized for the current avatar, false + * if it hasn't. + * @property {boolean} active - true if flow is enabled, false if it isn't. + * @property {boolean} colliding - true if collisions are enabled, false if they aren't. + * @property {Object} physicsData - The physics configuration for each group of joints + * that has been configured. + * @property {Object} collisions - The collisions configuration for each joint that + * has collisions configured. + * @property {Object} threads - The threads hat have been configured, with the name of the first joint as + * the ThreadName and an array of the indexes of all the joints in the thread as the value. + */ +/**jsdoc + * A set of physics options currently used in flow simulation. + * @typedef {object} MyAvatar.FlowPhysicsData + * @property {boolean} active - true to enable flow on the joint, false if it isn't., + * @property {number} radius - The thickness of segments and knots. (Needed for collisions.) + * @property {number} gravity - Y-value of the gravity vector. + * @property {number} inertia - Rotational inertia multiplier. + * @property {number} damping - The amount of damping on joint oscillation. + * @property {number} stiffness - How stiff each thread is. + * @property {number} delta - Delta time for every integration step. + * @property {number[]} jointIndices - The indexes of the joints the options are applied to. + */ +/**jsdoc + * A set of collision options currently used in flow simulation. + * @typedef {object} MyAvatar.FlowCollisionsData + * @property {number} radius - Collision sphere radius. + * @property {number} offset - Offset of the collision sphere from the joint. + * @property {number} jointIndex - The index of the joint the options are applied to. + */ QVariantMap MyAvatar::getFlowData() { QVariantMap result; if (QThread::currentThread() != thread()) { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index c7585311b8..bd112bfacc 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1563,16 +1563,17 @@ public: Q_INVOKABLE void useFlow(bool isActive, bool isCollidable, const QVariantMap& physicsConfig = QVariantMap(), const QVariantMap& collisionsConfig = QVariantMap()); /**jsdoc - * @function MyAvatar.getFlowData - * @returns {object} - */ + * Gets the current flow configuration. + * @function MyAvatar.getFlowData + * @returns {MyAvatar.FlowData} + */ Q_INVOKABLE QVariantMap getFlowData(); /**jsdoc - * returns the indices of every colliding flow joint - * @function MyAvatar.getCollidingFlowJoints - * @returns {int[]} - */ + * Gets the indexes of currently colliding flow joints. + * @function MyAvatar.getCollidingFlowJoints + * @returns {number[]} The indexes of currently colliding flow joints. + */ Q_INVOKABLE QVariantList getCollidingFlowJoints(); public slots: From 74edea80346209b6d7c7665360e78d2a212bef25 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 14 Mar 2019 10:25:31 +1300 Subject: [PATCH 164/446] Miscellaneous JSDoc fixes --- 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 bd112bfacc..8951bc7fed 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -652,7 +652,7 @@ public: * Restores a default role animation. *

Each avatar has an avatar-animation.json file that defines a set of animation roles. Animation roles map to easily * understandable actions that the avatar can perform, such as "idleStand", "idleTalk", or - * "walkFwd". To get the full list of roles, use {#link MyAvatar.getAnimationRoles}. For each role, + * "walkFwd". To get the full list of roles, use {@link MyAvatar.getAnimationRoles}. For each role, * the avatar-animation.json defines when the animation is used, the animation clip (.FBX) used, and how animations are * blended together with procedural data (such as look-at vectors, hand sensors etc.). You can change the animation clip * (.FBX) associated with a specified animation role using {@link MyAvatar.overrideRoleAnimation}. @@ -1555,7 +1555,7 @@ public: * @param {boolean} isActive - true if flow simulation is enabled on the joint, false if it isn't. * @param {boolean} isCollidable - true to enable collisions in the flow simulation, false to * disable. - * @param {Object} [physicsConfig>] - Physic configurations for particular entity + * @param {Object} [physicsConfig>] - Physics configurations for particular entity * and avatar joints. * @param {Object} [collisionsConfig] - Collision configurations for particular * entity and avatar joints. From 9d739277c8a358376532ded6156d587ca638871b Mon Sep 17 00:00:00 2001 From: amantley Date: Wed, 13 Mar 2019 14:26:27 -0700 Subject: [PATCH 165/446] changed the fix so that we allow the root to be child --- libraries/fbx/src/FBXSerializer.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 4bb499bd84..f4eb1d57d9 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -1162,6 +1162,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr if (_connectionParentMap.value(getID(connection.properties, 1)) == "0") { // don't assign the new parent qCDebug(modelformat) << "root node " << getID(connection.properties, 1) << " has discarded parent " << getID(connection.properties, 2); + _connectionChildMap.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else { _connectionParentMap.insert(getID(connection.properties, 1), getID(connection.properties, 2)); _connectionChildMap.insert(getID(connection.properties, 2), getID(connection.properties, 1)); From 42bf81201932158a8dae7d7bc70c39495f34b888 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Wed, 13 Mar 2019 15:03:29 -0700 Subject: [PATCH 166/446] Case 21704 - marketplace button formatting fixes --- interface/resources/qml/controlsUit/Button.qml | 10 ++++++++++ .../qml/hifi/commerce/marketplace/MarketplaceItem.qml | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/interface/resources/qml/controlsUit/Button.qml b/interface/resources/qml/controlsUit/Button.qml index 3c5626e29e..6da85ed6d3 100644 --- a/interface/resources/qml/controlsUit/Button.qml +++ b/interface/resources/qml/controlsUit/Button.qml @@ -143,6 +143,16 @@ Original.Button { horizontalAlignment: Text.AlignHCenter text: control.text Component.onCompleted: { + setTextPosition(); + } + onTextChanged: { + setTextPosition(); + } + function setTextPosition() { + // force TextMetrics to re-evaluate the text field and glyph sizes + // as for some reason it's not automatically being done. + buttonGlyphTextMetrics.text = buttonGlyph.text; + buttonTextMetrics.text = text; if (control.buttonGlyph !== "") { buttonText.x = buttonContentItem.width/2 - buttonTextMetrics.width/2 + (buttonGlyphTextMetrics.width + control.buttonGlyphRightMargin)/2; } else { diff --git a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml index 605a68fe73..ce692c04d9 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml @@ -298,7 +298,7 @@ Rectangle { property bool isFreeSpecial: isStocking || isUpdate enabled: isFreeSpecial || (availability === 'available') buttonGlyph: (enabled && !isUpdate && (price > 0)) ? hifi.glyphs.hfc : "" - text: isUpdate ? "UPDATE FOR FREE" : (isStocking ? "FREE STOCK" : (enabled ? (price || "FREE") : availability)) + text: isUpdate ? "UPDATE" : (isStocking ? "FREE STOCK" : (enabled ? (price || "FREE") : availability)) color: hifi.buttons.blue buttonGlyphSize: 24 From 559351a6ebf81402c853de196d9e73b92ffa5ddf Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Wed, 13 Mar 2019 15:33:46 -0700 Subject: [PATCH 167/446] Case 21701 - Items were showing up in marketplace as "updates" when they weren't --- .../resources/qml/hifi/commerce/marketplace/Marketplace.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index 6f8150028a..727ead1fde 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -569,6 +569,8 @@ Rectangle { standaloneOptimized: model.standalone_optimized onShowItem: { + // reset the edition back to -1 to clear the 'update item' status + marketplaceItem.edition = -1; MarketplaceScriptingInterface.getMarketplaceItem(item_id); } From c14b135f2b960acad5d3a089a1edddd82b826777 Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Wed, 13 Mar 2019 15:42:04 -0700 Subject: [PATCH 168/446] Fix flow touch and scale issues --- interface/src/avatar/MyAvatar.cpp | 3 +- libraries/animation/src/Flow.cpp | 66 ++++++++++++++++++++----------- libraries/animation/src/Flow.h | 8 +++- 3 files changed, 52 insertions(+), 25 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 9211be3b4f..ecf904c7f4 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -5381,7 +5381,7 @@ void MyAvatar::useFlow(bool isActive, bool isCollidable, const QVariantMap& phys } auto collisionJoints = collisionsConfig.keys(); if (collisionJoints.size() > 0) { - collisionSystem.resetCollisions(); + collisionSystem.clearSelfCollisions(); for (auto &jointName : collisionJoints) { int jointIndex = getJointIndex(jointName); FlowCollisionSettings collisionsSettings; @@ -5396,6 +5396,7 @@ void MyAvatar::useFlow(bool isActive, bool isCollidable, const QVariantMap& phys collisionSystem.addCollisionSphere(jointIndex, collisionsSettings); } } + flow.updateScale(); } } diff --git a/libraries/animation/src/Flow.cpp b/libraries/animation/src/Flow.cpp index 5bc2021e5e..1f9e72bf28 100644 --- a/libraries/animation/src/Flow.cpp +++ b/libraries/animation/src/Flow.cpp @@ -67,17 +67,23 @@ void FlowCollisionSystem::addCollisionSphere(int jointIndex, const FlowCollision auto collision = FlowCollisionSphere(jointIndex, settings, isTouch); collision.setPosition(position); if (isSelfCollision) { - _selfCollisions.push_back(collision); + if (!isTouch) { + _selfCollisions.push_back(collision); + } else { + _selfTouchCollisions.push_back(collision); + } } else { _othersCollisions.push_back(collision); } - }; + void FlowCollisionSystem::resetCollisions() { _allCollisions.clear(); _othersCollisions.clear(); + _selfTouchCollisions.clear(); _selfCollisions.clear(); } + FlowCollisionResult FlowCollisionSystem::computeCollision(const std::vector collisions) { FlowCollisionResult result; if (collisions.size() > 1) { @@ -106,6 +112,10 @@ void FlowCollisionSystem::setScale(float scale) { _selfCollisions[j]._radius = _selfCollisions[j]._initialRadius * scale; _selfCollisions[j]._offset = _selfCollisions[j]._initialOffset * scale; } + for (size_t j = 0; j < _selfTouchCollisions.size(); j++) { + _selfTouchCollisions[j]._radius = _selfTouchCollisions[j]._initialRadius * scale; + _selfTouchCollisions[j]._offset = _selfTouchCollisions[j]._initialOffset * scale; + } }; std::vector FlowCollisionSystem::checkFlowThreadCollisions(FlowThread* flowThread) { @@ -178,9 +188,9 @@ void FlowCollisionSystem::setCollisionSettingsByJoint(int jointIndex, const Flow } void FlowCollisionSystem::prepareCollisions() { _allCollisions.clear(); - _allCollisions.resize(_selfCollisions.size() + _othersCollisions.size()); - std::copy(_selfCollisions.begin(), _selfCollisions.begin() + _selfCollisions.size(), _allCollisions.begin()); - std::copy(_othersCollisions.begin(), _othersCollisions.begin() + _othersCollisions.size(), _allCollisions.begin() + _selfCollisions.size()); + _allCollisions.insert(_allCollisions.end(), _selfCollisions.begin(), _selfCollisions.end()); + _allCollisions.insert(_allCollisions.end(), _othersCollisions.begin(), _othersCollisions.end()); + _allCollisions.insert(_allCollisions.end(), _selfTouchCollisions.begin(), _selfTouchCollisions.end()); _othersCollisions.clear(); } @@ -273,18 +283,20 @@ void FlowJoint::setRecoveryPosition(const glm::vec3& recoveryPosition) { } void FlowJoint::update(float deltaTime) { - glm::vec3 accelerationOffset = glm::vec3(0.0f); - if (_settings._stiffness > 0.0f) { - glm::vec3 recoveryVector = _recoveryPosition - _currentPosition; - float recoveryFactor = powf(_settings._stiffness, 3.0f); - accelerationOffset = recoveryVector * recoveryFactor; - } - FlowNode::update(deltaTime, accelerationOffset); - if (_anchored) { - if (!_isHelper) { - _currentPosition = _updatedPosition; - } else { - _currentPosition = _parentPosition; + if (_settings._active) { + glm::vec3 accelerationOffset = glm::vec3(0.0f); + if (_settings._stiffness > 0.0f) { + glm::vec3 recoveryVector = _recoveryPosition - _currentPosition; + float recoveryFactor = powf(_settings._stiffness, 3.0f); + accelerationOffset = recoveryVector * recoveryFactor; + } + FlowNode::update(deltaTime, accelerationOffset); + if (_anchored) { + if (!_isHelper) { + _currentPosition = _updatedPosition; + } else { + _currentPosition = _parentPosition; + } } } }; @@ -674,6 +686,14 @@ bool Flow::updateRootFramePositions(const AnimPoseVec& absolutePoses, size_t thr return true; } +void Flow::updateCollisionJoint(FlowCollisionSphere& collision, AnimPoseVec& absolutePoses) { + glm::quat jointRotation; + getJointPositionInWorldFrame(absolutePoses, collision._jointIndex, collision._position, _entityPosition, _entityRotation); + getJointRotationInWorldFrame(absolutePoses, collision._jointIndex, jointRotation, _entityRotation); + glm::vec3 worldOffset = jointRotation * collision._offset; + collision._position = collision._position + worldOffset; +} + void Flow::updateJoints(AnimPoseVec& relativePoses, AnimPoseVec& absolutePoses) { updateAbsolutePoses(relativePoses, absolutePoses); for (auto &jointData : _flowJointData) { @@ -695,11 +715,11 @@ void Flow::updateJoints(AnimPoseVec& relativePoses, AnimPoseVec& absolutePoses) } auto &selfCollisions = _collisionSystem.getSelfCollisions(); for (auto &collision : selfCollisions) { - glm::quat jointRotation; - getJointPositionInWorldFrame(absolutePoses, collision._jointIndex, collision._position, _entityPosition, _entityRotation); - getJointRotationInWorldFrame(absolutePoses, collision._jointIndex, jointRotation, _entityRotation); - glm::vec3 worldOffset = jointRotation * collision._offset; - collision._position = collision._position + worldOffset; + updateCollisionJoint(collision, absolutePoses); + } + auto &selfTouchCollisions = _collisionSystem.getSelfTouchCollisions(); + for (auto &collision : selfTouchCollisions) { + updateCollisionJoint(collision, absolutePoses); } _collisionSystem.prepareCollisions(); } @@ -710,7 +730,7 @@ void Flow::setJoints(AnimPoseVec& relativePoses, const std::vector& overri for (int jointIndex : joints) { auto &joint = _flowJointData[jointIndex]; if (jointIndex >= 0 && jointIndex < (int)relativePoses.size() && !overrideFlags[jointIndex]) { - relativePoses[jointIndex].rot() = joint.getCurrentRotation(); + relativePoses[jointIndex].rot() = joint.getSettings()._active ? joint.getCurrentRotation() : joint.getInitialRotation(); } } } diff --git a/libraries/animation/src/Flow.h b/libraries/animation/src/Flow.h index ad81c2be77..5dc1a3ba3e 100644 --- a/libraries/animation/src/Flow.h +++ b/libraries/animation/src/Flow.h @@ -140,6 +140,7 @@ public: std::vector checkFlowThreadCollisions(FlowThread* flowThread); std::vector& getSelfCollisions() { return _selfCollisions; }; + std::vector& getSelfTouchCollisions() { return _selfTouchCollisions; }; void setOthersCollisions(const std::vector& othersCollisions) { _othersCollisions = othersCollisions; } void prepareCollisions(); void resetCollisions(); @@ -150,9 +151,11 @@ public: void setActive(bool active) { _active = active; } bool getActive() const { return _active; } const std::vector& getCollisions() const { return _selfCollisions; } + void clearSelfCollisions() { _selfCollisions.clear(); } protected: std::vector _selfCollisions; std::vector _othersCollisions; + std::vector _selfTouchCollisions; std::vector _allCollisions; float _scale { 1.0f }; bool _active { false }; @@ -210,7 +213,7 @@ public: bool isHelper() const { return _isHelper; } const FlowPhysicsSettings& getSettings() { return _settings; } - void setSettings(const FlowPhysicsSettings& settings) { _settings = settings; } + void setSettings(const FlowPhysicsSettings& settings) { _settings = settings; _initialRadius = _settings._radius; } const glm::vec3& getCurrentPosition() const { return _currentPosition; } int getIndex() const { return _index; } @@ -222,6 +225,7 @@ public: const glm::quat& getCurrentRotation() const { return _currentRotation; } const glm::vec3& getCurrentTranslation() const { return _initialTranslation; } const glm::vec3& getInitialPosition() const { return _initialPosition; } + const glm::quat& getInitialRotation() const { return _initialRotation; } bool isColliding() const { return _colliding; } protected: @@ -297,6 +301,7 @@ public: void setPhysicsSettingsForGroup(const QString& group, const FlowPhysicsSettings& settings); const std::map& getGroupSettings() const { return _groupSettings; } void cleanUp(); + void updateScale() { setScale(_scale); } signals: void onCleanup(); @@ -311,6 +316,7 @@ private: void setJoints(AnimPoseVec& relativePoses, const std::vector& overrideFlags); void updateJoints(AnimPoseVec& relativePoses, AnimPoseVec& absolutePoses); + void updateCollisionJoint(FlowCollisionSphere& collision, AnimPoseVec& absolutePoses); bool updateRootFramePositions(const AnimPoseVec& absolutePoses, size_t threadIndex); void updateGroupSettings(const QString& group, const FlowPhysicsSettings& settings); void setScale(float scale); From 32d5f7135f6e40ef591214ed700ec14f29cb1776 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Wed, 13 Mar 2019 14:50:50 -0700 Subject: [PATCH 169/446] Give the oven model-baker Baker an appropriate materialMappingBaseURL, but disable ParseMaterialMappingTask for now --- libraries/baking/src/ModelBaker.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index d38b965f6d..c120153ddf 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -249,12 +249,15 @@ void ModelBaker::bakeSourceCopy() { serializerMapping["combineParts"] = true; // set true so that OBJSerializer reads material info from material library hfm::Model::Pointer loadedModel = serializer->read(modelData, serializerMapping, _modelURL); - baker::Baker baker(loadedModel, serializerMapping, hifi::URL()); + baker::Baker baker(loadedModel, serializerMapping, _mappingURL); auto config = baker.getConfiguration(); // Enable compressed draco mesh generation config->getJobConfig("BuildDracoMesh")->setEnabled(true); // Do not permit potentially lossy modification of joint data meant for runtime ((PrepareJointsConfig*)config->getJobConfig("PrepareJoints"))->passthrough = true; + // The resources parsed from this job will not be used for now + // TODO: Proper full baking of all materials for a model + config->getJobConfig("ParseMaterialMapping")->setEnabled(false); // Begin hfm baking baker.run(); From 3aaa18f529e1d9dcc5bf2050da450db324c28e4b Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Wed, 13 Mar 2019 15:01:42 -0700 Subject: [PATCH 170/446] Might as well deduplicate indices when loading model for baking --- libraries/baking/src/ModelBaker.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index c120153ddf..b6378d6503 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -247,6 +247,7 @@ void ModelBaker::bakeSourceCopy() { } hifi::VariantHash serializerMapping = _mapping; serializerMapping["combineParts"] = true; // set true so that OBJSerializer reads material info from material library + serializerMapping["deduplicateIndices"] = true; // Draco compression also deduplicates, but we might as well shave it off to save on some earlier processing (currently FBXSerializer only) hfm::Model::Pointer loadedModel = serializer->read(modelData, serializerMapping, _modelURL); baker::Baker baker(loadedModel, serializerMapping, _mappingURL); From cb1f42afe53ea7fd7c28be644a9c136b65956763 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Wed, 13 Mar 2019 15:16:58 -0700 Subject: [PATCH 171/446] Copy pre-parsed node from FBXSerializer for baking --- libraries/baking/src/FBXBaker.cpp | 43 ++++++++++------------------- libraries/baking/src/FBXBaker.h | 1 - libraries/baking/src/ModelBaker.cpp | 9 ++++++ 3 files changed, 23 insertions(+), 30 deletions(-) diff --git a/libraries/baking/src/FBXBaker.cpp b/libraries/baking/src/FBXBaker.cpp index d2dc86c783..5e346ab8c8 100644 --- a/libraries/baking/src/FBXBaker.cpp +++ b/libraries/baking/src/FBXBaker.cpp @@ -50,35 +50,7 @@ FBXBaker::FBXBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputText void FBXBaker::bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) { _hfmModel = hfmModel; - // Load the root node from the FBX file - importScene(); - - if (shouldStop()) { - return; - } - - // enumerate the models and textures found in the scene and start a bake for them - rewriteAndBakeSceneTextures(); - - if (shouldStop()) { - return; - } - - rewriteAndBakeSceneModels(hfmModel->meshes, dracoMeshes, dracoMaterialLists); -} - -void FBXBaker::importScene() { - qDebug() << "file path: " << _originalModelFilePath.toLocal8Bit().data() << QDir(_originalModelFilePath).exists(); - - QFile fbxFile(_originalModelFilePath); - if (!fbxFile.open(QIODevice::ReadOnly)) { - handleError("Error opening " + _originalModelFilePath + " for reading"); - return; - } - - qCDebug(model_baking) << "Parsing" << _modelURL; - _rootNode = FBXSerializer().parseFBX(&fbxFile); - + #ifdef HIFI_DUMP_FBX { FBXToJSON fbxToJSON; @@ -92,6 +64,19 @@ void FBXBaker::importScene() { } } #endif + + if (shouldStop()) { + return; + } + + // enumerate the models and textures found in the scene and start a bake for them + rewriteAndBakeSceneTextures(); + + if (shouldStop()) { + return; + } + + rewriteAndBakeSceneModels(hfmModel->meshes, dracoMeshes, dracoMaterialLists); } void FBXBaker::replaceMeshNodeWithDraco(FBXNode& meshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList) { diff --git a/libraries/baking/src/FBXBaker.h b/libraries/baking/src/FBXBaker.h index f8a023f431..3c95273f8f 100644 --- a/libraries/baking/src/FBXBaker.h +++ b/libraries/baking/src/FBXBaker.h @@ -38,7 +38,6 @@ protected: virtual void bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) override; private: - void importScene(); void rewriteAndBakeSceneModels(const QVector& meshes, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists); void rewriteAndBakeSceneTextures(); void replaceMeshNodeWithDraco(FBXNode& meshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList); diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index b6378d6503..d906abca8f 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include @@ -250,6 +251,14 @@ void ModelBaker::bakeSourceCopy() { serializerMapping["deduplicateIndices"] = true; // Draco compression also deduplicates, but we might as well shave it off to save on some earlier processing (currently FBXSerializer only) hfm::Model::Pointer loadedModel = serializer->read(modelData, serializerMapping, _modelURL); + // Temporarily support copying the pre-parsed node from FBXSerializer, for better performance in FBXBaker + // TODO: Pure HFM baking + std::shared_ptr fbxSerializer = std::dynamic_pointer_cast(serializer); + if (fbxSerializer) { + qCDebug(model_baking) << "Parsing" << _modelURL; + _rootNode = fbxSerializer->_rootNode; + } + baker::Baker baker(loadedModel, serializerMapping, _mappingURL); auto config = baker.getConfiguration(); // Enable compressed draco mesh generation From a3412bb25ee5f42fab5d582c00faeae816d68f15 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Wed, 13 Mar 2019 16:11:13 -0700 Subject: [PATCH 172/446] Attempt to fix build errors --- libraries/baking/CMakeLists.txt | 2 -- libraries/baking/src/MaterialBaker.cpp | 4 +++- libraries/baking/src/baking/FSTBaker.h | 2 +- libraries/model-baker/CMakeLists.txt | 4 +++- tools/oven/src/DomainBaker.cpp | 8 ++++---- tools/oven/src/DomainBaker.h | 8 ++++---- 6 files changed, 15 insertions(+), 13 deletions(-) diff --git a/libraries/baking/CMakeLists.txt b/libraries/baking/CMakeLists.txt index aeb4346f93..73618427f6 100644 --- a/libraries/baking/CMakeLists.txt +++ b/libraries/baking/CMakeLists.txt @@ -4,5 +4,3 @@ setup_hifi_library(Concurrent) link_hifi_libraries(shared shaders graphics networking material-networking graphics-scripting ktx image fbx model-baker task) include_hifi_library_headers(gpu) include_hifi_library_headers(hfm) - -target_draco() diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp index 558adedf68..24d031c39e 100644 --- a/libraries/baking/src/MaterialBaker.cpp +++ b/libraries/baking/src/MaterialBaker.cpp @@ -11,6 +11,8 @@ #include "MaterialBaker.h" +#include + #include "QJsonObject" #include "QJsonDocument" @@ -124,7 +126,7 @@ void MaterialBaker::processMaterial() { return; } - QPair textureKey = { textureURL, it->second }; + QPair textureKey { textureURL, it->second }; if (!_textureBakers.contains(textureKey)) { QSharedPointer textureBaker { new TextureBaker(textureURL, it->second, _textureOutputDir), diff --git a/libraries/baking/src/baking/FSTBaker.h b/libraries/baking/src/baking/FSTBaker.h index aeb7286af3..85c7c93a37 100644 --- a/libraries/baking/src/baking/FSTBaker.h +++ b/libraries/baking/src/baking/FSTBaker.h @@ -21,7 +21,7 @@ public: FSTBaker(const QUrl& inputMappingURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); - virtual QUrl getFullOutputMappingURL() const; + virtual QUrl getFullOutputMappingURL() const override; signals: void fstLoaded(); diff --git a/libraries/model-baker/CMakeLists.txt b/libraries/model-baker/CMakeLists.txt index 22c240b487..6c0f220340 100644 --- a/libraries/model-baker/CMakeLists.txt +++ b/libraries/model-baker/CMakeLists.txt @@ -4,4 +4,6 @@ setup_hifi_library() link_hifi_libraries(shared shaders task gpu graphics hfm material-networking) include_hifi_library_headers(networking) include_hifi_library_headers(image) -include_hifi_library_headers(ktx) \ No newline at end of file +include_hifi_library_headers(ktx) + +target_draco() diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 5f8ec3a678..639ab8b948 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -145,7 +145,7 @@ void DomainBaker::loadLocalFile() { } } -void DomainBaker::addModelBaker(const QString& property, const QString& url, QJsonValueRef& jsonRef) { +void DomainBaker::addModelBaker(const QString& property, const QString& url, const QJsonValueRef& jsonRef) { // grab a QUrl for the model URL QUrl bakeableModelURL = getBakeableModelURL(url); if (!bakeableModelURL.isEmpty() && (_shouldRebakeOriginals || !isModelBaked(bakeableModelURL))) { @@ -185,7 +185,7 @@ void DomainBaker::addModelBaker(const QString& property, const QString& url, QJs } } -void DomainBaker::addTextureBaker(const QString& property, const QString& url, image::TextureUsage::Type type, QJsonValueRef& jsonRef) { +void DomainBaker::addTextureBaker(const QString& property, const QString& url, image::TextureUsage::Type type, const QJsonValueRef& jsonRef) { QString cleanURL = QUrl(url).adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment).toDisplayString(); auto idx = cleanURL.lastIndexOf('.'); auto extension = idx >= 0 ? url.mid(idx + 1).toLower() : ""; @@ -225,7 +225,7 @@ void DomainBaker::addTextureBaker(const QString& property, const QString& url, i } } -void DomainBaker::addScriptBaker(const QString& property, const QString& url, QJsonValueRef& jsonRef) { +void DomainBaker::addScriptBaker(const QString& property, const QString& url, const QJsonValueRef& jsonRef) { // grab a clean version of the URL without a query or fragment QUrl scriptURL = QUrl(url).adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); @@ -257,7 +257,7 @@ void DomainBaker::addScriptBaker(const QString& property, const QString& url, QJ _entitiesNeedingRewrite.insert(scriptURL, { property, jsonRef }); } -void DomainBaker::addMaterialBaker(const QString& property, const QString& data, bool isURL, QJsonValueRef& jsonRef) { +void DomainBaker::addMaterialBaker(const QString& property, const QString& data, bool isURL, const QJsonValueRef& jsonRef) { // grab a clean version of the URL without a query or fragment QString materialData; if (isURL) { diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h index c9f5a59672..81f5c345cd 100644 --- a/tools/oven/src/DomainBaker.h +++ b/tools/oven/src/DomainBaker.h @@ -73,10 +73,10 @@ private: bool _shouldRebakeOriginals { false }; - void addModelBaker(const QString& property, const QString& url, QJsonValueRef& jsonRef); - void addTextureBaker(const QString& property, const QString& url, image::TextureUsage::Type type, QJsonValueRef& jsonRef); - void addScriptBaker(const QString& property, const QString& url, QJsonValueRef& jsonRef); - void addMaterialBaker(const QString& property, const QString& data, bool isURL, QJsonValueRef& jsonRef); + void addModelBaker(const QString& property, const QString& url, const QJsonValueRef& jsonRef); + void addTextureBaker(const QString& property, const QString& url, image::TextureUsage::Type type, const QJsonValueRef& jsonRef); + void addScriptBaker(const QString& property, const QString& url, const QJsonValueRef& jsonRef); + void addMaterialBaker(const QString& property, const QString& data, bool isURL, const QJsonValueRef& jsonRef); }; #endif // hifi_DomainBaker_h From 27c7bf5c922965b24117f941d2b5111de3f4665a Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Wed, 13 Mar 2019 17:02:55 -0700 Subject: [PATCH 173/446] Remove duplicate FBX debug dump --- libraries/baking/src/FBXBaker.cpp | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/libraries/baking/src/FBXBaker.cpp b/libraries/baking/src/FBXBaker.cpp index 5e346ab8c8..371a492964 100644 --- a/libraries/baking/src/FBXBaker.cpp +++ b/libraries/baking/src/FBXBaker.cpp @@ -33,10 +33,6 @@ #include "ModelBakingLoggingCategory.h" #include "TextureBaker.h" -#ifdef HIFI_DUMP_FBX -#include "FBXToJSON.h" -#endif - FBXBaker::FBXBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) : ModelBaker(inputModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, hasBeenBaked) { @@ -50,20 +46,6 @@ FBXBaker::FBXBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputText void FBXBaker::bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) { _hfmModel = hfmModel; - -#ifdef HIFI_DUMP_FBX - { - FBXToJSON fbxToJSON; - fbxToJSON << _rootNode; - QFileInfo modelFile(_originalModelFilePath); - QString outFilename(_bakedOutputDir + "/" + modelFile.completeBaseName() + "_FBX.json"); - QFile jsonFile(outFilename); - if (jsonFile.open(QIODevice::WriteOnly)) { - jsonFile.write(fbxToJSON.str().c_str(), fbxToJSON.str().length()); - jsonFile.close(); - } - } -#endif if (shouldStop()) { return; From a3dfd09e26335f41c43664a58f47ef558d7a222f Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 13 Mar 2019 17:03:53 -0700 Subject: [PATCH 174/446] adding scrolling in audio settings window --- interface/resources/qml/hifi/audio/Audio.qml | 165 ++++++++++++++---- .../qml/hifi/audio/AudioTabButton.qml | 2 +- 2 files changed, 133 insertions(+), 34 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index da306f911b..50329f9fa4 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -11,7 +11,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -import QtQuick 2.7 +import QtQuick 2.10 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 @@ -31,6 +31,8 @@ Rectangle { property string title: "Audio Settings" property int switchHeight: 16 property int switchWidth: 40 + readonly property real verticalScrollWidth: 10 + readonly property real verticalScrollShaft: 8 signal sendToScript(var message); color: hifi.colors.baseGray; @@ -42,7 +44,7 @@ Rectangle { property bool isVR: AudioScriptingInterface.context === "VR" - property real rightMostInputLevelPos: 450 + property real rightMostInputLevelPos: 440 //placeholder for control sizes and paddings //recalculates dynamically in case of UI size is changed QtObject { @@ -60,8 +62,8 @@ Rectangle { id: bar spacing: 0 width: parent.width - height: 42 - currentIndex: isVR ? 1 : 0 + height: 28; + currentIndex: isVR ? 1 : 0; AudioControls.AudioTabButton { height: parent.height @@ -92,25 +94,74 @@ Rectangle { Component.onCompleted: enablePeakValues(); - Column { - id: column - spacing: 12; - anchors.top: bar.bottom - anchors.bottom: parent.bottom - anchors.bottomMargin: 5 + Flickable { + id: flickView; + anchors.top: bar.bottom; + anchors.left: parent.left; + anchors.bottom: parent.bottom; width: parent.width; + contentWidth: parent.width; + contentHeight: contentItem.childrenRect.height; + boundsBehavior: Flickable.DragOverBounds; + flickableDirection: Flickable.VerticalFlick; + property bool isScrolling: (contentHeight - height) > 10 ? true : false; + clip: true; - Separator { } + ScrollBar.vertical: ScrollBar { + policy: flickView.isScrolling ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff; + parent: flickView.parent; + anchors.top: flickView.top; + anchors.right: flickView.right; + anchors.bottom: flickView.bottom; + anchors.rightMargin: -verticalScrollWidth; //compensate flickView's right margin + background: Item { + implicitWidth: verticalScrollWidth; + Rectangle { + color: hifi.colors.darkGray30; + radius: 4; + anchors { + fill: parent; + topMargin: -1; // Finesse size + bottomMargin: -2; + } + } + } + contentItem: Item { + implicitWidth: verticalScrollShaft; + Rectangle { + radius: verticalScrollShaft/2; + color: hifi.colors.white30; + anchors { + fill: parent; + leftMargin: 2; // Finesse size and position. + topMargin: 1; + bottomMargin: 1; + } + } + } + } - RowLayout { + Separator { + id: firstSeparator; + anchors.top: parent.top; + } + + Item { + id: switchesContainer; x: 2 * margins.paddings; width: parent.width; + // switch heights + 2 * top margins + height: (root.switchHeight) * 3 + 48; + anchors.top: firstSeparator.bottom; + anchors.topMargin: 10; // mute is in its own row - ColumnLayout { - id: columnOne - spacing: 24; - x: margins.paddings + Item { + id: switchContainer; + x: margins.paddings; + width: parent.width / 2; + height: parent.height; + anchors.left: parent.left; HifiControlsUit.Switch { id: muteMic; height: root.switchHeight; @@ -129,8 +180,12 @@ Rectangle { } HifiControlsUit.Switch { + id: noiseReductionSwitch; height: root.switchHeight; switchWidth: root.switchWidth; + anchors.top: muteMic.bottom; + anchors.topMargin: 24 + anchors.left: parent.left labelTextOn: "Noise Reduction"; backgroundOnColor: "#E3E3E3"; checked: AudioScriptingInterface.noiseReduction; @@ -144,6 +199,9 @@ Rectangle { id: pttSwitch height: root.switchHeight; switchWidth: root.switchWidth; + anchors.top: noiseReductionSwitch.bottom + anchors.topMargin: 24 + anchors.left: parent.left labelTextOn: qsTr("Push To Talk (T)"); backgroundOnColor: "#E3E3E3"; checked: (bar.currentIndex === 0) ? AudioScriptingInterface.pushToTalkDesktop : AudioScriptingInterface.pushToTalkHMD; @@ -164,12 +222,18 @@ Rectangle { } } - ColumnLayout { - spacing: 24; + Item { + id: additionalSwitchContainer + width: switchContainer.width - margins.paddings; + height: parent.height; + anchors.top: parent.top + anchors.left: switchContainer.right; HifiControlsUit.Switch { id: warnMutedSwitch height: root.switchHeight; switchWidth: root.switchWidth; + anchors.top: parent.top + anchors.left: parent.left labelTextOn: qsTr("Warn when muted"); backgroundOnColor: "#E3E3E3"; checked: AudioScriptingInterface.warnWhenMuted; @@ -184,6 +248,9 @@ Rectangle { id: audioLevelSwitch height: root.switchHeight; switchWidth: root.switchWidth; + anchors.top: warnMutedSwitch.bottom + anchors.topMargin: 24 + anchors.left: parent.left labelTextOn: qsTr("Audio Level Meter"); backgroundOnColor: "#E3E3E3"; checked: AvatarInputs.showAudioTools; @@ -197,6 +264,9 @@ Rectangle { id: stereoInput; height: root.switchHeight; switchWidth: root.switchWidth; + anchors.top: audioLevelSwitch.bottom + anchors.topMargin: 24 + anchors.left: parent.left labelTextOn: qsTr("Stereo input"); backgroundOnColor: "#E3E3E3"; checked: AudioScriptingInterface.isStereoInput; @@ -210,17 +280,20 @@ Rectangle { } Item { - anchors.left: parent.left + id: pttTextContainer + anchors.top: switchesContainer.bottom; + anchors.topMargin: 10; + anchors.left: parent.left; width: rightMostInputLevelPos; height: pttText.height; RalewayRegular { - id: pttText + id: pttText; x: margins.paddings; color: hifi.colors.white; width: rightMostInputLevelPos; height: paintedHeight; wrapMode: Text.WordWrap; - font.italic: true + font.italic: true; size: 16; text: (bar.currentIndex === 0) ? qsTr("Press and hold the button \"T\" to talk.") : @@ -228,28 +301,35 @@ Rectangle { } } - Separator { } + Separator { + id: secondSeparator; + anchors.top: pttTextContainer.bottom; + anchors.topMargin: 10; + } Item { + id: inputDeviceHeader x: margins.paddings; - width: parent.width - margins.paddings*2 - height: 36 + width: parent.width - margins.paddings*2; + height: 36; + anchors.top: secondSeparator.bottom; + anchors.topMargin: 10; HiFiGlyphs { - width: margins.sizeCheckBox + width: margins.sizeCheckBox; text: hifi.glyphs.mic; color: hifi.colors.white; - anchors.left: parent.left - anchors.leftMargin: -size/4 //the glyph has empty space at left about 25% + anchors.left: parent.left; + anchors.leftMargin: -size/4; //the glyph has empty space at left about 25% anchors.verticalCenter: parent.verticalCenter; size: 30; } RalewayRegular { anchors.verticalCenter: parent.verticalCenter; - width: margins.sizeText + margins.sizeLevel - anchors.left: parent.left - anchors.leftMargin: margins.sizeCheckBox + width: margins.sizeText + margins.sizeLevel; + anchors.left: parent.left; + anchors.leftMargin: margins.sizeCheckBox; size: 16; color: hifi.colors.white; text: qsTr("Choose input device"); @@ -257,8 +337,10 @@ Rectangle { } ListView { - id: inputView - width: parent.width - margins.paddings*2 + id: inputView; + width: parent.width - margins.paddings*2; + anchors.top: inputDeviceHeader.bottom; + anchors.topMargin: 10; x: margins.paddings height: Math.min(150, contentHeight); spacing: 4; @@ -302,16 +384,26 @@ Rectangle { } } AudioControls.LoopbackAudio { + id: loopbackAudio x: margins.paddings + anchors.top: inputView.bottom; + anchors.topMargin: 10; visible: (bar.currentIndex === 1 && isVR) || (bar.currentIndex === 0 && !isVR); anchors { left: parent.left; leftMargin: margins.paddings } } - Separator {} + Separator { + id: thirdSeparator; + anchors.top: loopbackAudio.bottom; + anchors.topMargin: 10; + } Item { + id: outputDeviceHeader; + anchors.topMargin: 10; + anchors.top: thirdSeparator.bottom; x: margins.paddings; width: parent.width - margins.paddings*2 height: 36 @@ -342,6 +434,8 @@ Rectangle { width: parent.width - margins.paddings*2 x: margins.paddings height: Math.min(360 - inputView.height, contentHeight); + anchors.top: outputDeviceHeader.bottom; + anchors.topMargin: 10; spacing: 4; snapMode: ListView.SnapToItem; clip: true; @@ -372,6 +466,8 @@ Rectangle { Item { id: gainContainer x: margins.paddings; + anchors.top: outputView.bottom; + anchors.topMargin: 10; width: parent.width - margins.paddings*2 height: gainSliderTextMetrics.height @@ -430,7 +526,10 @@ Rectangle { } AudioControls.PlaySampleSound { + id: playSampleSound x: margins.paddings + anchors.top: gainContainer.bottom; + anchors.topMargin: 10; visible: (bar.currentIndex === 1 && isVR) || (bar.currentIndex === 0 && !isVR); diff --git a/interface/resources/qml/hifi/audio/AudioTabButton.qml b/interface/resources/qml/hifi/audio/AudioTabButton.qml index 32331ccb6e..c81377e524 100644 --- a/interface/resources/qml/hifi/audio/AudioTabButton.qml +++ b/interface/resources/qml/hifi/audio/AudioTabButton.qml @@ -16,7 +16,7 @@ import stylesUit 1.0 TabButton { id: control - font.pixelSize: height / 2 + font.pixelSize: 14 HifiConstants { id: hifi; } From 9b3b109d2222058ae686f8b07d7620eccfb7eec6 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 13 Mar 2019 17:09:54 -0700 Subject: [PATCH 175/446] make placename consistent with hostname after domain reset --- libraries/networking/src/AddressManager.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index f4221e3d49..517daf8ce5 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -834,6 +834,7 @@ bool AddressManager::setDomainInfo(const QUrl& domainURL, LookupTrigger trigger) } _domainURL = domainURL; + _shareablePlaceName.clear(); // clear any current place information _rootPlaceID = QUuid(); From 5b75eb34e855450dc4c4ae90a1d35d736c07dc0b Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Wed, 13 Mar 2019 17:26:02 -0700 Subject: [PATCH 176/446] Fix ModelBaker not properly checking if texture file name exists --- libraries/baking/src/FBXBaker.h | 1 - libraries/baking/src/ModelBaker.cpp | 16 ++++++++-------- libraries/baking/src/ModelBaker.h | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/libraries/baking/src/FBXBaker.h b/libraries/baking/src/FBXBaker.h index 3c95273f8f..257efbe983 100644 --- a/libraries/baking/src/FBXBaker.h +++ b/libraries/baking/src/FBXBaker.h @@ -43,7 +43,6 @@ private: void replaceMeshNodeWithDraco(FBXNode& meshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList); hfm::Model::Pointer _hfmModel; - QHash _textureNameMatchCount; QHash _remappedTexturePaths; bool _pendingErrorEmission { false }; diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index d906abca8f..77584beb1b 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -417,10 +417,7 @@ QString ModelBaker::compressTexture(QString modelTextureFileName, image::Texture // construct the new baked texture file name and file path // ensuring that the baked texture will have a unique name // even if there was another texture with the same name at a different path - baseTextureFileName = createBaseTextureFileName(modelTextureFileInfo); - // If two textures have the same URL but are used differently, we need to process them separately - QString addMapChannel = QString::fromStdString("_" + std::to_string(textureType)); - baseTextureFileName += addMapChannel; + baseTextureFileName = createBaseTextureFileName(modelTextureFileInfo, textureType); _remappedTexturePaths[urlToTexture] = baseTextureFileName; } @@ -631,12 +628,15 @@ void ModelBaker::checkIfTexturesFinished() { } } -QString ModelBaker::createBaseTextureFileName(const QFileInfo& textureFileInfo) { +QString ModelBaker::createBaseTextureFileName(const QFileInfo& textureFileInfo, const image::TextureUsage::Type textureType) { + // If two textures have the same URL but are used differently, we need to process them separately + QString addMapChannel = QString::fromStdString("_" + std::to_string(textureType)); + + QString baseTextureFileName{ textureFileInfo.completeBaseName() + addMapChannel }; + // first make sure we have a unique base name for this texture // in case another texture referenced by this model has the same base name - auto& nameMatches = _textureNameMatchCount[textureFileInfo.baseName()]; - - QString baseTextureFileName{ textureFileInfo.completeBaseName() }; + auto& nameMatches = _textureNameMatchCount[baseTextureFileName]; if (nameMatches > 0) { // there are already nameMatches texture with this name diff --git a/libraries/baking/src/ModelBaker.h b/libraries/baking/src/ModelBaker.h index f1ef6db56d..6ee7511ce3 100644 --- a/libraries/baking/src/ModelBaker.h +++ b/libraries/baking/src/ModelBaker.h @@ -97,7 +97,7 @@ private slots: void handleAbortedTexture(); private: - QString createBaseTextureFileName(const QFileInfo & textureFileInfo); + QString createBaseTextureFileName(const QFileInfo & textureFileInfo, const image::TextureUsage::Type textureType); QUrl getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded = false); void bakeTexture(const QUrl & textureURL, image::TextureUsage::Type textureType, const QDir & outputDir, const QString & bakedFilename, const QByteArray & textureContent); From aeb56ff22a7b56af8634155fdb10a41437ee2513 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Thu, 14 Mar 2019 01:08:11 +0100 Subject: [PATCH 177/446] EntityList -> re-focus the rename field rather then re-selecting the text fully EntityProperties -> ignore selection updates when nothing is changed and window is focused --- scripts/system/html/js/entityList.js | 4 ++-- scripts/system/html/js/entityProperties.js | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index 8482591771..b15c4e6703 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -459,7 +459,7 @@ function loaded() { isRenameFieldBeingMoved = true; document.body.appendChild(elRenameInput); // keep the focus - elRenameInput.select(); + elRenameInput.focus(); } } @@ -475,7 +475,7 @@ function loaded() { elCell.innerHTML = ""; elCell.appendChild(elRenameInput); // keep the focus - elRenameInput.select(); + elRenameInput.focus(); isRenameFieldBeingMoved = false; } diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 863168d7fd..f501df7933 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -3325,6 +3325,13 @@ function loaded() { } let hasSelectedEntityChanged = lastEntityID !== '"' + selectedEntityProperties.id + '"'; + + if (!hasSelectedEntityChanged && document.hasFocus()) { + // in case the selection has not changed and we still have focus on the properties page, + // we will ignore the event. + return; + } + let doSelectElement = !hasSelectedEntityChanged; // the event bridge and json parsing handle our avatar id string differently. From 5e430c98c598185ba175a12636cc6d9053b872c7 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Thu, 14 Mar 2019 09:43:32 -0700 Subject: [PATCH 178/446] Attempt to fix build warnings --- libraries/baking/src/MaterialBaker.cpp | 6 +++--- .../model-baker/src/model-baker/BuildDracoMeshTask.cpp | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp index 24d031c39e..57dcde67de 100644 --- a/libraries/baking/src/MaterialBaker.cpp +++ b/libraries/baking/src/MaterialBaker.cpp @@ -109,7 +109,7 @@ void MaterialBaker::processMaterial() { QUrl textureURL = url.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); // FIXME: this isn't properly handling bumpMaps or glossMaps - static const std::unordered_map MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP { + static const std::unordered_map MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP({ { graphics::Material::MapChannel::EMISSIVE_MAP, image::TextureUsage::EMISSIVE_TEXTURE }, { graphics::Material::MapChannel::ALBEDO_MAP, image::TextureUsage::ALBEDO_TEXTURE }, { graphics::Material::MapChannel::METALLIC_MAP, image::TextureUsage::METALLIC_TEXTURE }, @@ -118,7 +118,7 @@ void MaterialBaker::processMaterial() { { graphics::Material::MapChannel::OCCLUSION_MAP, image::TextureUsage::OCCLUSION_TEXTURE }, { graphics::Material::MapChannel::LIGHTMAP_MAP, image::TextureUsage::LIGHTMAP_TEXTURE }, { graphics::Material::MapChannel::SCATTERING_MAP, image::TextureUsage::SCATTERING_TEXTURE } - }; + }); auto it = MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP.find(mapChannel); if (it == MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP.end()) { @@ -126,7 +126,7 @@ void MaterialBaker::processMaterial() { return; } - QPair textureKey { textureURL, it->second }; + QPair textureKey(textureURL, it->second); if (!_textureBakers.contains(textureKey)) { QSharedPointer textureBaker { new TextureBaker(textureURL, it->second, _textureOutputDir), diff --git a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp index e45b2bf584..46b170fd25 100644 --- a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp +++ b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp @@ -16,6 +16,11 @@ #pragma warning( push ) #pragma warning( disable : 4267 ) #endif +// gcc and clang +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wsign-compare" +#endif #include #include @@ -23,6 +28,9 @@ #ifdef _WIN32 #pragma warning( pop ) #endif +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif #include "ModelBakerLogging.h" #include "ModelMath.h" From 6c9c58c657edc0c5ac83d8aaf59aa3f9f870c32a Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Thu, 14 Mar 2019 11:36:40 -0700 Subject: [PATCH 179/446] Remove unneeded wait. --- tools/nitpick/src/TestCreator.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tools/nitpick/src/TestCreator.cpp b/tools/nitpick/src/TestCreator.cpp index f45a23e459..bbeef11a1f 100644 --- a/tools/nitpick/src/TestCreator.cpp +++ b/tools/nitpick/src/TestCreator.cpp @@ -851,10 +851,7 @@ void TestCreator::createRecursiveScript(const QString& directory, bool interacti textStream << " nitpick = createNitpick(Script.resolvePath(\".\"));" << endl; textStream << " testsRootPath = nitpick.getTestsRootPath();" << endl << endl; textStream << " nitpick.enableRecursive();" << endl; - textStream << " nitpick.enableAuto();" << endl << endl; - textStream << " if (typeof Test !== 'undefined') {" << endl; - textStream << " Test.wait(10000);" << endl; - textStream << " }" << endl; + textStream << " nitpick.enableAuto();" << endl; textStream << "} else {" << endl; textStream << " depth++" << endl; textStream << "}" << endl << endl; From 10eae54d8c44358e81b673a7cf50cb0a317782cb Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Thu, 14 Mar 2019 12:06:43 -0700 Subject: [PATCH 180/446] changing content height and snap mode to allow scroll --- interface/resources/qml/hifi/audio/Audio.qml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index 50329f9fa4..cd0f290da4 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -342,9 +342,8 @@ Rectangle { anchors.top: inputDeviceHeader.bottom; anchors.topMargin: 10; x: margins.paddings - height: Math.min(150, contentHeight); + height: contentHeight; spacing: 4; - snapMode: ListView.SnapToItem; clip: true; model: AudioScriptingInterface.devices.input; delegate: Item { @@ -433,11 +432,10 @@ Rectangle { id: outputView width: parent.width - margins.paddings*2 x: margins.paddings - height: Math.min(360 - inputView.height, contentHeight); + height: contentHeight; anchors.top: outputDeviceHeader.bottom; anchors.topMargin: 10; spacing: 4; - snapMode: ListView.SnapToItem; clip: true; model: AudioScriptingInterface.devices.output; delegate: Item { From 4470cf9ae5d87808ef935abf51a5291fdf9bf06d Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Thu, 14 Mar 2019 12:35:54 -0700 Subject: [PATCH 181/446] Marketplace category dropdown UI tweaks --- .../hifi/commerce/marketplace/Marketplace.qml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index 0f9e9e3620..c703a0e564 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -391,7 +391,7 @@ Rectangle { top: parent.top topMargin: 100 } - width: parent.width/2 + width: parent.width*2/3 color: hifi.colors.white @@ -431,7 +431,7 @@ Rectangle { anchors.leftMargin: 15 anchors.top:parent.top anchors.bottom: parent.bottom - anchors.left: parent.left + anchors.left: categoryItemCount.right elide: Text.ElideRight text: model.name @@ -445,23 +445,23 @@ Rectangle { anchors { top: parent.top bottom: parent.bottom - topMargin: 5 - bottomMargin: 5 + topMargin: 7 + bottomMargin: 7 leftMargin: 10 rightMargin: 10 - left: categoriesItemText.right + left: parent.left } width: childrenRect.width - color: hifi.colors.blueHighlight + color: hifi.colors.faintGray radius: height/2 RalewaySemiBold { anchors.top: parent.top anchors.bottom: parent.bottom - width: paintedWidth+30 + width: 50 text: model.count - color: hifi.colors.white + color: hifi.colors.lightGrayText horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter size: 16 From 19f856b760bbf71ca032eccc45f797c2f47b8faa Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 12 Mar 2019 15:01:11 -0700 Subject: [PATCH 182/446] Switch Oculus mobile to single draw FBO with multiple color attachments --- .../src/main/assets/shaders/present.frag | 37 +++ .../src/main/assets/shaders/present.vert | 21 ++ .../oculus/OculusMobileActivity.java | 7 +- .../oculusMobile/src/ovr/Framebuffer.cpp | 141 +++++++---- libraries/oculusMobile/src/ovr/Framebuffer.h | 26 +- libraries/oculusMobile/src/ovr/VrHandler.cpp | 232 +++++++++++++++--- 6 files changed, 372 insertions(+), 92 deletions(-) create mode 100644 android/libraries/oculus/src/main/assets/shaders/present.frag create mode 100644 android/libraries/oculus/src/main/assets/shaders/present.vert diff --git a/android/libraries/oculus/src/main/assets/shaders/present.frag b/android/libraries/oculus/src/main/assets/shaders/present.frag new file mode 100644 index 0000000000..4fbec70f57 --- /dev/null +++ b/android/libraries/oculus/src/main/assets/shaders/present.frag @@ -0,0 +1,37 @@ +#version 320 es + +precision highp float; +precision highp sampler2D; + +layout(location = 0) in vec4 vTexCoordLR; + +layout(location = 0) out vec4 FragColorL; +layout(location = 1) out vec4 FragColorR; + +uniform sampler2D sampler; + +// https://software.intel.com/en-us/node/503873 + +// sRGB ====> Linear +vec3 color_sRGBToLinear(vec3 srgb) { + return mix(pow((srgb + vec3(0.055)) / vec3(1.055), vec3(2.4)), srgb / vec3(12.92), vec3(lessThanEqual(srgb, vec3(0.04045)))); +} + +vec4 color_sRGBAToLinear(vec4 srgba) { + return vec4(color_sRGBToLinear(srgba.xyz), srgba.w); +} + +// Linear ====> sRGB +vec3 color_LinearTosRGB(vec3 lrgb) { + return mix(vec3(1.055) * pow(vec3(lrgb), vec3(0.41666)) - vec3(0.055), vec3(lrgb) * vec3(12.92), vec3(lessThan(lrgb, vec3(0.0031308)))); +} + +vec4 color_LinearTosRGBA(vec4 lrgba) { + return vec4(color_LinearTosRGB(lrgba.xyz), lrgba.w); +} + +// FIXME switch to texelfetch for getting from the source texture +void main() { + FragColorL = color_LinearTosRGBA(texture(sampler, vTexCoordLR.xy)); + FragColorR = color_LinearTosRGBA(texture(sampler, vTexCoordLR.zw)); +} diff --git a/android/libraries/oculus/src/main/assets/shaders/present.vert b/android/libraries/oculus/src/main/assets/shaders/present.vert new file mode 100644 index 0000000000..dfd6b1412f --- /dev/null +++ b/android/libraries/oculus/src/main/assets/shaders/present.vert @@ -0,0 +1,21 @@ +#version 320 es + +layout(location = 0) out vec4 vTexCoordLR; + +void main(void) { + const float depth = 0.0; + const vec4 UNIT_QUAD[4] = vec4[4]( + vec4(-1.0, -1.0, depth, 1.0), + vec4(1.0, -1.0, depth, 1.0), + vec4(-1.0, 1.0, depth, 1.0), + vec4(1.0, 1.0, depth, 1.0) + ); + vec4 pos = UNIT_QUAD[gl_VertexID]; + gl_Position = pos; + vTexCoordLR.xy = pos.xy; + vTexCoordLR.xy += 1.0; + vTexCoordLR.y *= 0.5; + vTexCoordLR.x *= 0.25; + vTexCoordLR.zw = vTexCoordLR.xy; + vTexCoordLR.z += 0.5; +} diff --git a/android/libraries/oculus/src/main/java/io/highfidelity/oculus/OculusMobileActivity.java b/android/libraries/oculus/src/main/java/io/highfidelity/oculus/OculusMobileActivity.java index 8ee22749c9..19865e7751 100644 --- a/android/libraries/oculus/src/main/java/io/highfidelity/oculus/OculusMobileActivity.java +++ b/android/libraries/oculus/src/main/java/io/highfidelity/oculus/OculusMobileActivity.java @@ -7,6 +7,7 @@ // package io.highfidelity.oculus; +import android.content.res.AssetManager; import android.os.Bundle; import android.util.Log; import android.view.Surface; @@ -24,7 +25,7 @@ public class OculusMobileActivity extends QtActivity implements SurfaceHolder.Ca private static final String TAG = OculusMobileActivity.class.getSimpleName(); static { System.loadLibrary("oculusMobile"); } - private native void nativeOnCreate(); + private native void nativeOnCreate(AssetManager assetManager); private native static void nativeOnResume(); private native static void nativeOnPause(); private native static void nativeOnSurfaceChanged(Surface s); @@ -53,7 +54,7 @@ public class OculusMobileActivity extends QtActivity implements SurfaceHolder.Ca mView = new SurfaceView(this); mView.getHolder().addCallback(this); - nativeOnCreate(); + nativeOnCreate(getAssets()); questNativeOnCreate(); } @@ -81,7 +82,7 @@ public class OculusMobileActivity extends QtActivity implements SurfaceHolder.Ca Log.w(TAG, "QQQ onResume"); super.onResume(); //Reconnect the global reference back to handler - nativeOnCreate(); + nativeOnCreate(getAssets()); questNativeOnResume(); nativeOnResume(); diff --git a/libraries/oculusMobile/src/ovr/Framebuffer.cpp b/libraries/oculusMobile/src/ovr/Framebuffer.cpp index 0f59eef614..57c45d3159 100644 --- a/libraries/oculusMobile/src/ovr/Framebuffer.cpp +++ b/libraries/oculusMobile/src/ovr/Framebuffer.cpp @@ -7,59 +7,44 @@ // #include "Framebuffer.h" +#include + #include -#include #include #include #include +#include "Helpers.h" + using namespace ovr; void Framebuffer::updateLayer(int eye, ovrLayerProjection2& layer, const ovrMatrix4f* projectionMatrix ) const { auto& layerTexture = layer.Textures[eye]; - layerTexture.ColorSwapChain = _swapChain; - layerTexture.SwapChainIndex = _index; + layerTexture.ColorSwapChain = _swapChainInfos[eye].swapChain; + layerTexture.SwapChainIndex = _swapChainInfos[eye].index; if (projectionMatrix) { layerTexture.TexCoordsFromTanAngles = ovrMatrix4f_TanAngleMatrixFromProjection( projectionMatrix ); } layerTexture.TextureRect = { 0, 0, 1, 1 }; } +void Framebuffer::SwapChainInfo::destroy() { + if (swapChain != nullptr) { + vrapi_DestroyTextureSwapChain(swapChain); + swapChain = nullptr; + } + index = -1; + length = -1; +} + void Framebuffer::create(const glm::uvec2& size) { _size = size; - _index = 0; - _validTexture = false; - - // Depth renderbuffer - /* glGenRenderbuffers(1, &_depth); - glBindRenderbuffer(GL_RENDERBUFFER, _depth); - glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, _size.x, _size.y); - glBindRenderbuffer(GL_RENDERBUFFER, 0); -*/ - // Framebuffer - glGenFramebuffers(1, &_fbo); - // glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo); - // glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depth); - // glBindFramebuffer(GL_FRAMEBUFFER, 0); - - _swapChain = vrapi_CreateTextureSwapChain3(VRAPI_TEXTURE_TYPE_2D, GL_RGBA8, _size.x, _size.y, 1, 3); - - _length = vrapi_GetTextureSwapChainLength(_swapChain); - if (!_length) { - __android_log_write(ANDROID_LOG_WARN, "QQQ_OVR", "Unable to count swap chain textures"); - return; - } - - for (int i = 0; i < _length; ++i) { - GLuint chainTexId = vrapi_GetTextureSwapChainHandle(_swapChain, i); - glBindTexture(GL_TEXTURE_2D, chainTexId); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - } + ovr::for_each_eye([&](ovrEye eye) { + _swapChainInfos[eye].create(size); + }); glBindTexture(GL_TEXTURE_2D, 0); + glGenFramebuffers(1, &_fbo); } void Framebuffer::destroy() { @@ -67,28 +52,80 @@ void Framebuffer::destroy() { glDeleteFramebuffers(1, &_fbo); _fbo = 0; } - if (0 != _depth) { - glDeleteRenderbuffers(1, &_depth); - _depth = 0; - } - if (_swapChain != nullptr) { - vrapi_DestroyTextureSwapChain(_swapChain); - _swapChain = nullptr; - } - _index = -1; - _length = -1; + + ovr::for_each_eye([&](ovrEye eye) { + _swapChainInfos[eye].destroy(); + }); } void Framebuffer::advance() { - _index = (_index + 1) % _length; - _validTexture = false; + ovr::for_each_eye([&](ovrEye eye) { + _swapChainInfos[eye].advance(); + }); } -void Framebuffer::bind() { - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo); - if (!_validTexture) { - GLuint chainTexId = vrapi_GetTextureSwapChainHandle(_swapChain, _index); - glFramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, chainTexId, 0); - _validTexture = true; +void Framebuffer::bind(GLenum target) { + glBindFramebuffer(target, _fbo); + _swapChainInfos[0].bind(target, GL_COLOR_ATTACHMENT0); + _swapChainInfos[1].bind(target, GL_COLOR_ATTACHMENT1); +} + +void Framebuffer::invalidate(GLenum target) { + static const std::array INVALIDATE_ATTACHMENTS {{ GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 }}; + glInvalidateFramebuffer(target, static_cast(INVALIDATE_ATTACHMENTS.size()), INVALIDATE_ATTACHMENTS.data()); +} + + +void Framebuffer::drawBuffers(ovrEye eye) const { + static const std::array, 3> EYE_DRAW_BUFFERS { { + {GL_COLOR_ATTACHMENT0, GL_NONE}, + {GL_NONE, GL_COLOR_ATTACHMENT1}, + {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1} + } }; + + switch(eye) { + case VRAPI_EYE_LEFT: + case VRAPI_EYE_RIGHT: + case VRAPI_EYE_COUNT: { + const auto& eyeDrawBuffers = EYE_DRAW_BUFFERS[eye]; + glDrawBuffers(static_cast(eyeDrawBuffers.size()), eyeDrawBuffers.data()); + } + break; + + default: + throw std::runtime_error("Invalid eye for drawBuffers"); + } +} + +void Framebuffer::SwapChainInfo::create(const glm::uvec2 &size) { + index = 0; + validTexture = false; + swapChain = vrapi_CreateTextureSwapChain3(VRAPI_TEXTURE_TYPE_2D, GL_RGBA8, size.x, size.y, 1, 3); + length = vrapi_GetTextureSwapChainLength(swapChain); + if (!length) { + __android_log_write(ANDROID_LOG_WARN, "QQQ_OVR", "Unable to count swap chain textures"); + throw std::runtime_error("Unable to create Oculus texture swap chain"); + } + + for (int i = 0; i < length; ++i) { + GLuint chainTexId = vrapi_GetTextureSwapChainHandle(swapChain, i); + glBindTexture(GL_TEXTURE_2D, chainTexId); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } +} + +void Framebuffer::SwapChainInfo::advance() { + index = (index + 1) % length; + validTexture = false; +} + +void Framebuffer::SwapChainInfo::bind(uint32_t target, uint32_t attachment) { + if (!validTexture) { + GLuint chainTexId = vrapi_GetTextureSwapChainHandle(swapChain, index); + glFramebufferTexture(target, attachment, chainTexId, 0); + validTexture = true; } } diff --git a/libraries/oculusMobile/src/ovr/Framebuffer.h b/libraries/oculusMobile/src/ovr/Framebuffer.h index 5127574462..4600d91534 100644 --- a/libraries/oculusMobile/src/ovr/Framebuffer.h +++ b/libraries/oculusMobile/src/ovr/Framebuffer.h @@ -9,6 +9,7 @@ #include #include +#include #include @@ -20,15 +21,28 @@ public: void create(const glm::uvec2& size); void advance(); void destroy(); - void bind(); + void bind(GLenum target = GL_DRAW_FRAMEBUFFER); + void invalidate(GLenum target = GL_DRAW_FRAMEBUFFER); + void drawBuffers(ovrEye eye) const; - uint32_t _depth { 0 }; + const glm::uvec2& size() const { return _size; } + +private: uint32_t _fbo{ 0 }; - int _length{ -1 }; - int _index{ -1 }; - bool _validTexture{ false }; glm::uvec2 _size; - ovrTextureSwapChain* _swapChain{ nullptr }; + struct SwapChainInfo { + int length{ -1 }; + int index{ -1 }; + bool validTexture{ false }; + ovrTextureSwapChain* swapChain{ nullptr }; + + void create(const glm::uvec2& size); + void destroy(); + void advance(); + void bind(GLenum target, GLenum attachment); + }; + + SwapChainInfo _swapChainInfos[VRAPI_FRAME_LAYER_EYE_MAX]; }; } // namespace ovr \ No newline at end of file diff --git a/libraries/oculusMobile/src/ovr/VrHandler.cpp b/libraries/oculusMobile/src/ovr/VrHandler.cpp index 6cc2ec9526..8748ec83cb 100644 --- a/libraries/oculusMobile/src/ovr/VrHandler.cpp +++ b/libraries/oculusMobile/src/ovr/VrHandler.cpp @@ -9,37 +9,186 @@ #include #include +#include +#include #include +#include +#include #include #include #include //#include + #include "GLContext.h" #include "Helpers.h" #include "Framebuffer.h" +static AAssetManager* ASSET_MANAGER = nullptr; +#define USE_BLIT_PRESENT 0 + +#if !USE_BLIT_PRESENT + + + +static std::string getTextAsset(const char* assetPath) { + if (!ASSET_MANAGER || !assetPath) { + return nullptr; + } + AAsset* asset = AAssetManager_open(ASSET_MANAGER, assetPath, AASSET_MODE_BUFFER); + if (!asset) { + return {}; + } + + auto length = AAsset_getLength(asset); + if (0 == length) { + AAsset_close(asset); + return {}; + } + + auto buffer = AAsset_getBuffer(asset); + if (!buffer) { + AAsset_close(asset); + return {}; + } + + std::string result { static_cast(buffer), static_cast(length) }; + AAsset_close(asset); + return result; +} + +static std::string getShaderInfoLog(GLuint glshader) { + std::string result; + GLint infoLength = 0; + glGetShaderiv(glshader, GL_INFO_LOG_LENGTH, &infoLength); + if (infoLength > 0) { + char* temp = new char[infoLength]; + glGetShaderInfoLog(glshader, infoLength, NULL, temp); + result = std::string(temp); + delete[] temp; + } + return result; +} + +static GLuint buildShader(GLenum shaderDomain, const char* shader) { + GLuint glshader = glCreateShader(shaderDomain); + if (!glshader) { + throw std::runtime_error("Bad shader"); + } + + glShaderSource(glshader, 1, &shader, NULL); + glCompileShader(glshader); + + GLint compiled = 0; + glGetShaderiv(glshader, GL_COMPILE_STATUS, &compiled); + + // if compilation fails + if (!compiled) { + std::string compileError = getShaderInfoLog(glshader); + glDeleteShader(glshader); + __android_log_print(ANDROID_LOG_WARN, "QQQ_OVR", "Shader compile error: %s", compileError.c_str()); + return 0; + } + + return glshader; +} + +static std::string getProgramInfoLog(GLuint glprogram) { + std::string result; + GLint infoLength = 0; + glGetProgramiv(glprogram, GL_INFO_LOG_LENGTH, &infoLength); + if (infoLength > 0) { + char* temp = new char[infoLength]; + glGetProgramInfoLog(glprogram, infoLength, NULL, temp); + result = std::string(temp); + delete[] temp; + } + return result; +} + +static GLuint buildProgram(const char* vertex, const char* fragment) { + // A brand new program: + GLuint glprogram { 0 }, glvertex { 0 }, glfragment { 0 }; + + try { + glprogram = glCreateProgram(); + if (0 == glprogram) { + throw std::runtime_error("Failed to create program, is GL context current?"); + } + + glvertex = buildShader(GL_VERTEX_SHADER, vertex); + if (0 == glvertex) { + throw std::runtime_error("Failed to create or compile vertex shader"); + } + glAttachShader(glprogram, glvertex); + + glfragment = buildShader(GL_FRAGMENT_SHADER, fragment); + if (0 == glfragment) { + throw std::runtime_error("Failed to create or compile fragment shader"); + } + glAttachShader(glprogram, glfragment); + + GLint linked { 0 }; + glLinkProgram(glprogram); + glGetProgramiv(glprogram, GL_LINK_STATUS, &linked); + + if (!linked) { + std::string linkErrorLog = getProgramInfoLog(glprogram); + __android_log_print(ANDROID_LOG_WARN, "QQQ_OVR", "Program link error: %s", linkErrorLog.c_str()); + throw std::runtime_error("Failed to link program, is the interface between the fragment and vertex shaders correct?"); + } + + + } catch(const std::runtime_error& error) { + if (0 != glprogram) { + glDeleteProgram(glprogram); + glprogram = 0; + } + } + + if (0 != glvertex) { + glDeleteShader(glvertex); + } + + if (0 != glfragment) { + glDeleteShader(glfragment); + } + + if (0 == glprogram) { + throw std::runtime_error("Failed to build program"); + } + + return glprogram; +} + +#endif using namespace ovr; static thread_local bool isRenderThread { false }; struct VrSurface : public TaskQueue { - using HandlerTask = VrHandler::HandlerTask; + using HandlerTask = ovr::VrHandler::HandlerTask; JavaVM* vm{nullptr}; jobject oculusActivity{ nullptr }; ANativeWindow* nativeWindow{ nullptr }; - VrHandler* handler{nullptr}; + ovr::VrHandler* handler{nullptr}; ovrMobile* session{nullptr}; bool resumed { false }; - GLContext vrglContext; - Framebuffer eyeFbos[2]; - uint32_t readFbo{0}; + ovr::GLContext vrglContext; + ovr::Framebuffer eyesFbo; + +#if USE_BLIT_PRESENT + GLuint readFbo { 0 }; +#else + GLuint renderProgram { 0 }; + GLuint renderVao { 0 }; +#endif std::atomic presentIndex{1}; double displayTime{0}; // Not currently set by anything @@ -76,6 +225,16 @@ struct VrSurface : public TaskQueue { vrglContext.create(currentDisplay, currentContext, noErrorContext); vrglContext.makeCurrent(); +#if USE_BLIT_PRESENT + glGenFramebuffers(1, &readFbo); +#else + glGenVertexArrays(1, &renderVao); + const char* vertex = nullptr; + auto vertexShader = getTextAsset("shaders/present.vert"); + auto fragmentShader = getTextAsset("shaders/present.frag"); + renderProgram = buildProgram(vertexShader.c_str(), fragmentShader.c_str()); +#endif + glm::uvec2 eyeTargetSize; withEnv([&](JNIEnv* env){ ovrJava java{ vm, env, oculusActivity }; @@ -85,10 +244,7 @@ struct VrSurface : public TaskQueue { }; }); __android_log_print(ANDROID_LOG_WARN, "QQQ_OVR", "QQQ Eye Size %d, %d", eyeTargetSize.x, eyeTargetSize.y); - ovr::for_each_eye([&](ovrEye eye) { - eyeFbos[eye].create(eyeTargetSize); - }); - glGenFramebuffers(1, &readFbo); + eyesFbo.create(eyeTargetSize); vrglContext.doneCurrent(); } @@ -178,38 +334,51 @@ struct VrSurface : public TaskQueue { void presentFrame(uint32_t sourceTexture, const glm::uvec2 &sourceSize, const ovrTracking2& tracking) { ovrLayerProjection2 layer = vrapi_DefaultLayerProjection2(); layer.HeadPose = tracking.HeadPose; + + eyesFbo.bind(); if (sourceTexture) { + eyesFbo.invalidate(); +#if USE_BLIT_PRESENT glBindFramebuffer(GL_READ_FRAMEBUFFER, readFbo); glFramebufferTexture(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, sourceTexture, 0); - GLenum framebufferStatus = glCheckFramebufferStatus(GL_READ_FRAMEBUFFER); - if (GL_FRAMEBUFFER_COMPLETE != framebufferStatus) { - __android_log_print(ANDROID_LOG_WARN, "QQQ_OVR", "incomplete framebuffer"); - } - } - GLenum invalidateAttachment = GL_COLOR_ATTACHMENT0; - - ovr::for_each_eye([&](ovrEye eye) { - const auto &eyeTracking = tracking.Eye[eye]; - auto &eyeFbo = eyeFbos[eye]; - const auto &destSize = eyeFbo._size; - eyeFbo.bind(); - glInvalidateFramebuffer(GL_DRAW_FRAMEBUFFER, 1, &invalidateAttachment); - if (sourceTexture) { + const auto &destSize = eyesFbo.size(); + ovr::for_each_eye([&](ovrEye eye) { auto sourceWidth = sourceSize.x / 2; auto sourceX = (eye == VRAPI_EYE_LEFT) ? 0 : sourceWidth; + // Each eye blit uses a different draw buffer + eyesFbo.drawBuffers(eye); glBlitFramebuffer( sourceX, 0, sourceX + sourceWidth, sourceSize.y, 0, 0, destSize.x, destSize.y, GL_COLOR_BUFFER_BIT, GL_NEAREST); - } - eyeFbo.updateLayer(eye, layer, &eyeTracking.ProjectionMatrix); - eyeFbo.advance(); - }); - if (sourceTexture) { - glInvalidateFramebuffer(GL_READ_FRAMEBUFFER, 1, &invalidateAttachment); + }); + static const std::array READ_INVALIDATE_ATTACHMENTS {{ GL_COLOR_ATTACHMENT0 }}; + glInvalidateFramebuffer(GL_READ_FRAMEBUFFER, (GLuint)READ_INVALIDATE_ATTACHMENTS.size(), READ_INVALIDATE_ATTACHMENTS.data()); glFramebufferTexture(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 0, 0); +#else + eyesFbo.drawBuffers(VRAPI_EYE_COUNT); + const auto &destSize = eyesFbo.size(); + glViewport(0, 0, destSize.x, destSize.y); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, sourceTexture); + glBindVertexArray(renderVao); + glUseProgram(renderProgram); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glUseProgram(0); + glBindVertexArray(0); +#endif + } else { + eyesFbo.drawBuffers(VRAPI_EYE_COUNT); + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); } - glFlush(); + + ovr::for_each_eye([&](ovrEye eye) { + const auto &eyeTracking = tracking.Eye[eye]; + eyesFbo.updateLayer(eye, layer, &eyeTracking.ProjectionMatrix); + }); + + eyesFbo.advance(); ovrLayerHeader2 *layerHeader = &layer.Header; ovrSubmitFrameDescription2 frameDesc = {}; @@ -321,8 +490,9 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *, void *) { return JNI_VERSION_1_6; } -JNIEXPORT void JNICALL Java_io_highfidelity_oculus_OculusMobileActivity_nativeOnCreate(JNIEnv* env, jobject obj) { +JNIEXPORT void JNICALL Java_io_highfidelity_oculus_OculusMobileActivity_nativeOnCreate(JNIEnv* env, jobject obj, jobject assetManager) { __android_log_write(ANDROID_LOG_WARN, "QQQ_JNI", __FUNCTION__); + ASSET_MANAGER = AAssetManager_fromJava(env, assetManager); SURFACE.onCreate(env, obj); } From 53b5a599b1c7600908ed5d41ddb27886c2eb814a Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 15 Mar 2019 09:12:16 +1300 Subject: [PATCH 183/446] Reinstate avatar script type tag --- tools/jsdoc/plugins/hifi.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tools/jsdoc/plugins/hifi.js b/tools/jsdoc/plugins/hifi.js index b4350ddbdb..184da9b618 100644 --- a/tools/jsdoc/plugins/hifi.js +++ b/tools/jsdoc/plugins/hifi.js @@ -107,6 +107,9 @@ exports.handlers = { if (e.doclet.hifiClientEntity) { rows.push("Client Entity Scripts"); } + if (e.doclet.hifiAvatar) { + rows.push("Avatar Scripts"); + } if (e.doclet.hifiServerEntity) { rows.push("Server Entity Scripts"); } @@ -155,6 +158,13 @@ exports.defineTags = function (dictionary) { } }); + // @hifi-avatar-script + dictionary.defineTag("hifi-avatar", { + onTagged: function (doclet, tag) { + doclet.hifiAvatar = true; + } + }); + // @hifi-client-entity dictionary.defineTag("hifi-client-entity", { onTagged: function (doclet, tag) { From 3016860bab91aa296a2f002a9b1aa03a8b7b3abd Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Thu, 14 Mar 2019 13:29:56 -0700 Subject: [PATCH 184/446] Fix QFile::open complaining the device was already open in TextureBaker::processTexture --- libraries/image/src/image/Image.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/image/src/image/Image.cpp b/libraries/image/src/image/Image.cpp index 6aa09c4d0f..2488b15fcd 100644 --- a/libraries/image/src/image/Image.cpp +++ b/libraries/image/src/image/Image.cpp @@ -205,7 +205,11 @@ QImage processRawImageData(QIODevice& content, const std::string& filename) { // Help the QImage loader by extracting the image file format from the url filename ext. // Some tga are not created properly without it. auto filenameExtension = filename.substr(filename.find_last_of('.') + 1); - content.open(QIODevice::ReadOnly); + if (!content.isReadable()) { + content.open(QIODevice::ReadOnly); + } else { + content.reset(); + } if (filenameExtension == "tga") { QImage image = image::readTGA(content); From bc696d6db628adca894b9f5fe46920d0ee1cac15 Mon Sep 17 00:00:00 2001 From: amantley Date: Thu, 14 Mar 2019 13:49:13 -0700 Subject: [PATCH 185/446] fixed memory leak caused by bone length scale computation --- libraries/animation/src/AnimClip.cpp | 66 +++++++++++++++------------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index 4fe02e9307..1a922e507d 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -124,41 +124,45 @@ void AnimClip::copyFromNetworkAnim() { _anim.resize(animFrameCount); // find the size scale factor for translation in the animation. - const int avatarHipsParentIndex = avatarSkeleton->getParentIndex(avatarSkeleton->nameToJointIndex("Hips")); - const int animHipsParentIndex = animSkeleton.getParentIndex(animSkeleton.nameToJointIndex("Hips")); - const AnimPose& avatarHipsAbsoluteDefaultPose = avatarSkeleton->getAbsoluteDefaultPose(avatarSkeleton->nameToJointIndex("Hips")); - const AnimPose& animHipsAbsoluteDefaultPose = animSkeleton.getAbsoluteDefaultPose(animSkeleton.nameToJointIndex("Hips")); - - // the get the units and the heights for the animation and the avatar - const float avatarUnitScale = extractScale(avatarSkeleton->getGeometryOffset()).y; - const float animationUnitScale = extractScale(animModel.offset).y; - const float avatarHeightInMeters = avatarUnitScale * avatarHipsAbsoluteDefaultPose.trans().y; - const float animHeightInMeters = animationUnitScale * animHipsAbsoluteDefaultPose.trans().y; - - // get the parent scales for the avatar and the animation - float avatarHipsParentScale = 1.0f; - if (avatarHipsParentIndex >= 0) { - const AnimPose& avatarHipsParentAbsoluteDefaultPose = avatarSkeleton->getAbsoluteDefaultPose(avatarHipsParentIndex); - avatarHipsParentScale = avatarHipsParentAbsoluteDefaultPose.scale().y; - } - float animHipsParentScale = 1.0f; - if (animHipsParentIndex >= 0) { - const AnimPose& animationHipsParentAbsoluteDefaultPose = animSkeleton.getAbsoluteDefaultPose(animHipsParentIndex); - animHipsParentScale = animationHipsParentAbsoluteDefaultPose.scale().y; - } - - const float EPSILON = 0.0001f; float boneLengthScale = 1.0f; - // compute the ratios for the units, the heights in meters, and the parent scales - if ((fabsf(animHeightInMeters) > EPSILON) && (animationUnitScale > EPSILON) && (animHipsParentScale > EPSILON)) { - const float avatarToAnimationHeightRatio = avatarHeightInMeters / animHeightInMeters; - const float unitsRatio = 1.0f / (avatarUnitScale / animationUnitScale); - const float parentScaleRatio = 1.0f / (avatarHipsParentScale / animHipsParentScale); + const int avatarHipsIndex = avatarSkeleton->nameToJointIndex("Hips"); + const int animHipsIndex = animSkeleton.nameToJointIndex("Hips"); + if (avatarHipsIndex != -1 && animHipsIndex != -1) { + const int avatarHipsParentIndex = avatarSkeleton->getParentIndex(avatarHipsIndex); + const int animHipsParentIndex = animSkeleton.getParentIndex(animHipsIndex); - boneLengthScale = avatarToAnimationHeightRatio * unitsRatio * parentScaleRatio; + const AnimPose& avatarHipsAbsoluteDefaultPose = avatarSkeleton->getAbsoluteDefaultPose(avatarHipsIndex); + const AnimPose& animHipsAbsoluteDefaultPose = animSkeleton.getAbsoluteDefaultPose(animHipsIndex); + + // the get the units and the heights for the animation and the avatar + const float avatarUnitScale = extractScale(avatarSkeleton->getGeometryOffset()).y; + const float animationUnitScale = extractScale(animModel.offset).y; + const float avatarHeightInMeters = avatarUnitScale * avatarHipsAbsoluteDefaultPose.trans().y; + const float animHeightInMeters = animationUnitScale * animHipsAbsoluteDefaultPose.trans().y; + + // get the parent scales for the avatar and the animation + float avatarHipsParentScale = 1.0f; + if (avatarHipsParentIndex != -1) { + const AnimPose& avatarHipsParentAbsoluteDefaultPose = avatarSkeleton->getAbsoluteDefaultPose(avatarHipsParentIndex); + avatarHipsParentScale = avatarHipsParentAbsoluteDefaultPose.scale().y; + } + float animHipsParentScale = 1.0f; + if (animHipsParentIndex != -1) { + const AnimPose& animationHipsParentAbsoluteDefaultPose = animSkeleton.getAbsoluteDefaultPose(animHipsParentIndex); + animHipsParentScale = animationHipsParentAbsoluteDefaultPose.scale().y; + } + + const float EPSILON = 0.0001f; + // compute the ratios for the units, the heights in meters, and the parent scales + if ((fabsf(animHeightInMeters) > EPSILON) && (animationUnitScale > EPSILON) && (animHipsParentScale > EPSILON)) { + const float avatarToAnimationHeightRatio = avatarHeightInMeters / animHeightInMeters; + const float unitsRatio = 1.0f / (avatarUnitScale / animationUnitScale); + const float parentScaleRatio = 1.0f / (avatarHipsParentScale / animHipsParentScale); + + boneLengthScale = avatarToAnimationHeightRatio * unitsRatio * parentScaleRatio; + } } - for (int frame = 0; frame < animFrameCount; frame++) { const HFMAnimationFrame& animFrame = animModel.animationFrames[frame]; From fd65f511408c55d616671de94a7ad60c9269f1d0 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 14 Mar 2019 14:09:06 -0700 Subject: [PATCH 186/446] Fix gamma correction on Quest --- .../libraries/oculus/src/main/assets/shaders/present.frag | 8 +++++--- libraries/oculusMobile/src/ovr/Framebuffer.cpp | 4 +++- libraries/oculusMobile/src/ovr/VrHandler.cpp | 1 + 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/android/libraries/oculus/src/main/assets/shaders/present.frag b/android/libraries/oculus/src/main/assets/shaders/present.frag index 4fbec70f57..f5d96932f8 100644 --- a/android/libraries/oculus/src/main/assets/shaders/present.frag +++ b/android/libraries/oculus/src/main/assets/shaders/present.frag @@ -30,8 +30,10 @@ vec4 color_LinearTosRGBA(vec4 lrgba) { return vec4(color_LinearTosRGB(lrgba.xyz), lrgba.w); } -// FIXME switch to texelfetch for getting from the source texture +// FIXME switch to texelfetch for getting from the source texture? void main() { - FragColorL = color_LinearTosRGBA(texture(sampler, vTexCoordLR.xy)); - FragColorR = color_LinearTosRGBA(texture(sampler, vTexCoordLR.zw)); + //FragColorL = color_LinearTosRGBA(texture(sampler, vTexCoordLR.xy)); + //FragColorR = color_LinearTosRGBA(texture(sampler, vTexCoordLR.zw)); + FragColorL = texture(sampler, vTexCoordLR.xy); + FragColorR = texture(sampler, vTexCoordLR.zw); } diff --git a/libraries/oculusMobile/src/ovr/Framebuffer.cpp b/libraries/oculusMobile/src/ovr/Framebuffer.cpp index 57c45d3159..a1dc1841de 100644 --- a/libraries/oculusMobile/src/ovr/Framebuffer.cpp +++ b/libraries/oculusMobile/src/ovr/Framebuffer.cpp @@ -100,7 +100,9 @@ void Framebuffer::drawBuffers(ovrEye eye) const { void Framebuffer::SwapChainInfo::create(const glm::uvec2 &size) { index = 0; validTexture = false; - swapChain = vrapi_CreateTextureSwapChain3(VRAPI_TEXTURE_TYPE_2D, GL_RGBA8, size.x, size.y, 1, 3); + // GL_SRGB8_ALPHA8 and GL_RGBA8 appear to behave the same here. The only thing that changes the + // output gamma behavior is VRAPI_MODE_FLAG_FRONT_BUFFER_SRGB passed to vrapi_EnterVrMode + swapChain = vrapi_CreateTextureSwapChain3(VRAPI_TEXTURE_TYPE_2D, GL_SRGB8_ALPHA8, size.x, size.y, 1, 3); length = vrapi_GetTextureSwapChainLength(swapChain); if (!length) { __android_log_write(ANDROID_LOG_WARN, "QQQ_OVR", "Unable to count swap chain textures"); diff --git a/libraries/oculusMobile/src/ovr/VrHandler.cpp b/libraries/oculusMobile/src/ovr/VrHandler.cpp index 8748ec83cb..a7b0f9f8ee 100644 --- a/libraries/oculusMobile/src/ovr/VrHandler.cpp +++ b/libraries/oculusMobile/src/ovr/VrHandler.cpp @@ -313,6 +313,7 @@ struct VrSurface : public TaskQueue { ovrJava java{ vm, env, oculusActivity }; ovrModeParms modeParms = vrapi_DefaultModeParms(&java); modeParms.Flags |= VRAPI_MODE_FLAG_NATIVE_WINDOW; + modeParms.Flags |= VRAPI_MODE_FLAG_FRONT_BUFFER_SRGB; if (noErrorContext) { modeParms.Flags |= VRAPI_MODE_FLAG_CREATE_CONTEXT_NO_ERROR; } From c985fc735de4f28bcb70f487b94eb39bc8aff1aa Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 14 Mar 2019 14:43:43 -0700 Subject: [PATCH 187/446] clean up avatar rendering and ring gizmo normals --- .../src/avatars-renderer/Avatar.cpp | 54 ------------------- .../src/avatars-renderer/Avatar.h | 5 -- .../src/avatars-renderer/SkeletonModel.cpp | 16 +++--- libraries/render-utils/src/GeometryCache.cpp | 6 +-- 4 files changed, 9 insertions(+), 72 deletions(-) diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index f3e671143b..46a810f6a4 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -1661,60 +1661,6 @@ int Avatar::parseDataFromBuffer(const QByteArray& buffer) { return bytesRead; } -int Avatar::_jointConesID = GeometryCache::UNKNOWN_ID; - -// render a makeshift cone section that serves as a body part connecting joint spheres -void Avatar::renderJointConnectingCone(gpu::Batch& batch, glm::vec3 position1, glm::vec3 position2, - float radius1, float radius2, const glm::vec4& color) { - - auto geometryCache = DependencyManager::get(); - - if (_jointConesID == GeometryCache::UNKNOWN_ID) { - _jointConesID = geometryCache->allocateID(); - } - - glm::vec3 axis = position2 - position1; - float length = glm::length(axis); - - if (length > 0.0f) { - - axis /= length; - - glm::vec3 perpSin = glm::vec3(1.0f, 0.0f, 0.0f); - glm::vec3 perpCos = glm::normalize(glm::cross(axis, perpSin)); - perpSin = glm::cross(perpCos, axis); - - float angleb = 0.0f; - QVector points; - - for (int i = 0; i < NUM_BODY_CONE_SIDES; i ++) { - - // the rectangles that comprise the sides of the cone section are - // referenced by "a" and "b" in one dimension, and "1", and "2" in the other dimension. - int anglea = angleb; - angleb = ((float)(i+1) / (float)NUM_BODY_CONE_SIDES) * TWO_PI; - - float sa = sinf(anglea); - float sb = sinf(angleb); - float ca = cosf(anglea); - float cb = cosf(angleb); - - glm::vec3 p1a = position1 + perpSin * sa * radius1 + perpCos * ca * radius1; - glm::vec3 p1b = position1 + perpSin * sb * radius1 + perpCos * cb * radius1; - glm::vec3 p2a = position2 + perpSin * sa * radius2 + perpCos * ca * radius2; - glm::vec3 p2b = position2 + perpSin * sb * radius2 + perpCos * cb * radius2; - - points << p1a << p1b << p2a << p1b << p2a << p2b; - } - - PROFILE_RANGE_BATCH(batch, __FUNCTION__); - // TODO: this is really inefficient constantly recreating these vertices buffers. It would be - // better if the avatars cached these buffers for each of the joints they are rendering - geometryCache->updateVertices(_jointConesID, points, color); - geometryCache->renderVertices(batch, gpu::TRIANGLES, _jointConesID); - } -} - float Avatar::getSkeletonHeight() const { Extents extents = _skeletonModel->getBindExtents(); return extents.maximum.y - extents.minimum.y; diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 6c31f9fc93..d81b04d4b2 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -296,9 +296,6 @@ public: virtual int parseDataFromBuffer(const QByteArray& buffer) override; - static void renderJointConnectingCone(gpu::Batch& batch, glm::vec3 position1, glm::vec3 position2, - float radius1, float radius2, const glm::vec4& color); - /**jsdoc * Set the offset applied to the current avatar. The offset adjusts the position that the avatar is rendered. For example, * with an offset of { x: 0, y: 0.1, z: 0 }, your avatar will appear to be raised off the ground slightly. @@ -665,8 +662,6 @@ protected: AvatarTransit _transit; std::mutex _transitLock; - static int _jointConesID; - int _voiceSphereID; float _displayNameTargetAlpha { 1.0f }; diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp index ea71ff128c..fbcf36a8c9 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp @@ -338,24 +338,20 @@ void SkeletonModel::computeBoundingShape() { void SkeletonModel::renderBoundingCollisionShapes(RenderArgs* args, gpu::Batch& batch, float scale, float alpha) { auto geometryCache = DependencyManager::get(); // draw a blue sphere at the capsule top point - glm::vec3 topPoint = _translation + getRotation() * (scale * (_boundingCapsuleLocalOffset + (0.5f * _boundingCapsuleHeight) * Vectors::UNIT_Y)); - + glm::vec3 topPoint = _translation + _rotation * (scale * (_boundingCapsuleLocalOffset + (0.5f * _boundingCapsuleHeight) * Vectors::UNIT_Y)); batch.setModelTransform(Transform().setTranslation(topPoint).postScale(scale * _boundingCapsuleRadius)); geometryCache->renderSolidSphereInstance(args, batch, glm::vec4(0.6f, 0.6f, 0.8f, alpha)); // draw a yellow sphere at the capsule bottom point - glm::vec3 bottomPoint = topPoint - glm::vec3(0.0f, scale * _boundingCapsuleHeight, 0.0f); - glm::vec3 axis = topPoint - bottomPoint; - + glm::vec3 bottomPoint = topPoint - _rotation * glm::vec3(0.0f, scale * _boundingCapsuleHeight, 0.0f); batch.setModelTransform(Transform().setTranslation(bottomPoint).postScale(scale * _boundingCapsuleRadius)); geometryCache->renderSolidSphereInstance(args, batch, glm::vec4(0.8f, 0.8f, 0.6f, alpha)); // draw a green cylinder between the two points - glm::vec3 origin(0.0f); - batch.setModelTransform(Transform().setTranslation(bottomPoint)); - geometryCache->bindSimpleProgram(batch); - Avatar::renderJointConnectingCone(batch, origin, axis, scale * _boundingCapsuleRadius, scale * _boundingCapsuleRadius, - glm::vec4(0.6f, 0.8f, 0.6f, alpha)); + float capsuleDiameter = 2.0f * _boundingCapsuleRadius; + glm::vec3 cylinderDimensions = glm::vec3(capsuleDiameter, _boundingCapsuleHeight, capsuleDiameter); + batch.setModelTransform(Transform().setScale(scale * cylinderDimensions).setRotation(_rotation).setTranslation(0.5f * (topPoint + bottomPoint))); + geometryCache->renderSolidShapeInstance(args, batch, GeometryCache::Shape::Cylinder, glm::vec4(0.6f, 0.8f, 0.6f, alpha)); } bool SkeletonModel::hasSkeleton() { diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index e322dc9d2b..0f400e00ee 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -1029,7 +1029,7 @@ void GeometryCache::updateVertices(int id, const QVector& points, con int* colorData = new int[details.vertices]; int* colorDataAt = colorData; - const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f); + const glm::vec3 NORMAL(0.0f, 1.0f, 0.0f); auto pointCount = points.size(); auto colorCount = colors.size(); int compactColor = 0; @@ -1107,7 +1107,7 @@ void GeometryCache::updateVertices(int id, const QVector& points, con int* colorData = new int[details.vertices]; int* colorDataAt = colorData; - const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f); + const glm::vec3 NORMAL(0.0f, 1.0f, 0.0f); auto pointCount = points.size(); auto colorCount = colors.size(); for (auto i = 0; i < pointCount; i++) { @@ -1195,7 +1195,7 @@ void GeometryCache::updateVertices(int id, const QVector& points, con int* colorData = new int[details.vertices]; int* colorDataAt = colorData; - const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f); + const glm::vec3 NORMAL(0.0f, 1.0f, 0.0f); for (int i = 0; i < points.size(); i++) { glm::vec3 point = points[i]; glm::vec2 texCoord = texCoords[i]; From f9f2b6f8ac5220068c3ec68d66f53e1cb1b981f2 Mon Sep 17 00:00:00 2001 From: David Back Date: Thu, 14 Mar 2019 15:03:33 -0700 Subject: [PATCH 188/446] avatar exporter 0.3.4/0.3.5 changes to master --- .../{ => AvatarExporter}/AvatarExporter.cs | 767 ++++++--- .../Assets/Editor/AvatarExporter/Average.mat | 76 + .../Assets/Editor/AvatarExporter/Floor.mat | 76 + .../AvatarExporter/HeightReference.prefab | 1393 +++++++++++++++++ .../Assets/Editor/AvatarExporter/Line.mat | 76 + .../Editor/AvatarExporter/ShortOrTall.mat | 76 + .../Editor/AvatarExporter/TooShortOrTall.mat | 76 + tools/unity-avatar-exporter/Assets/README.txt | 13 +- .../avatarExporter.unitypackage | Bin 16045 -> 74582 bytes 9 files changed, 2296 insertions(+), 257 deletions(-) rename tools/unity-avatar-exporter/Assets/Editor/{ => AvatarExporter}/AvatarExporter.cs (67%) create mode 100644 tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Average.mat create mode 100644 tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Floor.mat create mode 100644 tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/HeightReference.prefab create mode 100644 tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Line.mat create mode 100644 tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/ShortOrTall.mat create mode 100644 tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/TooShortOrTall.mat diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs similarity index 67% rename from tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs rename to tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs index c25a962824..142e4ae35a 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs @@ -6,15 +6,18 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -using UnityEngine; using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; using System; -using System.IO; using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; class AvatarExporter : MonoBehaviour { // update version number for every PR that changes this file, also set updated version in README file - static readonly string AVATAR_EXPORTER_VERSION = "0.3.3"; + static readonly string AVATAR_EXPORTER_VERSION = "0.3.5"; static readonly float HIPS_GROUND_MIN_Y = 0.01f; static readonly float HIPS_SPINE_CHEST_MIN_SEPARATION = 0.001f; @@ -22,6 +25,9 @@ class AvatarExporter : MonoBehaviour { static readonly string EMPTY_WARNING_TEXT = "None"; static readonly string TEXTURES_DIRECTORY = "textures"; static readonly string DEFAULT_MATERIAL_NAME = "No Name"; + static readonly string HEIGHT_REFERENCE_PREFAB = "Assets/Editor/AvatarExporter/HeightReference.prefab"; + static readonly Vector3 PREVIEW_CAMERA_PIVOT = new Vector3(0.0f, 1.755f, 0.0f); + static readonly Vector3 PREVIEW_CAMERA_DIRECTION = new Vector3(0.0f, 0.0f, -1.0f); // TODO: use regex static readonly string[] RECOMMENDED_UNITY_VERSIONS = new string[] { @@ -298,18 +304,17 @@ class AvatarExporter : MonoBehaviour { if (!string.IsNullOrEmpty(occlusionMap)) { json += "\"occlusionMap\": \"" + occlusionMap + "\", "; } - json += "\"emissive\": [" + emissive.r + ", " + emissive.g + ", " + emissive.b + "] "; + json += "\"emissive\": [" + emissive.r + ", " + emissive.g + ", " + emissive.b + "]"; if (!string.IsNullOrEmpty(emissiveMap)) { - json += "\", emissiveMap\": \"" + emissiveMap + "\""; + json += ", \"emissiveMap\": \"" + emissiveMap + "\""; } - json += "} }"; + json += " } }"; return json; } } static string assetPath = ""; - static string assetName = ""; - + static string assetName = ""; static ModelImporter modelImporter; static HumanDescription humanDescription; @@ -317,12 +322,23 @@ class AvatarExporter : MonoBehaviour { static Dictionary humanoidToUserBoneMappings = new Dictionary(); static BoneTreeNode userBoneTree = new BoneTreeNode(); static Dictionary failedAvatarRules = new Dictionary(); + static string warnings = ""; static Dictionary textureDependencies = new Dictionary(); static Dictionary materialMappings = new Dictionary(); static Dictionary materialDatas = new Dictionary(); - static List materialAlternateStandardShader = new List(); - static Dictionary materialUnsupportedShader = new Dictionary(); + static List alternateStandardShaderMaterials = new List(); + static List unsupportedShaderMaterials = new List(); + + static Scene previewScene; + static string previousScene = ""; + static Vector3 previousScenePivot = Vector3.zero; + static Quaternion previousSceneRotation = Quaternion.identity; + static float previousSceneSize = 0.0f; + static bool previousSceneOrthographic = false; + static UnityEngine.Object avatarResource; + static GameObject avatarPreviewObject; + static GameObject heightReferenceObject; [MenuItem("High Fidelity/Export New Avatar")] static void ExportNewAvatar() { @@ -339,8 +355,8 @@ class AvatarExporter : MonoBehaviour { EditorUtility.DisplayDialog("About", "High Fidelity, Inc.\nAvatar Exporter\nVersion " + AVATAR_EXPORTER_VERSION, "Ok"); } - static void ExportSelectedAvatar(bool updateAvatar) { - // ensure everything is saved to file before exporting + static void ExportSelectedAvatar(bool updateExistingAvatar) { + // ensure everything is saved to file before doing anything AssetDatabase.SaveAssets(); string[] guids = Selection.assetGUIDs; @@ -364,6 +380,11 @@ class AvatarExporter : MonoBehaviour { " the Rig section of it's Inspector window.", "Ok"); return; } + + avatarResource = AssetDatabase.LoadAssetAtPath(assetPath, typeof(UnityEngine.Object)); + humanDescription = modelImporter.humanDescription; + + string textureWarnings = SetTextureDependencies(); // if the rig is optimized we should de-optimize it during the export process bool shouldDeoptimizeGameObjects = modelImporter.optimizeGameObjects; @@ -371,28 +392,23 @@ class AvatarExporter : MonoBehaviour { modelImporter.optimizeGameObjects = false; modelImporter.SaveAndReimport(); } - - humanDescription = modelImporter.humanDescription; - string textureWarnings = SetTextureDependencies(); + SetBoneAndMaterialInformation(); + if (shouldDeoptimizeGameObjects) { + // switch back to optimized game object in case it was originally optimized + modelImporter.optimizeGameObjects = true; + modelImporter.SaveAndReimport(); + } + // check if we should be substituting a bone for a missing UpperChest mapping AdjustUpperChestMapping(); // format resulting avatar rule failure strings // consider export-blocking avatar rules to be errors and show them in an error dialog, // and also include any other avatar rule failures plus texture warnings as warnings in the dialog - if (shouldDeoptimizeGameObjects) { - // switch back to optimized game object in case it was originally optimized - modelImporter.optimizeGameObjects = true; - modelImporter.SaveAndReimport(); - } - - // format resulting bone rule failure strings - // consider export-blocking bone rules to be errors and show them in an error dialog, - // and also include any other bone rule failures plus texture warnings as warnings in the dialog string boneErrors = ""; - string warnings = ""; + warnings = ""; foreach (var failedAvatarRule in failedAvatarRules) { if (Array.IndexOf(EXPORT_BLOCKING_AVATAR_RULES, failedAvatarRule.Key) >= 0) { boneErrors += failedAvatarRule.Value + "\n\n"; @@ -400,15 +416,16 @@ class AvatarExporter : MonoBehaviour { warnings += failedAvatarRule.Value + "\n\n"; } } - foreach (string materialName in materialAlternateStandardShader) { - warnings += "The material " + materialName + " is not using the recommended variation of the Standard shader. " + - "We recommend you change it to Standard (Roughness setup) shader for improved performance.\n\n"; - } - foreach (var material in materialUnsupportedShader) { - warnings += "The material " + material.Key + " is using an unsupported shader " + material.Value + - ". Please change it to a Standard shader type.\n\n"; - } + + // add material and texture warnings after bone-related warnings + AddMaterialWarnings(); warnings += textureWarnings; + + // remove trailing newlines at the end of the warnings + if (!string.IsNullOrEmpty(warnings)) { + warnings = warnings.Substring(0, warnings.LastIndexOf("\n\n")); + } + if (!string.IsNullOrEmpty(boneErrors)) { // if there are both errors and warnings then warnings will be displayed with errors in the error dialog if (!string.IsNullOrEmpty(warnings)) { @@ -421,150 +438,157 @@ class AvatarExporter : MonoBehaviour { return; } + // since there are no errors we can now open the preview scene in place of the user's scene + if (!OpenPreviewScene()) { + return; + } + + // show None instead of blank warnings if there are no warnings in the export windows + if (string.IsNullOrEmpty(warnings)) { + warnings = EMPTY_WARNING_TEXT; + } + string documentsFolder = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments); string hifiFolder = documentsFolder + "\\High Fidelity Projects"; - if (updateAvatar) { // Update Existing Avatar menu option - bool copyModelToExport = false; + if (updateExistingAvatar) { // Update Existing Avatar menu option + // open update existing project popup window including project to update, scale, and warnings + // default the initial file chooser location to HiFi projects folder in user documents folder + ExportProjectWindow window = ScriptableObject.CreateInstance(); string initialPath = Directory.Exists(hifiFolder) ? hifiFolder : documentsFolder; - - // open file explorer defaulting to hifi projects folder in user documents to select target fst to update - string exportFstPath = EditorUtility.OpenFilePanel("Select .fst to update", initialPath, "fst"); - if (exportFstPath.Length == 0) { // file selection cancelled - return; - } - exportFstPath = exportFstPath.Replace('/', '\\'); - - // lookup the project name field from the fst file to update - string projectName = ""; - try { - string[] lines = File.ReadAllLines(exportFstPath); - foreach (string line in lines) { - int separatorIndex = line.IndexOf("="); - if (separatorIndex >= 0) { - string key = line.Substring(0, separatorIndex).Trim(); - if (key == "name") { - projectName = line.Substring(separatorIndex + 1).Trim(); - break; - } - } - } - } catch { - EditorUtility.DisplayDialog("Error", "Failed to read from existing file " + exportFstPath + - ". Please check the file and try again.", "Ok"); - return; - } - - string exportModelPath = Path.GetDirectoryName(exportFstPath) + "\\" + assetName + ".fbx"; - if (File.Exists(exportModelPath)) { - // if the fbx in Unity Assets is newer than the fbx in the target export - // folder or vice-versa then ask to replace the older fbx with the newer fbx - DateTime assetModelWriteTime = File.GetLastWriteTime(assetPath); - DateTime targetModelWriteTime = File.GetLastWriteTime(exportModelPath); - if (assetModelWriteTime > targetModelWriteTime) { - int option = EditorUtility.DisplayDialogComplex("Error", "The " + assetName + - ".fbx model in the Unity Assets folder is newer than the " + exportModelPath + - " model.\n\nDo you want to replace the older .fbx with the newer .fbx?", - "Yes", "No", "Cancel"); - if (option == 2) { // Cancel - return; - } - copyModelToExport = option == 0; // Yes - } else if (assetModelWriteTime < targetModelWriteTime) { - int option = EditorUtility.DisplayDialogComplex("Error", "The " + exportModelPath + - " model is newer than the " + assetName + ".fbx model in the Unity Assets folder." + - "\n\nDo you want to replace the older .fbx with the newer .fbx and re-import it?", - "Yes", "No" , "Cancel"); - if (option == 2) { // Cancel - return; - } else if (option == 0) { // Yes - copy model to Unity project - // copy the fbx from the project folder to Unity Assets, overwriting the existing fbx, and re-import it - try { - File.Copy(exportModelPath, assetPath, true); - } catch { - EditorUtility.DisplayDialog("Error", "Failed to copy existing file " + exportModelPath + " to " + assetPath + - ". Please check the location and try again.", "Ok"); - return; - } - AssetDatabase.ImportAsset(assetPath); - - // set model to Humanoid animation type and force another refresh on it to process Humanoid - modelImporter = ModelImporter.GetAtPath(assetPath) as ModelImporter; - modelImporter.animationType = ModelImporterAnimationType.Human; - EditorUtility.SetDirty(modelImporter); - modelImporter.SaveAndReimport(); - - // redo parent names, joint mappings, and user bone positions due to the fbx change - // as well as re-check the avatar rules for failures - humanDescription = modelImporter.humanDescription; - SetBoneAndMaterialInformation(); - } - } - } else { - // if no matching fbx exists in the target export folder then ask to copy fbx over - int option = EditorUtility.DisplayDialogComplex("Error", "There is no existing " + exportModelPath + - " model.\n\nDo you want to copy over the " + assetName + - ".fbx model from the Unity Assets folder?", "Yes", "No", "Cancel"); - if (option == 2) { // Cancel - return; - } - copyModelToExport = option == 0; // Yes - } - - // copy asset fbx over deleting any existing fbx if we agreed to overwrite it - if (copyModelToExport) { - try { - File.Copy(assetPath, exportModelPath, true); - } catch { - EditorUtility.DisplayDialog("Error", "Failed to copy existing file " + assetPath + " to " + exportModelPath + - ". Please check the location and try again.", "Ok"); - return; - } - } - - // delete existing fst file since we will write a new file - // TODO: updating fst should only rewrite joint mappings and joint rotation offsets to existing file - try { - File.Delete(exportFstPath); - } catch { - EditorUtility.DisplayDialog("Error", "Failed to overwrite existing file " + exportFstPath + - ". Please check the file and try again.", "Ok"); - return; - } - - // write out a new fst file in place of the old file - if (!WriteFST(exportFstPath, projectName)) { - return; - } - - // copy any external texture files to the project's texture directory that are considered dependencies of the model - string texturesDirectory = GetTextureDirectory(exportFstPath); - if (!CopyExternalTextures(texturesDirectory)) { - return; - } - - // display success dialog with any avatar rule warnings - string successDialog = "Avatar successfully updated!"; - if (!string.IsNullOrEmpty(warnings)) { - successDialog += "\n\nWarnings:\n" + warnings; - } - EditorUtility.DisplayDialog("Success!", successDialog, "Ok"); + window.Init(initialPath, warnings, updateExistingAvatar, avatarPreviewObject, OnUpdateExistingProject, OnExportWindowClose); } else { // Export New Avatar menu option // create High Fidelity Projects folder in user documents folder if it doesn't exist if (!Directory.Exists(hifiFolder)) { Directory.CreateDirectory(hifiFolder); } - if (string.IsNullOrEmpty(warnings)) { - warnings = EMPTY_WARNING_TEXT; - } - - // open a popup window to enter new export project name and project location + // open export new project popup window including project name, project location, scale, and warnings + // default the initial project location path to the High Fidelity Projects folder above ExportProjectWindow window = ScriptableObject.CreateInstance(); - window.Init(hifiFolder, warnings, OnExportProjectWindowClose); + window.Init(hifiFolder, warnings, updateExistingAvatar, avatarPreviewObject, OnExportNewProject, OnExportWindowClose); } } - static void OnExportProjectWindowClose(string projectDirectory, string projectName, string warnings) { + static void OnUpdateExistingProject(string exportFstPath, string projectName, float scale) { + bool copyModelToExport = false; + + // lookup the project name field from the fst file to update + projectName = ""; + try { + string[] lines = File.ReadAllLines(exportFstPath); + foreach (string line in lines) { + int separatorIndex = line.IndexOf("="); + if (separatorIndex >= 0) { + string key = line.Substring(0, separatorIndex).Trim(); + if (key == "name") { + projectName = line.Substring(separatorIndex + 1).Trim(); + break; + } + } + } + } catch { + EditorUtility.DisplayDialog("Error", "Failed to read from existing file " + exportFstPath + + ". Please check the file and try again.", "Ok"); + return; + } + + string exportModelPath = Path.GetDirectoryName(exportFstPath) + "\\" + assetName + ".fbx"; + if (File.Exists(exportModelPath)) { + // if the fbx in Unity Assets is newer than the fbx in the target export + // folder or vice-versa then ask to replace the older fbx with the newer fbx + DateTime assetModelWriteTime = File.GetLastWriteTime(assetPath); + DateTime targetModelWriteTime = File.GetLastWriteTime(exportModelPath); + if (assetModelWriteTime > targetModelWriteTime) { + int option = EditorUtility.DisplayDialogComplex("Error", "The " + assetName + + ".fbx model in the Unity Assets folder is newer than the " + exportModelPath + + " model.\n\nDo you want to replace the older .fbx with the newer .fbx?", + "Yes", "No", "Cancel"); + if (option == 2) { // Cancel + return; + } + copyModelToExport = option == 0; // Yes + } else if (assetModelWriteTime < targetModelWriteTime) { + int option = EditorUtility.DisplayDialogComplex("Error", "The " + exportModelPath + + " model is newer than the " + assetName + ".fbx model in the Unity Assets folder." + + "\n\nDo you want to replace the older .fbx with the newer .fbx and re-import it?", + "Yes", "No" , "Cancel"); + if (option == 2) { // Cancel + return; + } else if (option == 0) { // Yes - copy model to Unity project + // copy the fbx from the project folder to Unity Assets, overwriting the existing fbx, and re-import it + try { + File.Copy(exportModelPath, assetPath, true); + } catch { + EditorUtility.DisplayDialog("Error", "Failed to copy existing file " + exportModelPath + " to " + assetPath + + ". Please check the location and try again.", "Ok"); + return; + } + AssetDatabase.ImportAsset(assetPath); + + // set model to Humanoid animation type and force another refresh on it to process Humanoid + modelImporter = ModelImporter.GetAtPath(assetPath) as ModelImporter; + modelImporter.animationType = ModelImporterAnimationType.Human; + EditorUtility.SetDirty(modelImporter); + modelImporter.SaveAndReimport(); + + // redo parent names, joint mappings, and user bone positions due to the fbx change + // as well as re-check the avatar rules for failures + humanDescription = modelImporter.humanDescription; + SetBoneAndMaterialInformation(); + } + } + } else { + // if no matching fbx exists in the target export folder then ask to copy fbx over + int option = EditorUtility.DisplayDialogComplex("Error", "There is no existing " + exportModelPath + + " model.\n\nDo you want to copy over the " + assetName + + ".fbx model from the Unity Assets folder?", "Yes", "No", "Cancel"); + if (option == 2) { // Cancel + return; + } + copyModelToExport = option == 0; // Yes + } + + // copy asset fbx over deleting any existing fbx if we agreed to overwrite it + if (copyModelToExport) { + try { + File.Copy(assetPath, exportModelPath, true); + } catch { + EditorUtility.DisplayDialog("Error", "Failed to copy existing file " + assetPath + " to " + exportModelPath + + ". Please check the location and try again.", "Ok"); + return; + } + } + + // delete existing fst file since we will write a new file + // TODO: updating fst should only rewrite joint mappings and joint rotation offsets to existing file + try { + File.Delete(exportFstPath); + } catch { + EditorUtility.DisplayDialog("Error", "Failed to overwrite existing file " + exportFstPath + + ". Please check the file and try again.", "Ok"); + return; + } + + // write out a new fst file in place of the old file + if (!WriteFST(exportFstPath, projectName, scale)) { + return; + } + + // copy any external texture files to the project's texture directory that are considered dependencies of the model + string texturesDirectory = GetTextureDirectory(exportFstPath); + if (!CopyExternalTextures(texturesDirectory)) { + return; + } + + // display success dialog with any avatar rule warnings + string successDialog = "Avatar successfully updated!"; + if (!string.IsNullOrEmpty(warnings)) { + successDialog += "\n\nWarnings:\n" + warnings; + } + EditorUtility.DisplayDialog("Success!", successDialog, "Ok"); + } + + static void OnExportNewProject(string projectDirectory, string projectName, float scale) { // copy the fbx from the Unity Assets folder to the project directory string exportModelPath = projectDirectory + assetName + ".fbx"; File.Copy(assetPath, exportModelPath); @@ -577,7 +601,7 @@ class AvatarExporter : MonoBehaviour { // write out the avatar.fst file to the project directory string exportFstPath = projectDirectory + "avatar.fst"; - if (!WriteFST(exportFstPath, projectName)) { + if (!WriteFST(exportFstPath, projectName, scale)) { return; } @@ -592,16 +616,27 @@ class AvatarExporter : MonoBehaviour { if (warnings != EMPTY_WARNING_TEXT) { successDialog += "Warnings:\n" + warnings; } - successDialog += "Note: If you are using any external textures with your model, " + + successDialog += "\n\nNote: If you are using any external textures with your model, " + "please ensure those textures are copied to " + texturesDirectory; EditorUtility.DisplayDialog("Success!", successDialog, "Ok"); } + + static void OnExportWindowClose() { + // close the preview avatar scene and go back to user's previous scene when export project windows close + ClosePreviewScene(); + } - static bool WriteFST(string exportFstPath, string projectName) { + // The High Fidelity FBX Serializer omits the colon based prefixes. This will make the jointnames compatible. + static string removeTypeFromJointname(string jointName) { + return jointName.Substring(jointName.IndexOf(':') + 1); + } + + static bool WriteFST(string exportFstPath, string projectName, float scale) { // write out core fields to top of fst file try { - File.WriteAllText(exportFstPath, "name = " + projectName + "\ntype = body+head\nscale = 1\nfilename = " + - assetName + ".fbx\n" + "texdir = textures\n"); + File.WriteAllText(exportFstPath, "exporterVersion = " + AVATAR_EXPORTER_VERSION + "\nname = " + projectName + + "\ntype = body+head\nscale = " + scale + "\nfilename = " + assetName + + ".fbx\n" + "texdir = textures\n"); } catch { EditorUtility.DisplayDialog("Error", "Failed to write file " + exportFstPath + ". Please check the location and try again.", "Ok"); @@ -612,7 +647,7 @@ class AvatarExporter : MonoBehaviour { foreach (var userBoneInfo in userBoneInfos) { if (userBoneInfo.Value.HasHumanMapping()) { string hifiJointName = HUMANOID_TO_HIFI_JOINT_NAME[userBoneInfo.Value.humanName]; - File.AppendAllText(exportFstPath, "jointMap = " + hifiJointName + " = " + userBoneInfo.Key + "\n"); + File.AppendAllText(exportFstPath, "jointMap = " + hifiJointName + " = " + removeTypeFromJointname(userBoneInfo.Key) + "\n"); } } @@ -653,7 +688,7 @@ class AvatarExporter : MonoBehaviour { // swap from left-handed (Unity) to right-handed (HiFi) coordinates and write out joint rotation offset to fst jointOffset = new Quaternion(-jointOffset.x, jointOffset.y, jointOffset.z, -jointOffset.w); - File.AppendAllText(exportFstPath, "jointRotationOffset2 = " + userBoneName + " = (" + jointOffset.x + ", " + + File.AppendAllText(exportFstPath, "jointRotationOffset2 = " + removeTypeFromJointname(userBoneName) + " = (" + jointOffset.x + ", " + jointOffset.y + ", " + jointOffset.z + ", " + jointOffset.w + ")\n"); } @@ -690,14 +725,13 @@ class AvatarExporter : MonoBehaviour { userBoneTree = new BoneTreeNode(); materialDatas.Clear(); - materialAlternateStandardShader.Clear(); - materialUnsupportedShader.Clear(); - + alternateStandardShaderMaterials.Clear(); + unsupportedShaderMaterials.Clear(); + SetMaterialMappings(); - - // instantiate a game object of the user avatar to traverse the bone tree to gather - // bone parents and positions as well as build a bone tree, then destroy it - UnityEngine.Object avatarResource = AssetDatabase.LoadAssetAtPath(assetPath, typeof(UnityEngine.Object)); + + // instantiate a game object of the user avatar to traverse the bone tree to gather + // bone parents and positions as well as build a bone tree, then destroy it GameObject assetGameObject = (GameObject)Instantiate(avatarResource); TraverseUserBoneTree(assetGameObject.transform); DestroyImmediate(assetGameObject); @@ -732,8 +766,8 @@ class AvatarExporter : MonoBehaviour { bool light = gameObject.GetComponent() != null; bool camera = gameObject.GetComponent() != null; - // if this is a mesh and the model is using external materials then store its material data to be exported - if (mesh && modelImporter.materialLocation == ModelImporterMaterialLocation.External) { + // if this is a mesh then store its material data to be exported if the material is mapped to an fbx material name + if (mesh) { Material[] materials = skinnedMeshRenderer != null ? skinnedMeshRenderer.sharedMaterials : meshRenderer.sharedMaterials; StoreMaterialData(materials); } else if (!light && !camera) { @@ -959,7 +993,8 @@ class AvatarExporter : MonoBehaviour { string userBoneName = ""; // avatar rule fails if bone is not mapped in Humanoid if (!humanoidToUserBoneMappings.TryGetValue(humanBoneName, out userBoneName)) { - failedAvatarRules.Add(avatarRule, "There is no " + humanBoneName + " bone mapped in Humanoid for the selected avatar."); + failedAvatarRules.Add(avatarRule, "There is no " + humanBoneName + + " bone mapped in Humanoid for the selected avatar."); } return userBoneName; } @@ -1072,11 +1107,11 @@ class AvatarExporter : MonoBehaviour { // don't store any material data for unsupported shader types if (Array.IndexOf(SUPPORTED_SHADERS, shaderName) == -1) { - if (!materialUnsupportedShader.ContainsKey(materialName)) { - materialUnsupportedShader.Add(materialName, shaderName); + if (!unsupportedShaderMaterials.Contains(materialName)) { + unsupportedShaderMaterials.Add(materialName); } continue; - } + } MaterialData materialData = new MaterialData(); materialData.albedo = material.GetColor("_Color"); @@ -1100,18 +1135,19 @@ class AvatarExporter : MonoBehaviour { // for non-roughness Standard shaders give a warning that is not the recommended Standard shader, // and invert smoothness for roughness if (shaderName == STANDARD_SHADER || shaderName == STANDARD_SPECULAR_SHADER) { - if (!materialAlternateStandardShader.Contains(materialName)) { - materialAlternateStandardShader.Add(materialName); + if (!alternateStandardShaderMaterials.Contains(materialName)) { + alternateStandardShaderMaterials.Add(materialName); } materialData.roughness = 1.0f - materialData.roughness; } - - // remap the material name from the Unity material name to the fbx material name that it overrides - if (materialMappings.ContainsKey(materialName)) { - materialName = materialMappings[materialName]; - } - if (!materialDatas.ContainsKey(materialName)) { - materialDatas.Add(materialName, materialData); + + // store the material data under each fbx material name that it overrides from the material mapping + foreach (var materialMapping in materialMappings) { + string fbxMaterialName = materialMapping.Key; + string unityMaterialName = materialMapping.Value; + if (unityMaterialName == materialName && !materialDatas.ContainsKey(fbxMaterialName)) { + materialDatas.Add(fbxMaterialName, materialData); + } } } } @@ -1136,20 +1172,110 @@ class AvatarExporter : MonoBehaviour { static void SetMaterialMappings() { materialMappings.Clear(); - // store the mappings from fbx material name to the Unity material name overriding it using external fbx mapping + // store the mappings from fbx material name to the Unity Material name that overrides it using external fbx mapping var objectMap = modelImporter.GetExternalObjectMap(); foreach (var mapping in objectMap) { var material = mapping.Value as UnityEngine.Material; if (material != null) { - materialMappings.Add(material.name, mapping.Key.name); + materialMappings.Add(mapping.Key.name, material.name); } } } + + static void AddMaterialWarnings() { + string alternateStandardShaders = ""; + string unsupportedShaders = ""; + // combine all material names for each material warning into a comma-separated string + foreach (string materialName in alternateStandardShaderMaterials) { + if (!string.IsNullOrEmpty(alternateStandardShaders)) { + alternateStandardShaders += ", "; + } + alternateStandardShaders += materialName; + } + foreach (string materialName in unsupportedShaderMaterials) { + if (!string.IsNullOrEmpty(unsupportedShaders)) { + unsupportedShaders += ", "; + } + unsupportedShaders += materialName; + } + if (alternateStandardShaderMaterials.Count > 1) { + warnings += "The materials " + alternateStandardShaders + " are not using the " + + "recommended variation of the Standard shader. We recommend you change " + + "them to Standard (Roughness setup) shader for improved performance.\n\n"; + } else if (alternateStandardShaderMaterials.Count == 1) { + warnings += "The material " + alternateStandardShaders + " is not using the " + + "recommended variation of the Standard shader. We recommend you change " + + "it to Standard (Roughness setup) shader for improved performance.\n\n"; + } + if (unsupportedShaderMaterials.Count > 1) { + warnings += "The materials " + unsupportedShaders + " are using an unsupported shader. " + + "Please change them to a Standard shader type.\n\n"; + } else if (unsupportedShaderMaterials.Count == 1) { + warnings += "The material " + unsupportedShaders + " is using an unsupported shader. " + + "Please change it to a Standard shader type.\n\n"; + } + } + + static bool OpenPreviewScene() { + // see if the user wants to save their current scene before opening preview avatar scene in place of user's scene + if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) { + return false; + } + + // store the user's current scene to re-open when done and open a new default scene in place of the user's scene + previousScene = EditorSceneManager.GetActiveScene().path; + previewScene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene); + + // instantiate a game object to preview the avatar and a game object for the height reference prefab at 0, 0, 0 + UnityEngine.Object heightReferenceResource = AssetDatabase.LoadAssetAtPath(HEIGHT_REFERENCE_PREFAB, typeof(UnityEngine.Object)); + avatarPreviewObject = (GameObject)Instantiate(avatarResource, Vector3.zero, Quaternion.identity); + heightReferenceObject = (GameObject)Instantiate(heightReferenceResource, Vector3.zero, Quaternion.identity); + + // store the camera pivot and rotation from the user's last scene to be restored later + // replace the camera pivot and rotation to point at the preview avatar object in the -Z direction (facing front of it) + var sceneView = SceneView.lastActiveSceneView; + if (sceneView != null) { + previousScenePivot = sceneView.pivot; + previousSceneRotation = sceneView.rotation; + previousSceneSize = sceneView.size; + previousSceneOrthographic = sceneView.orthographic; + sceneView.pivot = PREVIEW_CAMERA_PIVOT; + sceneView.rotation = Quaternion.LookRotation(PREVIEW_CAMERA_DIRECTION); + sceneView.orthographic = true; + sceneView.size = 5.0f; + } + + return true; + } + + static void ClosePreviewScene() { + // destroy the avatar and height reference game objects closing the scene + DestroyImmediate(avatarPreviewObject); + DestroyImmediate(heightReferenceObject); + + // re-open the scene the user had open before switching to the preview scene + if (!string.IsNullOrEmpty(previousScene)) { + EditorSceneManager.OpenScene(previousScene); + } + + // close the preview scene and flag it to be removed + EditorSceneManager.CloseScene(previewScene, true); + + // restore the camera pivot and rotation to the user's previous scene settings + var sceneView = SceneView.lastActiveSceneView; + if (sceneView != null) { + sceneView.pivot = previousScenePivot; + sceneView.rotation = previousSceneRotation; + sceneView.size = previousSceneSize; + sceneView.orthographic = previousSceneOrthographic; + } + } } class ExportProjectWindow : EditorWindow { const int WINDOW_WIDTH = 500; - const int WINDOW_HEIGHT = 460; + const int EXPORT_NEW_WINDOW_HEIGHT = 520; + const int UPDATE_EXISTING_WINDOW_HEIGHT = 465; const int BUTTON_FONT_SIZE = 16; const int LABEL_FONT_SIZE = 16; const int TEXT_FIELD_FONT_SIZE = 14; @@ -1157,28 +1283,62 @@ class ExportProjectWindow : EditorWindow { const int ERROR_FONT_SIZE = 12; const int WARNING_SCROLL_HEIGHT = 170; const string EMPTY_ERROR_TEXT = "None\n"; - + const int SLIDER_WIDTH = 340; + const int SCALE_TEXT_WIDTH = 60; + const float MIN_SCALE_SLIDER = 0.0f; + const float MAX_SCALE_SLIDER = 2.0f; + const int SLIDER_SCALE_EXPONENT = 10; + const float ACTUAL_SCALE_OFFSET = 1.0f; + const float DEFAULT_AVATAR_HEIGHT = 1.755f; + const float MAXIMUM_RECOMMENDED_HEIGHT = DEFAULT_AVATAR_HEIGHT * 1.5f; + const float MINIMUM_RECOMMENDED_HEIGHT = DEFAULT_AVATAR_HEIGHT * 0.25f; + readonly Color COLOR_YELLOW = Color.yellow; //new Color(0.9176f, 0.8274f, 0.0f); + readonly Color COLOR_BACKGROUND = new Color(0.5f, 0.5f, 0.5f); + + GameObject avatarPreviewObject; + bool updateExistingAvatar = false; string projectName = ""; string projectLocation = ""; + string initialProjectLocation = ""; string projectDirectory = ""; string errorText = EMPTY_ERROR_TEXT; - string warningText = ""; + string warningText = "\n"; Vector2 warningScrollPosition = new Vector2(0, 0); + string scaleWarningText = ""; + float sliderScale = 0.30103f; - public delegate void OnCloseDelegate(string projectDirectory, string projectName, string warnings); + public delegate void OnExportDelegate(string projectDirectory, string projectName, float scale); + OnExportDelegate onExportCallback; + + public delegate void OnCloseDelegate(); OnCloseDelegate onCloseCallback; - public void Init(string initialPath, string warnings, OnCloseDelegate closeCallback) { - minSize = new Vector2(WINDOW_WIDTH, WINDOW_HEIGHT); - maxSize = new Vector2(WINDOW_WIDTH, WINDOW_HEIGHT); - titleContent.text = "Export New Avatar"; - projectLocation = initialPath; + public void Init(string initialPath, string warnings, bool updateExisting, GameObject avatarObject, + OnExportDelegate exportCallback, OnCloseDelegate closeCallback) { + updateExistingAvatar = updateExisting; + float windowHeight = updateExistingAvatar ? UPDATE_EXISTING_WINDOW_HEIGHT : EXPORT_NEW_WINDOW_HEIGHT; + minSize = new Vector2(WINDOW_WIDTH, windowHeight); + maxSize = new Vector2(WINDOW_WIDTH, windowHeight); + avatarPreviewObject = avatarObject; + titleContent.text = updateExistingAvatar ? "Update Existing Avatar" : "Export New Avatar"; + initialProjectLocation = initialPath; + projectLocation = updateExistingAvatar ? "" : initialProjectLocation; warningText = warnings; + onExportCallback = exportCallback; onCloseCallback = closeCallback; + ShowUtility(); + + // if the avatar's starting height is outside of the recommended ranges, auto-adjust the scale to default height + float height = GetAvatarHeight(); + if (height < MINIMUM_RECOMMENDED_HEIGHT || height > MAXIMUM_RECOMMENDED_HEIGHT) { + float newScale = DEFAULT_AVATAR_HEIGHT / height; + SetAvatarScale(newScale); + scaleWarningText = "Avatar's scale automatically adjusted to be within the recommended range."; + } } - void OnGUI() { + void OnGUI() { // define UI styles for all GUI elements to be created GUIStyle buttonStyle = new GUIStyle(GUI.skin.button); buttonStyle.fontSize = BUTTON_FONT_SIZE; @@ -1192,35 +1352,82 @@ class ExportProjectWindow : EditorWindow { errorStyle.normal.textColor = Color.red; errorStyle.wordWrap = true; GUIStyle warningStyle = new GUIStyle(errorStyle); - warningStyle.normal.textColor = Color.yellow; + warningStyle.normal.textColor = COLOR_YELLOW; + GUIStyle sliderStyle = new GUIStyle(GUI.skin.horizontalSlider); + sliderStyle.fixedWidth = SLIDER_WIDTH; + GUIStyle sliderThumbStyle = new GUIStyle(GUI.skin.horizontalSliderThumb); + + // set the background for the window to a darker gray + Texture2D backgroundTexture = new Texture2D(1, 1, TextureFormat.RGBA32, false); + backgroundTexture.SetPixel(0, 0, COLOR_BACKGROUND); + backgroundTexture.Apply(); + GUI.DrawTexture(new Rect(0, 0, maxSize.x, maxSize.y), backgroundTexture, ScaleMode.StretchToFill); GUILayout.Space(10); - // Project name label and input text field - GUILayout.Label("Export project name:", labelStyle); - projectName = GUILayout.TextField(projectName, textStyle); + if (updateExistingAvatar) { + // Project file to update label and input text field + GUILayout.Label("Project file to update:", labelStyle); + projectLocation = GUILayout.TextField(projectLocation, textStyle); + } else { + // Project name label and input text field + GUILayout.Label("Export project name:", labelStyle); + projectName = GUILayout.TextField(projectName, textStyle); + + GUILayout.Space(10); + + // Project location label and input text field + GUILayout.Label("Export project location:", labelStyle); + projectLocation = GUILayout.TextField(projectLocation, textStyle); + } - GUILayout.Space(10); - - // Project location label and input text field - GUILayout.Label("Export project location:", labelStyle); - projectLocation = GUILayout.TextField(projectLocation, textStyle); - - // Browse button to open folder explorer that starts at project location path and then updates project location + // Browse button to open file/folder explorer and set project location if (GUILayout.Button("Browse", buttonStyle)) { - string result = EditorUtility.OpenFolderPanel("Select export location", projectLocation, ""); - if (result.Length > 0) { // folder selection not cancelled + string result = ""; + if (updateExistingAvatar) { + // open file explorer starting at hifi projects folder in user documents and select target fst to update + string initialPath = string.IsNullOrEmpty(projectLocation) ? initialProjectLocation : projectLocation; + result = EditorUtility.OpenFilePanel("Select .fst to update", initialPath, "fst"); + } else { + // open folder explorer starting at project location path and select folder to create project folder in + result = EditorUtility.OpenFolderPanel("Select export location", projectLocation, ""); + } + if (!string.IsNullOrEmpty(result)) { // file/folder selection not cancelled projectLocation = result.Replace('/', '\\'); } } - // Red error label text to display any file-related errors + // warning if scale is above/below recommended range or if scale was auto-adjusted initially + GUILayout.Label(scaleWarningText, warningStyle); + + // from left to right show scale label, scale slider itself, and scale value input with % value + // slider value itself is from 0.0 to 2.0, and actual scale is an exponent of it with an offset of 1 + // displayed scale is the actual scale value with 2 decimal places, and changing the displayed + // scale via keyboard does the inverse calculation to get the slider value via logarithm + GUILayout.BeginHorizontal(); + GUILayout.Label("Scale:", labelStyle); + sliderScale = GUILayout.HorizontalSlider(sliderScale, MIN_SCALE_SLIDER, MAX_SCALE_SLIDER, sliderStyle, sliderThumbStyle); + float actualScale = (Mathf.Pow(SLIDER_SCALE_EXPONENT, sliderScale) - ACTUAL_SCALE_OFFSET); + GUIStyle scaleInputStyle = new GUIStyle(textStyle); + scaleInputStyle.fixedWidth = SCALE_TEXT_WIDTH; + actualScale *= 100.0f; // convert to 100-based percentage for display purposes + string actualScaleStr = GUILayout.TextField(String.Format("{0:0.00}", actualScale), scaleInputStyle); + actualScaleStr = Regex.Replace(actualScaleStr, @"[^0-9.]", ""); + actualScale = float.Parse(actualScaleStr); + actualScale /= 100.0f; // convert back to 1.0-based percentage + SetAvatarScale(actualScale); + GUILayout.Label("%", labelStyle); + GUILayout.EndHorizontal(); + + GUILayout.Space(15); + + // red error label text to display any file-related errors GUILayout.Label("Error:", errorStyle); GUILayout.Label(errorText, errorStyle); GUILayout.Space(10); - // Yellow warning label text to display scrollable list of any bone-related warnings + // yellow warning label text to display scrollable list of any bone-related warnings GUILayout.Label("Warnings:", warningStyle); warningScrollPosition = GUILayout.BeginScrollView(warningScrollPosition, GUILayout.Width(WINDOW_WIDTH), GUILayout.Height(WARNING_SCROLL_HEIGHT)); @@ -1229,64 +1436,122 @@ class ExportProjectWindow : EditorWindow { GUILayout.Space(10); - // Export button which will verify project folder can actually be created + // export button will verify target project folder can actually be created (or target fst file is valid) // before closing popup window and calling back to initiate the export bool export = false; if (GUILayout.Button("Export", buttonStyle)) { export = true; if (!CheckForErrors(true)) { Close(); - onCloseCallback(projectDirectory, projectName, warningText); + onExportCallback(updateExistingAvatar ? projectLocation : projectDirectory, projectName, actualScale); } } - // Cancel button just closes the popup window without callback + // cancel button closes the popup window triggering the close callback to close the preview scene if (GUILayout.Button("Cancel", buttonStyle)) { Close(); } - // When either text field changes check for any errors if we didn't just check errors from clicking Export above + // when any value changes check for any errors and update scale warning if we are not exporting if (GUI.changed && !export) { CheckForErrors(false); + UpdateScaleWarning(); } } bool CheckForErrors(bool exporting) { errorText = EMPTY_ERROR_TEXT; // default to None if no errors found - projectDirectory = projectLocation + "\\" + projectName + "\\"; - if (projectName.Length > 0) { - // new project must have a unique folder name since the folder will be created for it - if (Directory.Exists(projectDirectory)) { - errorText = "A folder with the name " + projectName + - " already exists at that location.\nPlease choose a different project name or location."; + if (updateExistingAvatar) { + // if any text is set in the project file to update field verify that the file actually exists + if (projectLocation.Length > 0) { + if (!File.Exists(projectLocation)) { + errorText = "Please select a valid project file to update.\n"; + return true; + } + } else if (exporting) { + errorText = "Please select a project file to update.\n"; return true; } - } - if (projectLocation.Length > 0) { - // before clicking Export we can verify that the project location at least starts with a drive - if (!Char.IsLetter(projectLocation[0]) || projectLocation.Length == 1 || projectLocation[1] != ':') { - errorText = "Project location is invalid. Please choose a different project location.\n"; - return true; + } else { + projectDirectory = projectLocation + "\\" + projectName + "\\"; + if (projectName.Length > 0) { + // new project must have a unique folder name since the folder will be created for it + if (Directory.Exists(projectDirectory)) { + errorText = "A folder with the name " + projectName + + " already exists at that location.\nPlease choose a different project name or location."; + return true; + } } - } - if (exporting) { - // when exporting, project name and location must both be defined, and project location must - // be valid and accessible (we attempt to create the project folder at this time to verify this) - if (projectName.Length == 0) { - errorText = "Please define a project name.\n"; - return true; - } else if (projectLocation.Length == 0) { - errorText = "Please define a project location.\n"; - return true; - } else { - try { - Directory.CreateDirectory(projectDirectory); - } catch { + if (projectLocation.Length > 0) { + // before clicking Export we can verify that the project location at least starts with a drive + if (!Char.IsLetter(projectLocation[0]) || projectLocation.Length == 1 || projectLocation[1] != ':') { errorText = "Project location is invalid. Please choose a different project location.\n"; return true; } } - } + if (exporting) { + // when exporting, project name and location must both be defined, and project location must + // be valid and accessible (we attempt to create the project folder at this time to verify this) + if (projectName.Length == 0) { + errorText = "Please define a project name.\n"; + return true; + } else if (projectLocation.Length == 0) { + errorText = "Please define a project location.\n"; + return true; + } else { + try { + Directory.CreateDirectory(projectDirectory); + } catch { + errorText = "Project location is invalid. Please choose a different project location.\n"; + return true; + } + } + } + } + return false; } + + void UpdateScaleWarning() { + // called on any input changes + float height = GetAvatarHeight(); + if (height < MINIMUM_RECOMMENDED_HEIGHT) { + scaleWarningText = "The height of the avatar is below the recommended minimum."; + } else if (height > MAXIMUM_RECOMMENDED_HEIGHT) { + scaleWarningText = "The height of the avatar is above the recommended maximum."; + } else { + scaleWarningText = ""; + } + } + + float GetAvatarHeight() { + // height of an avatar model can be determined to be the max Y extents of the combined bounds for all its mesh renderers + Bounds bounds = new Bounds(); + var meshRenderers = avatarPreviewObject.GetComponentsInChildren(); + var skinnedMeshRenderers = avatarPreviewObject.GetComponentsInChildren(); + foreach (var renderer in meshRenderers) { + bounds.Encapsulate(renderer.bounds); + } + foreach (var renderer in skinnedMeshRenderers) { + bounds.Encapsulate(renderer.bounds); + } + return bounds.max.y; + } + + void SetAvatarScale(float actualScale) { + // set the new scale uniformly on the preview avatar's transform to show the resulting avatar size + avatarPreviewObject.transform.localScale = new Vector3(actualScale, actualScale, actualScale); + + // adjust slider scale value to match the new actual scale value + sliderScale = GetSliderScaleFromActualScale(actualScale); + } + + float GetSliderScaleFromActualScale(float actualScale) { + // since actual scale is an exponent of slider scale with an offset, do the logarithm operation to convert it back + return Mathf.Log(actualScale + ACTUAL_SCALE_OFFSET, SLIDER_SCALE_EXPONENT); + } + + void OnDestroy() { + onCloseCallback(); + } } diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Average.mat b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Average.mat new file mode 100644 index 0000000000..69421ca8e2 --- /dev/null +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Average.mat @@ -0,0 +1,76 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_Name: Average + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_ShaderKeywords: + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 0.5 + - _GlossyReflections: 1 + - _Metallic: 0 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 0.53309965, g: 0.8773585, b: 0.27727836, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Floor.mat b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Floor.mat new file mode 100644 index 0000000000..4c63832593 --- /dev/null +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Floor.mat @@ -0,0 +1,76 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_Name: Floor + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_ShaderKeywords: + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 0.5 + - _GlossyReflections: 1 + - _Metallic: 0 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/HeightReference.prefab b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/HeightReference.prefab new file mode 100644 index 0000000000..3a6b6b21fa --- /dev/null +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/HeightReference.prefab @@ -0,0 +1,1393 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1001 &100100000 +Prefab: + m_ObjectHideFlags: 1 + serializedVersion: 2 + m_Modification: + m_TransformParent: {fileID: 0} + m_Modifications: [] + m_RemovedComponents: [] + m_SourcePrefab: {fileID: 0} + m_RootGameObject: {fileID: 1663253797283788} + m_IsPrefabAsset: 1 +--- !u!1 &1046656866020106 +GameObject: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 224386929081752724} + - component: {fileID: 222160789105267064} + - component: {fileID: 114930405832365464} + m_Layer: 5 + m_Name: TwoAndHalfMText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1098451480288840 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4735851023856772} + - component: {fileID: 33008877752475126} + - component: {fileID: 23983268565997994} + m_Layer: 0 + m_Name: HalfMLine + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1107359137501064 +GameObject: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 224352215517075892} + - component: {fileID: 222924084127982026} + - component: {fileID: 114523909969846714} + m_Layer: 5 + m_Name: TwoMText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1108041172082256 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 224494569551489322} + - component: {fileID: 223961774962398002} + - component: {fileID: 114011556853048752} + - component: {fileID: 114521005238033952} + m_Layer: 5 + m_Name: Canvas + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1165326825168616 +GameObject: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 224593141416602104} + - component: {fileID: 222331762946337184} + - component: {fileID: 114101794169638918} + m_Layer: 5 + m_Name: OneAndHalfMText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1182485492886750 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4302978871272126} + - component: {fileID: 33686989621546016} + - component: {fileID: 23982106336197490} + m_Layer: 0 + m_Name: TwoAndHalfMLine + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1365616260555366 +GameObject: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 224613908675679132} + - component: {fileID: 222421911825862480} + - component: {fileID: 114276838631099888} + m_Layer: 5 + m_Name: OneMText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1398639835840810 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4460037940915778} + - component: {fileID: 33999849812690240} + - component: {fileID: 23416265009837404} + m_Layer: 0 + m_Name: Floor + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1534720920953066 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4413776654278098} + - component: {fileID: 33291071156168694} + - component: {fileID: 23550720950256080} + m_Layer: 0 + m_Name: Average + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1594624973687270 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4908828994703896} + - component: {fileID: 33726300519449444} + - component: {fileID: 23824769923661608} + m_Layer: 0 + m_Name: Tall + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1663253797283788 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4466308008297536} + m_Layer: 0 + m_Name: HeightReference + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1684603522306818 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4359301733271006} + - component: {fileID: 33170278100239952} + - component: {fileID: 23463284742561382} + m_Layer: 0 + m_Name: TwoMLine + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1758516477546936 +GameObject: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 224093314116541246} + - component: {fileID: 222104353024021134} + - component: {fileID: 114198955202599194} + m_Layer: 5 + m_Name: HalfMText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1843086377652878 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4967607462495426} + - component: {fileID: 33458427168817864} + - component: {fileID: 23807848267690204} + m_Layer: 0 + m_Name: Short + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1845490813592506 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4990347338131576} + - component: {fileID: 108630196659418708} + m_Layer: 0 + m_Name: Directional Light + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1883639722740524 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4177433262325602} + - component: {fileID: 33418961761515394} + - component: {fileID: 23536779434871182} + m_Layer: 0 + m_Name: TooShort + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1885741171197356 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4718462335765420} + - component: {fileID: 33030310456480364} + - component: {fileID: 23105277758912132} + m_Layer: 0 + m_Name: TooTall + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1919147340747728 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4440944676647488} + - component: {fileID: 33820823812379558} + - component: {fileID: 23886085173153614} + m_Layer: 0 + m_Name: OneAndHalfMLine + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1985295559338180 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4498194399146796} + - component: {fileID: 33041053251399642} + - component: {fileID: 23936786851965954} + m_Layer: 0 + m_Name: OneMLine + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &4177433262325602 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1883639722740524} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0.219375, z: -1} + m_LocalScale: {x: 200, y: 0.43875, z: 1} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!4 &4302978871272126 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1182485492886750} + m_LocalRotation: {x: 0.7071068, y: -0, z: -0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 2.5, z: -0.94} + m_LocalScale: {x: 200, y: 1.0000005, z: 0.0100000035} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 12 + m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0} +--- !u!4 &4359301733271006 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1684603522306818} + m_LocalRotation: {x: 0.7071068, y: -0, z: -0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 2, z: -0.94} + m_LocalScale: {x: 200, y: 1.0000005, z: 0.0100000035} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 11 + m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0} +--- !u!4 &4413776654278098 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1534720920953066} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 1.535625, z: -1} + m_LocalScale: {x: 200, y: 1.19375, z: 1} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 4 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!4 &4440944676647488 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1919147340747728} + m_LocalRotation: {x: 0.7071068, y: -0, z: -0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 1.5, z: -0.94} + m_LocalScale: {x: 200, y: 1.0000005, z: 0.0100000035} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 10 + m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0} +--- !u!4 &4460037940915778 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1398639835840810} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: -50, z: -0.5} + m_LocalScale: {x: 200, y: 100, z: 2} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!4 &4466308008297536 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1663253797283788} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 4990347338131576} + - {fileID: 4460037940915778} + - {fileID: 4177433262325602} + - {fileID: 4967607462495426} + - {fileID: 4413776654278098} + - {fileID: 4908828994703896} + - {fileID: 4718462335765420} + - {fileID: 224494569551489322} + - {fileID: 4735851023856772} + - {fileID: 4498194399146796} + - {fileID: 4440944676647488} + - {fileID: 4359301733271006} + - {fileID: 4302978871272126} + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!4 &4498194399146796 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1985295559338180} + m_LocalRotation: {x: 0.7071068, y: -0, z: -0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 1, z: -0.94} + m_LocalScale: {x: 200, y: 1.0000005, z: 0.0100000035} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 9 + m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0} +--- !u!4 &4718462335765420 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1885741171197356} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 502.6325, z: -1} + m_LocalScale: {x: 200, y: 1000, z: 1} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 6 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!4 &4735851023856772 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1098451480288840} + m_LocalRotation: {x: 0.7071068, y: 0, z: 0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 0.5, z: -0.94} + m_LocalScale: {x: 200, y: 1, z: 0.01} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 8 + m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0} +--- !u!4 &4908828994703896 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1594624973687270} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 2.3825, z: -1} + m_LocalScale: {x: 200, y: 0.5, z: 1} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 5 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!4 &4967607462495426 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1843086377652878} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0.68875, z: -1} + m_LocalScale: {x: 200, y: 0.5, z: 1} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!4 &4990347338131576 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1845490813592506} + m_LocalRotation: {x: -0.11086535, y: -0.8745676, z: 0.40781754, w: -0.23775047} + m_LocalPosition: {x: 0, y: 3, z: 77.17} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 50.000004, y: -210.41699, z: 0} +--- !u!23 &23105277758912132 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1885741171197356} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: d07e04b46b88ae54e9f418c8645f1580, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23416265009837404 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1398639835840810} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: 320b570da434d374985fe89d653ae75b, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23463284742561382 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1684603522306818} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: d1133891b03286946b3b0c63c1a57d08, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23536779434871182 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1883639722740524} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: d07e04b46b88ae54e9f418c8645f1580, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23550720950256080 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1534720920953066} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: 722779087c41d074eb632820263fc661, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23807848267690204 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1843086377652878} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: 1cd16d030e4890a4cab22d897ccfc8d8, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23824769923661608 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1594624973687270} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: 1cd16d030e4890a4cab22d897ccfc8d8, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23886085173153614 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1919147340747728} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: d1133891b03286946b3b0c63c1a57d08, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23936786851965954 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1985295559338180} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: d1133891b03286946b3b0c63c1a57d08, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23982106336197490 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1182485492886750} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: d1133891b03286946b3b0c63c1a57d08, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23983268565997994 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1098451480288840} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: d1133891b03286946b3b0c63c1a57d08, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!33 &33008877752475126 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1098451480288840} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33030310456480364 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1885741171197356} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33041053251399642 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1985295559338180} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33170278100239952 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1684603522306818} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33291071156168694 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1534720920953066} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33418961761515394 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1883639722740524} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33458427168817864 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1843086377652878} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33686989621546016 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1182485492886750} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33726300519449444 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1594624973687270} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33820823812379558 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1919147340747728} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33999849812690240 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1398639835840810} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!108 &108630196659418708 +Light: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1845490813592506} + m_Enabled: 1 + serializedVersion: 8 + m_Type: 1 + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_Intensity: 2 + m_Range: 10 + m_SpotAngle: 30 + m_CookieSize: 10 + m_Shadows: + m_Type: 0 + m_Resolution: -1 + m_CustomResolution: -1 + m_Strength: 1 + m_Bias: 0.05 + m_NormalBias: 0.4 + m_NearPlane: 0.2 + m_Cookie: {fileID: 0} + m_DrawHalo: 0 + m_Flare: {fileID: 0} + m_RenderMode: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_Lightmapping: 1 + m_LightShadowCasterMode: 0 + m_AreaSize: {x: 1, y: 1} + m_BounceIntensity: 1 + m_ColorTemperature: 6570 + m_UseColorTemperature: 0 + m_ShadowRadius: 0 + m_ShadowAngle: 0 +--- !u!114 &114011556853048752 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1108041172082256} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 1980459831, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_UiScaleMode: 0 + m_ReferencePixelsPerUnit: 100 + m_ScaleFactor: 1 + m_ReferenceResolution: {x: 800, y: 600} + m_ScreenMatchMode: 0 + m_MatchWidthOrHeight: 0 + m_PhysicalUnit: 3 + m_FallbackScreenDPI: 96 + m_DefaultSpriteDPI: 96 + m_DynamicPixelsPerUnit: 1 +--- !u!114 &114101794169638918 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1165326825168616} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, + Version=1.0.0.0, Culture=neutral, PublicKeyToken=null + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 128 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 0 + m_MaxSize: 256 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: '1.5m + +' +--- !u!114 &114198955202599194 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1758516477546936} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, + Version=1.0.0.0, Culture=neutral, PublicKeyToken=null + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 128 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 0 + m_MaxSize: 256 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 0.5m +--- !u!114 &114276838631099888 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1365616260555366} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, + Version=1.0.0.0, Culture=neutral, PublicKeyToken=null + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 128 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 0 + m_MaxSize: 256 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: '1.0m + +' +--- !u!114 &114521005238033952 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1108041172082256} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 1301386320, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreReversedGraphics: 1 + m_BlockingObjects: 0 + m_BlockingMask: + serializedVersion: 2 + m_Bits: 4294967295 +--- !u!114 &114523909969846714 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1107359137501064} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, + Version=1.0.0.0, Culture=neutral, PublicKeyToken=null + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 128 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 0 + m_MaxSize: 256 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: '2.0m + +' +--- !u!114 &114930405832365464 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1046656866020106} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, + Version=1.0.0.0, Culture=neutral, PublicKeyToken=null + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 128 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 0 + m_MaxSize: 256 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: '2.5m + +' +--- !u!222 &222104353024021134 +CanvasRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1758516477546936} + m_CullTransparentMesh: 0 +--- !u!222 &222160789105267064 +CanvasRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1046656866020106} + m_CullTransparentMesh: 0 +--- !u!222 &222331762946337184 +CanvasRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1165326825168616} + m_CullTransparentMesh: 0 +--- !u!222 &222421911825862480 +CanvasRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1365616260555366} + m_CullTransparentMesh: 0 +--- !u!222 &222924084127982026 +CanvasRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1107359137501064} + m_CullTransparentMesh: 0 +--- !u!223 &223961774962398002 +Canvas: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1108041172082256} + m_Enabled: 1 + serializedVersion: 3 + m_RenderMode: 2 + m_Camera: {fileID: 0} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_AdditionalShaderChannelsFlag: 0 + m_SortingLayerID: 0 + m_SortingOrder: 0 + m_TargetDisplay: 0 +--- !u!224 &224093314116541246 +RectTransform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1758516477546936} + m_LocalRotation: {x: 0, y: 1, z: 0, w: 0} + m_LocalPosition: {x: 0, y: 0, z: -0.395} + m_LocalScale: {x: 0.001, y: 0.001, z: 0.001} + m_Children: [] + m_Father: {fileID: 224494569551489322} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 180, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: -2, y: 0.55} + m_SizeDelta: {x: 300, y: 200} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!224 &224352215517075892 +RectTransform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1107359137501064} + m_LocalRotation: {x: -0, y: 1, z: -0, w: 0} + m_LocalPosition: {x: 0, y: 0, z: -0.395} + m_LocalScale: {x: 0.0009999999, y: 0.0009999999, z: 0.0009999999} + m_Children: [] + m_Father: {fileID: 224494569551489322} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 180, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: -2, y: 2.05} + m_SizeDelta: {x: 300, y: 200} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!224 &224386929081752724 +RectTransform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1046656866020106} + m_LocalRotation: {x: -0, y: 1, z: -0, w: 0} + m_LocalPosition: {x: 0, y: 0, z: -0.395} + m_LocalScale: {x: 0.0009999999, y: 0.0009999999, z: 0.0009999999} + m_Children: [] + m_Father: {fileID: 224494569551489322} + m_RootOrder: 4 + m_LocalEulerAnglesHint: {x: 0, y: 180, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: -2, y: 2.55} + m_SizeDelta: {x: 300, y: 200} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!224 &224494569551489322 +RectTransform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1108041172082256} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 224093314116541246} + - {fileID: 224613908675679132} + - {fileID: 224593141416602104} + - {fileID: 224352215517075892} + - {fileID: 224386929081752724} + m_Father: {fileID: 4466308008297536} + m_RootOrder: 7 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 1000, y: 1000} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!224 &224593141416602104 +RectTransform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1165326825168616} + m_LocalRotation: {x: -0, y: 1, z: -0, w: 0} + m_LocalPosition: {x: 0, y: 0, z: -0.395} + m_LocalScale: {x: 0.0009999999, y: 0.0009999999, z: 0.0009999999} + m_Children: [] + m_Father: {fileID: 224494569551489322} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 180, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: -2, y: 1.55} + m_SizeDelta: {x: 300, y: 200} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!224 &224613908675679132 +RectTransform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1365616260555366} + m_LocalRotation: {x: -0, y: 1, z: -0, w: 0} + m_LocalPosition: {x: 0, y: 0, z: -0.395} + m_LocalScale: {x: 0.0009999999, y: 0.0009999999, z: 0.0009999999} + m_Children: [] + m_Father: {fileID: 224494569551489322} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 180, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: -2, y: 1.05} + m_SizeDelta: {x: 300, y: 200} + m_Pivot: {x: 0.5, y: 0.5} diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Line.mat b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Line.mat new file mode 100644 index 0000000000..2f9a048c63 --- /dev/null +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Line.mat @@ -0,0 +1,76 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_Name: Line + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_ShaderKeywords: + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 0.5 + - _GlossyReflections: 1 + - _Metallic: 0 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 0, g: 0, b: 0, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/ShortOrTall.mat b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/ShortOrTall.mat new file mode 100644 index 0000000000..5543fef85e --- /dev/null +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/ShortOrTall.mat @@ -0,0 +1,76 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_Name: ShortOrTall + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_ShaderKeywords: + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 0.5 + - _GlossyReflections: 1 + - _Metallic: 0 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 0.91758025, g: 0.9622642, b: 0.28595585, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/TooShortOrTall.mat b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/TooShortOrTall.mat new file mode 100644 index 0000000000..4851a64056 --- /dev/null +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/TooShortOrTall.mat @@ -0,0 +1,76 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_Name: TooShortOrTall + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_ShaderKeywords: + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 0.5 + - _GlossyReflections: 1 + - _Metallic: 0 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 0.9056604, g: 0.19223925, b: 0.19223925, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} diff --git a/tools/unity-avatar-exporter/Assets/README.txt b/tools/unity-avatar-exporter/Assets/README.txt index 402719b497..767c093800 100644 --- a/tools/unity-avatar-exporter/Assets/README.txt +++ b/tools/unity-avatar-exporter/Assets/README.txt @@ -1,6 +1,6 @@ High Fidelity, Inc. Avatar Exporter -Version 0.3.3 +Version 0.3.5 Note: It is recommended to use Unity versions between 2017.4.17f1 and 2018.2.12f1 for this Avatar Exporter. @@ -9,15 +9,16 @@ To create a new avatar project: 2. Select the .fbx avatar that you imported in step 1 in the Assets window, and in the Rig section of the Inspector window set the Animation Type to Humanoid and choose Apply. 3. With the .fbx avatar still selected in the Assets window, choose High Fidelity menu > Export New Avatar. 4. Select a name for your avatar project (this will be used to create a directory with that name), as well as the target location for your project folder. -5. Once it is exported, your project directory will open in File Explorer. +5. If necessary, adjust the scale for your avatar so that it's height is within the recommended range. +6. Once it is exported, you will receive a successfully exported dialog with any warnings, and your project directory will open in File Explorer. To update an existing avatar project: -1. Select the existing .fbx avatar in the Assets window that you would like to re-export. -2. Choose High Fidelity menu > Update Existing Avatar and browse to the .fst file you would like to update. +1. Select the existing .fbx avatar in the Assets window that you would like to re-export and choose High Fidelity menu > Update Existing Avatar +2. Select the .fst project file that you wish to update. 3. If the .fbx file in your Unity Assets folder is newer than the existing .fbx file in your selected avatar project or vice-versa, you will be prompted if you wish to replace the older file with the newer file before performing the update. -4. Once it is updated, your project directory will open in File Explorer. +4. Once it is updated, you will receive a successfully exported dialog with any warnings, and your project directory will open in File Explorer. * WARNING * If you are using any external textures as part of your .fbx model, be sure they are copied into the textures folder that is created in the project folder after exporting a new avatar. -For further details including troubleshooting tips, see the full documentation at https://docs.highfidelity.com/create-and-explore/avatars/create-avatars/unity-extension +For further details including troubleshooting tips, see the full documentation at https://docs.highfidelity.com/create-and-explore/avatars/create-avatars/unity-extension \ No newline at end of file diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage index f7385e38311e93788a17308f7cb2a174c7f7f138..ee3f6abe01b509bba63360b2834400660f9f3901 100644 GIT binary patch literal 74582 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 Date: Fri, 15 Mar 2019 11:05:58 +1300 Subject: [PATCH 189/446] Update JSDoc README with solution for out-of-memory problem --- tools/jsdoc/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tools/jsdoc/README.md b/tools/jsdoc/README.md index f9864a21e4..f3dda84291 100644 --- a/tools/jsdoc/README.md +++ b/tools/jsdoc/README.md @@ -15,6 +15,13 @@ To generate html documentation for the High Fidelity JavaScript API: The out folder should contain index.html. +If you get a "JavaScript heap out of memory" error when running the `jsdoc` command you need to increase the amount of memory +available to it. For example, to increase the memory available to 2GB on Windows: +* `where jsdoc` to find the `jsdoc.cmd` file. +* Edit the `jsdoc.cmd` file to add `--max-old-space-size=2048` after the `node` and/or `node.exe` commands. + +Reference: https://medium.com/@vuongtran/how-to-solve-process-out-of-memory-in-node-js-5f0de8f8464c + To generate the grav automation files, run node gravPrep.js after you have made a JSdoc output folder. This will create files that are needed for hifi-grav and hifi-grav-content repos From 3c78e48f88ffe74868340a389ca0597b65ed0d27 Mon Sep 17 00:00:00 2001 From: David Back Date: Thu, 14 Mar 2019 17:05:29 -0700 Subject: [PATCH 190/446] prevent warnings on embedded materials --- .../Editor/AvatarExporter/AvatarExporter.cs | 5 +++++ .../avatarExporter.unitypackage | Bin 74582 -> 74600 bytes 2 files changed, 5 insertions(+) diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs index 142e4ae35a..f0d970031c 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs @@ -1105,6 +1105,11 @@ class AvatarExporter : MonoBehaviour { string materialName = material.name; string shaderName = material.shader.name; + // if this material isn't mapped externally then ignore it + if (!materialMappings.ContainsValue(materialName)) { + continue; + } + // don't store any material data for unsupported shader types if (Array.IndexOf(SUPPORTED_SHADERS, shaderName) == -1) { if (!unsupportedShaderMaterials.Contains(materialName)) { diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage index ee3f6abe01b509bba63360b2834400660f9f3901..48a9502079839c3aee59ead39531198dd5d4bf86 100644 GIT binary patch delta 72937 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 L^b|O508|42fq(tw delta 72877 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 z zKgEuJ?8J##rFPaQs(e-vt^a-ec zB7ZD-9D(izd;Md_hCYJXbbv}a12vmWDz>4zwQM@(jt8&JgQt=w|0hoCOFUXeXKrv>{zD{oi-*?;}*uU>$tbI zc0r2n&@=9L%zzItb~xyQgpCVBemldxZGR}<17=!~c3`Zn1KZ8O=$^bLnZ>pC*6H=t&6S0%<;9h)yFlGiv42$V z&5o+nURzvgZJj*RYHzTb?bh1-`uqlwb3Kh1RM0%_V5&JTQ52eraoEet)?|XpI$fZv^Hut;N%4Hn!GVr&{Z+m6NTlHTXJz zTdbdXe(;Qi24UUmS)S!|tm1&(+@S{i+5Z_207L~ zFERnL=}j5sVxv}rzxZo%ESSVVkh2yp{d>GD!1AuLvAVE&3p}z674 zGBiV%EZT8y_E?%tT(H?FVt;xEwze>)M)eFEy=vwz- z&Vkv7Vl45D>2z6oHMIh}7i!Py+Ashms#Q{}t$FT!>^%*pvZl(C9e)IUOGDe5?ObqB zO|6ke9M!9&*CXmQxOCJl+ug2~I?AY~RLLNZvMVVSQ|O~C9f9khK^pQfxtdZul{Cz( zq*T(9g&8`cHg>_$(NM+Gt7#R|=%Q+s)M`YT29t)g)yH5tAq)9vxGUa zU7`f|o-8I?CN&{hHnL6lLvNfgN48Fs0N?e+R05L*iu|X@ky7(mebv*7BzjJrmYCbV z+lO$9;e|6T_?j`#+l)s>9{t8a$LtUD3mboK_tP3LfiL&Ni9V$3~3IM z*TuZU(8MdBAoW&;m?f^SZp?3p2~T!Ky^9Ieq%sr7OU2SmWx8IgviV=7R%uLI#~bkD zcpYYgjat(>UT0IdX0u*vPWL7io}y2x0SZ%^Qgdbo>VGy7QngrZG^eNQ*70%$e#0cD zR4-Ml@RMPU!vt?O2@`;nrmM~AGSmuZleI>*QK}QQHJHj(nvEJzn*^D3hqa8Tm1gRt zT7$KAx>=ej&CDPo{86q{noarvEh|+aN5+ulKPT}_H>=al3c^Nc^-8r|;h^i&)l!us zt=4C%wSPFCq{E$pW2RndRzn=+Mva4oA5{<}#{sgRR@;*F&{I*AO0_y+CbI1!(k;2K@Mh>W}v6kLb(I!wW!ad8b^(Sg_Kew4b3RkssR0! zuzw{bz!>RP158Quqd7fO*2+V;GbtGAbr3m1Ww%+*0P8|tRqG|7n`>{g)BqEiieWUf z>HxT2Ap==%Hlf#(7yv0)f_jLwUWK2{N-CagZq@5`(4Vj^jhR}b5;mn)0tu9PQ|jfi zwwHiAb=2b#pA{zI!q^E7E!Vgitk$6`mw$=&W);@qxTPw=;8d=cbUZ=FP2T|;&BkZlxAOb!HkQ7LGlY8fXdEi&~>mZ+}Fr z0eMbpXUu7ISCGxqWOW*4Ttc0p@1+W8Uz1A{)TgTLtKeg1GQHfS?nB?p^@z`nN@-gB zC^xHm;F(OX*TK}6kx2SEQwP6}=x^eu9-1~}d$o!l$Yf%6f7G1^9raPJ7Y*?uUbsDq}3?WD(XMjhiRv>3C zbI{XB?i>rd@V5l+}8q; zG$LQC41x~D(}dm*jhxIIX-cALR-@6s+ypJ=?SR3*8BT`k)oQtpW+YEoO4AUS3pC2y z(W;guPpbxe3mXE{BAv$mGaRib*p)KCHK8-jRGN{iH9aG_S_}d6w11{y#L&q$#nmED ztJ0`Oo)(xl$mV zhgwtkS(SQw3wp>p^pl8v>Jdwcv_X2x?l}Xi}ew}6w=Eu^@;qfYE$yF zLfyknYv@nw;DRfZ#eZ_B*^JyQ7_5Y6#dsBdtxTtY&f<8zffEt#zm~upR-qqCPF6i~ zZEF=8w-NzLy?)oGLEV_mEi)@wn+XGPm13g-u&N>wH-k=ZZ!#I)JnAT)8W$B2&pmT6 zKue7Gm>d4E*R$_qNs&?9!Q}P~C}cTZbI0N#^vMHKhUmk}d(S<}BSJ4=ii8QoIIjK4`zp^k6Suwv`WN7_m zk)7~!MOMr|P3EKnrF<<^#gR+qWI@IJG^oPiNv5Ym#9Tc*;Tz~OQX$3$V^!E$yTsZX zAn^J1g{}6P`G1AhI_?f@2PX6=uUnYSRdIcF^YodOR=ceMVN9&M!yWcW3wGGyU@|Ar z_FC)Y=Fsx7NYph_n@VVL0g99e+@VWwMuY-n@SR5CaeL`(U_6 zlmMeKq6FHJL=jv|!9=So(%2BBOxsuN3;>o6c9AL9w}I=En@ zOOwGcxFQ=o%|siJx)nEAbxwQk(1~R*G8hmja?-V(j@`8!3F3we9r(5`6oSgZCP>v= zviEFJLVr?%meW=GD~y63B>S|5*YqqS2sd<_zF~Qu>-h#q!oXYp0jXiJt($CV&JA`g zZ{PN<5W>FQ?;HNEyN^}(P0xW+`jJwQ13iLv_HNyFY4zmoaUcjc{H|{<@xeeXe?LvD zii%<+vl$CB@)>tC@`ad_#*Q{qZ~B&pB3N|5Yk$~dOREy4gW)!G9Bz4b(e1!ZwX?|n zZKy?#hwTWD#O@h(VA#H~M`$H(7YZqG2c`!iiU8A3_JEE+60d4I0r!5G_FzrE#T#6n zz~19Kx)FpRNG3K0Gul);aD5vYF)Ob@N2&2z!oQ=%=|n|L)m22cZ+Moq;&v@#Tlz5@ zm4A#?(MfA!Qj!;Zgp_bVK)WwKA#3%S3OgaYq~u1;kL@g;R7&x-LaCVV8!M=^mkKNV zkc*#7WyiMb_Kh>9&jcD8UE~M!*kI^6#tliqii`fru-{+xT6=@wU?O7BC2&n{DZ{vx(b={8U4PHwj7gS|Tb!jS*uHa_1w!hu*Ti%qq8O== z2~Xn#e+U&HvPzl-_9UBOvd1oGQ+PzVAe%&it_y!HV=sL6E(Ji8p8>+-{!MpcWKt%Zpgp{&7R&cxa>PozO^Xz;1tdkY)XWYH87k^4A z^oH(Uz}FI`bRSdhXQZQN0|Eh{`Ihh*I-5BY&M3>T@|Lp@Iq-lyv?yMBO?E8Btk> z5u(1w6L{}a(sq(*VGqnLe_n57Yo`oBozh&M7_~NETa9zZIkhAorLjobNFj(5I)7*FR>n|D z^vM~>TX#Y^3zl`JWjAziH(U-UbdLOy=V4?dF~=gJ9TKv9wj_OGMj~kJnRegmiky(N z5!GTiC^ee2Nji~}Q{f@Q1q&mj7R*qsv00P>oe#i=<$kiD;*kLY=~oDSWF-{`d7_+P zvybjNl;(jXdXSyh4^hZSI4N;-`Ff=u>Ic zE{JKJT5Qd}(1lqJ&n({8@?23%nbxTl*ToD+6e+UE1_Y#J5Og9}t=+coYtb3po>CNN zGHS_HFW7Z=Jae#Xcc4ZOn5HydIgw4z&l)Cm7|VBuUPmo`8VsURYK@pmX=#5&chz&Q zyf7<$w`GXaEe6)!L_s+de}YcQ8Y@u%q%e6_hJPW(38)a1p(2W#OL=Xp56HNQm|zSD zRpi_V3O4EFck4d3k0GB7GN@!;8Uk-9NWg!y$(Y}Ehk*_&%TUnKG2_j^#)?G=C5r_i59xExut4RH3ajC>(0E?l8b?hNk&)f8nAni- z8}2;MpU{crZl$vVuj062mEviOy{L^zcd~0f$PS4pm;TcvazY_=#2~%f!ri2C$flj)ok0TItnjKyte4 zmd#Q|gsOqjFiC&H3f%csg7>2CVcP^^^7rkavukYQ4Mu@0$!Q0zsmtnNSL&EPlhVEk z!UUn(4#cAlMES9K1rE^>vI<(#*|nezc2ClIqe8pGN~HNZXKUW`%!4A^?y}mO z$XZ#QN&tT>-rhQxG;XE$(B$^qd@fn{j%I&ou`rf%cc+lk{|Z@g7>MFCq9d5yZZx|` z9ngrh7laF>$33eLMY~a+r2CQ8;kF<29x!Q|5+5XHwRCybo_irKwnMw26wcz|uLa}; zWc7hmmqv|f4yl-coYCA=zQHHfmFe3p7-x)|OPRAH z`b;5I2vL_~&6+X@RRN7q6s706d#t^EkeJTALZ(S$6m$Y$mAMUIj-|Cs{7^MRq^Ib{ z_`ZLufOuGs5yxt<LT%pK|*+WY6&{2V2gCw&#C3n1}N{%4SZxrT}iO1I?g9#pQzq4s~)i zj&RrR*&J5_JB|@|S3)|tH<7tk1=Yh%3A&(w^r0>T@C*%?rVZi~>!GA(LxNy&2m_+Q z7`TJsAe36@e8|~>8`hix{WBp%uh_phrmod9hkY6%Y{w3Oe-sJo?7A*)^Z~DldBlHg z_Gh26IUcYV#4X!l>dv_sWe_GMY8P_xPDFax0+2PECz;#*(19(|6`+ft*e3MJTWTst zL1|U4ya{38W5M>=gz})s#NbZ|u}>PemX+WZaV1co^9ku9K%bCMh*54z8-JV9g>dMN zI1L>lq2u@wbx7XPZ@F?}C>zmhxKV>qEpLax4L4K#SETQU?mclp#sHNuaG}r$8PI_G zukAOoW(&3-TCtjGt}b z?*;nobH}n%#IJMURzi5_P|?~`;F;694NyY*Cp-mIKn%~SU1XeaZJ}sTJU9&efNB#| zu!c-s%^lNr4j-Xax-jd7NTIa85Df+>vzV@vg9h!BRwiyvk^Y!}hP{7=k4Q0GY*^Ay zA=S&NXo(BLtT?8ytHVu4l%lU-3U@5NmpulQ8+X?;F0?z=alEnGq-m(>UqBM1$pb>7 zvH+SUqL37#g(L;=0+^N!dym$Qs9AUNY)a$y0{DnCtSCLcIFkg14ap5H&e7ynCmn6K z(jrMQO59bR%jy;<-MxQ7-@1>~J2rrAihhjL{eoD+35BYPpMbT3tXZuu`%pF_$ut|duj%&Bfgs5S!Qd8vO8T5~9VoE8DV&A&CY zKDSaC|01mt`XH%J2oI<+jx#k2nHjjmNIv9iQ9(tl!mW7Zz41!Cw+F5wl_|po@ZVphU65LGU7QUYTLx%Pw&=xnYQ--uk8EtnFpLCz%aGMl_e}SA*T#K~6QGn8s zqL1xyjPO39=~RC%44t}F` zB+u%3mcMI22^+gMU$c!sDER&=}+iDPHJ(H-J+FR~`|Y)O9_9uwoq{XYCfTM_BEx+)23 z5G@=kfR11b0)EC`10Ow=q1cqhp@zWo9-5@kOp+<_b)K`ZyTUmp^8@vm?LBs6qsx)VnrSG`|3b>0I93SYxoxPM$kI?U39`@`96jWUp;nBnF$EAqZN4G)h=x|8 zM+~ZCt0sQ_8AGa8MY0#FNWj8V5&<$=Z`-$M4VENpqI1f!BM{;d9EhfI2z(0)JN)fg zR9Q9G*eFU8XJ6f3kM*L!Rk@xXh$AgoU~Py;+>U=ZjuYwke{dW}mI~484g-=J7i@Ad z6igOo#1=x?)a_JzL*?p}62GO$?IBl4v2%r1g`J(~M>{o;7*qn;<-e&PWp{b#DZ0WS z+DinI>dGflsD)&)Q?~Z`3lX?E<8-vSFG@|*D&i8_r&dURSks?KKt3dg!c7l`KkT4a z8SZ~@potDgIDa`HeO~ zq-}vXmv1kBo1ovYFr^~mJtJ$OBDwA{p{_7SJn6hF`X8gJUZ?gCp0Cv zI?muJZ=p=CMp`IrgX=WfBU;aj8iOukzSe)_?P>8gCbtEUyQ~0;SEAr^SP(cedNniI zUGvl;4BAS1D=a1vMQ)16)Q8Xpr8HW=F3kR~nDT$V4G#BWLF4$P34Y)7cP-R@O*00_ z*inXCh7VI!VZ5V*Y+#ddjo*gMF3oHG;m(fb)8#IqYno)Y&zzJ)4FRMYFb1H_jwydo zMBWW``-esk17i#U`Y~k14Xj%ji#;}pGrjPw60OGh#APTLo>87MGJ~{*0l8n{*{fjJ zg&z?tQpkaAbwf8lY0GAhbofdRUCHJ7iP*8R4kpT*-ne6c+uO*K+;RCHciQ?UG%YN& zzbm$8P&bYCoe}&9AtOQAA(tF`aZZ1b*i3Qiws$t#EQVp<7nj@iZ1nVZk->fN{V|r; zMY7mcj70!ohuc5*%nPu2;RJ%kexT0Y0GI^*v6wv98g^O?ZJmPA_;$QnIOr3cmDnvW zWDcS{c>$WIiN*Gt-r}Z{xDc&0J#j3R+ucKNh%k(-0j1-5;r0fyum#?7~RZ;y{R9&txtSZlDw#ai8kjvM0DbWbT#50o;e%z z58^}^+XtLp%870jJUoA$OiAu0_;ywlx>=$zxs?E#vjbj~2da?PH9?)}ybQmd=6HA| zBR0*KSFFGqd8RRRx1T2QI=@El|q-4Gqv6 zCi-%+`vjpwEhYh^X*f`b9%Tp6iO4}8dI+027JZs3>|4gJ$>znRbHp!(FgF9va5*RX zShDO{p%ui63|&jsUeDIPG}43$W@*RG=F%~a+=P{F82o*6Kmo0aAg8Ko2tjhDV% zbhD<2A0?*(GHj$$!IkBJ*w82n7`RSf@Fa{v^T`NPc{>P;Mh8IHG;zf>n7Ii@$H3(& zQ-%&J@>kyav1`(XguQJGk5@{}2Ao77Pm+${1Fk;Bk4Qq6zZ;Ip8LT9$sPG170{T%P zc3B!17r~J3<`z-DGfQQ{jOiGr`?X zzOS@@_rfi>&g2@LQy@XoEp7q5PHi%DI)a~LKdcHgCIFfnM%T;Ej;uZ6I?GtcQGP1t zE|eVgOfF!U1WGcgFwPamHoKD8$=WJllLky@#aw?tz;NLyslMxFtbak5?qE2?v7vWF zwum1xg*eKN-td9vMs&tPpb1x{rm-I6-&*Od-W-3%Bu*A8sMZT0R`H z3rkjYbv}6#A~T+(DCyTrCd=l%Gg5AlqOM7hBQGgT))4m)rk9m3Y*|*VMeQzGOu{m`(PUl*Kcdw~cwUR1!e@AqVP0l9yOXq1hQ(WWE7VnIUZtZLrIx05(N- zzH32ubJ6RS87eMqq3ELY8+;1!YjROEY(l-=XmVDS(MH&$O-a5JG2lgLx8tLVL2b4A z0@1m+NAI*Gs-`SM+5yclUBIor`%b)~9RPpQ?nxUXb%6_iJ08XtNq3`Bwu3{A^k5R! zk?_eJ2l9}`w!llHF`(tr5IKP>Xh>X0gQa*Z3Javfp)f%ggo>*37P7yoktgLn04ym@ zL2?ADSPki-`w>CP?D;m1=}urm_W>_Jb#Qef?agnl&Zp!GXN&T41$vJnBV4Kk$%ua; zJVK^6Xfn+O$RbTDIf+o* zG>^N8p{5huQm&(%r*2pITRR^*(xC7 zQwB9lm=E|Yroi-fkB;#0=l>%;ZDZg&Pr3Txh%dKx7Otr{aw?8qzJ|>jJ=F()pE44_44)56A>+m zDhB#uyw zwR%E6sIC}xfYdlT$F0~B`JI2DM0}oSrbfo%ugEB%9qm{u@~Qdp>qWJvBEq7m?4kp7 zf}z=`1rWqf7B1<$!s@;&Zn(qiGdVcZ4+FGu#BzI}o329{K57AnrTc;R879Oi(*ebz zy@%1V8Yuxtif92V>vY8Sqr6iMP8d-U-lJjPH$;z(hmzULDaslZwSRv$yN3GCkwRn6 z5WccdWOgY;BT3B!ankcb(61yJ!58uB5Hc`DMeINTaC@perp6SJ>5C<{(E`&g{KZu~ zrs>A5lS*aWB3aAPIb4c5jC_G=JJr|9SW1(D7CHBsCnx7j&W@pGX{(>$GNsZKVN_j5 z%o`cCg;>VW^J$JHIYNJ^Ym!FE^AckM1M)U|GYtORQW6I%PA)pQ^LETo>3uv=8;5xW zFkw&TxXgvrDSI`F%J8M2xcf-C;T@pHoLp=+ISs|4!@<7kVV7jD;;4gH{G4A82n$a} zc-U8nuCP41(j#7}+uy|?)C5r{R>*KHPM>5^=}9(Vt7=O*XGedpfky1m*O&BDJJg+v z2db!In%|#(U#lRCJdvz0l*9q~MP~hdj$BS|kG+*iC-wpZtAyhwNJTbmU}ptT-I(qG z7*14j7*8i$nU)rim;%j7Wq?s5M)^HhiPrIWPU^a@D9QmPw35O{95JwJb3dL-h7Lx<&6zY%pIb-P^n zR7PTfRoMO1s&^}$C5J72%qSE_Eo~%2OEMX~cy4l$AbEG^z!%04C`caT0m)jKMM%ck zMR0F$N(ufYdcp{4S{wNgYih*Y?Dy3G9!TExx|rj1ac%*B59HV$vP}U;Gxw04Dl9NK z;n9%6z;k~Go^1yFPGk5FPfPGaRI2YNgkqTDEBcc1?_0{x8Y|C2qhl!w#={Z(Zu|*R zM7!zH^eq`f%0uI~h$?2^V7}fuxw^dET3Kiy`bh?lLqhG@s!5te=v}k{(C)!D64zZO){uDx-eo=GIB}q`< z4EXg$l~Q@8SSgk(z4Br9vnWJR@JY0df^-Ai4%Tw$?vst<(2syJ$ffHO*v~E~Zr9y+ z&YJ+?zT*tIXYUL>l6YZqoGW7J{=bphDqg+6MapKmL~Ng5+Z>Mp?V8Y&UC$z+Kynxv zz!HC{$}dGV#H=^kaB~DIjh;@NE~JRxI**4&`qqV3pX)WPpGPL@rvCX#lwsSo!I*lT z-GidBrkuAz!wMznN);h`aY=w@oi71GQ+@PeRC@k7YPI4n49Qk;H)cHbg26=0h?R)B zD$0mqNi947V;I>H*Z3iY*|fTe2j}CJndg6+x+t0KGhxYGokTr^45*{K6_;NgRiObR^*aIVZUVsB@SG$+o~V*G)~2fQ3=ATZx=;RY#B%-_ z-EQ7LfPo7H-ZA@2)=p;UJBbUIa)q!of>N5f)H=PjJioSv2VUEor%o;2xzz^i+P8mp zz<3-Tz4PAQ(a<};zO19y^!ARF-ZS9(9u2`~=2sSU6z>9t^CbBU`2EYQQ)_9A%~gox zXrD+Ly#<7No`kglvXwy@&5_0@jln$%+Hk%JE})6qm$AJWGA1fxRgls+c}k;>h>;ZP zj4^VYKurGlNdkOg8${rO17%6EC}n?z2>8ka*7RgabgG}$%*E)FjE~|+H;ZOr{@Cmn zKlz@)VIXofF|i5+Rij_Yn5PUlwz@NVp(L+9bRPN{@V)%r+0Y^KGo9*Rl(*uH z!|EIpjBas7QZln={B|qxlhiapkkcE*xFAa>d$!{$bW)o z#PG}JnMwahOuB3(k7@wI^_vWno_8!nFy~3h8O!Wu`VWvl{vZud&)cy95VCNXgpWj^ zQj_2pjfm$-Z0o?v^ulPjo7H~{pdgh4UgljpS)zJCVmfR*CwC~|>rgQRI+;0@a@gW` zLls|^*+Kwn{hQomocf0tX?UB`2Ae}Z9ISi_hZ(Ts-Es+@{8L#1I`p5+k$*~CUGG0w z6OMie#Hq%L8>~8~J#>W90&?OaC`)Wfyo=aE$y);)lO1-1ZcNaourPmll6n7uf$`43 zIJ3Cc-a5U$x`|WW#g(nQ92?JT+64r;9Ch7(2CU4}60W)o`E064;wxzQGqK4tdkP{$ zfTOdwO!DMtdbLhAvR2b!V5!;TUl^456ejtRPjsiZS$utmDH?^>{~g-SJTW$WgWjKX zZKq>*EhosM`4o~@SvY@1QwF+@jvQJujNoINZ6}JcN4;h0Sk`Qr1QWOf7s1ju;V}2v z$~dh8;0mejkj}IrQqECbxDYlEn%QkWKbIIPbQ|!L&h?}i+%X8sv0<$BRu=#r2^8B3_mp7NU zHruWBt=q1mro(>#zj6Lc*+zg3Go0bxw&g(tD;%V3<&FlFuG*gSlP*Zsx=+Wlx_)?y z=|1+}aF1{C05__VyauH87h_J9VXRzqk|DN>@o10aiM@>c z&;_1B68lT?oWU@_klNv~HwCb7unaUKg@_@tly%YcS-jRQ#|;oHgpj zX8AYGhdF*Dr|9B7W#ZzQGiy_W?MlQ!_gP)P+-f@NGL83eM zg_aX36juTA(2}w@CZM`ydi{g2?UTpGv*B^CFLDBsXBS{ZPP&6w_Hb6eFc}VV@sof_ zid1`=PSBV!w>0hIzQ^c+L*Am1+h@4jS-u9?9`=9OmEF;}0m(?vjyhI20gLc+AG0*8 zWMOnSnMeB8?XsBV2^OtHW}?N3Rv>+b9+DyD0xPkFke?w_AUa*Tzyu`~cPMqEx9bp2 zJk5J7hJy*`|G znnydiZ;6P}&_ooHahVu@|Ey>N8WH3NcjSQ~c{@!=7O~(En|PD0khmFIxBQ1&b??oD zS@`k*UJ<65_yn;ksSltdjM&@f_%_DodaHlEwXnF}I=QjBeiuP(5^f@?xK7UmPXIUo zIE7%by{eaxsK(8`h6QcG0_kCuiS%akcRONkX%GjJc_`?_3A*J;y)dWWr`u%1=LQGB zI@$tGz{6uSbl4%fN5RkWBFu>s{Kg#>6kS)_<=e8uVS#he3vV&~N|3bVnq5eV-j`L;O|eIA z-QGl~Bb9|o%_Yz(h8ret(a>uW_7SP?oU;2YDMmudEyqcHQ^yT~BQ><1(CC}br5%sp zMWy$+0kHJ;%eY=*0iOxBJRqD5q%MC9fREgadjY17Eii=0?3gk}GI)&Juc)uYS(*6) ziuP%yv{%7EcKTPQif?EG6rowH0rXCl5_<2v9T0@>H*m;L~ee z$(BZg1kYk8D8_mdl~-i5+^G=Xzv@X;LN2{uqEI8hiYn+DbNoN=hfU&-n~Z;U7;|3- znz;H&cOz9yo9VVJ)T*e+O??WqkV>kTcfv>+Z41ULo&(Ea0gS#G2;Dx_-VirNnMQO~ zr{S}^Iuq&~(J5b^tWa!3pJVxNCoVc^Hyjcj@#2(9wKA_i@4DcH6X!J7VB;`9bmEO> zqnLjz`;=*WbB#S=w6N7aGrxb(T5nHDq^2JyrATAyov30|itYl_j!8K#R|PvB!N_%O zVaEF-GJ};IS%nqt3*(B4imMbT#fxTt+v>WKi0PWSzUxgCw%Fgo=<0NtCsZV~(RdN@ z0ebn?GH@5tGr_T7Cd^6v7(u7-X)Pe=bl>%T8w=zElZ&wS!0G^CRyuzZtb-xielD1| z9qtWAfpa^Z{t%A_=YqJ(OOFC;?b*Pi3#}BHwjdk9jF#;`*gFoerkZ3xK|#>pF4l+* zB!rOAEU1Ve{8E%EB0@-k049(^5fRiAD=K;_ioIY#DOS25V(-`x3wFVVf}+@N_brc? zM)8W*eBYfvd2e@iW_N#fc6MfV2M5$D^blC3*r6Y?I=(2B0}~v9KY-v46*Z=+7Q;-P zNThPaV5b4)He8qROSa(6Nz4HJBsT%ZW#V{fi{kKM=ztO#A}!4<$*ykT)RL785FwLo ztlb@~TpDDFve^#%iNIHBry>@T>Ch%S6(IY1~%Y zgq63Yd@?yUd~P+)tzrZmDlwyeCY=uC4ZeVHNC*P_zUu}lFT(88WG241LmNb7$CW?a zFJG4;-yBLf0F%OirD~=?AVy6>Bya>;&q^2v!3g5NpVzQ8s^y!$e`=*_5!~OkUNtGT zjRCKc4V(D>Gn{{bR0lYsvZ@0*5)O4D#p_2Km_dwP{2}DAiERb}1-JaQoaF+y&+_>4 zuS8Annjun-N^~uvGTJXRQ5(A(qxt$sQCGg*R#F|-Cw^mLa?*n%yB&wfw}z=?AV3b1 zuSt<9(jrT>I!Z!O_$VsI6gdONWT7Aw%y6_vRT45um<@kI8%iQ(;zR;H9_snB`9Yx2 z!1o(G@LhQ#VK^Kjfr8;l+72PF2J)9UM?6jM47#K?;kWp-fY;uw`jqE(p;7D%Pg zcT^P5gly)C(Iq)VB4`9nRz5>6LWw2-rub#4_A^nETR5z^k;)k_glJGI*>QWP&ytnpGTF zWd;r&zeT+V(%-s3^QO85TOC`eZi!lxQ>dA`GUQ1)&RX3a#AyBf9YMg5>4Fw*BdsEy zsP%s{WJ6?`AuTG*OE>9?SuntzzUDWp7B{6 z+?OG{&HR~8wzT|-UT(RQd7}TYKIV!4$vuB8!^y-P_^0tO0aAf`QLi#(WCPwi&G!Pb z?&BSX^Bt4u7s8&~1W&b&+@I&BCdlQ9{%O?m#6Ll6bN)4FpuA3oNnAM*t}G5;;TBWk zG7=Pw7s2FHEPaLz~;f6H`sTPJr=s=39)k%VD~)y-AU>RB@>}0N1s># zpcz8)P$FBlpyB8l_KFD&-+0nUMQR~f}>2O+PV1>nR_RLKdr_$1}rExB^|kdh-#Q5KbgCY zf}cX8&f*9K6zMe3*x??Y%(hmRzLxXE$(8^68db{rcc1RIAi zMN(74AFgK@7&dY>%H}sf^Pu?#AWva=hLH_kUV*Uc2Zzw~fWUxB6#ttFC?F;ldM9h|AgU?fNXbs#$rwqNL}OHp$WcwzAX2`|RKokC znR>1ug^Ela5qQx|d1oOoIRt+QnW2F^mP|c?)CZaR@+j#m~gIsPdR=$(% zJ)GT?*CA+-O@Qto5Cr2S>&b#c#tZBuV^c5%vP2+fyYiwU)IX-sbJ3c(l$QMV)*%8B zr@7S<&jBeb3l*fEv}gp&ONap1ED`Qar)pFpicmi(FRoOcD4+WHnnr(ML9F?KOtiS+ zOFO#P8ADkhGeZ&fEpOpum{NYNR<>|KEd_faTeZc6DzIxgiYQ`(!VO^tAUz5Rt(j0u z)@%j|Z1iy~2p&M;GEMesYecG8AT3aF5)rOYr58h0^Op!Rh3T$Dw6$WgE0u1|&r(In z)=B};q=Zw{fI(0z3-o`Hh-`yJZB0f&Tb?{VTM$7W*)ZnlC+Zh0a8!()FYvT?n&9l| z>uEp1%?`>G8f}zZ37e@fB*fPV2(yO5Nbi89=?bOYCr<$Bu6=E$+Pk`eTPqc+n3$Q$ zR~hf_=H~3=YwHX$X}a1^vw@B-GxuhW5YJ-(j+B}{u-O9tt&B{fpC#mM9rb^1=CyS*n#wRK% z2R0l41E7?3(a(dTQRz4=A&1GvZW2V{4A#yLV0^u792}fI$(HCPHHs4wB8Y%_;=o1< zz0;#nM;M!%aVY?n1>M{P`~gQszZsz8t*j@Zdtm@zX+VFbpfd3m&6Ohn5hn>0N}wzdZs_J} z`B^1h5VvSF6xD5go2O|rHL@;dT{@yF9TJ?Vuo-$WkDmr4J;)JfpPh+ zuoyPs7&L!l8l!<-uzx}&{xEn1cw0CDpzDz$=FCSDd_u!py(apDqGUKIsAYs2!d3wu zSCqYc#@*X%;<-b zt8|o-&XFOUqCf*nngVJ4LHXQ}dyPmY#yd3>l- z$T6(+qE3;)MPI5aGxG1Ps?>BKGb%(`F;5)Afng9Zi6ZdWHq#xt?$~=W{(+3X=z^Zm zMWaau79RX9(L|GC1^*yxPm(Rx3_7r3<**58vhoFIVdA^|j01&aIZ>)8;z**TEKe%Y z#9e;_1rZGq>P@+pk&%-~S|I}efmGs~H=YQFPm6eLr3qYw&?*ZiBvT?57&5Umdaw)< zGEl$AErJM|%p|~HqNq01tB8~2<1QM?jc#Tk2BBX#xG~Kv{rJ3j!_p53ru2ZbXL${PF9%l=nPUiMbp82N%v0 z-RCXvizBQ}}(HN>C-xk^`G}{yuL(PA; z1WVZRI3aAQ*5Et&#+2?klvRNzo!R($ptu21L=veut?CbA=usUx9OXAMqm&g#;nx`5 zNa4g4S77l}S|LIpWP5@gse-KvM-vx@N=YC9Nuf{FP>zv|X04<_q;xgkWq|@AZ zOkprbNCu_>A{lbyIYp_;=v@P9Lmb2ff)r>KdNgeJWp&)lfp)3fl{U*D9QWUpd_HvUtoApH6*1P zqM<`2=rAfM!wEq$R}mctSV2!$Ik=I!6e+n!V60rXk%H=mi!?pCQWQc=DP-txnn*r# z=HE|RgEkjRz-kp;XxpirCZd0YQhJC%ZPoC@rzy(u({}$X>T!6j>4AUF$f8H7+!-$v zM2PUUA8;MAkV6(ULI*{m`U<3RLxx8YN0APhsY75n9s{YTi&_A*5~>1WELWa4o7i3d z?5spo5kxd%Pbk7rQRQ)YI2lq0Vv(DGEnrEIzJv%40fq2mvRsjvn3;bH4J+<-0VT>k z2eRX)PafOUre>r(Cn!7Ii8@X82rK4nfT6OJnS98Eu80t-Gznl-WOwGe;H$2FE04$E z{fzogSx+nS3JT{YI#%kCGSW-&8pNpl6unp_8H&;Ni@*SotT1bI6=-f{5FbJXax>@+ z=~;uwgV3>%}mmVHEPP>C=blw|sC{YGM-spj@;o!H&lI z1Lu$tkT`+}#mxsdfgCo@Cm~UqP02a=oe@Pwzf-RFU~YJHVT)MMa6^g+ z>~awrNIX)|52SYi9L1n-x!6Ug=mR>`hGa6jtY8RwCEX6HjNX4YfPg&$n}=3d&^ok! zBdAQ4Si%gE>NY;cM0^hJjYq98`QR5<#KB$~%WTKc>2VH15YL$`0~cX}0MWtzjm4V@ zK4C*H!mvOgQVkAO0-{tiI3OOA9L$OG7r^)_wt$1Rg$`nYX$y+L!*95ME&!>S(%pd| zfMf1VAwXuPlHq?H&jEPYH7c`EiEDDLgUVRbYrCwOl&EYcrzz`66{IM*G$;zb3yQQd zPf^g&_2tcVXe4kv;Hj5DE_JdXLQko;gCdQ$KG{&2pF>~SMgf+$2W+W)^QmmDCaNgz zOB6gnL^Wx8hC|;B=JG*7H?1FC%{h1;ZL&@TR-VT4@z)c3uT)=oM3>Ant@;AIl zLkAjm6RL=WgtDJ0ddOqzOE-vOg&KpKLIt6c&<2YP zTBQMG_yLy>NKF!>P#J`+rc4GV^p0@0Hotj7TgT=%PXtJ0m{U+)vCA!iV8&*uNDq`! zdKiBd#f3?aPg70}y(T;;p-ML|vYt%ojk~nm{Ymu}Y1B9)z~MoPkL(DEfkqj?5jJR1 zs2Db6_;585S=GX*SYwShGb%)Hwiw$5gRe1Jgmf_!7%xJX{WRX-hGa0taH#9iGAga) zM{pWX(I9>R@8!v$BIqc!7AmEHwJfg?HbH;lL0&soDQ#b`>FWm(*UN*2?+7#j4pN=c zyssa08SaFF;|HT@(6ze_eS|K_`g^Q-JT87&9?Fc!X$;N{Rce?+mY+uP6Ntimu|kB% zLC2mIrlj0Np*mpsU4W~L*5pkF*o{{<7&!Ur3aqE3C-IXP_`aKoWL*=qQ59}sQxbn; z+TuBM;&6)eP;34amOq1Eleon+YvoiHej?Yy#64wF;tRc0VyKcT)7;1+wu4&&oXD9H zQN#U=On7-gn1q9`V|>^iSbx~LIQyZb_$!v#e#rkP03_|kf`)e;M;$2x=L#e zJXSE2&uIC?lO4;63`HXuo-7TS(nP4U0rf&&I9i&NEb&YsuuVB|#A1$64quqwS06gm zm6L%T3{&#i+t>$&*A5#wtW}($U@hmt+a53>LnXI7j(D|yj^vU4)v^G>SK)ugLKIo{ zg3B(BADW~70&yT14aO#5@e%Rh&O{Hc;a~uPX^}99%5Vsp zd}yXXqY@8Y5r7y9dxwa^AkEE+pRDw!$WHGlUyfuyjKsr668?jPkR`E{b@ zD=G1ZkhIp=05<+s6uCsZt-F7t^4~M@TI{U9_v#Hz<;NO{LPYF~5}i~2mQ*IRuagCs zGL4MExHA2(3sV9(v=|)ZMw}NENC#)ZaYCke`B8VB zDNPTrOGV$LcZV%Eh7*7KFK83V8N4=N4r-KAaU*ZYRd5;re3S(5K(;{K54CrAO99vc z3i#6I1tKt0EM)RUuncwzJ+M&>0CJ!Tg7Y{$RfIRCbg&!YP=O>z=*h&&a>U?P zss%@%EWxraKMI*GfJ761Gw2H!;(oS9w*e1%4&*ryIh-5>0n(I`4U{>SD9TUZ$aITz zjA+B@NhCj&M;fC!k4}=2em_k;be1XC-;Bw$V6ZqW3yuk$X~JdGISjTbmt)K|V;N!B ztYa5?lK$R@23&tl=H})k8hYgaOJkTZNX85k6FSY<)Qn*c%A3*{#w0S0^a~yd+(A5?!j{OAi8iJMp*zh8W zkx)2bj5;1Hdg#hN`F9>nfCk8iP5vznero>1SSGj&CjI?SOY@&*YKG>&sfoE6!xWS^ zF*W}^|9{DYUA-bQLQ!T$NFPLoJdXTNDoaHA{rrzT44OaP-;7Q-=CWxPbcPAXpJvW7 zVVki{>85{d2AyL>`o|v{%|!Y9Cmy-^PiN3fNMuveFL?gX=YNaik1e63f9Ppe{zK+q z5aVxR2FITPe*KpJzv8h27ALYT*aE`vVhY)w&!TE;HpDvMf?r#+L34v=zNx0#n%bI9 z0x{6A?4c7`#(V~ymm>Ts-*8k5S^&in91foh6nuYkstMKDoNEl-0`Rv5l}N z5CA9FI2AVM*61i;lKG7W2HdwrZ3syQUFa}_h?03&HIzk6B<6&Yjlo}v4P$N@{0qnmN)`dA#4VO!L`9*P zi3CR!)J0JEyoM~(Zc(Ao%0VP^1Q2m+I(DLoO4in7P|5HXYz2HpV(6d6Q=BPB5eFv2 zYZqnF2Oyr#c+<`_5fodF3 zjVYa~n(!~+4a$0@Aa16B)wqBQp+}1egMH+V5Gx`p5}=lOfZd2A%FP~<1?`wo$Xaeu9wEi3fjx z8g_ZQ4R+*#n20Di0hy6dw29K`NU%3iPKs}oG77=`5dujF@JI53@jDz5RS@%LnMwI! zd&T)C63Yf%5DoK0fl?tDkniDe<6Mu604Y&|NFYwwh@gbxU=x`yL%t#aV%o^|AQUFy zJQl|gN>HZMko$9Bk(r^`Oyf!;EOUQfAHoE-1V_KoaEZS|j(hx)*#_Z8P;4T@c<}YV zpYMj_mlBbOYirsg%B63ljcPPL5IF#uBAC@48aatY0j&lU*pYZHhbRmXOAyM##Fz3+ zf_TyxBwDb7N`2N`fCS-UmtL(99kv261`;7!XE6e zpYWepA`nohA;SUb_w#@8v`+v3*V=#S3}pW?HZ@^j?~TD$;J5wfmpskbe_LV=P3ZS_ zWeItq;+CzzzfHCOnNLfD{7ZlAzjR}H`!8?-{ht57;%Tt|y4YAvaI~R{BmY5D&+q5I z>|xMp{-)+MHq(S*!e*G8jIaQXABz!eGgAhWV{YpIZ?gZ=%}jsWe}Bc(;`smT?Y|6j zdc*hwMwmxG8RBK-=s-568fy-5vzkaAC>cl=GXq9qyFq^j8}1<~z)EX8 zQq$1T5Pqc_lZVocv2oMHldp_a?&UWFuV?xqHP8-hN^L`!0YDw3VXv(PLLo;KD&WJ7 zuPc@c0hI*$BGgHdj~^>K8B051&qr#d;&-G%f*iF1A@&=kagi!#Vn!h&m6j&=aOHlZ zEE5p26eN&X4c-d|P{)5>Izao~OlD{UIVRXk8$Qz?G=>5c07VTdY$Lf7u#LSd=mqvf z`^G*1yeF86;{T>dI1N7mz3mae~j3$Fgi2^1NnoMvTdPC+# z9(WI4Djg68_yEfTz1@Z%-cZhvjHjMPzZ;4m^M`C_6Q5xy7yN%1ITGHq6$Sp4{_2cH zkD<>r@EKJ`ZB4+89#&Wq@L$`eBU2Ro3!8JoZhHGwB;v`me^q-8so8%`ThjTVe-(jP z^7LQXqm!9JKqWKsm$Wz27fb0aD*_r|8RPG)C1L@W3+hl!m9w@&jR_+0_z(^s3R?UX z8Z^bCL|%T>fb@UsC=e9ZaIugGRgvKXDd_M*JoHm`&|Jkr?1&AjEz`fSIT{^*>)M#Z zyP>1O88beg6CwrFwIQ=Q^f!b+*)NTgtB{2#CSJLFxN@X%oaQOyi80bq&tvyKHugOF z35H0BjbH%wuRr!L6a1?%!KI4Ru;MhVI1Ls5`B2x^>3@I!V*8(&84>?!Vv5=SOw51l zf4|~s#{Sn5YiK$#LHZI@yA>9|nPAC4Z)|Z%_S-J_Z+}`EX{Gci-J|IvQi|9;8S zZ2Tv-rvgWa{=xnK@8>`FX?gymgILs1egNt3e43yC#%6RA6Z!dXV#fGA|9{1^c(T(( zZOz_rMb&?{x0~Ps{u>AX-(D5`>oEP}MiQxSusz7=5vlTe)9ZE9ZH}B;?Lbx;uR4B4 zx4g)%GrBXk&G`1@%c`+)v1LYc-px---CpQZ;M10=Hm_H=!S^4GIQ}N!qnmr3*qKYu z4&K${Rn$k<+;Dfx+)E=YE!S7BPS5{%XL0oFPu+hkB(q%~BuBYa%ureWx$j+r!hF@f zhg46G{#MD_9GhI3J|n;WZGOVlfy3w2-HSM8KYL?lgg|9;%*QhCWBizh9eg97Bu*rm z9KNaUM(-8+?vV}U^;?m3#migi19o3XeO{2OS^eqpiXB6>Ej%#}_+R z==P;w-M+QnW7ieWy6bygC-f#C4c=FHcl@IfPriPxd46dVs~hK2ZK-qi=%8n;qSIb+ zlh#qgo@|MJGot&%+86!L&YH4)luvw#M~Q#IP|~`{_w#vm4`KinK|(ym5r6 z=Gn#AiwCy6xI{bcO4=Fwa6wOt4~L$W8kWSHkV1xMzACFRHT}cw{L~{Udn!`OY=m(i z=eFzr>1)m9v0q*>sO<|k^WH2H?e0+hAw70epN;{1y_n;iZYC}-Ef>;kI+3;Z-&IMd zPYBk3CaX->Carq#IrZVC?iKH!p0~SfxzKTYB7I(W&6D1uaE0&`F_>2 zzecc!u38p6-f*yAK)^z`i+K|5g-hQ~P)VDf{r*UL%z^BOO2tFa!PFu6G$YblM!%uYHuhQQ){c;s>}QRL*`3DZ z2Cco#2-5C!W#oj>Yy7{fJ0ASAleLy%+!?>z-rms$PQ7+sJ2dBP*J1pa_h*N#%;WB= z3oRemGw%6oz&9+Xgz;G(M-y#4@2=E;+GgHk+$0C_LSy?7Ta^%rAHDtBfw`us8l@dV zPYwS1>QHPL`wT}E>A1*R>Fe)jx*G?0z;+`VrBPywwJp1;j~ zTd<*~@KdN}_51TiHI5$3gYJzxZh0$cnZ*f}iqzzdA3hB`{x0jth`ix0(Z$v`*St8D zvaVO?ozYJo+)9rv{IVY)K`rZlsMT>BDZQXRsDHNCo8jaKly$Gq?&)_^eZ=VRJ*=lJ zG6^uz4CG!tVxFa%6UtgWaix8~_wP;*s9_g~E4ROy`}%c{xKW+nRy^OB<8kb2#mclb z_qkW?G<1TsGX@XnA6pZw5qyv{vdxAWcI(w&#D}aZG|Ek>jO?&tfMNB2+Mw054eE|+ zR>X`=zFglnv&>fQQI1E=N!7q9W82ZlpQ9(2E=kxs+97B7fYE(j?1HM_Mm+dt&{Jh> z&DlLOi;6m|+)Z`mGWEm(^_8&LP zASyY_>B(6?jo9xsKjwUWrxKeLUT6?^YjnZ=2&*?{OT9aFq3l_5{qX>Y!s_dcNiK`O z{N>?GiY5QVBXtw&igiAv?&m*$s+iyD(x&J=8s#S~&vdWOe;c!Z;@ItOXGW||^}1kT zB-nK}bIXG#OU&nPE-SShb@n{_5m$TVQc1|yU2ixRdaH+KYehH?_>reOtLEadc-F1e z-o7edO%lJXFt`+yw8P6^a`iw?_ffP-7xuP`ta|CwaaAU}Ytn9RoU!{jdR<}mto~fb zp81mdQ70;ST5p|yKS!neCyBaef9PKQ?QQIFFW(-2Ey<4k9Q5V!#z97lCb3I&rXJ$- zckaWn?yG}iP)4Yn_rK>GnAJ4CI%E@YfKlS0;Id!)FW-8uy=Y4w4 z{XR7@@6VMQ(-UUTO1OHEK5jo*KC)oUuLv5P14Fa&t97DrBN`o zU+RJtS#e8$7_;rn+H~!^(msG&r~Bzcy07zv0>4t#{Vrp>W^Fk2=-L%0vd6hSlnTf2 z4Ow^oTBK#_E>u0ZtiM*t0d`5pb$fe^VSej&eu`_i;D^JKj(>d`o30uBDS>Zwe|Das z$>V!puXxPe&T12M;qX_^>U1s3Z!vql93(d;1^17C-ei^X(PQJMl{yAfnLQu&%?6V_ zaL%pz6HCtc%-Ug4@%j0dfSTO-AFbVQs++6rS(slm;l$vAXUsWw-BT9@sfM)ar8nqB z*rQFlDw3@}H!d-gDrcNL7yIMO*qis=BYzmgz0Lj_a42Q!A(3i){#9y_M_B3kYToA> z`^&rM^JR^{LQv}nM+oe}Ln z9#{Qz`cPRXeUFQqUUfE!qtaZ?U8hf7{Or}s-dm=adOs6dO6I z`1CeKyBv9cXvC}ic-ZNSZ`&{3$fslf%-f%TPUf$lLmJeV9ArDaFk{4$w2`w{=1(rI zKc`ueX?B@=a=0_kyGyBO(ymP-17<%0^1Af;v77^c z263k!-1^QPe3>p8>b)tttn1ZdYUkT7O_yl<-MhVhYAO*rb92|J&z3Qr) zTGlfT9@81`Icxs&3wmaYnA=zsr z3yKHEcyi2&cA8Rpki$=Pwl%Kw&g->dyXWCGY87v`Jl@j1?MlNe)r^akv+mMNFQnal zCpK807I(7NA?K*-oAU!N_P_N!Y*6$p77}n_3V9#tJ9dzRu;$ArnTc|tk8MC zpms~UH~mlcG4~j=@U&W1`0EXSFFksC_j9byy%)%+_ubZ5%)j8Tw}0KoZ7EvTv5nbi|a*dr#~*scPi65 z>0V1J?Y(Txy0*o&RlC*O)*9};U>iq1I#2yE|5}xLLPXKU887xFGHj23*^kVv(tDRh za<=@?t^7>UV7IGB&JJ0mMqBcB8_B=j&^4~t)RL@wR0Zfx?417SD|dpg)~s8HcXrph zcSoXUQQkRl-LX?P&quw@t1B^@af){*i}Y&c8lB;L*3NKCIkw6#YA$O^kAv&4O;PXa zH)(j}H1TfrBU+Ukc6{uAMZdCh9!G58wf@8HnQ1Hb<~x3U<)j+QboR|L(~FDkJ1CLe z&a=2sI(g{5YB*wFJ;G~%~q z*1H*xve!~0+a}K-sce2@Tc6cdE17kqZBnUS7^lk>v3t+aTEYJDX*CALAyanb_bKj_ z7B_5jB%&G7FM@V9C$ALc6sfX=DIVYlMC zEMB(MI;D46h}vUS@*h?XrBe?Y?9mq&pIpG)KbuBQc<8q}@a5VwrN{O~uSovH4vxGl zKA^I^s!iX283&Au4?C13gr^l9emmzpKhKbKJeqs$Sp}orGFIWAWsLWkJ2iW-`(<&9 zyQHXHe?9j`Z@;D9!5Odm&XyP@d%K48at=+{|7KaXYUK5>>nRBbeV;t)B-vyc{o{#| zw|87#yNOE*)R$zXzlrCq*Gsbq&yKfJx!tS73vbPTah@eHPl}R;@3}oXZFgo)+g>{6 zT74qwCVg{$SAPFXw?fLYzWdT^9sY3ioV)K!-trR5iG3^895nZopW&)@Q_Z!l8)Grz znA&=N@!XSr*3advC{4-k_pZ~XMfzXcmKzjO#J&E^aZ~MbY`9ZT=9UB}mZCQ+^qjglT#93ZCMm{-i-(Gv}+c+zgGba{BRd0St z-f(Gg%3xbMCx@QVt~4&2rfSGi$s|oXI>q?OKJM*(_p4p2rg#tVdmDK7P3Bf}`!4CK zHOq(TJnyab{nf=Rk`{T&k)6Tfg_-)n^R0(}o8)WzU8T>Pvyf7lz&m7YB?zAS+VR<+ z>bsB4t5rR*l=X!==;3SO>5$n4pUM4yToVqTpS!$Yx3~#AC&w&son`0##?X53S?{@} zh3o>qcstFEBiZeGsQ{sIc~kDKf;3}p`>u4+jkJndb&HHU8?yaZvmYPoy}Djhe)i*k zpfg)ZJ82!y)245KKEc3IwQWL)_MlS-U)(qA6VAF}J5RGPIcuo7g&E@`+1UPmI@|qH z)bY|8>sC3|zkRjwlv4=rvFe`aBj+bPP9MC}+MvCwibcY%`F&o_$#~;+tSt7f=1o@D zE~`C;X_m#Ew8)#LE9tk&?%vb>JGzg5zm^jFzIWJKV}t%D#1kiHp0!(@NE*8K?PKki z&(DXRTH<4AQT|8P*(EQJK0NeUU8}gu>c7y{Cz+Y4m=4YQxTK~&GCQI`sMqr{=|@SJ z(V0)0N%2%?6N?LtP2sfpc7gI9MubaqI7^w%ND-(KF-E`E{oaGQ6Vn|`@}^aUDy zf(2b4?@Wz%o&U;7V@g;-qO&LIho}Cs>0YGm>uo3L4f-;4%M3=Bo1}X|W$Jv^y&%CM z@1iU5)79)It-Gkd_I|CzqLMc5AUF8L+H$wHC&!K7P$j62A;rcnSjL@lZtA$w_Vab_ zJRR9XC1?MrJd2~M83}7B58sr3rTe&d{8TmaXh4^>Y4Lk!KU?MCDJi)hyC`CQP0^O? z=Yp?fYYmGC*DX{%>`98fwQ1RTO7zK@*L?@&`@D9WG&Z$cmiN{ z^AAze<_;14IB~N3{!B;D>2XQR9^Kb2J$WX;t<$2*Z*-@q%>UBqok53jBXOC%sBP)Cx`!qzk+k~wpit+U$nMgT9tYSw*iw2DB7<8PpssQsn0);+O%hI*-KNauUK z|0q_ix3t~zbWKI&=&)xOrdAnG3Qrjr9X@Kd)*l&B+6V7yp1kC;J2fw#KlLu-ZXw*BIhz3#E0G1Ez>sw1K@qtr8=Uq7n4d)Wa}27m?t8r0oljNuH9TFoVGAZYDwQ5j4Y5LE}!IO&n73s%%YcPz*<9s(LAV zq5V3Iw(NFUi{iMBVP}W-IN4F<$%a$!ul1qy%BfU;-Cy)EsBFwQ15)|H9qQe}-ggPi z>3C_g&*!>bo;~82U#GPlYkOwQYReM8SvD(bRW6^{X#g&=N&@&QGlGT2?8p(NaZtQZqUU@ow3Rhkj+Z%mGZ}ETXqfY%KZCbEnwqpPvSb;3>w2wqQ&!D8bxJMd zhTGbK^R}g)(^%v^H{f{YH|nhTZ`YoGxvuas8FPEvijbsl9SSe2+g7<|i)|l| z&HFO=OV2pt*<;$pO6>H{yfMjARUIDMv6pK7XTK92GG5MANeyuuI<7CAT#xR5d)zB2 z{!q8Ge9M6aN#8r4TUTK6{fo)P>IBWg)p3{iK4fU#-pSY_7&=rxEAyhmvIEPgW^wnc zmX!AK+`3`SvI5hstMVUdmv-Bl7H;SfU&-l|klgn5*^~1b>N}pV_Ov)*m)sgC~qf0n{#FS-6-phU%yg4MGU=U^b z)35alA0Mwd+11yfa8?Ens3ZZ_)s>uA1FH^+Lw9xx{nO{sLW7knw8_2tdgh%v7+*suI z4oTNxVeIo-kJDT9j~bAFj27-G0_N&7tg@U#+J$+4EwPS#pqgbIG3xL@6-O3@;r~#j zQoDziA-h|rj@H9_9?jk)sZ)C)elYRd?#=b@pKaOna6)*JdA z*-v{O|D@(=?NhY>^Z1Axw>o+b=#pPpWw0ka|IHEe;nrqT&8L}Hb=^`D(q{YjT8k&^ zBkOBpj0!%TFw^Og*-oEi4Ndzuy#?>6YHN;Z>Bq$-9nc(?H%%}2@t+?(!XEayYGCa- z^TR8~`Jy{tg(8=KIrqPGtU6wMZE3qnl0wzc-MQhf@AMJm6}1^*>Ss#X?&1HybXzy$ zV2aPnK972nCkH20{pnGuaXVsWs_KU5PJ$bGU*Au9epyhTY43QaO^-Or^@_^fTT;|N zSNGGb*!v()T{H94wIhPmXOuW0~K~kAK!ST9R_)!m&Z0rjaHc3{rbxbE|i5c71-WzJq2)kWQcXH3iRdm#2@2 z2rqb-;2G(a^{|?GduI2zW!K|YeENJNeNKs_V%L>Dz8xlKrg{ysROz#C>|e)Jwb6^~ zsy*cPM)tRV`?b$^9Id%1nDaPsB7ag$O2IwVmGcG_?k<0ybLP3qQqi{Chd(6uT|02_ zRza0Pr<>hP)+T*8B)HwDa<@Uu^V(wtlJ-@M4+Fc!QNHfIx^Bj-%I9N$j4-W^HrZL1 zn6Xd2`pxvY@g6Q=Ma3z1#ilu>pv}+3HZ#q1G_CWp1?+1ztG`fabsFC{(kbF32 zkpA^mz}UwL!4FdHwD(U)Syszj*{N?I^5W#1Jz0B3y|bV5#c%KT!Qb|OHy;stY|Blp zg&w49@1|^d@cB~2oG;}M-k*}hLULN8FwR_*ofvEM2`yu9VwhU#rJZ}_Z|sw3_yE*du?z^vu(^ZLen*LU`q z%Za<+Y;z7fqZS(T;M<#ssG0TkFVhc+Z=AmNhh>f@?XR2N9u6{hpplIirgcc)74V^_ zfKeUvrRv6y(eu1Ie$hPB#W(Vy+Hx&UX)`Uwk5w^^6gn%lpokQ9VtKaDB`bQ_dH&1_^hLkO4*7J1<`5h1CEX60hd$RsPL*C>J?5s3$s7Z zC`#@Uv9o?rn!%``Wwm!-Mh#0W>ASRlmR}vIxssj{m8KC?Xc`vKztde(;`eL2md^G* zpZ|T&`8>h*$CPnB);(XzvDh9w?+f%boo1{(=0N|DIX`zhY0BIVCxTT^(eHANBXiy- zzPfWk=s9=L58GE6;kh>~mw3KgF?LSRel@1LcYQnyH!PSHwqN5@>>bm5FDk&x2>@$jNk+MdV7RDHim|?~i zsVR|)Hj*f`tNv0Uv{Fe)MI=jqiZ)6~r4rJ2pCN|+^!2sO|NDD=U;XF4``)|vp8GrJ z+;h&o2L%o9|M?x)ko5o6`+txq5CMW^(;tllkslDKk49l|!|@-#^(s2%{kiN-s*$=`{O^o;Mi@dE44dIbkEL|o^zf-v^Iq{U@uVr$JkqcVA$pJDPDB2 z4W{=@@ZRk)UAv3e4ej?v_&E>&ZbI@Jxcdl+!=X`FG@ghDXuuN)?CPdW_KUjW=0K&M zap@Wq3=W6I5pXyN1%?MF()-%K8=s%H73wQNLI&H5)iXFgGXZtSR{l(6RERl#O5;+M8^&w;z3C*Z9${!*j5(51v zBGGs(3`fk+B;-;&Vgb&uSR@|8V+lmmpm-#rFc1NQMB#}96xdfWC?2tZk`R%I0}~yG zM-Ga|KM6!6M1U}VNF>-KOhBQq-@#S65Qr!gP^>Um91#n%6e1cmC>7B}91@QQnivc< z2to}?MFwfIqM{fP@B@qF}o_9)|*&I2w&d5(bx|NN|RKCK0@rh(iOxOZZzFTGA+7 zAVrY`6o!Ds5P=ZI;j#ZvDT;#jsVyx04@?}I1q9ODGKHV4m^S+ z0<1&dl%l=r(4Us2KxYGBMByMT7K_IHV^N7i0+|O-MI0U|27}8|3<^nvCoq*UupI&!ip(^a20`A^@ERNbXL#_&K>-66`G75gT0lPpE3`oc7MP6$kT3xS*dsvCVEW^qR2=cJ1qz43iaoh!7eB*eK`*(EJB8Zo*R)LK12t)u`5P@ca!UG=(>N{-J+(;nUY6+wquv@}f_@M3vKo;1_i9=$4 z!K!8m)gFz*gXJC?LjY@m!2}(h{zoB3z~X^`0Br>N{Xf)Bje*UNfP82y9$uh9gF1}B zA0X+#iA}(qJUEAh{e!S?2^cI2MX|AV6Xg!i0d_Pae=K8oMD0jJpx)c=pEr7 zy2^&lR)FaNtq@Ri@KrX10RRIm7YS|Q}3eF)dzRl;qA>*tR8Tf#w_cwl%0CWnFa>%BJw9Z{Ot?#DEV zn8B%t)P%Y&_5+Y+cTa%fs^?eK$sR@|zPiL^0A7-$e`W@F1j#G#JwOB-Q7lVcV2Q+ecG`Wtb z{k+CM7md@gJ=Ha_Ll6;S*F^n1n*BHoIo`~DB9YTm^$m&*?BMs2t7kBOdTR|lg8ZAz z5?+)HVaprB$3KLjX$U*-5FY3u{F_6VNQRE|8$2d=h;`o(>-!;GO@l--eH%B>0H8oW z4it{=a2-%MO$EPpL4f2UX+`{TVbyQ`a~nlD&9FaNbp+uhnmF|Ls*a$gKUr_Y{V)zW zxenJ^R5*8Ad@l~!9sTcrPze1OT4N9XejWh{KaM*NvBZ6xlryyHCo734O*G*?P~k@)|Aehu*B^c+e7#X@YI z3B&Fz3JE%h1mT4J2#i9js10yOioos)b`~}YgG;E~Uy13>G3WBNF(jrhoDPm2@U?yO z*_rwKl^U%8`8%lOuH;BA6iQFAIUT00U042nxNdK9JCI7wnX-UMa{F4hHzCu&d+d<| z1JdR3>B_REf4a&IAI9g2hmXt{_Z+ z48m`)u1GB5Yj!?VNFS$?0m+x_Zc1~dv-c3YQW;=a{G4H$_9S6qvVR)V{ov^W^?w+i zW&;V>)lq#K8AKEeyxBWUc63iaKtT{y%mA}xV9hRMn9_QGk_Li7>zN+FJLpeYVAtyk zbEDB26n1VcCP=CSGEp&nLBcKexcAgz=h-3Ad`(PyyFvxM==p*Th2+KjdW!;+jpD-g zys;?-_ngwhApA7{}%ljNp?ZvTp%<=0iJ(|gdvlhQ79Jz5l<$&k_j%u`tvY; z0e%N;b>~9-ays+=E%vKn{0`&yyYS1g8;Tp{FJ~C;zeWBI+pmW4`#t#O+yuv!_~i&o z{I|$|-(ma?7Mlak*j!M9<(lha)eix#1KjBRJ1*eGUhac`2B(Ml7ea&MpffHUh<>3mzi=S>o~X!; z%#y=r`3s@JnInMv#3$$C^%pWr&eNf}Z$@x>!G7UD^gD@->jr|;o$w2x(eJ!|t{Vu> zxc*-{{C~Kmi!9uD_js zyZ$D9-8u$0k;VkM2zw4Cn@^&3Eq%L}Dc*G7?qv|V=MX>oaw-KZrFtv%EXTo*-X=g_ z(*`oZ3X;9Tn+~_D>m`0nU%J;fZ`t@VC^R=;K=7Ut11gExwM!V=UBF&K_7r1!N+~3U zl_!bTwUR^iY3o4h7%@lz;0#52&teRJP?^!MTJORi&AIoF-8=)jA6U2c60k4TZ7E*f6b8xH z5AGKZi)X)V0h2QDZeJSCC%I7ln4D#7^7k%Lkw~zMb6{R95pz0?Za{G-`BUkCeq6#$ z`eiozrj?0pIp}F)Z{Nubs&~I74p@i+)`aX~J+62-SC1>msOkcEDv|^?P`kn(q(DS6 z$oEF>VLj-OKIcC6`KF;KiODo|q0oG(u2k3xz@`zk%c9bE4t&iKU_}k2crvXhj0H5R zZ`T;~nqZ7cu$>DOaz4_FG4>39B6RNv#(|OR=_rLlGl%U&eZSdNU{7`Nb+=@&GZpmQ zv2tI*q=GN9+kj>R2plTwOd>C5zs$%Am~MzTHq-$9{5*Yayctwq3daq$(XM}&IA+{cp1j2qG}6K%VAbJIQ+Nl^jFSUuHm>Kt95ekZ8~t&Y#YIoIiWT$$ZiZ zGT>phB!(Nsx2N-#G?;I8ndJ=KVe#V9BPy(5rpfdLturKfvaP4=8}Ltdg4te~!tNV9 zPgcyPxlw5p%>|~KFe`>f+Jr&!cBhi3_5adrfADgReotw1f2W&g>mW5D_>YDPcp7Gk zIy8!(FN5T%p`5hm&!eocr7x$@bZFr1>4EkZv4}<>uKo0@Ta{zuoc0aC^@|^h@hXFqf_+h|* zAK)j0fgd2{`zK5O7c3xwy$R-VDCpOsp6zCzPXPpCQe3(b^L6M9Jn7`+VAR-sg;EUh4s-KiZvHRg=BRITb8sv!@Na^^Gc*n?iMSH<5I6)4 zSS;a$@u2qq#?6O8KMeYR73et(H7FEHMQvarP7y;A&7Z{Nu29o&T590g*LCouH;Dnp zxF?vhfA?VA5aH7dad-&Fo!#SPYX4RD;Akow(BeR(KR9!LnmfD4XB55r1{}=}qW!wN z$1UCJyDa=p|2%qtPZ`B-HTVRBG_Z+XzU-dpu~hM zYIvuXU-Cj)o)i@=e%guSnS0O&bA?Zoz+L(OrY6gcR8iVNqk zOh@(9>aGS)Oe2b?FNu904Y3=8f_f-R^{4v|{6zm*2FL3n!3f|X_{?GyH_kFnPC8^p z#O_oWKXaZzMD_(9>0c1US@07l-}fCe{yQfhSpNRx$p@lUQE;>u2%^EGFx)u#ICab+ zQ-1t^zf8XGOCtAsC*M!ze4InTrJS#ivh}m`&HSWnTLU$ffmy3?NMIF*1HN&1z{gPo zD`1J>1>g@1GXS}M6$Xf@{%;t_N!<}Ilf%UXgOEAL(cX87e_LV z`TkcG6ES>o%Y_E^RSKC5@`M8EOoG64#1SZH@RNXcb-_?vwSe1);;RWP`Xt1UUNE@9 zesKiEzWV)kVJBCY)39qk8Tip$JlT%8er0wf zV3~w@+c1;(Ef;zy{CfkLMFWIF{P;B#{vjNijejgS7ZnZTAB{)jhw=YAE`1n!Of9yP zUrXPg1V&?aAREzI?431VH|uD6cL5%MF?{|1TtnlZLGh%@}_?HF$Ux*j{ZMNjYZUln=i>Z;mZ6MF9-R6u^ zNKTJDIZR;d0n?F+ReX9(9__I#bJ_D4^lp}@cPP#(oRn|%WLBk}3sHFU!TL{sai|hU zjpsTc+qa{Pva+%!Rwdp1{P|gvrsDgZ@2liG*C#0MSAFtQ^+}#Qx&6#C`eR&G?I|Sf z3ZLQ$p=J$R{s_cv{+R(O+JSC>D<#OvMI6NqJ*3&g#gi>5V&ej#*A!>nnCwy3#-Q?5Z)*`XW;V z9WT(PsW)D-$UB{E7e|nP&JgnE6KpcNyb+O=Sl;}m#AzY#O39Afx<0(DP4|yP_zS8= z%g697;V0~rwz}QSpP?XfY>iO!OT+HLmN+Z0NJ}sG_QivKaV$pdj+nc159&6knzIyf z(3o&Pp=mla!#V-J>Ac>o>KKV>yj8A>PUiu!W`8AC?IOg>F-B*9T~1er3A>*Udn8%{ zKH^RARX~4N!zzYZL*H&-fB&6 zDXYcL;ycNhjg~SY6|Z;3%zF^=p0}(e?aLAqvo;USY@utPmbcwVe_DS%%iVCP=^UQ; z&M6!5vsT|F8`R4QKe$xV@=)i3Wq=Y|O|Ev6^{9z|Dmv>YzbR<7L2i1f99e$LZRzMh z-Z4D#kDq>8A+r;2F;03-`e|_qHNBgpPcNQ6tIN}tcOGw^8+h(h=l-_E*I&GKOnp#0 zwwNEoXV-GLl*$ZA#(MGueKLH0Y&FqM-=8cp%w`9OVimF zM#l}D)*}8-uAy?yXd(Xh3#8>8T>ppq%YWd1A;2P3u!!Gq{mbS5ko50bn2;_FOlT>){RPu+&BuN^j``+@s5U>sc7M~C(vG{X+M zv-YF-QMyl>Vfr$_D{M(%!;^Qn5zU3l>?_En*SHLy;drR5hG6JS# zs;9oEGsT7ed%p?C`2WVwu>%x;R~Kh{QJFB2f7kcSNmLqm`S1A_7*9{If5OC*&Sd_s z4tAZ_{5wY!oZJg~NecWe--Az;qLX|%SwQ!@kUeFFe!g^9*Dj~sx2N`iCybbXz6PEY z8q8>Wir7ROP?cRyrBRs7o)7dEujoDS8g?}Ip9g*3-g78Wf1>q2r>l2-+0I;4PpOR; z@U^*MGm| z8r1*Sy{6LiBK?mRfZ>Z7iP}lKXjYk@Y+Oh z&1=5q-S2AK5i6Bh^^v(urBzRViCZov)g4YtqkDNxfBvw1`>N#Q2ayx^%1p!G1}=js zS~SGwbAIzAEiGys9nN0N6n?NcK`h{%9Z|A`K1S>F<LB?W7LUzC)bT|6zI ztmEF}hf`DwnwHLd+wi1te>5&={I!u%th>thrJABkC8m}tmK(dHtYvudCZhUMPiR$D z@fKghlggU5R$Mu`ZMsfILaa>qS&>)I%D25P-|V@2=BJuzw{?VWqY=}zbap4M z@NRkgYOL%&-77q1*=Fm1X6CbQL3TTrom6KHc&x}2UE?@nq(Y_f#wCoUOaIj=G}w!lHqRS zSdUk)wlAjBN0Oh0V$9^3JMSmrh@Ypw(1lx7|DVKK(|T za>DMyt(FP8n-F7lzJQnJ++4d~bM)nym_3WeXY-I36_|5oTj9haP?fTwbm0&&HVWeMAVgkI4O7}Yz?kB>#FTGrzCZy){3e#AKgDU zU4G9rd03q3ReRZXW-1M9YbwZ};NReLa*gGeINK$n zS?IA5!ag~F9>rwyKr!vqb*8wnR{m*~QIEDcxfd`0nD(H(sr=Jj{FbztsnXA>0(uQe z9nD(j{QXLtdGd*}=1(4l?({-bO)yH{x+T@*#fNKK6dJ_v$7j4wRo!)>I$X%vs~{u( zk86Ir`s0k#mlhxHth-Y9JaTn*%Mt#6+A=L#gKKrkJ&%mqN(5$hs^A82 zK{7OdZkcI#)_srAUXStXN+o8j-RDtZY%U+2LAGi$TO4ztY2o|w@_;Q(tAjtjxIWQs zvqsvPhjm*ocg)mlNDB$9x!oF;z$2Pm%o{wTjmmtN;c$3rQ*z-=+oy>9mE-jq6r^Tu z6yC<1J#zPI(QyXLZC=UkJb%9U8gpBM(P)`}^PApWckF!Cba}DAH{nS!Z?fU)SM~Sa z`5V+FPx91Cu2LCoe=p_GxaI}-8>&`8b0x2?X;#_VChxstt$H@^OYhlU%kU!} zh!sH(Ip10ND!6f0(C32CEqp$f3W)Q0$+yj3tQldu5JjmqUKtZUf^W>EM@iwUShC`t zlUJSN$#8$#;X5x$T6I)Q#>o03Ng7Y}*Nkjw&QSad5rLp)x386i*ElyP*{^1{r!_VO z=rNiR4ZG@}+#VSsm}VOEhEKbHPGmNJ@d($7f*nskU2U0rJ8-nX+o`T-F>8_9x9DQQ z`7!3FN>l=;k9l>)*%=wQefv3b{pb4nizSO(+C#VU39@9*%zQ@}?QvIQ=cD}F(uqz> zv^T#)yjD4!uwO07OR)t69W<7lZP*Z~o$jL4wl~xDp^O(+)y`L5WmBeqB0c0%bGzl2 z_vL#MQtLZk@IByb>8yGt_!>>Px!q-k-fJsNt>jb#+hu!pddRMeerJ|3m%N~{HOb6x zV)6Q|=iYc)>b`k%`TSai;L=A0P77z*8iI!%Q-M(&**5q`jr3YJ}XY z^sz1@#(t@-YSB=3U7;y|DwLALA{`QH+pXifJY(=Hs z+`4yWhTFCjJIp~OP36NAfyp7TGX2G0eGLz0bu2#Ff`(a@S<>HbR5-IY3vZkv)7;2EN%wKCA zI4v>m+QhSwmCJpT43$hQ00%($zm2pbEYkMW+!Jblz%#jXn_}8}idUlP6!#zpF*lET z&x5qh%)`D<&%a$te8}HA`biAtZfvuTs(84b_FJ{96W=}0duetkM=o|eiZ82zQH_(rlFKQcV+Z&{s*H#fmFp5of z9&4>*O?|RHwd}Oa;~=H56$#_cEW3JcheJW@i*VAyr)yE0J+O}n>ep{ve%M*iDZVdc ztp3VPMvgUaM%wJ(ja7VEluzInwXUTq>dkZuc(o>lFNv0N>c*K!f2rkry-V+Yx~%eE zO0?Qy>U#T{R;#$psD_=>a~A}?zC#qfdne60+80e(rlcTC9GA0OBlY10pLgX^>4zN^ z+GfU97l;!&JF($u*H_$}uzQl)@81vYjf1OY9UxJM@(@z|p=>GJj z;rW`YGN{^X<3zp)NtPX7YcWzl5z(<;&Q?f>@38f(u#Vb&r3J4))fzSV3dsv8&kK^Z zt^Z5&srl+r*K*lUHTBZ`>9=GwDP>L?u`^{+9RY-i(3qka>Oh~RSL?g$Br(~M?;T!W-w?0-r^9)(k zG(+#~ zE#WWee|z6v3UPcT37tjijT6Tg%SbA+Vh^0&(|BU`S;y219kCnrwN*kFJ-&lk)b^y* zGTx&gXeWa3`E%51FC~#xEho%9Yv)O|28*(EO;#1B$?^M4(4RS0YHE`GbDk%kou@3h zm*4!ztvJGJYI0*ko{{LG{KuD`RZKXLQQmZ2fBk`3!rkoC_Xu^^y!VP#{AbUZX3ay` zJ#hF~a4vCS(qcEcbK@m6_dA!lC$6zH@Di{{w5t$$wn2Ynv<%Kd_AIjD@|0cS?q}C( zMJH1+ZY=fMc&wdFW2pn~rkt|+N%duR8_WVy1h*_GH=Jad)cJf%=(&jCC5V=Tr#Gz9 zf4L@oq|z3v6#O!?o+_|^e&!MVlXE8#NOR_lMPPY@=WJOTWp%AS%;|YshJxB1#l2Ed z=Azo!^G|E0hP=Gv&$Pk^J3k{GXnwYF?4*P%lg2Er;&04S`bf=t%v*ftqVb#EBO2sS zADVEhA=34cT1*?&{;uw>NZKYrKS~yPe|D>HPK@QOTl2{EumwKKZ%e#gsqmd93Dzt2WASNkODi8fUtWd6af6}<~ zLL)zf%I#2hHA`3<@=1JCa)Io5wPe!zQ)EO-b9CCm1qGzB5{p7}q%N#FcRYS$_1b4F z>U|flBHcy}3umpUk9-T6jRHGN-}^<|nvKtxA$?guyautuk4nYF1`4@-t{~;SnGzT& zIy%h)ZDEbu@!(`tMMuY`+g@i_e=p?_hi5H&v~k4_8};R@lp=CkVs+PI5*HoY))Bp7 z*P|J;w|qL+ZY)rDiocDfcS@-;dhPQ!a@4;*tlOs%e?89CIlb(iYxw-?Sv4BBe0?S= zyeuR{dZiwJo-c!UC~Hd{mlWo|YP|p33lGA|Ds$EMDnHvVJ7?9S$7G4be~z~;hsSIl zbsc-Up7ub&a(3(U_DMM{7NHkh_DPI+p0>$R<*gir+JfIfID<3FyO8&~VxbGwu*BBq znX^&?Zhgc0>lrk2M}}ye)VKuvlZdD@i_|DG;dx`jP@8kh?_EroF~aGM_`+4>n#RpG zB~W4|gqU_9d%<7!kz!->f8%rAaweMc+lu<^9s3tPNVsLX;K>79f=-Q|DhPclDvB>E zBj0UE4E+=zGfhpN5~miM8Gq)1*>sxKDUZ*g;XYBfTfJ^sU(+k?d~{6f3QcQM0LyL% zzRvV*r-!uL^u|4JH>RC(m>YHZ<6)IudI`pk+qNDZcjCyphu5O;f2rs4iUSY?k27;u z8oZe^YnzeHO34j7#-FTJC{%&a$<`#pb?AlRb49XHx1sb2pIxzwCKaAC(GO7HhjAMd zGS)r%>{97@>5*FnwhvAB(S(dIk~=VMA_ zGPg`IiLDWw+x&l&Y3JoN3VeMK8JHz9S1p2hg*`e_0#DyLVVFIhAc#4W zGQ&os=}=YJlN>&hG9~+nqyIhsiK<151e`(%&tIFX7>})dAif zHzzJV(0ONeE2dIH)F1h7!lI*3dxO1}L1`24O$Ytz3}$b&tM<%PL7Z#L%uHYM z>@CfNRQm8iWR^?uz2KOj>QP6xz3-$ksDP)@!=g+ebwz)EY{-XHj=kyEK ziMOOZv=ndN{`&s;bbm(YJxy7Dr{@PQdM1$9mdt4j^bweM1|=3hR#1Nmq4nnCQ4ew~ zgPT;3=0Jzc^ObYVW`EAnG^`kBu0J|r&qtP0_|jTse=B?S7O@bC(Dq<){{_vs7@ad} zR`#+wleLpe#gE zyL>zT!29Ki^Hbi%yn5!?_SdfF5Lbn}&#u3*t-b#-AQ+Q>CH`uBP~MGQRGuz?MuRcCIK1xikFkdv?SsfedK`LAty0lVsE~e8|!rgwI`c_-& z4u8_k2#pG!#r0`7wiSJC z@L;+~)6^7=Jj9w)mL02_X2=PzGq6rYDft_ye{5d=2;07Fq3}D|yXT7V=9O}b9BvxP zuPVNB$htk-P+rsrMWb>hP3<=@flL zhG_M*=)I(Mkj2VzhM(^yY;LATvzAHhInZIduwgZ`_PU9-q@A9x_?wIa_#~>|^`a-$ ze5Vmp=7ecr4+>n)&eADi%n}T9-BJ+ee^V%XC@iqMH{?7xk(ljzG#{n z^jLG3Y^xZa#HZ}!HsOQ{V(uh)CUb#mZoKLTVq)T$V7ct5Q|2vK*XCYTihAH?C3E0m z*^ws?8yG6An#mIHO4haJU7WYrkTl`|xy7_OvHjVMmIos;74M&wOfOjDR9>Qie{E0S zdtW0ec$4`io^cD7ywA0DWZm2`b4rrm+Y@C|b(rbS4J#2iyVq2i(FS^RWm{_NeW?3Y zMU8LMuu~#l+}}GvP${O~%s$&OXES5ILGc?qPkD6{3|ZWbS# z9?tN2$$yMS(M3p~A0caUfiHBcrF6KAv%$7RwVML%e722+;Rt8V^*mRiKk06Xym&Y~ zcwWtCo|$E=k=P5QjL}E0qaq^7h}+{OL|j6SM;v-3$dcc`|A5P@e)cd0rRty8-yIBK*vf=BmVaC38=CeJlggR1NmJpo5!`{g2q4EK*>E#bPaO+@pv zx6GEYt4cL=+Kd#GIR|NFJ7LS}__EwX6Q*p)a;KOWDKTzR1%3D@#7gbjIMVrzVf@ak zL2oL`W(SPkl(>?%JLFU6f3+R5DPH*tZog)Qhm{1BOAD^uJEpFtJ*%Q(%Y6k0cLBpg ztXK22{l%k^`3u@M9cc}6U8cAF+KrM|{O%<(kG!LlFRAb)L}}>Hnr-%&M_4Itqob-q zrLTBlbA{{$Uz1adb4zyEQD5+e+hx#n+sf_5*O}PZ=_s>W1(csne_8IPgWM_;SCFH% zeidH)at7{xUWMX5Z_B53<3`LV2oc#&5!(~AVv$SUjD+W-M~!)zl0x1TRT(ZIei?0Y z+=>65kw%;ueY#Qz`i7`t`OV}{G4j%}3u6{|qoPkj?~9xHI&p8?c0ZvE}8y;TzpqEi*{)i`lG|5_%XXnXPInm{VTil z)r{QFVP>~>i=UU%7up_pQzzL-%x8mHgARYY2>S7oUn6m ziXeG|rlOak?e2)4DtW)z`c8`TflGq=ll5gbZ`3Y*SX=eLi+1YW+t`J%2Wx`X=pQs; zX>|~FmVY|_j4xNzw@N5v;xatq5zo0~d-nyR{Amv(4++$I@$$IPu3JG{XivtcZ{;Nu^Komnl2Qf7(X+@O4=ATsv}DqfYa{v0-&S%ucQYGg(@$8+GSg~DiQ zIoT0<~Snd?{zGEy(>dK>a2ZQ zOFr8ECJ$ZHVe-yp`^v>wY3CFYP;;=#tc_5>(i1mU7fBY2d5u@f#;#Qxe~dT5)@|X< zB@Q0=qx)kUVx8p_!%XE$98NUKTdlQ?^(xkG$eZ_SsZCsAc9Mml5OI~l!nnA@M=BRy z7j-tPe^6+dGSM#0|4fOAxj4>TLu2;H$;u&oAF{OcDW{7@=skY3H}#N;&Fb7s&yg<` zC0oYD&asulINy)|F!Qiq?k?Q=1BZ+cPST%|hn-s(A#vzVUP@Wpq%zPW z-Gqo}QClU+1Jt>e`)4-J^p<{Xg;LhoFPnMif3S1xWRd2*wXe058E5H6o}*S;-J zxIICPHA^q;MswJRFNv?79h0I+i2L|_DcU-QkB;1)VyjW&60Q{&{$5tn+_8A1$Ytx) z>Q+=^P-sQe8`^pmrM>bhL~=!Z>H=I1$iRF1J6HhmJJoQN4xjtj_EuCF~)zmh1;|o%6F9v=dvjlx4D50Z4 z^|4Q@9Pxcw0`=VM*QP@A>AaFAlmq#Ue;LNo9@Q9wOzo-d5ZQTLjVS93k|)PCpUl21ZDo@gq*k;yV8b-p<{}Y$(*69-^Kr(JIZ3N8 zNv#|Gx#^DQE(w+cmd|tdYgZNX7Jc51CjlNTzb&UFR8_U(N&8$R9zzum=J`h7e_OOq z_P(Us8?tVL+^FeiUpSw>EK+{jv{dvV3+Ej^zkKSc5j>82PAdCt@o}Iu8a~XS7kgOi z&vRE4uph<0+A3~8kl&(Zi#9w=3|psP8yn3Na7psSNX7||ihG2mB?b9&95zl{lAGRI zCg)vwCuok5%8Aq$IS60#tnv1te+n{w@yS!EBH4ECjmOk+Au@5g`xck##74c4im1#o zxHWQv^`zMGSvv%Hij_YFX0JTVu#b4RugERL=-Ne{c$a@JJ?Y z&Vt6Hh0W1Xnl4XWOfL#+FJG6Tckn;#on=s5-5G#)StPi-Cb$Gx+*yjdLvXj??ry=| z-7Q!kAMO?$5<~t&)zxr-FxIY&)adB_bk7X#;_BV1ymU= zUL}!P7k2RcypzX*=rLiYfBohbXL5R{^Fw~NPEd?~{~OC0)QA#};8X88!_FideVkoT z1)sjDm+*#rP?QRp-2)m!EqU+!uy$@Aq(nbGd@dTFMTDyP<#g}vtGbw0*7P^0 z#HTQ5WR|@N-i}H#ymJ#9D4bxJ;Q3oUT0>Xy`9an-Fo8-8&TRu5jukDBNLp@S4U4 z0krS(LVN?P$b^Zye@){RD<8Y1;lVp${vf|({2V?PFJsk%0Dyirf01r8FqLONO!VuD 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 Date: Thu, 14 Mar 2019 13:40:23 -0700 Subject: [PATCH 191/446] Fix baked material JSONS using wrong texture paths --- libraries/baking/src/MaterialBaker.cpp | 8 +++++--- libraries/baking/src/MaterialBaker.h | 3 ++- tools/oven/src/BakerCLI.cpp | 2 +- tools/oven/src/DomainBaker.cpp | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp index 57dcde67de..2752890f55 100644 --- a/libraries/baking/src/MaterialBaker.cpp +++ b/libraries/baking/src/MaterialBaker.cpp @@ -27,11 +27,12 @@ std::function MaterialBaker::_getNextOvenWorkerThreadOperator; static int materialNum = 0; -MaterialBaker::MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir) : +MaterialBaker::MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir, const QUrl& destinationPath) : _materialData(materialData), _isURL(isURL), _bakedOutputDir(bakedOutputDir), - _textureOutputDir(bakedOutputDir + "/materialTextures/" + QString::number(materialNum++)) + _textureOutputDir(bakedOutputDir + "/materialTextures/" + QString::number(materialNum++)), + _destinationPath(destinationPath) { } @@ -162,10 +163,11 @@ void MaterialBaker::handleFinishedTextureBaker() { qCDebug(material_baking) << "Re-writing texture references to" << baker->getTextureURL(); auto newURL = QUrl(_textureOutputDir).resolved(baker->getMetaTextureFileName()); + auto relativeURL = QDir(_bakedOutputDir).relativeFilePath(newURL.toString()); // Replace the old texture URLs for (auto networkMaterial : _materialsNeedingRewrite.values(textureKey)) { - networkMaterial->getTextureMap(baker->getMapChannel())->getTextureSource()->setUrl(newURL); + networkMaterial->getTextureMap(baker->getMapChannel())->getTextureSource()->setUrl(_destinationPath.resolved(relativeURL)); } } else { // this texture failed to bake - this doesn't fail the entire bake but we need to add the errors from diff --git a/libraries/baking/src/MaterialBaker.h b/libraries/baking/src/MaterialBaker.h index b1678e5634..98f931b61c 100644 --- a/libraries/baking/src/MaterialBaker.h +++ b/libraries/baking/src/MaterialBaker.h @@ -23,7 +23,7 @@ static const QString BAKED_MATERIAL_EXTENSION = ".baked.json"; class MaterialBaker : public Baker { Q_OBJECT public: - MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir); + MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir, const QUrl& destinationPath); QString getMaterialData() const { return _materialData; } bool isURL() const { return _isURL; } @@ -56,6 +56,7 @@ private: QString _bakedOutputDir; QString _textureOutputDir; QString _bakedMaterialData; + QUrl _destinationPath; QScriptEngine _scriptEngine; static std::function _getNextOvenWorkerThreadOperator; diff --git a/tools/oven/src/BakerCLI.cpp b/tools/oven/src/BakerCLI.cpp index 1aae6ccb72..2946db650c 100644 --- a/tools/oven/src/BakerCLI.cpp +++ b/tools/oven/src/BakerCLI.cpp @@ -61,7 +61,7 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString& outputPath, const QString& _baker = std::unique_ptr { new JSBaker(inputUrl, outputPath) }; _baker->moveToThread(Oven::instance().getNextWorkerThread()); } else if (type == MATERIAL_EXTENSION) { - _baker = std::unique_ptr { new MaterialBaker(inputUrl.toDisplayString(), true, outputPath) }; + _baker = std::unique_ptr { new MaterialBaker(inputUrl.toDisplayString(), true, outputPath, QUrl(outputPath)) }; _baker->moveToThread(Oven::instance().getNextWorkerThread()); } else { // If the type doesn't match the above, we assume we have a texture, and the type specified is the diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 639ab8b948..544786f03e 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -271,7 +271,7 @@ void DomainBaker::addMaterialBaker(const QString& property, const QString& data, // setup a baker for this material QSharedPointer materialBaker { - new MaterialBaker(data, isURL, _contentOutputPath), + new MaterialBaker(data, isURL, _contentOutputPath, _destinationPath), &MaterialBaker::deleteLater }; From c8209aa976860eae67f74cf08b7004f5a3229b02 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Thu, 14 Mar 2019 14:31:14 -0700 Subject: [PATCH 192/446] Do not have multiple copies of the original texture file in the baked output --- libraries/baking/src/TextureBaker.cpp | 17 +++++++++-------- libraries/baking/src/TextureBaker.h | 1 + 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/libraries/baking/src/TextureBaker.cpp b/libraries/baking/src/TextureBaker.cpp index 8591cbd0aa..dfc684ddee 100644 --- a/libraries/baking/src/TextureBaker.cpp +++ b/libraries/baking/src/TextureBaker.cpp @@ -47,17 +47,19 @@ TextureBaker::TextureBaker(const QUrl& textureURL, image::TextureUsage::Type tex auto originalFilename = textureURL.fileName(); _baseFilename = originalFilename.left(originalFilename.lastIndexOf('.')); } + + _originalCopyFilePath = _outputDirectory.absoluteFilePath(_textureURL.fileName()); } void TextureBaker::bake() { // once our texture is loaded, kick off a the processing connect(this, &TextureBaker::originalTextureLoaded, this, &TextureBaker::processTexture); - if (_originalTexture.isEmpty()) { + if (_originalTexture.isEmpty() && !QFile(_originalCopyFilePath.toString()).exists()) { // first load the texture (either locally or remotely) loadTexture(); } else { - // we already have a texture passed to us, use that + // we already have a texture passed to us, or the texture is already saved, so use that emit originalTextureLoaded(); } } @@ -128,23 +130,22 @@ void TextureBaker::processTexture() { TextureMeta meta; - QString newFilename = _textureURL.fileName(); - QString addMapChannel = QString::fromStdString("_" + std::to_string(_textureType)); - newFilename.replace(QString("."), addMapChannel + "."); - QString originalCopyFilePath = _outputDirectory.absoluteFilePath(newFilename); + QString originalCopyFilePath = _originalCopyFilePath.toString(); + // Copy the original file into the baked output directory if it doesn't exist yet { QFile file { originalCopyFilePath }; - if (!file.open(QIODevice::WriteOnly) || file.write(_originalTexture) == -1) { + if (!file.exists() && (!file.open(QIODevice::WriteOnly) || file.write(_originalTexture) == -1)) { handleError("Could not write original texture for " + _textureURL.toString()); return; } // IMPORTANT: _originalTexture is empty past this point _originalTexture.clear(); _outputFiles.push_back(originalCopyFilePath); - meta.original = _metaTexturePathPrefix + newFilename; + meta.original = _metaTexturePathPrefix + _originalCopyFilePath.fileName(); } + // Load the copy of the original file from the baked output directory. New images will be created using the original as the source data. auto buffer = std::static_pointer_cast(std::make_shared(originalCopyFilePath)); if (!buffer->open(QIODevice::ReadOnly)) { handleError("Could not open original file at " + originalCopyFilePath); diff --git a/libraries/baking/src/TextureBaker.h b/libraries/baking/src/TextureBaker.h index 84e7c57aa1..9b86d875e9 100644 --- a/libraries/baking/src/TextureBaker.h +++ b/libraries/baking/src/TextureBaker.h @@ -73,6 +73,7 @@ private: QDir _outputDirectory; QString _metaTextureFileName; QString _metaTexturePathPrefix; + QUrl _originalCopyFilePath; std::atomic _abortProcessing { false }; From c54b23f6477c9a84f526832504324a60dcc5a053 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Thu, 14 Mar 2019 15:18:45 -0700 Subject: [PATCH 193/446] Make material baking output unique textures per usage like model baking does --- libraries/baking/src/MaterialBaker.cpp | 4 ++- libraries/baking/src/MaterialBaker.h | 2 ++ libraries/baking/src/ModelBaker.cpp | 24 +------------ libraries/baking/src/ModelBaker.h | 4 ++- .../baking/src/baking/TextureFileNamer.cpp | 34 +++++++++++++++++++ .../baking/src/baking/TextureFileNamer.h | 30 ++++++++++++++++ 6 files changed, 73 insertions(+), 25 deletions(-) create mode 100644 libraries/baking/src/baking/TextureFileNamer.cpp create mode 100644 libraries/baking/src/baking/TextureFileNamer.h diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp index 2752890f55..9fc359fe9e 100644 --- a/libraries/baking/src/MaterialBaker.cpp +++ b/libraries/baking/src/MaterialBaker.cpp @@ -129,8 +129,10 @@ void MaterialBaker::processMaterial() { QPair textureKey(textureURL, it->second); if (!_textureBakers.contains(textureKey)) { + auto baseTextureFileName = _textureFileNamer.createBaseTextureFileName(textureURL.fileName(), it->second); + QSharedPointer textureBaker { - new TextureBaker(textureURL, it->second, _textureOutputDir), + new TextureBaker(textureURL, it->second, _textureOutputDir, "", baseTextureFileName), &TextureBaker::deleteLater }; textureBaker->setMapChannel(mapChannel); diff --git a/libraries/baking/src/MaterialBaker.h b/libraries/baking/src/MaterialBaker.h index 98f931b61c..41ce31380e 100644 --- a/libraries/baking/src/MaterialBaker.h +++ b/libraries/baking/src/MaterialBaker.h @@ -15,6 +15,7 @@ #include "Baker.h" #include "TextureBaker.h" +#include "baking/TextureFileNamer.h" #include @@ -60,6 +61,7 @@ private: QScriptEngine _scriptEngine; static std::function _getNextOvenWorkerThreadOperator; + TextureFileNamer _textureFileNamer; }; #endif // !hifi_MaterialBaker_h diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index 77584beb1b..0a5341cce4 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -417,7 +417,7 @@ QString ModelBaker::compressTexture(QString modelTextureFileName, image::Texture // construct the new baked texture file name and file path // ensuring that the baked texture will have a unique name // even if there was another texture with the same name at a different path - baseTextureFileName = createBaseTextureFileName(modelTextureFileInfo, textureType); + baseTextureFileName = _textureFileNamer.createBaseTextureFileName(modelTextureFileInfo, textureType); _remappedTexturePaths[urlToTexture] = baseTextureFileName; } @@ -628,28 +628,6 @@ void ModelBaker::checkIfTexturesFinished() { } } -QString ModelBaker::createBaseTextureFileName(const QFileInfo& textureFileInfo, const image::TextureUsage::Type textureType) { - // If two textures have the same URL but are used differently, we need to process them separately - QString addMapChannel = QString::fromStdString("_" + std::to_string(textureType)); - - QString baseTextureFileName{ textureFileInfo.completeBaseName() + addMapChannel }; - - // first make sure we have a unique base name for this texture - // in case another texture referenced by this model has the same base name - auto& nameMatches = _textureNameMatchCount[baseTextureFileName]; - - if (nameMatches > 0) { - // there are already nameMatches texture with this name - // append - and that number to our baked texture file name so that it is unique - baseTextureFileName += "-" + QString::number(nameMatches); - } - - // increment the number of name matches - ++nameMatches; - - return baseTextureFileName; -} - void ModelBaker::setWasAborted(bool wasAborted) { if (wasAborted != _wasAborted.load()) { Baker::setWasAborted(wasAborted); diff --git a/libraries/baking/src/ModelBaker.h b/libraries/baking/src/ModelBaker.h index 6ee7511ce3..17af2604a2 100644 --- a/libraries/baking/src/ModelBaker.h +++ b/libraries/baking/src/ModelBaker.h @@ -19,6 +19,7 @@ #include "Baker.h" #include "TextureBaker.h" +#include "baking/TextureFileNamer.h" #include "ModelBakingLoggingCategory.h" @@ -97,7 +98,6 @@ private slots: void handleAbortedTexture(); private: - QString createBaseTextureFileName(const QFileInfo & textureFileInfo, const image::TextureUsage::Type textureType); QUrl getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded = false); void bakeTexture(const QUrl & textureURL, image::TextureUsage::Type textureType, const QDir & outputDir, const QString & bakedFilename, const QByteArray & textureContent); @@ -109,6 +109,8 @@ private: bool _pendingErrorEmission { false }; bool _hasBeenBaked { false }; + + TextureFileNamer _textureFileNamer; }; #endif // hifi_ModelBaker_h diff --git a/libraries/baking/src/baking/TextureFileNamer.cpp b/libraries/baking/src/baking/TextureFileNamer.cpp new file mode 100644 index 0000000000..612d89e604 --- /dev/null +++ b/libraries/baking/src/baking/TextureFileNamer.cpp @@ -0,0 +1,34 @@ +// +// TextureFileNamer.cpp +// libraries/baking/src/baking +// +// Created by Sabrina Shanman on 2019/03/14. +// Copyright 2019 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 +// + +#include "TextureFileNamer.h" + +QString TextureFileNamer::createBaseTextureFileName(const QFileInfo& textureFileInfo, const image::TextureUsage::Type textureType) { + // If two textures have the same URL but are used differently, we need to process them separately + QString addMapChannel = QString::fromStdString("_" + std::to_string(textureType)); + + QString baseTextureFileName{ textureFileInfo.baseName() + addMapChannel }; + + // first make sure we have a unique base name for this texture + // in case another texture referenced by this model has the same base name + auto& nameMatches = _textureNameMatchCount[baseTextureFileName]; + + if (nameMatches > 0) { + // there are already nameMatches texture with this name + // append - and that number to our baked texture file name so that it is unique + baseTextureFileName += "-" + QString::number(nameMatches); + } + + // increment the number of name matches + ++nameMatches; + + return baseTextureFileName; +} diff --git a/libraries/baking/src/baking/TextureFileNamer.h b/libraries/baking/src/baking/TextureFileNamer.h new file mode 100644 index 0000000000..9049588ef1 --- /dev/null +++ b/libraries/baking/src/baking/TextureFileNamer.h @@ -0,0 +1,30 @@ +// +// TextureFileNamer.h +// libraries/baking/src/baking +// +// Created by Sabrina Shanman on 2019/03/14. +// Copyright 2019 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 +// + +#ifndef hifi_TextureFileNamer_h +#define hifi_TextureFileNamer_h + +#include +#include + +#include + +class TextureFileNamer { +public: + TextureFileNamer() {} + + QString createBaseTextureFileName(const QFileInfo& textureFileInfo, const image::TextureUsage::Type textureType); + +protected: + QHash _textureNameMatchCount; +}; + +#endif // hifi_TextureFileNamer_h From d8b4419fd0d899ed48513f572f478be893ac37ee Mon Sep 17 00:00:00 2001 From: David Back Date: Thu, 14 Mar 2019 17:19:21 -0700 Subject: [PATCH 194/446] fix null object error on cancel export --- .../Editor/AvatarExporter/AvatarExporter.cs | 21 ++++++++++-------- .../avatarExporter.unitypackage | Bin 74600 -> 74623 bytes 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs index f0d970031c..c5bc5eb84e 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs @@ -1531,16 +1531,19 @@ class ExportProjectWindow : EditorWindow { float GetAvatarHeight() { // height of an avatar model can be determined to be the max Y extents of the combined bounds for all its mesh renderers - Bounds bounds = new Bounds(); - var meshRenderers = avatarPreviewObject.GetComponentsInChildren(); - var skinnedMeshRenderers = avatarPreviewObject.GetComponentsInChildren(); - foreach (var renderer in meshRenderers) { - bounds.Encapsulate(renderer.bounds); + if (avatarPreviewObject != null) { + Bounds bounds = new Bounds(); + var meshRenderers = avatarPreviewObject.GetComponentsInChildren(); + var skinnedMeshRenderers = avatarPreviewObject.GetComponentsInChildren(); + foreach (var renderer in meshRenderers) { + bounds.Encapsulate(renderer.bounds); + } + foreach (var renderer in skinnedMeshRenderers) { + bounds.Encapsulate(renderer.bounds); + } + return bounds.max.y; } - foreach (var renderer in skinnedMeshRenderers) { - bounds.Encapsulate(renderer.bounds); - } - return bounds.max.y; + return 0.0f; } void SetAvatarScale(float actualScale) { diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage index 48a9502079839c3aee59ead39531198dd5d4bf86..3e2d6f2aed318abc6382f1a914ec3fb5e87dee69 100644 GIT binary patch delta 73664 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�(cEK1(J6z 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 K6EI-_Rs#UBWxK@y delta 73684 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%0DDvbGex2CKm{QT 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 Date: Thu, 14 Mar 2019 20:02:19 -0700 Subject: [PATCH 195/446] Store device ifconfig results in ifconfig.txt. --- tools/nitpick/src/TestRunnerMobile.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index ed48c37290..73f1da5d26 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -67,14 +67,26 @@ void TestRunnerMobile::connectDevice() { if (!_adbInterface) { _adbInterface = new AdbInterface(); } - + + // Get list of devices QString devicesFullFilename{ _workingFolder + "/devices.txt" }; QString command = _adbInterface->getAdbCommand() + " devices -l > " + devicesFullFilename; appendLog(command); system(command.toStdString().c_str()); if (!QFile::exists(devicesFullFilename)) { - QMessageBox::critical(0, "Internal error", "devicesFullFilename not found"); + QMessageBox::critical(0, "Internal error", "devices.txt not found"); + exit (-1); + } + + // Get device IP address + QString ifconfigFullFilename{ _workingFolder + "/ifconfig.txt" }; + command = _adbInterface->getAdbCommand() + " shell ifconfig > " + ifconfigFullFilename; + appendLog(command); + system(command.toStdString().c_str()); + + if (!QFile::exists(ifconfigFullFilename)) { + QMessageBox::critical(0, "Internal error", "ifconfig.txt not found"); exit (-1); } From 041db33578d0bc2973b904ecf8d9a431cd5352c7 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Fri, 15 Mar 2019 10:20:58 -0700 Subject: [PATCH 196/446] Attempt to fix build warnings --- libraries/baking/src/MaterialBaker.cpp | 21 +++++++++++---------- libraries/fbx/src/FBXSerializer_Mesh.cpp | 8 ++++++++ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp index 9fc359fe9e..b2392e0cb7 100644 --- a/libraries/baking/src/MaterialBaker.cpp +++ b/libraries/baking/src/MaterialBaker.cpp @@ -110,16 +110,17 @@ void MaterialBaker::processMaterial() { QUrl textureURL = url.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); // FIXME: this isn't properly handling bumpMaps or glossMaps - static const std::unordered_map MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP({ - { graphics::Material::MapChannel::EMISSIVE_MAP, image::TextureUsage::EMISSIVE_TEXTURE }, - { graphics::Material::MapChannel::ALBEDO_MAP, image::TextureUsage::ALBEDO_TEXTURE }, - { graphics::Material::MapChannel::METALLIC_MAP, image::TextureUsage::METALLIC_TEXTURE }, - { graphics::Material::MapChannel::ROUGHNESS_MAP, image::TextureUsage::ROUGHNESS_TEXTURE }, - { graphics::Material::MapChannel::NORMAL_MAP, image::TextureUsage::NORMAL_TEXTURE }, - { graphics::Material::MapChannel::OCCLUSION_MAP, image::TextureUsage::OCCLUSION_TEXTURE }, - { graphics::Material::MapChannel::LIGHTMAP_MAP, image::TextureUsage::LIGHTMAP_TEXTURE }, - { graphics::Material::MapChannel::SCATTERING_MAP, image::TextureUsage::SCATTERING_TEXTURE } - }); + static std::unordered_map MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP; + if (MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP.empty()) { + MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::EMISSIVE_MAP] = image::TextureUsage::EMISSIVE_TEXTURE; + MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::ALBEDO_MAP] = image::TextureUsage::ALBEDO_TEXTURE; + MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::METALLIC_MAP] = image::TextureUsage::METALLIC_TEXTURE; + MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::ROUGHNESS_MAP] = image::TextureUsage::ROUGHNESS_TEXTURE; + MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::NORMAL_MAP] = image::TextureUsage::NORMAL_TEXTURE; + MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::OCCLUSION_MAP] = image::TextureUsage::OCCLUSION_TEXTURE; + MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::LIGHTMAP_MAP] = image::TextureUsage::LIGHTMAP_TEXTURE; + MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::SCATTERING_MAP] = image::TextureUsage::SCATTERING_TEXTURE; + } auto it = MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP.find(mapChannel); if (it == MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP.end()) { diff --git a/libraries/fbx/src/FBXSerializer_Mesh.cpp b/libraries/fbx/src/FBXSerializer_Mesh.cpp index 2f5286291c..c34b4678c7 100644 --- a/libraries/fbx/src/FBXSerializer_Mesh.cpp +++ b/libraries/fbx/src/FBXSerializer_Mesh.cpp @@ -13,12 +13,20 @@ #pragma warning( push ) #pragma warning( disable : 4267 ) #endif +// gcc and clang +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wsign-compare" +#endif #include #ifdef _WIN32 #pragma warning( pop ) #endif +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif #include #include From 2794a134c17d48f85044d2ebd8d7dee8c9127544 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 15 Mar 2019 10:44:59 -0700 Subject: [PATCH 197/446] Add master injector gain to audio-mixer --- .../src/audio/AudioMixerClientData.h | 3 + .../src/audio/AudioMixerSlave.cpp | 56 ++++++++++++------- assignment-client/src/audio/AudioMixerSlave.h | 7 ++- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index 653749f619..f9d113c53d 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -84,6 +84,8 @@ public: float getMasterAvatarGain() const { return _masterAvatarGain; } void setMasterAvatarGain(float gain) { _masterAvatarGain = gain; } + float getMasterInjectorGain() const { return _masterInjectorGain; } + void setMasterInjectorGain(float gain) { _masterInjectorGain = gain; } AudioLimiter audioLimiter; @@ -189,6 +191,7 @@ private: int _frameToSendStats { 0 }; float _masterAvatarGain { 1.0f }; // per-listener mixing gain, applied only to avatars + float _masterInjectorGain { 1.0f }; // per-listener mixing gain, applied only to injectors CodecPluginPointer _codec; QString _selectedCodecName; diff --git a/assignment-client/src/audio/AudioMixerSlave.cpp b/assignment-client/src/audio/AudioMixerSlave.cpp index a920b45161..f7f8e8a9c1 100644 --- a/assignment-client/src/audio/AudioMixerSlave.cpp +++ b/assignment-client/src/audio/AudioMixerSlave.cpp @@ -50,7 +50,7 @@ void sendEnvironmentPacket(const SharedNodePointer& node, AudioMixerClientData& // mix helpers inline float approximateGain(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd); -inline float computeGain(float masterListenerGain, const AvatarAudioStream& listeningNodeStream, +inline float computeGain(float masterAvatarGain, float masterInjectorGain, const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, float distance, bool isEcho); inline float computeAzimuth(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition); @@ -338,8 +338,8 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { } if (!isThrottling) { - updateHRTFParameters(stream, *listenerAudioStream, - listenerData->getMasterAvatarGain()); + updateHRTFParameters(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), + listenerData->getMasterInjectorGain()); } return false; }); @@ -363,8 +363,8 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { } if (!isThrottling) { - updateHRTFParameters(stream, *listenerAudioStream, - listenerData->getMasterAvatarGain()); + updateHRTFParameters(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), + listenerData->getMasterInjectorGain()); } return false; }); @@ -381,13 +381,13 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { stream.approximateVolume = approximateVolume(stream, listenerAudioStream); } else { if (shouldBeSkipped(stream, *listener, *listenerAudioStream, *listenerData)) { - addStream(stream, *listenerAudioStream, 0.0f, isSoloing); + addStream(stream, *listenerAudioStream, 0.0f, 0.0f, isSoloing); streams.skipped.push_back(move(stream)); ++stats.activeToSkipped; return true; } - addStream(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), + addStream(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), listenerData->getMasterInjectorGain(), isSoloing); if (shouldBeInactive(stream)) { @@ -423,7 +423,7 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { return true; } - addStream(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), + addStream(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), listenerData->getMasterInjectorGain(), isSoloing); if (shouldBeInactive(stream)) { @@ -491,7 +491,9 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { void AudioMixerSlave::addStream(AudioMixerClientData::MixableStream& mixableStream, AvatarAudioStream& listeningNodeStream, - float masterListenerGain, bool isSoloing) { + float masterAvatarGain, + float masterInjectorGain, + bool isSoloing) { ++stats.totalMixes; auto streamToAdd = mixableStream.positionalStream; @@ -504,9 +506,10 @@ void AudioMixerSlave::addStream(AudioMixerClientData::MixableStream& mixableStre float distance = glm::max(glm::length(relativePosition), EPSILON); float azimuth = isEcho ? 0.0f : computeAzimuth(listeningNodeStream, listeningNodeStream, relativePosition); - float gain = masterListenerGain; + float gain = masterAvatarGain; if (!isSoloing) { - gain = computeGain(masterListenerGain, listeningNodeStream, *streamToAdd, relativePosition, distance, isEcho); + gain = computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd, relativePosition, + distance, isEcho); } const int HRTF_DATASET_INDEX = 1; @@ -585,8 +588,9 @@ void AudioMixerSlave::addStream(AudioMixerClientData::MixableStream& mixableStre } void AudioMixerSlave::updateHRTFParameters(AudioMixerClientData::MixableStream& mixableStream, - AvatarAudioStream& listeningNodeStream, - float masterListenerGain) { + AvatarAudioStream& listeningNodeStream, + float masterAvatarGain, + float masterInjectorGain) { auto streamToAdd = mixableStream.positionalStream; // check if this is a server echo of a source back to itself @@ -595,7 +599,8 @@ void AudioMixerSlave::updateHRTFParameters(AudioMixerClientData::MixableStream& glm::vec3 relativePosition = streamToAdd->getPosition() - listeningNodeStream.getPosition(); float distance = glm::max(glm::length(relativePosition), EPSILON); - float gain = computeGain(masterListenerGain, listeningNodeStream, *streamToAdd, relativePosition, distance, isEcho); + float gain = computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd, relativePosition, + distance, isEcho); float azimuth = isEcho ? 0.0f : computeAzimuth(listeningNodeStream, listeningNodeStream, relativePosition); mixableStream.hrtf->setParameterHistory(azimuth, distance, gain); @@ -720,6 +725,7 @@ float approximateGain(const AvatarAudioStream& listeningNodeStream, const Positi // injector: apply attenuation if (streamToAdd.getType() == PositionalAudioStream::Injector) { gain *= reinterpret_cast(&streamToAdd)->getAttenuationRatio(); + // injector: skip master gain } // avatar: skip attenuation - it is too costly to approximate @@ -729,16 +735,23 @@ float approximateGain(const AvatarAudioStream& listeningNodeStream, const Positi float distance = glm::length(relativePosition); return gain / distance; - // avatar: skip master gain - it is constant for all streams + // avatar: skip master gain } -float computeGain(float masterListenerGain, const AvatarAudioStream& listeningNodeStream, - const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, float distance, bool isEcho) { +float computeGain(float masterAvatarGain, + float masterInjectorGain, + const AvatarAudioStream& listeningNodeStream, + const PositionalAudioStream& streamToAdd, + const glm::vec3& relativePosition, + float distance, + bool isEcho) { float gain = 1.0f; // injector: apply attenuation if (streamToAdd.getType() == PositionalAudioStream::Injector) { gain *= reinterpret_cast(&streamToAdd)->getAttenuationRatio(); + // apply master gain + gain *= masterInjectorGain; // avatar: apply fixed off-axis attenuation to make them quieter as they turn away } else if (!isEcho && (streamToAdd.getType() == PositionalAudioStream::Microphone)) { @@ -754,8 +767,8 @@ float computeGain(float masterListenerGain, const AvatarAudioStream& listeningNo gain *= offAxisCoefficient; - // apply master gain, only to avatars - gain *= masterListenerGain; + // apply master gain + gain *= masterAvatarGain; } auto& audioZones = AudioMixer::getAudioZones(); @@ -797,8 +810,9 @@ float computeGain(float masterListenerGain, const AvatarAudioStream& listeningNo return gain; } -float computeAzimuth(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, - const glm::vec3& relativePosition) { +float computeAzimuth(const AvatarAudioStream& listeningNodeStream, + const PositionalAudioStream& streamToAdd, + const glm::vec3& relativePosition) { glm::quat inverseOrientation = glm::inverse(listeningNodeStream.getOrientation()); glm::vec3 rotatedSourcePosition = inverseOrientation * relativePosition; diff --git a/assignment-client/src/audio/AudioMixerSlave.h b/assignment-client/src/audio/AudioMixerSlave.h index 3d979da1fc..9765ea8639 100644 --- a/assignment-client/src/audio/AudioMixerSlave.h +++ b/assignment-client/src/audio/AudioMixerSlave.h @@ -57,10 +57,13 @@ private: bool prepareMix(const SharedNodePointer& listener); void addStream(AudioMixerClientData::MixableStream& mixableStream, AvatarAudioStream& listeningNodeStream, - float masterListenerGain, bool isSoloing); + float masterAvatarGain, + float masterInjectorGain, + bool isSoloing); void updateHRTFParameters(AudioMixerClientData::MixableStream& mixableStream, AvatarAudioStream& listeningNodeStream, - float masterListenerGain); + float masterAvatarGain, + float masterInjectorGain); void resetHRTFState(AudioMixerClientData::MixableStream& mixableStream); void addStreams(Node& listener, AudioMixerClientData& listenerData); From 5dab4c00106a244666a03d0c6e5cadadca5f3845 Mon Sep 17 00:00:00 2001 From: danteruiz Date: Fri, 15 Mar 2019 10:47:48 -0700 Subject: [PATCH 198/446] remove white-space --- scripts/system/notifications.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index 469f30cd23..7fdb863a83 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -257,7 +257,7 @@ notice.isFacingAvatar = false; notificationText = Overlays.addOverlay("text3d", notice); - notifications.push(notificationText); + notifications.push(notificationText); } else { notifications.push(Overlays.addOverlay("image3d", notice)); } From a5a305f1816cb56c9a0434d41c446d74ccca38d2 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 15 Mar 2019 12:05:51 -0700 Subject: [PATCH 199/446] Handle InjectorGainSet packet at the audio-mixer --- assignment-client/src/audio/AudioMixer.cpp | 1 + .../src/audio/AudioMixerClientData.cpp | 14 ++++++++++++++ assignment-client/src/audio/AudioMixerClientData.h | 1 + libraries/networking/src/udt/PacketHeaders.h | 2 +- 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index f67c54239e..201e24d4b9 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -97,6 +97,7 @@ AudioMixer::AudioMixer(ReceivedMessage& message) : PacketType::RadiusIgnoreRequest, PacketType::RequestsDomainListData, PacketType::PerAvatarGainSet, + PacketType::InjectorGainSet, PacketType::AudioSoloRequest }, this, "queueAudioPacket"); diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 90698bfac8..b8d3ec62a6 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -92,6 +92,9 @@ int AudioMixerClientData::processPackets(ConcurrentAddedStreams& addedStreams) { case PacketType::PerAvatarGainSet: parsePerAvatarGainSet(*packet, node); break; + case PacketType::InjectorGainSet: + parseInjectorGainSet(*packet, node); + break; case PacketType::NodeIgnoreRequest: parseNodeIgnoreRequest(packet, node); break; @@ -205,6 +208,17 @@ void AudioMixerClientData::parsePerAvatarGainSet(ReceivedMessage& message, const } } +void AudioMixerClientData::parseInjectorGainSet(ReceivedMessage& message, const SharedNodePointer& node) { + QUuid uuid = node->getUUID(); + + uint8_t packedGain; + message.readPrimitive(&packedGain); + float gain = unpackFloatGainFromByte(packedGain); + + setMasterInjectorGain(gain); + qCDebug(audio) << "Setting MASTER injector gain for " << uuid << " to " << gain; +} + void AudioMixerClientData::setGainForAvatar(QUuid nodeID, float gain) { auto it = std::find_if(_streams.active.cbegin(), _streams.active.cend(), [nodeID](const MixableStream& mixableStream){ return mixableStream.nodeStreamID.nodeID == nodeID && mixableStream.nodeStreamID.streamID.isNull(); diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index f9d113c53d..4a1ca7f9b5 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -63,6 +63,7 @@ public: void negotiateAudioFormat(ReceivedMessage& message, const SharedNodePointer& node); void parseRequestsDomainListData(ReceivedMessage& message); void parsePerAvatarGainSet(ReceivedMessage& message, const SharedNodePointer& node); + void parseInjectorGainSet(ReceivedMessage& message, const SharedNodePointer& node); void parseNodeIgnoreRequest(QSharedPointer message, const SharedNodePointer& node); void parseRadiusIgnoreRequest(QSharedPointer message, const SharedNodePointer& node); void parseSoloRequest(QSharedPointer message, const SharedNodePointer& node); diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 0ec7c40ca4..413ff14b17 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -57,7 +57,7 @@ public: ICEServerQuery, OctreeStats, SetAvatarTraits, - UNUSED_PACKET_TYPE, + InjectorGainSet, AssignmentClientStatus, NoisyMute, AvatarIdentity, From cc2868a6f4ba5097d88045863584e3e83c7d3862 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 15 Mar 2019 12:46:47 -0700 Subject: [PATCH 200/446] Can get server IP. --- tools/nitpick/src/TestRunnerMobile.cpp | 84 +++++++++++++++++++++++++- tools/nitpick/src/TestRunnerMobile.h | 3 + 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 73f1da5d26..ad5151bcc0 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -9,6 +9,7 @@ // #include "TestRunnerMobile.h" +#include #include #include #include @@ -67,7 +68,7 @@ void TestRunnerMobile::connectDevice() { if (!_adbInterface) { _adbInterface = new AdbInterface(); } - + // Get list of devices QString devicesFullFilename{ _workingFolder + "/devices.txt" }; QString command = _adbInterface->getAdbCommand() + " devices -l > " + devicesFullFilename; @@ -202,6 +203,8 @@ void TestRunnerMobile::runInterface() { _adbInterface = new AdbInterface(); } + sendServerIPToDevice(); + _statusLabel->setText("Starting Interface"); QString testScript = (_runFullSuite->isChecked()) @@ -245,3 +248,82 @@ void TestRunnerMobile::pullFolder() { _statusLabel->setText("Pull complete"); #endif } + +void TestRunnerMobile::sendServerIPToDevice() { + // Get device IP + QFile ifconfigFile{ _workingFolder + "/ifconfig.txt" }; + if (!ifconfigFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not open 'ifconfig.txt'"); + exit(-1); + } + + QTextStream stream(&ifconfigFile); + QString line = ifconfigFile.readLine(); + while (!line.isNull()) { + // The device IP is in the line following the "wlan0" line + line = ifconfigFile.readLine(); + if (line.left(6) == "wlan0 ") { + break; + } + } + + // The following line looks like this "inet addr:192.168.0.15 Bcast:192.168.0.255 Mask:255.255.255.0" + // Extract the address and mask + line = ifconfigFile.readLine(); + QStringList lineParts = line.split(':'); + if (lineParts.size() < 4) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "IP address line not in expected format: " + line); + exit(-1); + } + + qint64 deviceIP = convertToBinary(lineParts[1].split(' ')[0]); + qint64 deviceMask = convertToBinary(lineParts[3].split(' ')[0]); + qint64 deviceSubnet = deviceMask & deviceIP; + + // The device needs to be on the same subnet as the server + // To find which of our IPs is the server - choose the 1st that is on the same subnet + // If more than one found then report an error + + QString serverIP; + + QList interfaces = QNetworkInterface::allInterfaces(); + for (int i = 0; i < interfaces.count(); i++) { + QList entries = interfaces.at(i).addressEntries(); + for (int j = 0; j < entries.count(); j++) { + if (entries.at(j).ip().protocol() == QAbstractSocket::IPv4Protocol) { + qint64 hostIP = convertToBinary(entries.at(j).ip().toString()); + qint64 hostMask = convertToBinary(entries.at(j).netmask().toString()); + qint64 hostSubnet = hostMask & hostIP; + + if (hostSubnet == deviceSubnet) { + if (!serverIP.isNull()) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Cannot identify server IP (multiple interfaces on device submask)"); + return; + } else { + union { + uint32_t ip; + uint8_t bytes[4]; + } u; + u.ip = hostIP; + + serverIP = QString::number(u.bytes[3]) + '.' + QString::number(u.bytes[2]) + '.' + QString::number(u.bytes[1]) + '.' + QString::number(u.bytes[0]); + } + } + } + } + } + + ifconfigFile.close(); +} + +qint64 TestRunnerMobile::convertToBinary(const QString& str) { + QString binary; + foreach (const QString& s, str.split(".")) { + binary += QString::number(s.toInt(), 2).rightJustified(8, '0'); + } + + return binary.toLongLong(NULL, 2); +} \ No newline at end of file diff --git a/tools/nitpick/src/TestRunnerMobile.h b/tools/nitpick/src/TestRunnerMobile.h index 7dbf5456b3..7554a075c8 100644 --- a/tools/nitpick/src/TestRunnerMobile.h +++ b/tools/nitpick/src/TestRunnerMobile.h @@ -52,6 +52,9 @@ public: void pullFolder(); + void sendServerIPToDevice(); + qint64 convertToBinary (const QString& str); + private: QPushButton* _connectDeviceButton; QPushButton* _pullFolderButton; From 8748a7561b4f9f8b235593b608783652a05e7470 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 15 Mar 2019 12:48:32 -0700 Subject: [PATCH 201/446] fix lighting/color grading for everything --- .../src/RenderablePolyLineEntityItem.cpp | 7 +- .../src/RenderableShapeEntityItem.cpp | 10 +-- .../src/RenderableTextEntityItem.cpp | 19 +++- .../render-utils/src/DeferredBufferWrite.slh | 8 +- libraries/render-utils/src/GeometryCache.cpp | 86 +------------------ libraries/render-utils/src/GeometryCache.h | 20 ----- libraries/render-utils/src/TextRenderer3D.cpp | 4 +- libraries/render-utils/src/TextRenderer3D.h | 2 +- .../render-utils/src/forward_sdf_text3D.slf | 57 ++++++++++++ .../src/forward_simple_textured.slf | 15 ++-- .../forward_simple_textured_transparent.slf | 22 +++-- .../src/render-utils/forward_sdf_text3D.slp | 1 + libraries/render-utils/src/sdf_text3D.slf | 44 ++-------- libraries/render-utils/src/sdf_text3D.slh | 63 ++++++++++++++ libraries/render-utils/src/sdf_text3D.slv | 11 ++- .../src/sdf_text3D_transparent.slf | 43 ++-------- libraries/render-utils/src/simple.slv | 1 - .../src/simple_transparent_textured.slf | 39 ++++++--- libraries/render-utils/src/text/Font.cpp | 60 ++++++++----- libraries/render-utils/src/text/Font.h | 3 +- 20 files changed, 260 insertions(+), 255 deletions(-) create mode 100644 libraries/render-utils/src/forward_sdf_text3D.slf create mode 100644 libraries/render-utils/src/render-utils/forward_sdf_text3D.slp create mode 100644 libraries/render-utils/src/sdf_text3D.slh diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp index 7050393221..98f79780be 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp @@ -46,12 +46,7 @@ PolyLineEntityRenderer::PolyLineEntityRenderer(const EntityItemPointer& entity) void PolyLineEntityRenderer::buildPipeline() { // FIXME: opaque pipeline - gpu::ShaderPointer program; - if (DISABLE_DEFERRED) { - program = gpu::Shader::createProgram(shader::entities_renderer::program::paintStroke_forward); - } else { - program = gpu::Shader::createProgram(shader::entities_renderer::program::paintStroke); - } + gpu::ShaderPointer program = gpu::Shader::createProgram(DISABLE_DEFERRED ? shader::entities_renderer::program::paintStroke_forward : shader::entities_renderer::program::paintStroke); { gpu::StatePointer state = gpu::StatePointer(new gpu::State()); diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 20837070d8..0ba3adbe9b 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -277,16 +277,10 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) { } else if (!useMaterialPipeline(materials)) { // FIXME, support instanced multi-shape rendering using multidraw indirect outColor.a *= _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; - render::ShapePipelinePointer pipeline; - if (_renderLayer == RenderLayer::WORLD && !DISABLE_DEFERRED) { - pipeline = outColor.a < 1.0f ? geometryCache->getTransparentShapePipeline() : geometryCache->getOpaqueShapePipeline(); - } else { - pipeline = outColor.a < 1.0f ? geometryCache->getForwardTransparentShapePipeline() : geometryCache->getForwardOpaqueShapePipeline(); - } if (render::ShapeKey(args->_globalShapeKey).isWireframe() || _primitiveMode == PrimitiveMode::LINES) { - geometryCache->renderWireShapeInstance(args, batch, geometryShape, outColor, pipeline); + geometryCache->renderWireShapeInstance(args, batch, geometryShape, outColor, args->_shapePipeline); } else { - geometryCache->renderSolidShapeInstance(args, batch, geometryShape, outColor, pipeline); + geometryCache->renderSolidShapeInstance(args, batch, geometryShape, outColor, args->_shapePipeline); } } else { if (args->_renderMode != render::Args::RenderMode::SHADOW_RENDER_MODE) { diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp index 107847826c..5cd0abae68 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp @@ -21,6 +21,8 @@ #include +#include "DeferredLightingEffect.h" + using namespace render; using namespace render::entities; @@ -162,7 +164,7 @@ void TextEntityRenderer::doRender(RenderArgs* args) { glm::vec4 backgroundColor; Transform modelTransform; glm::vec3 dimensions; - bool forwardRendered; + bool layered; withReadLock([&] { modelTransform = _renderTransform; dimensions = _dimensions; @@ -172,7 +174,7 @@ void TextEntityRenderer::doRender(RenderArgs* args) { textColor = EntityRenderer::calculatePulseColor(textColor, _pulseProperties, _created); backgroundColor = glm::vec4(_backgroundColor, fadeRatio * _backgroundAlpha); backgroundColor = EntityRenderer::calculatePulseColor(backgroundColor, _pulseProperties, _created); - forwardRendered = _renderLayer != RenderLayer::WORLD || DISABLE_DEFERRED; + layered = _renderLayer != RenderLayer::WORLD; }); // Render background @@ -184,6 +186,11 @@ void TextEntityRenderer::doRender(RenderArgs* args) { Q_ASSERT(args->_batch); gpu::Batch& batch = *args->_batch; + // FIXME: we need to find a better way of rendering text so we don't have to do this + if (layered) { + DependencyManager::get()->setupKeyLightBatch(args, batch); + } + auto transformToTopLeft = modelTransform; transformToTopLeft.setRotation(EntityItem::getBillboardRotation(transformToTopLeft.getTranslation(), transformToTopLeft.getRotation(), _billboardMode, args->getViewFrustum().getPosition())); transformToTopLeft.postTranslate(dimensions * glm::vec3(-0.5f, 0.5f, 0.0f)); // Go to the top left @@ -192,7 +199,7 @@ void TextEntityRenderer::doRender(RenderArgs* args) { if (backgroundColor.a > 0.0f) { batch.setModelTransform(transformToTopLeft); auto geometryCache = DependencyManager::get(); - geometryCache->bindSimpleProgram(batch, false, backgroundColor.a < 1.0f, false, false, false, true, forwardRendered); + geometryCache->bindSimpleProgram(batch, false, backgroundColor.a < 1.0f, false, false, false, true, layered); geometryCache->renderQuad(batch, minCorner, maxCorner, backgroundColor, _geometryID); } @@ -203,7 +210,11 @@ void TextEntityRenderer::doRender(RenderArgs* args) { batch.setModelTransform(transformToTopLeft); glm::vec2 bounds = glm::vec2(dimensions.x - (_leftMargin + _rightMargin), dimensions.y - (_topMargin + _bottomMargin)); - _textRenderer->draw(batch, _leftMargin / scale, -_topMargin / scale, _text, textColor, bounds / scale, forwardRendered); + _textRenderer->draw(batch, _leftMargin / scale, -_topMargin / scale, _text, textColor, bounds / scale, layered); + } + + if (layered) { + DependencyManager::get()->unsetKeyLightBatch(batch); } } diff --git a/libraries/render-utils/src/DeferredBufferWrite.slh b/libraries/render-utils/src/DeferredBufferWrite.slh index ea32c5ecb3..66d0aa2ddb 100644 --- a/libraries/render-utils/src/DeferredBufferWrite.slh +++ b/libraries/render-utils/src/DeferredBufferWrite.slh @@ -29,7 +29,7 @@ float evalOpaqueFinalAlpha(float alpha, float mapAlpha) { <@include LightingModel.slh@> void packDeferredFragment(vec3 normal, float alpha, vec3 albedo, float roughness, float metallic, vec3 emissive, float occlusion, float scattering) { - if (alpha != 1.0) { + if (alpha < 1.0) { discard; } @@ -42,7 +42,7 @@ void packDeferredFragment(vec3 normal, float alpha, vec3 albedo, float roughness } void packDeferredFragmentLightmap(vec3 normal, float alpha, vec3 albedo, float roughness, float metallic, vec3 lightmap) { - if (alpha != 1.0) { + if (alpha < 1.0) { discard; } @@ -54,7 +54,7 @@ void packDeferredFragmentLightmap(vec3 normal, float alpha, vec3 albedo, float r } void packDeferredFragmentUnlit(vec3 normal, float alpha, vec3 color) { - if (alpha != 1.0) { + if (alpha < 1.0) { discard; } _fragColor0 = vec4(color, packUnlit()); @@ -64,7 +64,7 @@ void packDeferredFragmentUnlit(vec3 normal, float alpha, vec3 color) { } void packDeferredFragmentTranslucent(vec3 normal, float alpha, vec3 albedo, float roughness) { - if (alpha <= 0.0) { + if (alpha < 1.e-6) { discard; } _fragColor0 = vec4(albedo.rgb, alpha); diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 0f400e00ee..c189798a42 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -722,8 +722,6 @@ gpu::ShaderPointer GeometryCache::_unlitFadeShader; render::ShapePipelinePointer GeometryCache::_simpleOpaquePipeline; render::ShapePipelinePointer GeometryCache::_simpleTransparentPipeline; -render::ShapePipelinePointer GeometryCache::_forwardSimpleOpaquePipeline; -render::ShapePipelinePointer GeometryCache::_forwardSimpleTransparentPipeline; render::ShapePipelinePointer GeometryCache::_simpleOpaqueFadePipeline; render::ShapePipelinePointer GeometryCache::_simpleTransparentFadePipeline; render::ShapePipelinePointer GeometryCache::_simpleWirePipeline; @@ -803,8 +801,6 @@ void GeometryCache::initializeShapePipelines() { if (!_simpleOpaquePipeline) { _simpleOpaquePipeline = getShapePipeline(false, false, true, false); _simpleTransparentPipeline = getShapePipeline(false, true, true, false); - _forwardSimpleOpaquePipeline = getShapePipeline(false, false, true, false, false, true); - _forwardSimpleTransparentPipeline = getShapePipeline(false, true, true, false, false, true); _simpleOpaqueFadePipeline = getFadingShapePipeline(false, false, false, false, false); _simpleTransparentFadePipeline = getFadingShapePipeline(false, true, false, false, false); _simpleWirePipeline = getShapePipeline(false, false, true, true); @@ -836,14 +832,6 @@ render::ShapePipelinePointer GeometryCache::getFadingShapePipeline(bool textured ); } -render::ShapePipelinePointer GeometryCache::getOpaqueShapePipeline(bool isFading) { - return isFading ? _simpleOpaqueFadePipeline : _simpleOpaquePipeline; -} - -render::ShapePipelinePointer GeometryCache::getTransparentShapePipeline(bool isFading) { - return isFading ? _simpleTransparentFadePipeline : _simpleTransparentPipeline; -} - void GeometryCache::renderShape(gpu::Batch& batch, Shape shape) { batch.setInputFormat(getSolidStreamFormat()); _shapes[shape].draw(batch); @@ -2018,77 +2006,6 @@ void GeometryCache::renderLine(gpu::Batch& batch, const glm::vec2& p1, const glm batch.draw(gpu::LINES, 2, 0); } - -void GeometryCache::renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2, - const glm::vec4& color, float glowIntensity, float glowWidth, int id) { - - // Disable glow lines on OSX -#ifndef Q_OS_WIN - glowIntensity = 0.0f; -#endif - - if (glowIntensity <= 0.0f) { - if (color.a >= 1.0f) { - bindSimpleProgram(batch, false, false, false, true, true); - } else { - bindSimpleProgram(batch, false, true, false, true, true); - } - renderLine(batch, p1, p2, color, id); - return; - } - - // Compile the shaders - static std::once_flag once; - std::call_once(once, [&] { - auto state = std::make_shared(); - auto program = gpu::Shader::createProgram(shader::render_utils::program::glowLine); - state->setCullMode(gpu::State::CULL_NONE); - state->setDepthTest(true, false, gpu::LESS_EQUAL); - state->setBlendFunction(true, - gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - - PrepareStencil::testMask(*state); - _glowLinePipeline = gpu::Pipeline::create(program, state); - }); - - batch.setPipeline(_glowLinePipeline); - - Vec3Pair key(p1, p2); - bool registered = (id != UNKNOWN_ID); - BatchItemDetails& details = _registeredLine3DVBOs[id]; - - // if this is a registered quad, and we have buffers, then check to see if the geometry changed and rebuild if needed - if (registered && details.isCreated) { - Vec3Pair& lastKey = _lastRegisteredLine3D[id]; - if (lastKey != key) { - details.clear(); - _lastRegisteredLine3D[id] = key; - } - } - - const int NUM_VERTICES = 4; - if (!details.isCreated) { - details.isCreated = true; - details.uniformBuffer = std::make_shared(); - - struct LineData { - vec4 p1; - vec4 p2; - vec4 color; - float width; - }; - - LineData lineData { vec4(p1, 1.0f), vec4(p2, 1.0f), color, glowWidth }; - details.uniformBuffer->resize(sizeof(LineData)); - details.uniformBuffer->setSubData(0, lineData); - } - - // The shader requires no vertices, only uniforms. - batch.setUniformBuffer(0, details.uniformBuffer); - batch.draw(gpu::TRIANGLE_STRIP, NUM_VERTICES, 0); -} - void GeometryCache::useSimpleDrawPipeline(gpu::Batch& batch, bool noBlend) { static std::once_flag once; std::call_once(once, [&]() { @@ -2282,8 +2199,7 @@ gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool transp _unlitShader = _forwardUnlitShader; } else { _simpleShader = gpu::Shader::createProgram(simple_textured); - // Use the forward pipeline for both here, otherwise transparents will be unlit - _transparentShader = gpu::Shader::createProgram(forward_simple_textured_transparent); + _transparentShader = gpu::Shader::createProgram(simple_transparent_textured); _unlitShader = gpu::Shader::createProgram(simple_textured_unlit); } }); diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index 5c4cc67adf..cd3454bf38 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -181,17 +181,6 @@ public: static void initializeShapePipelines(); - render::ShapePipelinePointer getOpaqueShapePipeline() { assert(_simpleOpaquePipeline != nullptr); return _simpleOpaquePipeline; } - render::ShapePipelinePointer getTransparentShapePipeline() { assert(_simpleTransparentPipeline != nullptr); return _simpleTransparentPipeline; } - render::ShapePipelinePointer getForwardOpaqueShapePipeline() { assert(_forwardSimpleOpaquePipeline != nullptr); return _forwardSimpleOpaquePipeline; } - render::ShapePipelinePointer getForwardTransparentShapePipeline() { assert(_forwardSimpleTransparentPipeline != nullptr); return _forwardSimpleTransparentPipeline; } - render::ShapePipelinePointer getOpaqueFadeShapePipeline() { assert(_simpleOpaqueFadePipeline != nullptr); return _simpleOpaqueFadePipeline; } - render::ShapePipelinePointer getTransparentFadeShapePipeline() { assert(_simpleTransparentFadePipeline != nullptr); return _simpleTransparentFadePipeline; } - render::ShapePipelinePointer getOpaqueShapePipeline(bool isFading); - render::ShapePipelinePointer getTransparentShapePipeline(bool isFading); - render::ShapePipelinePointer getWireShapePipeline() { assert(_simpleWirePipeline != nullptr); return GeometryCache::_simpleWirePipeline; } - - // Static (instanced) geometry void renderShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& colorBuffer); void renderWireShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& colorBuffer); @@ -317,12 +306,6 @@ public: void renderLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2, const glm::vec4& color1, const glm::vec4& color2, int id); - void renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2, - const glm::vec4& color, float glowIntensity, float glowWidth, int id); - - void renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2, const glm::vec4& color, int id) - { renderGlowLine(batch, p1, p2, color, 1.0f, 0.05f, id); } - void renderDashedLine(gpu::Batch& batch, const glm::vec3& start, const glm::vec3& end, const glm::vec4& color, int id) { renderDashedLine(batch, start, end, color, 0.05f, 0.025f, id); } @@ -478,12 +461,9 @@ private: static gpu::ShaderPointer _unlitFadeShader; static render::ShapePipelinePointer _simpleOpaquePipeline; static render::ShapePipelinePointer _simpleTransparentPipeline; - static render::ShapePipelinePointer _forwardSimpleOpaquePipeline; - static render::ShapePipelinePointer _forwardSimpleTransparentPipeline; static render::ShapePipelinePointer _simpleOpaqueFadePipeline; static render::ShapePipelinePointer _simpleTransparentFadePipeline; static render::ShapePipelinePointer _simpleWirePipeline; - gpu::PipelinePointer _glowLinePipeline; static QHash _simplePrograms; diff --git a/libraries/render-utils/src/TextRenderer3D.cpp b/libraries/render-utils/src/TextRenderer3D.cpp index 93edc4217d..8ef0dc0d73 100644 --- a/libraries/render-utils/src/TextRenderer3D.cpp +++ b/libraries/render-utils/src/TextRenderer3D.cpp @@ -67,11 +67,11 @@ float TextRenderer3D::getFontSize() const { } void TextRenderer3D::draw(gpu::Batch& batch, float x, float y, const QString& str, const glm::vec4& color, - const glm::vec2& bounds, bool forwardRendered) { + const glm::vec2& bounds, bool layered) { // The font does all the OpenGL work if (_font) { _color = color; - _font->drawString(batch, _drawInfo, str, _color, _effectType, { x, y }, bounds, forwardRendered); + _font->drawString(batch, _drawInfo, str, _color, _effectType, { x, y }, bounds, layered); } } diff --git a/libraries/render-utils/src/TextRenderer3D.h b/libraries/render-utils/src/TextRenderer3D.h index b6475ab0ed..6c91411e1d 100644 --- a/libraries/render-utils/src/TextRenderer3D.h +++ b/libraries/render-utils/src/TextRenderer3D.h @@ -39,7 +39,7 @@ public: float getFontSize() const; // Pixel size void draw(gpu::Batch& batch, float x, float y, const QString& str, const glm::vec4& color = glm::vec4(1.0f), - const glm::vec2& bounds = glm::vec2(-1.0f), bool forwardRendered = false); + const glm::vec2& bounds = glm::vec2(-1.0f), bool layered = false); private: TextRenderer3D(const char* family, float pointSize, int weight = -1, bool italic = false, diff --git a/libraries/render-utils/src/forward_sdf_text3D.slf b/libraries/render-utils/src/forward_sdf_text3D.slf new file mode 100644 index 0000000000..09b10c0c42 --- /dev/null +++ b/libraries/render-utils/src/forward_sdf_text3D.slf @@ -0,0 +1,57 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// sdf_text3D_transparent.frag +// fragment shader +// +// Created by Bradley Austin Davis on 2015-02-04 +// Based on fragment shader code from +// https://github.com/paulhoux/Cinder-Samples/blob/master/TextRendering/include/text/Text.cpp +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +<@include DefaultMaterials.slh@> + +<@include ForwardGlobalLight.slh@> +<$declareEvalSkyboxGlobalColor()$> + +<@include gpu/Transform.slh@> +<$declareStandardCameraTransform()$> + +<@include render-utils/ShaderConstants.h@> + +<@include sdf_text3D.slh@> +<$declareEvalSDFSuperSampled()$> + +layout(location=RENDER_UTILS_ATTR_POSITION_ES) in vec4 _positionES; +layout(location=RENDER_UTILS_ATTR_NORMAL_WS) in vec3 _normalWS; +layout(location=RENDER_UTILS_ATTR_COLOR) in vec4 _color; +layout(location=RENDER_UTILS_ATTR_TEXCOORD01) in vec4 _texCoord01; +#define _texCoord0 _texCoord01.xy +#define _texCoord1 _texCoord01.zw + +layout(location=0) out vec4 _fragColor0; + +void main() { + float a = evalSDFSuperSampled(_texCoord0); + + float alpha = a * _color.a; + if (alpha <= 0.0) { + discard; + } + + TransformCamera cam = getTransformCamera(); + vec3 fragPosition = _positionES.xyz; + + _fragColor0 = vec4(evalSkyboxGlobalColor( + cam._viewInverse, + 1.0, + DEFAULT_OCCLUSION, + fragPosition, + normalize(_normalWS), + _color.rgb, + DEFAULT_FRESNEL, + DEFAULT_METALLIC, + DEFAULT_ROUGHNESS), + 1.0); +} \ No newline at end of file diff --git a/libraries/render-utils/src/forward_simple_textured.slf b/libraries/render-utils/src/forward_simple_textured.slf index ca31550b40..373ab13d1a 100644 --- a/libraries/render-utils/src/forward_simple_textured.slf +++ b/libraries/render-utils/src/forward_simple_textured.slf @@ -11,6 +11,7 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +<@include gpu/Color.slh@> <@include DefaultMaterials.slh@> <@include ForwardGlobalLight.slh@> @@ -21,10 +22,8 @@ <@include render-utils/ShaderConstants.h@> -// the albedo texture LAYOUT(binding=0) uniform sampler2D originalTexture; -// the interpolated normal layout(location=RENDER_UTILS_ATTR_NORMAL_WS) in vec3 _normalWS; layout(location=RENDER_UTILS_ATTR_COLOR) in vec4 _color; layout(location=RENDER_UTILS_ATTR_TEXCOORD01) in vec4 _texCoord01; @@ -36,7 +35,11 @@ layout(location=0) out vec4 _fragColor0; void main(void) { vec4 texel = texture(originalTexture, _texCoord0); - float colorAlpha = _color.a * texel.a; + texel = mix(texel, color_sRGBAToLinear(texel), float(_color.a <= 0.0)); + vec3 albedo = _color.xyz * texel.xyz; + float metallic = DEFAULT_METALLIC; + + vec3 fresnel = getFresnelF0(metallic, albedo); TransformCamera cam = getTransformCamera(); vec3 fragPosition = _positionES.xyz; @@ -47,9 +50,9 @@ void main(void) { DEFAULT_OCCLUSION, fragPosition, normalize(_normalWS), - _color.rgb * texel.rgb, - DEFAULT_FRESNEL, - DEFAULT_METALLIC, + albedo, + fresnel, + metallic, DEFAULT_ROUGHNESS), 1.0); } \ No newline at end of file diff --git a/libraries/render-utils/src/forward_simple_textured_transparent.slf b/libraries/render-utils/src/forward_simple_textured_transparent.slf index 11d51bbd78..1b5047507b 100644 --- a/libraries/render-utils/src/forward_simple_textured_transparent.slf +++ b/libraries/render-utils/src/forward_simple_textured_transparent.slf @@ -11,6 +11,7 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +<@include gpu/Color.slh@> <@include DefaultMaterials.slh@> <@include ForwardGlobalLight.slh@> @@ -21,22 +22,25 @@ <@include render-utils/ShaderConstants.h@> -// the albedo texture LAYOUT(binding=0) uniform sampler2D originalTexture; -// the interpolated normal +layout(location=RENDER_UTILS_ATTR_POSITION_ES) in vec4 _positionES; layout(location=RENDER_UTILS_ATTR_NORMAL_WS) in vec3 _normalWS; layout(location=RENDER_UTILS_ATTR_COLOR) in vec4 _color; layout(location=RENDER_UTILS_ATTR_TEXCOORD01) in vec4 _texCoord01; #define _texCoord0 _texCoord01.xy #define _texCoord1 _texCoord01.zw -layout(location=RENDER_UTILS_ATTR_POSITION_ES) in vec4 _positionES; layout(location=0) out vec4 _fragColor0; void main(void) { vec4 texel = texture(originalTexture, _texCoord0); - float colorAlpha = _color.a * texel.a; + texel = mix(texel, color_sRGBAToLinear(texel), float(_color.a <= 0.0)); + vec3 albedo = _color.xyz * texel.xyz; + float alpha = _color.a * texel.a; + float metallic = DEFAULT_METALLIC; + + vec3 fresnel = getFresnelF0(metallic, albedo); TransformCamera cam = getTransformCamera(); vec3 fragPosition = _positionES.xyz; @@ -47,10 +51,10 @@ void main(void) { DEFAULT_OCCLUSION, fragPosition, normalize(_normalWS), - _color.rgb * texel.rgb, - DEFAULT_FRESNEL, - DEFAULT_METALLIC, + albedo, + fresnel, + metallic, DEFAULT_EMISSIVE, - DEFAULT_ROUGHNESS, colorAlpha), - colorAlpha); + DEFAULT_ROUGHNESS, alpha), + alpha); } \ No newline at end of file diff --git a/libraries/render-utils/src/render-utils/forward_sdf_text3D.slp b/libraries/render-utils/src/render-utils/forward_sdf_text3D.slp new file mode 100644 index 0000000000..3eea3a0da0 --- /dev/null +++ b/libraries/render-utils/src/render-utils/forward_sdf_text3D.slp @@ -0,0 +1 @@ +VERTEX sdf_text3D diff --git a/libraries/render-utils/src/sdf_text3D.slf b/libraries/render-utils/src/sdf_text3D.slf index b070fc44cf..91c73e9eec 100644 --- a/libraries/render-utils/src/sdf_text3D.slf +++ b/libraries/render-utils/src/sdf_text3D.slf @@ -13,54 +13,22 @@ <@include DeferredBufferWrite.slh@> <@include render-utils/ShaderConstants.h@> -LAYOUT(binding=0) uniform sampler2D Font; +<@include sdf_text3D.slh@> +<$declareEvalSDFSuperSampled()$> -struct TextParams { - vec4 color; - vec4 outline; -}; - -LAYOUT(binding=0) uniform textParamsBuffer { - TextParams params; -}; - -// the interpolated normal layout(location=RENDER_UTILS_ATTR_NORMAL_WS) in vec3 _normalWS; +layout(location=RENDER_UTILS_ATTR_COLOR) in vec4 _color; layout(location=RENDER_UTILS_ATTR_TEXCOORD01) in vec4 _texCoord01; #define _texCoord0 _texCoord01.xy #define _texCoord1 _texCoord01.zw -#define TAA_TEXTURE_LOD_BIAS -3.0 - -const float interiorCutoff = 0.8; -const float outlineExpansion = 0.2; -const float taaBias = pow(2.0, TAA_TEXTURE_LOD_BIAS); - -float evalSDF(vec2 texCoord) { - // retrieve signed distance - float sdf = textureLod(Font, texCoord, TAA_TEXTURE_LOD_BIAS).g; - sdf = mix(sdf, mix(sdf + outlineExpansion, 1.0 - sdf, float(sdf > interiorCutoff)), float(params.outline.x > 0.0)); - - // Rely on TAA for anti-aliasing - return step(0.5, sdf); -} - void main() { - vec2 dxTexCoord = dFdx(_texCoord0) * 0.5 * taaBias; - vec2 dyTexCoord = dFdy(_texCoord0) * 0.5 * taaBias; - - // Perform 4x supersampling for anisotropic filtering - float a; - a = evalSDF(_texCoord0); - a += evalSDF(_texCoord0 + dxTexCoord); - a += evalSDF(_texCoord0 + dyTexCoord); - a += evalSDF(_texCoord0 + dxTexCoord + dyTexCoord); - a *= 0.25; + float a = evalSDFSuperSampled(_texCoord0); packDeferredFragment( normalize(_normalWS), - a * params.color.a, - params.color.rgb, + a, + _color.rgb, DEFAULT_ROUGHNESS, DEFAULT_METALLIC, DEFAULT_EMISSIVE, diff --git a/libraries/render-utils/src/sdf_text3D.slh b/libraries/render-utils/src/sdf_text3D.slh new file mode 100644 index 0000000000..3297596efd --- /dev/null +++ b/libraries/render-utils/src/sdf_text3D.slh @@ -0,0 +1,63 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> + +// Generated on <$_SCRIBE_DATE$> +// +// Created by Sam Gondelman on 3/15/19 +// Copyright 2019 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 not SDF_TEXT3D_SLH@> +<@def SDF_TEXT3D_SLH@> + +LAYOUT(binding=0) uniform sampler2D Font; + +struct TextParams { + vec4 color; + vec4 outline; +}; + +LAYOUT(binding=0) uniform textParamsBuffer { + TextParams params; +}; + +<@func declareEvalSDFSuperSampled()@> + +#define TAA_TEXTURE_LOD_BIAS -3.0 + +const float interiorCutoff = 0.8; +const float outlineExpansion = 0.2; +const float taaBias = pow(2.0, TAA_TEXTURE_LOD_BIAS); + +float evalSDF(vec2 texCoord) { + // retrieve signed distance + float sdf = textureLod(Font, texCoord, TAA_TEXTURE_LOD_BIAS).g; + sdf = mix(sdf, mix(sdf + outlineExpansion, 1.0 - sdf, float(sdf > interiorCutoff)), float(params.outline.x > 0.0)); + + // Rely on TAA for anti-aliasing + return step(0.5, sdf); +} + +float evalSDFSuperSampled(vec2 texCoord) { + vec2 dxTexCoord = dFdx(texCoord) * 0.5 * taaBias; + vec2 dyTexCoord = dFdy(texCoord) * 0.5 * taaBias; + + // Perform 4x supersampling for anisotropic filtering + float a; + a = evalSDF(texCoord); + a += evalSDF(texCoord + dxTexCoord); + a += evalSDF(texCoord + dyTexCoord); + a += evalSDF(texCoord + dxTexCoord + dyTexCoord); + a *= 0.25; + + return a; +} + +<@endfunc@> + +<@endif@> + diff --git a/libraries/render-utils/src/sdf_text3D.slv b/libraries/render-utils/src/sdf_text3D.slv index 5f4df86d56..274e09e6ad 100644 --- a/libraries/render-utils/src/sdf_text3D.slv +++ b/libraries/render-utils/src/sdf_text3D.slv @@ -11,18 +11,23 @@ // <@include gpu/Inputs.slh@> -<@include gpu/Transform.slh@> +<@include gpu/Color.slh@> <@include render-utils/ShaderConstants.h@> +<@include gpu/Transform.slh@> <$declareStandardTransform()$> +<@include sdf_text3D.slh@> + // the interpolated normal -layout(location=RENDER_UTILS_ATTR_NORMAL_WS) out vec3 _normalWS; -layout(location=RENDER_UTILS_ATTR_TEXCOORD01) out vec4 _texCoord01; layout(location=RENDER_UTILS_ATTR_POSITION_ES) out vec4 _positionES; +layout(location=RENDER_UTILS_ATTR_NORMAL_WS) out vec3 _normalWS; +layout(location=RENDER_UTILS_ATTR_COLOR) out vec4 _color; +layout(location=RENDER_UTILS_ATTR_TEXCOORD01) out vec4 _texCoord01; void main() { _texCoord01.xy = inTexCoord0.xy; + _color = color_sRGBAToLinear(params.color); // standard transform TransformCamera cam = getTransformCamera(); diff --git a/libraries/render-utils/src/sdf_text3D_transparent.slf b/libraries/render-utils/src/sdf_text3D_transparent.slf index 311c849915..c4a80091de 100644 --- a/libraries/render-utils/src/sdf_text3D_transparent.slf +++ b/libraries/render-utils/src/sdf_text3D_transparent.slf @@ -20,53 +20,22 @@ <@include render-utils/ShaderConstants.h@> -LAYOUT(binding=0) uniform sampler2D Font; - -struct TextParams { - vec4 color; - vec4 outline; -}; - -LAYOUT(binding=0) uniform textParamsBuffer { - TextParams params; -}; +<@include sdf_text3D.slh@> +<$declareEvalSDFSuperSampled()$> layout(location=RENDER_UTILS_ATTR_POSITION_ES) in vec4 _positionES; layout(location=RENDER_UTILS_ATTR_NORMAL_WS) in vec3 _normalWS; +layout(location=RENDER_UTILS_ATTR_COLOR) in vec4 _color; layout(location=RENDER_UTILS_ATTR_TEXCOORD01) in vec4 _texCoord01; #define _texCoord0 _texCoord01.xy #define _texCoord1 _texCoord01.zw layout(location=0) out vec4 _fragColor0; -#define TAA_TEXTURE_LOD_BIAS -3.0 - -const float interiorCutoff = 0.8; -const float outlineExpansion = 0.2; -const float taaBias = pow(2.0, TAA_TEXTURE_LOD_BIAS); - -float evalSDF(vec2 texCoord) { - // retrieve signed distance - float sdf = textureLod(Font, texCoord, TAA_TEXTURE_LOD_BIAS).g; - sdf = mix(sdf, mix(sdf + outlineExpansion, 1.0 - sdf, float(sdf > interiorCutoff)), float(params.outline.x > 0.0)); - - // Rely on TAA for anti-aliasing - return step(0.5, sdf); -} - void main() { - vec2 dxTexCoord = dFdx(_texCoord0) * 0.5 * taaBias; - vec2 dyTexCoord = dFdy(_texCoord0) * 0.5 * taaBias; + float a = evalSDFSuperSampled(_texCoord0); - // Perform 4x supersampling for anisotropic filtering - float a; - a = evalSDF(_texCoord0); - a += evalSDF(_texCoord0 + dxTexCoord); - a += evalSDF(_texCoord0 + dyTexCoord); - a += evalSDF(_texCoord0 + dxTexCoord + dyTexCoord); - a *= 0.25; - - float alpha = a * params.color.a; + float alpha = a * _color.a; if (alpha <= 0.0) { discard; } @@ -80,7 +49,7 @@ void main() { DEFAULT_OCCLUSION, fragPosition, normalize(_normalWS), - params.color.rgb, + _color.rgb, DEFAULT_FRESNEL, DEFAULT_METALLIC, DEFAULT_EMISSIVE, diff --git a/libraries/render-utils/src/simple.slv b/libraries/render-utils/src/simple.slv index 0dd4e55f26..460ed53281 100644 --- a/libraries/render-utils/src/simple.slv +++ b/libraries/render-utils/src/simple.slv @@ -19,7 +19,6 @@ <@include render-utils/ShaderConstants.h@> -// the interpolated normal layout(location=RENDER_UTILS_ATTR_NORMAL_WS) out vec3 _normalWS; layout(location=RENDER_UTILS_ATTR_NORMAL_MS) out vec3 _normalMS; layout(location=RENDER_UTILS_ATTR_COLOR) out vec4 _color; diff --git a/libraries/render-utils/src/simple_transparent_textured.slf b/libraries/render-utils/src/simple_transparent_textured.slf index bd29ff2ec9..f1bb2b1ea2 100644 --- a/libraries/render-utils/src/simple_transparent_textured.slf +++ b/libraries/render-utils/src/simple_transparent_textured.slf @@ -11,31 +11,50 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +<@include DefaultMaterials.slh@> <@include gpu/Color.slh@> -<@include DeferredBufferWrite.slh@> - <@include render-utils/ShaderConstants.h@> -// the albedo texture +<@include ForwardGlobalLight.slh@> +<$declareEvalGlobalLightingAlphaBlended()$> + +<@include gpu/Transform.slh@> +<$declareStandardCameraTransform()$> + LAYOUT(binding=0) uniform sampler2D originalTexture; -// the interpolated normal +layout(location=RENDER_UTILS_ATTR_POSITION_ES) in vec4 _positionES; layout(location=RENDER_UTILS_ATTR_NORMAL_WS) in vec3 _normalWS; layout(location=RENDER_UTILS_ATTR_COLOR) in vec4 _color; layout(location=RENDER_UTILS_ATTR_TEXCOORD01) in vec4 _texCoord01; #define _texCoord0 _texCoord01.xy #define _texCoord1 _texCoord01.zw +layout(location=0) out vec4 _fragColor0; + void main(void) { vec4 texel = texture(originalTexture, _texCoord0); texel = mix(texel, color_sRGBAToLinear(texel), float(_color.a <= 0.0)); - texel.rgb *= _color.rgb; - texel.a *= abs(_color.a); + vec3 albedo = _color.xyz * texel.xyz; + float alpha = _color.a * texel.a; + float metallic = DEFAULT_METALLIC; - packDeferredFragmentTranslucent( + vec3 fresnel = getFresnelF0(metallic, albedo); + + TransformCamera cam = getTransformCamera(); + vec3 fragPosition = _positionES.xyz; + + _fragColor0 = vec4(evalGlobalLightingAlphaBlendedWithHaze( + cam._viewInverse, + 1.0, + DEFAULT_OCCLUSION, + fragPosition, normalize(_normalWS), - texel.a, - texel.rgb, - DEFAULT_ROUGHNESS); + albedo, + fresnel, + metallic, + DEFAULT_EMISSIVE, + DEFAULT_ROUGHNESS, alpha), + alpha); } \ No newline at end of file diff --git a/libraries/render-utils/src/text/Font.cpp b/libraries/render-utils/src/text/Font.cpp index e0e99da020..364e24c5ac 100644 --- a/libraries/render-utils/src/text/Font.cpp +++ b/libraries/render-utils/src/text/Font.cpp @@ -13,6 +13,8 @@ #include "FontFamilies.h" #include "../StencilMaskPass.h" +#include "DisableDeferred.h" + static std::mutex fontMutex; struct TextureVertex { @@ -221,25 +223,43 @@ void Font::setupGPU() { // Setup render pipeline { - gpu::ShaderPointer program = gpu::Shader::createProgram(shader::render_utils::program::sdf_text3D); - auto state = std::make_shared(); - state->setCullMode(gpu::State::CULL_BACK); - state->setDepthTest(true, true, gpu::LESS_EQUAL); - state->setBlendFunction(false, - gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - PrepareStencil::testMaskDrawShape(*state); - _pipeline = gpu::Pipeline::create(program, state); + { + gpu::ShaderPointer program = gpu::Shader::createProgram(shader::render_utils::program::forward_sdf_text3D); + auto state = std::make_shared(); + state->setCullMode(gpu::State::CULL_BACK); + state->setDepthTest(true, true, gpu::LESS_EQUAL); + state->setBlendFunction(false, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + PrepareStencil::testMaskDrawShape(*state); + _layeredPipeline = gpu::Pipeline::create(program, state); + } - gpu::ShaderPointer programTransparent = gpu::Shader::createProgram(shader::render_utils::program::sdf_text3D_transparent); - auto transparentState = std::make_shared(); - transparentState->setCullMode(gpu::State::CULL_BACK); - transparentState->setDepthTest(true, true, gpu::LESS_EQUAL); - transparentState->setBlendFunction(true, - gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - PrepareStencil::testMask(*transparentState); - _transparentPipeline = gpu::Pipeline::create(programTransparent, transparentState); + if (DISABLE_DEFERRED) { + _pipeline = _layeredPipeline; + } else { + gpu::ShaderPointer program = gpu::Shader::createProgram(shader::render_utils::program::sdf_text3D); + auto state = std::make_shared(); + state->setCullMode(gpu::State::CULL_BACK); + state->setDepthTest(true, true, gpu::LESS_EQUAL); + state->setBlendFunction(false, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + PrepareStencil::testMaskDrawShape(*state); + _pipeline = gpu::Pipeline::create(program, state); + } + + { + gpu::ShaderPointer program = gpu::Shader::createProgram(shader::render_utils::program::sdf_text3D_transparent); + auto state = std::make_shared(); + state->setCullMode(gpu::State::CULL_BACK); + state->setDepthTest(true, true, gpu::LESS_EQUAL); + state->setBlendFunction(true, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + PrepareStencil::testMask(*state); + _transparentPipeline = gpu::Pipeline::create(program, state); + } } // Sanity checks @@ -343,7 +363,7 @@ void Font::buildVertices(Font::DrawInfo& drawInfo, const QString& str, const glm } void Font::drawString(gpu::Batch& batch, Font::DrawInfo& drawInfo, const QString& str, const glm::vec4& color, - EffectType effectType, const glm::vec2& origin, const glm::vec2& bounds, bool forwardRendered) { + EffectType effectType, const glm::vec2& origin, const glm::vec2& bounds, bool layered) { if (str == "") { return; } @@ -370,7 +390,7 @@ void Font::drawString(gpu::Batch& batch, Font::DrawInfo& drawInfo, const QString } // need the gamma corrected color here - batch.setPipeline(forwardRendered || (color.a < 1.0f) ? _transparentPipeline : _pipeline); + batch.setPipeline(color.a < 1.0f ? _transparentPipeline : (layered ? _layeredPipeline : _pipeline)); batch.setInputFormat(_format); batch.setInputBuffer(0, drawInfo.verticesBuffer, 0, _format->getChannels().at(0)._stride); batch.setResourceTexture(render_utils::slot::texture::TextFont, _texture); diff --git a/libraries/render-utils/src/text/Font.h b/libraries/render-utils/src/text/Font.h index 26cc4e46c3..28af5bac43 100644 --- a/libraries/render-utils/src/text/Font.h +++ b/libraries/render-utils/src/text/Font.h @@ -46,7 +46,7 @@ public: // Render string to batch void drawString(gpu::Batch& batch, DrawInfo& drawInfo, const QString& str, const glm::vec4& color, EffectType effectType, - const glm::vec2& origin, const glm::vec2& bound, bool forwardRendered); + const glm::vec2& origin, const glm::vec2& bound, bool layered); static Pointer load(const QString& family); @@ -81,6 +81,7 @@ private: // gpu structures gpu::PipelinePointer _pipeline; + gpu::PipelinePointer _layeredPipeline; gpu::PipelinePointer _transparentPipeline; gpu::TexturePointer _texture; gpu::Stream::FormatPointer _format; From 9182db8bd40b0accc57aa65f963cc7a3efdf981f Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Fri, 15 Mar 2019 12:56:43 -0700 Subject: [PATCH 202/446] Case 21025 conditionalizing TabletWebView features --- interface/resources/qml/hifi/Card.qml | 5 ++-- interface/resources/qml/hifi/NameCard.qml | 24 ++++++++++++------- .../qml/hifi/commerce/wallet/Wallet.qml | 7 ++++-- .../qml/hifi/commerce/wallet/WalletHome.qml | 6 ++++- .../qml/hifi/tablet/TabletAddressDialog.qml | 7 ++++-- interface/src/Application.cpp | 3 +++ 6 files changed, 36 insertions(+), 16 deletions(-) diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index fc49bcf048..9fb8067371 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -40,6 +40,7 @@ Item { property bool isConcurrency: action === 'concurrency'; property bool isAnnouncement: action === 'announcement'; property bool isStacked: !isConcurrency && drillDownToPlace; + property bool has3DHTML: PlatformInfo.has3DHTML(); property int textPadding: 10; @@ -298,7 +299,7 @@ Item { StateImage { id: actionIcon; - visible: !isAnnouncement; + visible: !isAnnouncement && has3DHTML; imageURL: "../../images/info-icon-2-state.svg"; size: 30; buttonState: messageArea.containsMouse ? 1 : 0; @@ -315,7 +316,7 @@ Item { } MouseArea { id: messageArea; - visible: !isAnnouncement; + visible: !isAnnouncement && has3DHTML; width: parent.width; height: messageHeight; anchors.top: lobby.bottom; diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 646fc881e1..c92afe9e14 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -46,6 +46,8 @@ Item { property string placeName: "" property string profilePicBorderColor: (connectionStatus == "connection" ? hifi.colors.indigoAccent : (connectionStatus == "friend" ? hifi.colors.greenHighlight : "transparent")) property alias avImage: avatarImage + property bool has3DHTML: PlatformInfo.has3DHTML(); + Item { id: avatarImage visible: profileUrl !== "" && userName !== ""; @@ -94,10 +96,12 @@ Item { enabled: (selected && activeTab == "nearbyTab") || isMyCard; hoverEnabled: enabled onClicked: { - userInfoViewer.url = Account.metaverseServerURL + "/users/" + userName; - userInfoViewer.visible = true; + if (Phas3DHTML) { + userInfoViewer.url = Account.metaverseServerURL + "/users/" + userName; + userInfoViewer.visible = true; + } } - onEntered: infoHoverImage.visible = true; + onEntered: infoHoverImage.visible = has3DHTML; onExited: infoHoverImage.visible = false; } } @@ -352,7 +356,7 @@ Item { } StateImage { id: nameCardConnectionInfoImage - visible: selected && !isMyCard && pal.activeTab == "connectionsTab" + visible: selected && !isMyCard && pal.activeTab == "connectionsTab" && has3DHTML imageURL: "../../images/info-icon-2-state.svg" // PLACEHOLDER!!! size: 32; buttonState: 0; @@ -364,8 +368,10 @@ Item { enabled: selected hoverEnabled: true onClicked: { - userInfoViewer.url = Account.metaverseServerURL + "/users/" + userName; - userInfoViewer.visible = true; + if(has3DHTML) { + userInfoViewer.url = Account.metaverseServerURL + "/users/" + userName; + userInfoViewer.visible = true; + } } onEntered: { nameCardConnectionInfoImage.buttonState = 1; @@ -376,8 +382,7 @@ Item { } FiraSansRegular { id: nameCardConnectionInfoText - visible: selected && !isMyCard && pal.activeTab == "connectionsTab" - width: parent.width + visible: selected && !isMyCard && pal.activeTab == "connectionsTab" && PlatformInfo.has3DHTML() height: displayNameTextPixelSize size: displayNameTextPixelSize - 4 anchors.left: nameCardConnectionInfoImage.right @@ -391,9 +396,10 @@ Item { id: nameCardRemoveConnectionImage visible: selected && !isMyCard && pal.activeTab == "connectionsTab" text: hifi.glyphs.close - size: 28; + size: 24; x: 120 anchors.verticalCenter: nameCardConnectionInfoImage.verticalCenter + anchors.left: has3DHTML ? nameCardConnectionInfoText.right + 10 : avatarImage.right } MouseArea { anchors.fill:nameCardRemoveConnectionImage diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index ea74549084..7c2b86ef99 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -32,6 +32,7 @@ Rectangle { property string initialActiveViewAfterStatus5: "walletInventory"; property bool keyboardRaised: false; property bool isPassword: false; + property bool has3DHTML: PlatformInfo.has3DHTML(); anchors.fill: (typeof parent === undefined) ? undefined : parent; @@ -335,8 +336,10 @@ Rectangle { Connections { onSendSignalToWallet: { if (msg.method === 'transactionHistory_usernameLinkClicked') { - userInfoViewer.url = msg.usernameLink; - userInfoViewer.visible = true; + if (has3DHTML) { + userInfoViewer.url = msg.usernameLink; + userInfoViewer.visible = true; + } } else { sendToScript(msg); } diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index eb8aa0f809..06d07a28c9 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -24,6 +24,8 @@ Item { HifiConstants { id: hifi; } id: root; + + property bool has3DHTML: PlatformInfo.has3DHTML(); onVisibleChanged: { if (visible) { @@ -333,7 +335,9 @@ Item { onLinkActivated: { if (link.indexOf("users/") !== -1) { - sendSignalToWallet({method: 'transactionHistory_usernameLinkClicked', usernameLink: link}); + if (has3DHTML) { + sendSignalToWallet({method: 'transactionHistory_usernameLinkClicked', usernameLink: link}); + } } else { sendSignalToWallet({method: 'transactionHistory_linkClicked', itemId: model.marketplace_item}); } diff --git a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml index 4edae017d1..1342e55b5d 100644 --- a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml +++ b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml @@ -35,6 +35,7 @@ StackView { property int cardWidth: 212; property int cardHeight: 152; property var tablet: null; + property bool has3DHTML: PlatformInfo.has3DHTML(); RootHttpRequest { id: http; } signal sendToScript(var message); @@ -75,8 +76,10 @@ StackView { } function goCard(targetString, standaloneOptimized) { if (0 !== targetString.indexOf('hifi://')) { - var card = tabletWebView.createObject(); - card.url = addressBarDialog.metaverseServerUrl + targetString; + if(has3DHTML) { + var card = tabletWebView.createObject(); + card.url = addressBarDialog.metaverseServerUrl + targetString; + } card.parentStackItem = root; root.push(card); return; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index de4a6bb167..417c44fc1f 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3043,6 +3043,9 @@ void Application::initializeUi() { QUrl{ "hifi/commerce/wallet/Wallet.qml" }, QUrl{ "hifi/commerce/wallet/WalletHome.qml" }, QUrl{ "hifi/tablet/TabletAddressDialog.qml" }, + QUrl{ "hifi/Card.qml" }, + QUrl{ "hifi/Pal.qml" }, + QUrl{ "hifi/NameCard.qml" }, }, platformInfoCallback); QmlContextCallback ttsCallback = [](QQmlContext* context) { From 83bac723ef10a0c35024c9efb6fc493fafc5ac1b Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 14 Mar 2019 10:59:42 -0700 Subject: [PATCH 203/446] fix wearable duplication on domain switch --- interface/src/Application.cpp | 4 +-- interface/src/avatar/MyAvatar.cpp | 28 +++++++++++++++++++ interface/src/avatar/MyAvatar.h | 6 ++++ .../entities/src/EntityScriptingInterface.cpp | 12 ++------ 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index de4a6bb167..879426ec96 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5772,6 +5772,7 @@ void Application::reloadResourceCaches() { queryOctree(NodeType::EntityServer, PacketType::EntityQuery); + getMyAvatar()->prepareAvatarEntityDataForReload(); // Clear the entities and their renderables getEntities()->clear(); @@ -6947,9 +6948,6 @@ void Application::updateWindowTitle() const { } void Application::clearDomainOctreeDetails(bool clearAll) { - // before we delete all entities get MyAvatar's AvatarEntityData ready - getMyAvatar()->prepareAvatarEntityDataForReload(); - // if we're about to quit, we really don't need to do the rest of these things... if (_aboutToQuit) { return; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 9211be3b4f..02ef91cdba 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3450,6 +3450,34 @@ float MyAvatar::getGravity() { return _characterController.getGravity(); } +void MyAvatar::setSessionUUID(const QUuid& sessionUUID) { + QUuid oldID = getSessionUUID(); + Avatar::setSessionUUID(sessionUUID); + QUuid id = getSessionUUID(); + if (id != oldID) { + auto treeRenderer = DependencyManager::get(); + EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; + if (entityTree) { + QList avatarEntityIDs; + _avatarEntitiesLock.withReadLock([&] { + avatarEntityIDs = _packedAvatarEntityData.keys(); + }); + entityTree->withWriteLock([&] { + for (const auto& entityID : avatarEntityIDs) { + auto entity = entityTree->findEntityByID(entityID); + if (!entity) { + continue; + } + entity->setOwningAvatarID(id); + if (entity->getParentID() == oldID) { + entity->setParentID(id); + } + } + }); + } + } +} + void MyAvatar::increaseSize() { float minScale = getDomainMinScale(); float maxScale = getDomainMaxScale(); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index e516364f61..aadc8ee268 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1213,6 +1213,12 @@ public: public slots: + /**jsdoc + * @function MyAvatar.setSessionUUID + * @param {Uuid} sessionUUID + */ + virtual void setSessionUUID(const QUuid& sessionUUID) override; + /**jsdoc * Increase the avatar's scale by five percent, up to a minimum scale of 1000. * @function MyAvatar.increaseSize diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 22cd26eac6..6610439183 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -1646,11 +1646,9 @@ bool EntityScriptingInterface::actionWorker(const QUuid& entityID, auto nodeList = DependencyManager::get(); const QUuid myNodeID = nodeList->getSessionUUID(); - EntityItemProperties properties; - EntityItemPointer entity; bool doTransmit = false; - _entityTree->withWriteLock([this, &entity, entityID, myNodeID, &doTransmit, actor, &properties] { + _entityTree->withWriteLock([this, &entity, entityID, myNodeID, &doTransmit, actor] { EntitySimulationPointer simulation = _entityTree->getSimulation(); entity = _entityTree->findEntityByEntityItemID(entityID); if (!entity) { @@ -1669,16 +1667,12 @@ bool EntityScriptingInterface::actionWorker(const QUuid& entityID, doTransmit = actor(simulation, entity); _entityTree->entityChanged(entity); - if (doTransmit) { - properties.setEntityHostType(entity->getEntityHostType()); - properties.setOwningAvatarID(entity->getOwningAvatarID()); - } }); // transmit the change if (doTransmit) { - _entityTree->withReadLock([&] { - properties = entity->getProperties(); + EntityItemProperties properties = _entityTree->resultWithReadLock([&] { + return entity->getProperties(); }); properties.setActionDataDirty(); From 9e8e389e99c406c22dadea2987e07cd8e9075aaa Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 15 Mar 2019 13:38:43 -0700 Subject: [PATCH 204/446] Improved detection of device model. --- tools/nitpick/src/TestRunnerMobile.cpp | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index ad5151bcc0..72b3ea5cc9 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -98,6 +98,8 @@ void TestRunnerMobile::connectDevice() { QString line2 = devicesFile.readLine(); const QString DEVICE{ "device" }; + const QString MODEL{ "model" }; + if (line2.contains("unauthorized")) { QMessageBox::critical(0, "Unauthorized device detected", "Please allow USB debugging on device"); } else if (line2.contains(DEVICE)) { @@ -110,10 +112,21 @@ void TestRunnerMobile::connectDevice() { QStringList tokens = line2.split(QRegExp("[\r\n\t ]+")); QString deviceID = tokens[0]; - QString modelID = tokens[3].split(':')[1]; + // Find the model entry + int i; + for (i = 0; i < tokens.size(); ++i) { + if (tokens[i].contains(MODEL)) { + break; + } + } + _modelName = "UNKNOWN"; - if (modelNames.count(modelID) == 1) { - _modelName = modelNames[modelID]; + if (i < tokens.size()) { + QString modelID = tokens[i].split(':')[1]; + + if (modelNames.count(modelID) == 1) { + _modelName = modelNames[modelID]; + } } _detectedDeviceLabel->setText(_modelName + " [" + deviceID + "]"); From b91a1d930a1303b3d0a9c38daa3115f79176862e Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 15 Mar 2019 13:44:01 -0700 Subject: [PATCH 205/446] Missing newline. --- tools/nitpick/src/TestRunnerMobile.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 72b3ea5cc9..0710e48008 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -339,4 +339,4 @@ qint64 TestRunnerMobile::convertToBinary(const QString& str) { } return binary.toLongLong(NULL, 2); -} \ No newline at end of file +} From e8cac1f5985453a704b6c8dd2a687e880a9907f0 Mon Sep 17 00:00:00 2001 From: David Back Date: Fri, 15 Mar 2019 14:14:16 -0700 Subject: [PATCH 206/446] fix 2017 Unity versions --- .../Editor/AvatarExporter/AvatarExporter.cs | 11 ++++++----- tools/unity-avatar-exporter/Assets/README.txt | 2 +- .../avatarExporter.unitypackage | Bin 74623 -> 74591 bytes 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs index c5bc5eb84e..87f401d478 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs @@ -17,7 +17,7 @@ using System.Text.RegularExpressions; class AvatarExporter : MonoBehaviour { // update version number for every PR that changes this file, also set updated version in README file - static readonly string AVATAR_EXPORTER_VERSION = "0.3.5"; + static readonly string AVATAR_EXPORTER_VERSION = "0.3.6"; static readonly float HIPS_GROUND_MIN_Y = 0.01f; static readonly float HIPS_SPINE_CHEST_MIN_SEPARATION = 0.001f; @@ -56,6 +56,8 @@ class AvatarExporter : MonoBehaviour { "2018.1.0f2", "2017.4.18f1", "2017.4.17f1", + "2017.4.16f1", + "2017.4.15f1", }; static readonly Dictionary HUMANOID_TO_HIFI_JOINT_NAME = new Dictionary { @@ -1159,8 +1161,7 @@ class AvatarExporter : MonoBehaviour { static string GetMaterialTexture(Material material, string textureProperty) { // ensure the texture property name exists in this material and return its texture directory path if so - string[] textureNames = material.GetTexturePropertyNames(); - if (Array.IndexOf(textureNames, textureProperty) >= 0) { + if (material.HasProperty(textureProperty)) { Texture texture = material.GetTexture(textureProperty); if (texture) { foreach (var textureDependency in textureDependencies) { @@ -1214,10 +1215,10 @@ class AvatarExporter : MonoBehaviour { } if (unsupportedShaderMaterials.Count > 1) { warnings += "The materials " + unsupportedShaders + " are using an unsupported shader. " + - "Please change them to a Standard shader type.\n\n"; + "We recommend you change them to a Standard shader type.\n\n"; } else if (unsupportedShaderMaterials.Count == 1) { warnings += "The material " + unsupportedShaders + " is using an unsupported shader. " + - "Please change it to a Standard shader type.\n\n"; + "We recommend you change it to a Standard shader type.\n\n"; } } diff --git a/tools/unity-avatar-exporter/Assets/README.txt b/tools/unity-avatar-exporter/Assets/README.txt index 767c093800..5b228ebf75 100644 --- a/tools/unity-avatar-exporter/Assets/README.txt +++ b/tools/unity-avatar-exporter/Assets/README.txt @@ -1,6 +1,6 @@ High Fidelity, Inc. Avatar Exporter -Version 0.3.5 +Version 0.3.6 Note: It is recommended to use Unity versions between 2017.4.17f1 and 2018.2.12f1 for this Avatar Exporter. diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage index 3e2d6f2aed318abc6382f1a914ec3fb5e87dee69..05ad49baa6bfa1696cb12f659750246f2a6d2d9e 100644 GIT binary patch literal 74591 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

Ly1hzSwUsq8RmDStkK6pT6F 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

=)-SS*oWeSIvIm~V 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ફV}v>+vQ2ynK6RmQ**XvRgr;$h1}&DMk2nm!lYX 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 B?KS`a From 2300fe471d8909aaeb63b664cd095220075fdfb3 Mon Sep 17 00:00:00 2001 From: amantley Date: Thu, 14 Mar 2019 13:18:12 -0700 Subject: [PATCH 214/446] we now read frame zero of the animation to override the bind pose in fbxs that are before version 7500 --- libraries/fbx/src/FBXSerializer.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 52f4189bdb..8ff3005ddc 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -441,6 +441,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr QString hifiGlobalNodeID; unsigned int meshIndex = 0; haveReportedUnhandledRotationOrder = false; + int fbxVersionNumber = -1; foreach (const FBXNode& child, node.children) { if (child.name == "FBXHeaderExtension") { @@ -463,6 +464,9 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } } + } else if (object.name == "FBXVersion") { + fbxVersionNumber = object.properties.at(0).toInt(); + qCDebug(modelformat) << "the fbx version number " << fbxVersionNumber; } } } else if (child.name == "GlobalSettings") { @@ -1309,8 +1313,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr joint.bindTransformFoundInCluster = false; - hfmModel.joints.append(joint); - QString rotationID = localRotations.value(modelID); AnimationCurve xRotCurve = animationCurves.value(xComponents.value(rotationID)); AnimationCurve yRotCurve = animationCurves.value(yComponents.value(rotationID)); @@ -1333,7 +1335,13 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr xPosCurve.values.isEmpty() ? defaultPosValues.x : xPosCurve.values.at(i % xPosCurve.values.size()), yPosCurve.values.isEmpty() ? defaultPosValues.y : yPosCurve.values.at(i % yPosCurve.values.size()), zPosCurve.values.isEmpty() ? defaultPosValues.z : zPosCurve.values.at(i % zPosCurve.values.size())); + if ((fbxVersionNumber < 7500) && (i == 0)) { + joint.translation = hfmModel.animationFrames[i].translations[jointIndex]; + joint.rotation = hfmModel.animationFrames[i].rotations[jointIndex]; + } + } + hfmModel.joints.append(joint); } // NOTE: shapeVertices are in joint-frame From ca0379f6de206502722aa27f31137870fc762677 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 15 Mar 2019 17:24:50 -0700 Subject: [PATCH 215/446] Send InjectorGainSet packet to the audio-mixer --- libraries/networking/src/NodeList.cpp | 25 +++++++++++++++++++++++++ libraries/networking/src/NodeList.h | 4 ++++ 2 files changed, 29 insertions(+) diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index e6eb6087b0..eec710322e 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -265,6 +265,8 @@ void NodeList::reset(bool skipDomainHandlerReset) { _avatarGainMap.clear(); _avatarGainMapLock.unlock(); + _injectorGain = 0.0f; + if (!skipDomainHandlerReset) { // clear the domain connection information, unless they're the ones that asked us to reset _domainHandler.softReset(); @@ -1087,6 +1089,29 @@ float NodeList::getAvatarGain(const QUuid& nodeID) { return 0.0f; } +void NodeList::setInjectorGain(float gain) { + auto audioMixer = soloNodeOfType(NodeType::AudioMixer); + if (audioMixer) { + // setup the packet + auto setInjectorGainPacket = NLPacket::create(PacketType::InjectorGainSet, sizeof(float), true); + + // We need to convert the gain in dB (from the script) to an amplitude before packing it. + setInjectorGainPacket->writePrimitive(packFloatGainToByte(fastExp2f(gain / 6.02059991f))); + + qCDebug(networking) << "Sending Set Injector Gain packet with Gain:" << gain; + + sendPacket(std::move(setInjectorGainPacket), *audioMixer); + _injectorGain = gain; + + } else { + qWarning() << "Couldn't find audio mixer to send set gain request"; + } +} + +float NodeList::getInjectorGain() { + return _injectorGain; +} + void NodeList::kickNodeBySessionID(const QUuid& nodeID) { // send a request to domain-server to kick the node with the given session ID // the domain-server will handle the persistence of the kick (via username or IP) diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index e135bc937d..d2a1212d64 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -83,6 +83,8 @@ public: bool isPersonalMutingNode(const QUuid& nodeID) const; void setAvatarGain(const QUuid& nodeID, float gain); float getAvatarGain(const QUuid& nodeID); + void setInjectorGain(float gain); + float getInjectorGain(); void kickNodeBySessionID(const QUuid& nodeID); void muteNodeBySessionID(const QUuid& nodeID); @@ -181,6 +183,8 @@ private: mutable QReadWriteLock _avatarGainMapLock; tbb::concurrent_unordered_map _avatarGainMap; + std::atomic _injectorGain { 0.0f }; + void sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode); #if defined(Q_OS_ANDROID) Setting::Handle _ignoreRadiusEnabled { "IgnoreRadiusEnabled", false }; From b5a45be7b986995e8be2e5f1fffe071331b3e514 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Fri, 15 Mar 2019 01:08:53 +0100 Subject: [PATCH 216/446] avatar doctor documentation urls --- interface/src/avatar/AvatarDoctor.cpp | 65 ++++++++++++++------------- interface/src/avatar/AvatarDoctor.h | 2 + 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/interface/src/avatar/AvatarDoctor.cpp b/interface/src/avatar/AvatarDoctor.cpp index 04a426c3db..43e50ea049 100644 --- a/interface/src/avatar/AvatarDoctor.cpp +++ b/interface/src/avatar/AvatarDoctor.cpp @@ -55,7 +55,7 @@ static QStringList HAND_MAPPING_SUFFIXES = { "HandThumb1", }; -const QUrl DEFAULT_DOCS_URL = QUrl("https://docs.highfidelity.com/create/avatars/create-avatars.html#create-your-own-avatar"); +const QUrl PACKAGE_AVATAR_DOCS_BASE_URL = QUrl("https://docs.highfidelity.com/create/avatars/package-avatar.html"); AvatarDoctor::AvatarDoctor(const QUrl& avatarFSTFileUrl) : _avatarFSTFileUrl(avatarFSTFileUrl) { @@ -85,7 +85,7 @@ void AvatarDoctor::startDiagnosing() { const auto resourceLoaded = [this, resource](bool success) { // MODEL if (!success) { - _errors.push_back({ "Model file cannot be opened.", DEFAULT_DOCS_URL }); + addError("Model file cannot be opened.", "missing-file"); emit complete(getErrors()); return; } @@ -93,45 +93,45 @@ void AvatarDoctor::startDiagnosing() { const auto model = resource.data(); const auto avatarModel = resource.data()->getHFMModel(); if (!avatarModel.originalURL.endsWith(".fbx")) { - _errors.push_back({ "Unsupported avatar model format.", DEFAULT_DOCS_URL }); + addError("Unsupported avatar model format.", "unsupported-format"); emit complete(getErrors()); return; } // RIG if (avatarModel.joints.isEmpty()) { - _errors.push_back({ "Avatar has no rig.", DEFAULT_DOCS_URL }); + addError("Avatar has no rig.", "no-rig"); } else { auto jointNames = avatarModel.getJointNames(); if (avatarModel.joints.length() > NETWORKED_JOINTS_LIMIT) { - _errors.push_back({tr( "Avatar has over %n bones.", "", NETWORKED_JOINTS_LIMIT), DEFAULT_DOCS_URL }); + addError(tr( "Avatar has over %n bones.", "", NETWORKED_JOINTS_LIMIT), "maximum-bone-limit"); } // Avatar does not have Hips bone mapped if (!jointNames.contains("Hips")) { - _errors.push_back({ "Hips are not mapped.", DEFAULT_DOCS_URL }); + addError("Hips are not mapped.", "hips-not-mapped"); } if (!jointNames.contains("Spine")) { - _errors.push_back({ "Spine is not mapped.", DEFAULT_DOCS_URL }); + addError("Spine is not mapped.", "spine-not-mapped"); } if (!jointNames.contains("Spine1")) { - _errors.push_back({ "Chest (Spine1) is not mapped.", DEFAULT_DOCS_URL }); + addError("Chest (Spine1) is not mapped.", "chest-not-mapped"); } if (!jointNames.contains("Neck")) { - _errors.push_back({ "Neck is not mapped.", DEFAULT_DOCS_URL }); + addError("Neck is not mapped.", "neck-not-mapped"); } if (!jointNames.contains("Head")) { - _errors.push_back({ "Head is not mapped.", DEFAULT_DOCS_URL }); + addError("Head is not mapped.", "head-not-mapped"); } if (!jointNames.contains("LeftEye")) { if (jointNames.contains("RightEye")) { - _errors.push_back({ "LeftEye is not mapped.", DEFAULT_DOCS_URL }); + addError("LeftEye is not mapped.", "eye-not-mapped"); } else { - _errors.push_back({ "Eyes are not mapped.", DEFAULT_DOCS_URL }); + addError("Eyes are not mapped.", "eye-not-mapped"); } } else if (!jointNames.contains("RightEye")) { - _errors.push_back({ "RightEye is not mapped.", DEFAULT_DOCS_URL }); + addError("RightEye is not mapped.", "eye-not-mapped"); } const auto checkJointAsymmetry = [jointNames] (const QStringList& jointMappingSuffixes) { @@ -159,13 +159,13 @@ void AvatarDoctor::startDiagnosing() { }; if (checkJointAsymmetry(ARM_MAPPING_SUFFIXES)) { - _errors.push_back({ "Asymmetrical arm bones.", DEFAULT_DOCS_URL }); + addError("Asymmetrical arm bones.", "asymmetrical-bones"); } if (checkJointAsymmetry(HAND_MAPPING_SUFFIXES)) { - _errors.push_back({ "Asymmetrical hand bones.", DEFAULT_DOCS_URL }); + addError("Asymmetrical hand bones.", "asymmetrical-bones"); } if (checkJointAsymmetry(LEG_MAPPING_SUFFIXES)) { - _errors.push_back({ "Asymmetrical leg bones.", DEFAULT_DOCS_URL }); + addError("Asymmetrical leg bones.", "asymmetrical-bones"); } // Multiple skeleton root joints checkup @@ -177,7 +177,7 @@ void AvatarDoctor::startDiagnosing() { } if (skeletonRootJoints > 1) { - _errors.push_back({ "Multiple top-level joints found.", DEFAULT_DOCS_URL }); + addError("Multiple top-level joints found.", "multiple-top-level-joints"); } Rig rig; @@ -191,9 +191,9 @@ void AvatarDoctor::startDiagnosing() { const float RECOMMENDED_MAX_HEIGHT = DEFAULT_AVATAR_HEIGHT * 1.5f; if (avatarHeight < RECOMMENDED_MIN_HEIGHT) { - _errors.push_back({ "Avatar is possibly too short.", DEFAULT_DOCS_URL }); + addError("Avatar is possibly too short.", "short-avatar"); } else if (avatarHeight > RECOMMENDED_MAX_HEIGHT) { - _errors.push_back({ "Avatar is possibly too tall.", DEFAULT_DOCS_URL }); + addError("Avatar is possibly too tall.", "tall-avatar"); } // HipsNotOnGround @@ -204,7 +204,7 @@ void AvatarDoctor::startDiagnosing() { const auto hipJoint = avatarModel.joints.at(avatarModel.getJointIndex("Hips")); if (hipsPosition.y < HIPS_GROUND_MIN_Y) { - _errors.push_back({ "Hips are on ground.", DEFAULT_DOCS_URL }); + addError("Hips are on ground.", "hips-on-ground"); } } } @@ -223,7 +223,7 @@ void AvatarDoctor::startDiagnosing() { const auto hipsToSpine = glm::length(hipsPosition - spinePosition); const auto spineToChest = glm::length(spinePosition - chestPosition); if (hipsToSpine < HIPS_SPINE_CHEST_MIN_SEPARATION && spineToChest < HIPS_SPINE_CHEST_MIN_SEPARATION) { - _errors.push_back({ "Hips/Spine/Chest overlap.", DEFAULT_DOCS_URL }); + addError("Hips/Spine/Chest overlap.", "overlap-error"); } } } @@ -240,21 +240,21 @@ void AvatarDoctor::startDiagnosing() { const auto& uniqueJointValues = jointValues.toSet(); for (const auto& jointName: uniqueJointValues) { if (jointValues.count(jointName) > 1) { - _errors.push_back({ tr("%1 is mapped multiple times.").arg(jointName), DEFAULT_DOCS_URL }); + addError(tr("%1 is mapped multiple times.").arg(jointName), "mapped-multiple-times"); } } } if (!isDescendantOfJointWhenJointsExist("Spine", "Hips")) { - _errors.push_back({ "Spine is not a child of Hips.", DEFAULT_DOCS_URL }); + addError("Spine is not a child of Hips.", "spine-not-child"); } if (!isDescendantOfJointWhenJointsExist("Spine1", "Spine")) { - _errors.push_back({ "Spine1 is not a child of Spine.", DEFAULT_DOCS_URL }); + addError("Spine1 is not a child of Spine.", "spine1-not-child"); } if (!isDescendantOfJointWhenJointsExist("Head", "Spine1")) { - _errors.push_back({ "Head is not a child of Spine1.", DEFAULT_DOCS_URL }); + addError("Head is not a child of Spine1.", "head-not-child"); } } @@ -300,7 +300,7 @@ void AvatarDoctor::startDiagnosing() { connect(resource.data(), &GeometryResource::finished, this, resourceLoaded); } } else { - _errors.push_back({ "Model file cannot be opened", DEFAULT_DOCS_URL }); + addError("Model file cannot be opened", "missing-file"); emit complete(getErrors()); } } @@ -345,7 +345,7 @@ void AvatarDoctor::diagnoseTextures() { QUrl(avatarModel.originalURL)).resolved(QUrl("textures")); if (texturesFound == 0) { - _errors.push_back({ tr("No textures assigned."), DEFAULT_DOCS_URL }); + addError(tr("No textures assigned."), "no-textures-assigned"); } if (!externalTextures.empty()) { @@ -356,11 +356,10 @@ void AvatarDoctor::diagnoseTextures() { auto checkTextureLoadingComplete = [this]() mutable { if (_checkedTextureCount == _externalTextureCount) { if (_missingTextureCount > 0) { - _errors.push_back({ tr("Missing %n texture(s).","", _missingTextureCount), DEFAULT_DOCS_URL }); + addError(tr("Missing %n texture(s).","", _missingTextureCount), "missing-textures"); } if (_unsupportedTextureCount > 0) { - _errors.push_back({ tr("%n unsupported texture(s) found.", "", _unsupportedTextureCount), - DEFAULT_DOCS_URL }); + addError(tr("%n unsupported texture(s) found.", "", _unsupportedTextureCount), "unsupported-textures"); } emit complete(getErrors()); @@ -411,6 +410,12 @@ void AvatarDoctor::diagnoseTextures() { } } +void AvatarDoctor::addError(const QString& errorMessage, const QString& docFragment) { + QUrl documentationURL = PACKAGE_AVATAR_DOCS_BASE_URL; + documentationURL.setFragment(docFragment); + _errors.push_back({ errorMessage, documentationURL }); +} + QVariantList AvatarDoctor::getErrors() const { QVariantList result; for (const auto& error : _errors) { diff --git a/interface/src/avatar/AvatarDoctor.h b/interface/src/avatar/AvatarDoctor.h index 780f600bed..1465a5defc 100644 --- a/interface/src/avatar/AvatarDoctor.h +++ b/interface/src/avatar/AvatarDoctor.h @@ -40,6 +40,8 @@ signals: private: void diagnoseTextures(); + void addError(const QString& errorMessage, const QString& docFragment); + QUrl _avatarFSTFileUrl; QVector _errors; From 714115adef8b485f95872c3a409f0c1e44b8abaf Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Fri, 15 Mar 2019 21:36:46 +0100 Subject: [PATCH 217/446] - Video, and new Docs url in QML --- .../hifi/avatarPackager/AvatarPackagerApp.qml | 11 ++++++++-- .../avatarPackager/AvatarPackagerHeader.qml | 20 ++++++++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml index 8afc60fd90..278ce36362 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml @@ -133,7 +133,7 @@ Item { states: [ State { name: AvatarPackagerState.main - PropertyChanges { target: avatarPackagerHeader; title: qsTr("Avatar Packager"); docsEnabled: true; backButtonVisible: false } + PropertyChanges { target: avatarPackagerHeader; title: qsTr("Avatar Packager"); docsEnabled: true; videoEnabled: true; backButtonVisible: false } PropertyChanges { target: avatarPackagerMain; visible: true } PropertyChanges { target: avatarPackagerFooter; content: avatarPackagerMain.footer } }, @@ -229,7 +229,11 @@ Item { } function openDocs() { - Qt.openUrlExternally("https://docs.highfidelity.com/create/avatars/create-avatars#how-to-package-your-avatar"); + Qt.openUrlExternally("https://docs.highfidelity.com/create/avatars/package-avatar.html"); + } + + function openVideo() { + Qt.openUrlExternally("https://youtu.be/zrkEowu_yps"); } AvatarPackagerHeader { @@ -243,6 +247,9 @@ Item { onDocsButtonClicked: { avatarPackager.openDocs(); } + onVideoButtonClicked: { + avatarPackager.openVideo(); + } } Item { diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml index 25201bf81e..31528a8557 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml @@ -13,6 +13,7 @@ ShadowRectangle { property string title: qsTr("Avatar Packager") property alias docsEnabled: docs.visible + property alias videoEnabled: video.visible property bool backButtonVisible: true // If false, is not visible and does not take up space property bool backButtonEnabled: true // If false, is not visible but does not affect space property bool canRename: false @@ -24,6 +25,7 @@ ShadowRectangle { signal backButtonClicked signal docsButtonClicked + signal videoButtonClicked RalewayButton { id: back @@ -126,6 +128,20 @@ ShadowRectangle { } } + RalewayButton { + id: video + visible: false + size: 28 + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: docs.left + anchors.rightMargin: 16 + + text: qsTr("Video") + + onClicked: videoButtonClicked() + } + RalewayButton { id: docs visible: false @@ -137,8 +153,6 @@ ShadowRectangle { text: qsTr("Docs") - onClicked: { - docsButtonClicked(); - } + onClicked: docsButtonClicked() } } From 5f3e31b119bd5ad6feda0fa6590254b1a090def2 Mon Sep 17 00:00:00 2001 From: danteruiz Date: Fri, 15 Mar 2019 18:15:18 -0700 Subject: [PATCH 218/446] add ui to kick api --- .../qml/dialogs/TabletMessageBox.qml | 2 +- .../resources/qml/hifi/tablet/TabletRoot.qml | 1 - libraries/script-engine/CMakeLists.txt | 2 +- .../src/UsersScriptingInterface.cpp | 41 ++++++++++++++++++- .../src/UsersScriptingInterface.h | 4 ++ libraries/ui/src/OffscreenUi.cpp | 6 +++ libraries/ui/src/OffscreenUi.h | 5 ++- .../ui/src/ui/TabletScriptingInterface.cpp | 1 + 8 files changed, 56 insertions(+), 6 deletions(-) diff --git a/interface/resources/qml/dialogs/TabletMessageBox.qml b/interface/resources/qml/dialogs/TabletMessageBox.qml index 1e6f0734ad..4411651a0f 100644 --- a/interface/resources/qml/dialogs/TabletMessageBox.qml +++ b/interface/resources/qml/dialogs/TabletMessageBox.qml @@ -28,7 +28,7 @@ TabletModalWindow { id: mouse; anchors.fill: parent } - + function click(button) { clickedButton = button; selected(button); diff --git a/interface/resources/qml/hifi/tablet/TabletRoot.qml b/interface/resources/qml/hifi/tablet/TabletRoot.qml index 8d237d146a..5559c36fd1 100644 --- a/interface/resources/qml/hifi/tablet/TabletRoot.qml +++ b/interface/resources/qml/hifi/tablet/TabletRoot.qml @@ -117,7 +117,6 @@ Rectangle { if (loader.item.hasOwnProperty("gotoPreviousApp")) { loader.item.gotoPreviousApp = true; } - screenChanged("Web", url) }); } diff --git a/libraries/script-engine/CMakeLists.txt b/libraries/script-engine/CMakeLists.txt index 82c408f386..e3eb8684d1 100644 --- a/libraries/script-engine/CMakeLists.txt +++ b/libraries/script-engine/CMakeLists.txt @@ -17,6 +17,6 @@ if (NOT ANDROID) endif () -link_hifi_libraries(shared networking octree shaders gpu procedural graphics material-networking model-networking ktx recording avatars fbx hfm entities controllers animation audio physics image midi) +link_hifi_libraries(shared networking octree shaders gpu procedural graphics material-networking model-networking ktx recording avatars fbx hfm entities controllers animation audio physics image midi ui qml) # ui includes gl, but link_hifi_libraries does not use transitive includes, so gl must be explicit include_hifi_library_headers(gl) diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp index fef11c12e9..631f0eb743 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.cpp +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -12,6 +12,8 @@ #include "UsersScriptingInterface.h" #include +#include +#include UsersScriptingInterface::UsersScriptingInterface() { // emit a signal when kick permissions have changed @@ -52,8 +54,43 @@ float UsersScriptingInterface::getAvatarGain(const QUuid& nodeID) { } void UsersScriptingInterface::kick(const QUuid& nodeID) { - // ask the NodeList to kick the user with the given session ID - DependencyManager::get()->kickNodeBySessionID(nodeID); + bool waitingForKickResponse = _kickResponseLock.resultWithReadLock([&] { return _waitingForKickResponse; }); + if (getCanKick() && !waitingForKickResponse) { + + + auto avatarHashMap = DependencyManager::get(); + auto avatar = avatarHashMap->getAvatarBySessionID(nodeID); + + QString userName; + + if (avatar) { + userName = avatar->getSessionDisplayName(); + } else { + userName = nodeID.toString(); + } + + QString kickMessage = "Do you wish to kick " + userName + " from your domain"; + ModalDialogListener* dlg = OffscreenUi::asyncQuestion("Kick User", kickMessage, + QMessageBox::Yes | QMessageBox::No); + + if (dlg->getDialogItem()) { + + QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { + QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); + + bool yes = (static_cast(answer.toInt()) == QMessageBox::Yes); + // ask the NodeList to kick the user with the given session ID + + if (yes) { + DependencyManager::get()->kickNodeBySessionID(nodeID); + } + + _kickResponseLock.withWriteLock([&] { _waitingForKickResponse = false; }); + }); + + _kickResponseLock.withWriteLock([&] { _waitingForKickResponse = true; }); + } + } } void UsersScriptingInterface::mute(const QUuid& nodeID) { diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index 57de205066..0e3f9be0e0 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -15,6 +15,7 @@ #define hifi_UsersScriptingInterface_h #include +#include /**jsdoc * @namespace Users @@ -195,6 +196,9 @@ signals: private: bool getRequestsDomainListData(); void setRequestsDomainListData(bool requests); + + ReadWriteLockable _kickResponseLock; + bool _waitingForKickResponse { false }; }; diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 137cffde94..2f2d38fe2a 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -240,6 +240,12 @@ class MessageBoxListener : public ModalDialogListener { return static_cast(_result.toInt()); } +protected slots: + virtual void onDestroyed() override { + ModalDialogListener::onDestroyed(); + onSelected(QMessageBox::NoButton); + } + private slots: void onSelected(int button) { _result = button; diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index 46dbdbdf13..6abbc486d0 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -34,6 +34,9 @@ class ModalDialogListener : public QObject { Q_OBJECT friend class OffscreenUi; +public: + QQuickItem* getDialogItem() { return _dialog; }; + protected: ModalDialogListener(QQuickItem* dialog); virtual ~ModalDialogListener(); @@ -43,7 +46,7 @@ signals: void response(const QVariant& value); protected slots: - void onDestroyed(); + virtual void onDestroyed(); protected: QQuickItem* _dialog; diff --git a/libraries/ui/src/ui/TabletScriptingInterface.cpp b/libraries/ui/src/ui/TabletScriptingInterface.cpp index 7a1c37af33..bddb306dca 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.cpp +++ b/libraries/ui/src/ui/TabletScriptingInterface.cpp @@ -368,6 +368,7 @@ void TabletProxy::setToolbarMode(bool toolbarMode) { if (toolbarMode) { #if !defined(DISABLE_QML) + closeDialog(); // create new desktop window auto tabletRootWindow = new TabletRootWindow(); tabletRootWindow->initQml(QVariantMap()); From 2ab8eb98e8ee5c945f8a98b03fc7aa17f28dda77 Mon Sep 17 00:00:00 2001 From: danteruiz Date: Sun, 17 Mar 2019 14:00:41 -0700 Subject: [PATCH 219/446] better implementation --- interface/src/Application.cpp | 36 ++++++++++++++++ interface/src/Application.h | 1 + libraries/script-engine/CMakeLists.txt | 2 +- .../src/UsersScriptingInterface.cpp | 42 +++---------------- .../src/UsersScriptingInterface.h | 8 ++++ 5 files changed, 52 insertions(+), 37 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index de4a6bb167..581b260751 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2342,6 +2342,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo return viewFrustum.getPosition(); }); + DependencyManager::get()->setKickConfirmationOperator([this] (const QUuid& nodeID) { userKickConfirmation(nodeID); }); + render::entities::WebEntityRenderer::setAcquireWebSurfaceOperator([this](const QString& url, bool htmlContent, QSharedPointer& webSurface, bool& cachedWebSurface) { bool isTablet = url == TabletScriptingInterface::QML; if (htmlContent) { @@ -3287,6 +3289,40 @@ void Application::onDesktopRootItemCreated(QQuickItem* rootItem) { #endif } +void Application::userKickConfirmation(const QUuid& nodeID) { + auto avatarHashMap = DependencyManager::get(); + auto avatar = avatarHashMap->getAvatarBySessionID(nodeID); + + QString userName; + + if (avatar) { + userName = avatar->getSessionDisplayName(); + } else { + userName = nodeID.toString(); + } + + QString kickMessage = "Do you wish to kick " + userName + " from your domain"; + ModalDialogListener* dlg = OffscreenUi::asyncQuestion("Kick User", kickMessage, + QMessageBox::Yes | QMessageBox::No); + + if (dlg->getDialogItem()) { + + QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { + QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); + + bool yes = (static_cast(answer.toInt()) == QMessageBox::Yes); + // ask the NodeList to kick the user with the given session ID + + if (yes) { + DependencyManager::get()->kickNodeBySessionID(nodeID); + } + + DependencyManager::get()->setWaitForKickResponse(false); + }); + DependencyManager::get()->setWaitForKickResponse(true); + } +} + void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditionalContextProperties) { surfaceContext->setContextProperty("Users", DependencyManager::get().data()); surfaceContext->setContextProperty("HMD", DependencyManager::get().data()); diff --git a/interface/src/Application.h b/interface/src/Application.h index a8cc9450c5..762ac9585a 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -593,6 +593,7 @@ private: void toggleTabletUI(bool shouldOpen = false) const; static void setupQmlSurface(QQmlContext* surfaceContext, bool setAdditionalContextProperties); + void userKickConfirmation(const QUuid& nodeID); MainWindow* _window; QElapsedTimer& _sessionRunTimer; diff --git a/libraries/script-engine/CMakeLists.txt b/libraries/script-engine/CMakeLists.txt index e3eb8684d1..82c408f386 100644 --- a/libraries/script-engine/CMakeLists.txt +++ b/libraries/script-engine/CMakeLists.txt @@ -17,6 +17,6 @@ if (NOT ANDROID) endif () -link_hifi_libraries(shared networking octree shaders gpu procedural graphics material-networking model-networking ktx recording avatars fbx hfm entities controllers animation audio physics image midi ui qml) +link_hifi_libraries(shared networking octree shaders gpu procedural graphics material-networking model-networking ktx recording avatars fbx hfm entities controllers animation audio physics image midi) # ui includes gl, but link_hifi_libraries does not use transitive includes, so gl must be explicit include_hifi_library_headers(gl) diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp index 631f0eb743..9beb52f20a 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.cpp +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -12,8 +12,6 @@ #include "UsersScriptingInterface.h" #include -#include -#include UsersScriptingInterface::UsersScriptingInterface() { // emit a signal when kick permissions have changed @@ -54,42 +52,14 @@ float UsersScriptingInterface::getAvatarGain(const QUuid& nodeID) { } void UsersScriptingInterface::kick(const QUuid& nodeID) { - bool waitingForKickResponse = _kickResponseLock.resultWithReadLock([&] { return _waitingForKickResponse; }); - if (getCanKick() && !waitingForKickResponse) { - - auto avatarHashMap = DependencyManager::get(); - auto avatar = avatarHashMap->getAvatarBySessionID(nodeID); - - QString userName; - - if (avatar) { - userName = avatar->getSessionDisplayName(); - } else { - userName = nodeID.toString(); - } - - QString kickMessage = "Do you wish to kick " + userName + " from your domain"; - ModalDialogListener* dlg = OffscreenUi::asyncQuestion("Kick User", kickMessage, - QMessageBox::Yes | QMessageBox::No); - - if (dlg->getDialogItem()) { - - QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { - QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); - - bool yes = (static_cast(answer.toInt()) == QMessageBox::Yes); - // ask the NodeList to kick the user with the given session ID - - if (yes) { - DependencyManager::get()->kickNodeBySessionID(nodeID); - } - - _kickResponseLock.withWriteLock([&] { _waitingForKickResponse = false; }); - }); - - _kickResponseLock.withWriteLock([&] { _waitingForKickResponse = true; }); + if (_kickConfirmationOperator) { + bool waitingForKickResponse = _kickResponseLock.resultWithReadLock([&] { return _waitingForKickResponse; }); + if (getCanKick() && !waitingForKickResponse) { + _kickConfirmationOperator(nodeID); } + } else { + DependencyManager::get()->kickNodeBySessionID(nodeID); } } diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index 0e3f9be0e0..f8ca974b8b 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -39,6 +39,12 @@ class UsersScriptingInterface : public QObject, public Dependency { public: UsersScriptingInterface(); + void setKickConfirmationOperator(std::function kickConfirmationOperator) { + _kickConfirmationOperator = kickConfirmationOperator; + } + + bool getWaitForKickResponse() { return _kickResponseLock.resultWithReadLock([&] { return _waitingForKickResponse; }); } + void setWaitForKickResponse(bool waitForKickResponse) { _kickResponseLock.withWriteLock([&] { _waitingForKickResponse = waitForKickResponse; }); } public slots: @@ -197,6 +203,8 @@ private: bool getRequestsDomainListData(); void setRequestsDomainListData(bool requests); + std::function _kickConfirmationOperator; + ReadWriteLockable _kickResponseLock; bool _waitingForKickResponse { false }; }; From db22fa9eec728540f6f6ed32d3b52d1f9fd18bea Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 18 Mar 2019 09:22:06 -0700 Subject: [PATCH 220/446] show up audio screen using Settings > Audio --- .../resources/qml/hifi/dialogs/Audio.qml | 27 ------------------- interface/src/Menu.cpp | 10 ++++--- 2 files changed, 7 insertions(+), 30 deletions(-) delete mode 100644 interface/resources/qml/hifi/dialogs/Audio.qml diff --git a/interface/resources/qml/hifi/dialogs/Audio.qml b/interface/resources/qml/hifi/dialogs/Audio.qml deleted file mode 100644 index 4ce9e14c42..0000000000 --- a/interface/resources/qml/hifi/dialogs/Audio.qml +++ /dev/null @@ -1,27 +0,0 @@ -// -// Audio.qml -// -// Created by Zach Pomerantz on 6/12/2017 -// Copyright 2017 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 "../../windows" -import "../audio" - -ScrollingWindow { - id: root; - - resizable: true; - destroyOnHidden: true; - width: 400; - height: 577; - minSize: Qt.vector2d(400, 500); - - Audio { id: audio; width: root.width } - - objectName: "AudioDialog"; - title: audio.title; -} diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 810e21daf5..394c07e842 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -270,10 +270,14 @@ Menu::Menu() { // Settings > Audio... action = addActionToQMenuAndActionHash(settingsMenu, "Audio..."); connect(action, &QAction::triggered, [] { - static const QUrl widgetUrl("hifi/dialogs/Audio.qml"); static const QUrl tabletUrl("hifi/audio/Audio.qml"); - static const QString name("AudioDialog"); - qApp->showDialog(widgetUrl, tabletUrl, name); + auto tablet = DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system"); + auto hmd = DependencyManager::get(); + tablet->pushOntoStack(tabletUrl); + + if (!hmd->getShouldShowTablet()) { + hmd->toggleShouldShowTablet(); + } }); // Settings > Graphics... From 6ed4937dc0f627074bb533d5b5c1d938a03a599c Mon Sep 17 00:00:00 2001 From: milad Date: Mon, 18 Mar 2019 10:12:32 -0700 Subject: [PATCH 221/446] Updated engine code to not emit if dominant hand isn't actually changed --- interface/src/avatar/MyAvatar.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 02ef91cdba..9813070308 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -324,8 +324,11 @@ QString MyAvatar::getDominantHand() const { void MyAvatar::setDominantHand(const QString& hand) { if (hand == DOMINANT_LEFT_HAND || hand == DOMINANT_RIGHT_HAND) { - _dominantHand.set(hand); - emit dominantHandChanged(hand); + bool changed = (hand != _dominantHand.get()); + if (changed) { + _dominantHand.set(hand); + emit dominantHandChanged(hand); + } } } From fe28eaca7cca368c481eabb6b5ad8dd5a3af9b60 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Mon, 18 Mar 2019 11:23:48 -0700 Subject: [PATCH 222/446] fix typo --- interface/resources/qml/hifi/NameCard.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index c92afe9e14..4e578f8274 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -96,7 +96,7 @@ Item { enabled: (selected && activeTab == "nearbyTab") || isMyCard; hoverEnabled: enabled onClicked: { - if (Phas3DHTML) { + if (has3DHTML) { userInfoViewer.url = Account.metaverseServerURL + "/users/" + userName; userInfoViewer.visible = true; } From ea501331464bf10ebfeaf85f75e7cad263cdb8c1 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 18 Mar 2019 12:05:17 -0700 Subject: [PATCH 223/446] working on adding particle shape types --- .../src/RenderableModelEntityItem.cpp | 4 - .../RenderableParticleEffectEntityItem.cpp | 117 ++++++++++++------ .../src/RenderableParticleEffectEntityItem.h | 10 +- .../entities/src/EntityItemProperties.cpp | 26 ++-- .../entities/src/ParticleEffectEntityItem.cpp | 23 ++++ .../entities/src/ParticleEffectEntityItem.h | 9 +- libraries/entities/src/ZoneEntityItem.cpp | 4 - 7 files changed, 138 insertions(+), 55 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 643e5afb70..2fdffde8a3 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -307,10 +307,6 @@ void RenderableModelEntityItem::setShapeType(ShapeType type) { } void RenderableModelEntityItem::setCompoundShapeURL(const QString& url) { - // because the caching system only allows one Geometry per url, and because this url might also be used - // as a visual model, we need to change this url in some way. We add a "collision-hull" query-arg so it - // will end up in a different hash-key in ResourceCache. TODO: It would be better to use the same URL and - // parse it twice. auto currentCompoundShapeURL = getCompoundShapeURL(); ModelEntityItem::setCompoundShapeURL(url); if (getCompoundShapeURL() != currentCompoundShapeURL || !getModel()) { diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp index c139fbf320..2168347554 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp @@ -9,13 +9,11 @@ // #include "RenderableParticleEffectEntityItem.h" - #include #include #include - using namespace render; using namespace render::entities; @@ -79,6 +77,14 @@ bool ParticleEffectEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedE return true; } + if (_shapeType != entity->getShapeType()) { + return true; + } + + if (_compoundShapeURL != entity->getCompoundShapeURL()) { + return true; + } + return false; } @@ -87,11 +93,17 @@ void ParticleEffectEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePoi if (!newParticleProperties.valid()) { qCWarning(entitiesrenderer) << "Bad particle properties"; } - - if (resultWithReadLock([&]{ return _particleProperties != newParticleProperties; })) { + + if (resultWithReadLock([&] { return _particleProperties != newParticleProperties; })) { _timeUntilNextEmit = 0; - withWriteLock([&]{ + withWriteLock([&] { _particleProperties = newParticleProperties; + _shapeType = entity->getShapeType(); + QString compoundShapeURL = entity->getCompoundShapeURL(); + if (_compoundShapeURL != compoundShapeURL) { + _compoundShapeURL = compoundShapeURL; + fetchGeometryResource(); + } if (!_prevEmitterShouldTrailInitialized) { _prevEmitterShouldTrailInitialized = true; _prevEmitterShouldTrail = _particleProperties.emission.shouldTrail; @@ -104,10 +116,10 @@ void ParticleEffectEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePoi }); _emitting = entity->getIsEmitting(); - bool textureEmpty = resultWithReadLock([&]{ return _particleProperties.textures.isEmpty(); }); + bool textureEmpty = resultWithReadLock([&] { return _particleProperties.textures.isEmpty(); }); if (textureEmpty) { if (_networkTexture) { - withWriteLock([&] { + withWriteLock([&] { _networkTexture.reset(); }); } @@ -116,11 +128,11 @@ void ParticleEffectEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePoi entity->setVisuallyReady(true); }); } else { - bool textureNeedsUpdate = resultWithReadLock([&]{ + bool textureNeedsUpdate = resultWithReadLock([&] { return !_networkTexture || _networkTexture->getURL() != QUrl(_particleProperties.textures); }); if (textureNeedsUpdate) { - withWriteLock([&] { + withWriteLock([&] { _networkTexture = DependencyManager::get()->getTexture(_particleProperties.textures); }); } @@ -144,7 +156,7 @@ void ParticleEffectEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePoi void ParticleEffectEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) { // Fill in Uniforms structure ParticleUniforms particleUniforms; - withReadLock([&]{ + withReadLock([&] { particleUniforms.radius.start = _particleProperties.radius.range.start; particleUniforms.radius.middle = _particleProperties.radius.gradient.target; particleUniforms.radius.finish = _particleProperties.radius.range.finish; @@ -183,7 +195,8 @@ Item::Bound ParticleEffectEntityRenderer::getBound() { static const size_t VERTEX_PER_PARTICLE = 4; -ParticleEffectEntityRenderer::CpuParticle ParticleEffectEntityRenderer::createParticle(uint64_t now, const Transform& baseTransform, const particle::Properties& particleProperties) { +ParticleEffectEntityRenderer::CpuParticle ParticleEffectEntityRenderer::createParticle(uint64_t now, const Transform& baseTransform, const particle::Properties& particleProperties, + const ShapeType& shapeType, const GeometryResource::Pointer& geometryResource) { CpuParticle particle; const auto& accelerationSpread = particleProperties.emission.acceleration.spread; @@ -221,33 +234,53 @@ ParticleEffectEntityRenderer::CpuParticle ParticleEffectEntityRenderer::createPa float azimuth; if (azimuthFinish >= azimuthStart) { - azimuth = azimuthStart + (azimuthFinish - azimuthStart) * randFloat(); + azimuth = azimuthStart + (azimuthFinish - azimuthStart) * randFloat(); } else { azimuth = azimuthStart + (TWO_PI + azimuthFinish - azimuthStart) * randFloat(); } - if (emitDimensions == Vectors::ZERO) { + if (emitDimensions == Vectors::ZERO || shapeType == ShapeType::SHAPE_TYPE_NONE) { // Point emitDirection = glm::quat(glm::vec3(PI_OVER_TWO - elevation, 0.0f, azimuth)) * Vectors::UNIT_Z; } else { - // Ellipsoid - float radiusScale = 1.0f; - if (emitRadiusStart < 1.0f) { - float randRadius = - emitRadiusStart + randFloatInRange(0.0f, particle::MAXIMUM_EMIT_RADIUS_START - emitRadiusStart); - radiusScale = 1.0f - std::pow(1.0f - randRadius, 3.0f); + glm::vec3 emitPosition; + switch (shapeType) { + case ShapeType::SHAPE_TYPE_BOX: + + case ShapeType::SHAPE_TYPE_CAPSULE_X: + case ShapeType::SHAPE_TYPE_CAPSULE_Y: + case ShapeType::SHAPE_TYPE_CAPSULE_Z: + + case ShapeType::SHAPE_TYPE_CYLINDER_X: + case ShapeType::SHAPE_TYPE_CYLINDER_Y: + case ShapeType::SHAPE_TYPE_CYLINDER_Z: + + case ShapeType::SHAPE_TYPE_CIRCLE: + case ShapeType::SHAPE_TYPE_PLANE: + + case ShapeType::SHAPE_TYPE_COMPOUND: + + case ShapeType::SHAPE_TYPE_SPHERE: + case ShapeType::SHAPE_TYPE_ELLIPSOID: + default: { + float radiusScale = 1.0f; + if (emitRadiusStart < 1.0f) { + float randRadius = + emitRadiusStart + randFloatInRange(0.0f, particle::MAXIMUM_EMIT_RADIUS_START - emitRadiusStart); + radiusScale = 1.0f - std::pow(1.0f - randRadius, 3.0f); + } + + glm::vec3 radii = radiusScale * 0.5f * emitDimensions; + float x = radii.x * glm::cos(elevation) * glm::cos(azimuth); + float y = radii.y * glm::cos(elevation) * glm::sin(azimuth); + float z = radii.z * glm::sin(elevation); + emitPosition = glm::vec3(x, y, z); + emitDirection = glm::normalize(glm::vec3(radii.x > 0.0f ? x / (radii.x * radii.x) : 0.0f, + radii.y > 0.0f ? y / (radii.y * radii.y) : 0.0f, + radii.z > 0.0f ? z / (radii.z * radii.z) : 0.0f)); + } } - glm::vec3 radii = radiusScale * 0.5f * emitDimensions; - float x = radii.x * glm::cos(elevation) * glm::cos(azimuth); - float y = radii.y * glm::cos(elevation) * glm::sin(azimuth); - float z = radii.z * glm::sin(elevation); - glm::vec3 emitPosition = glm::vec3(x, y, z); - emitDirection = glm::normalize(glm::vec3( - radii.x > 0.0f ? x / (radii.x * radii.x) : 0.0f, - radii.y > 0.0f ? y / (radii.y * radii.y) : 0.0f, - radii.z > 0.0f ? z / (radii.z * radii.z) : 0.0f - )); particle.relativePosition += emitOrientation * emitPosition; } } @@ -267,20 +300,25 @@ void ParticleEffectEntityRenderer::stepSimulation() { const auto now = usecTimestampNow(); const auto interval = std::min(USECS_PER_SECOND / 60, now - _lastSimulated); _lastSimulated = now; - + particle::Properties particleProperties; - withReadLock([&]{ + ShapeType shapeType; + GeometryResource::Pointer geometryResource; + withReadLock([&] { particleProperties = _particleProperties; + shapeType = _shapeType; + geometryResource = _geometryResource; }); const auto& modelTransform = getModelTransform(); - if (_emitting && particleProperties.emitting()) { + if (_emitting && particleProperties.emitting() && + (_shapeType != ShapeType::SHAPE_TYPE_COMPOUND || (_geometryResource && _geometryResource->isLoaded()))) { uint64_t emitInterval = particleProperties.emitIntervalUsecs(); if (emitInterval > 0 && interval >= _timeUntilNextEmit) { auto timeRemaining = interval; while (timeRemaining > _timeUntilNextEmit) { // emit particle - _cpuParticles.push_back(createParticle(now, modelTransform, particleProperties)); + _cpuParticles.push_back(createParticle(now, modelTransform, particleProperties, shapeType, geometryResource)); _timeUntilNextEmit = emitInterval; if (emitInterval < timeRemaining) { timeRemaining -= emitInterval; @@ -297,7 +335,7 @@ void ParticleEffectEntityRenderer::stepSimulation() { } const float deltaTime = (float)interval / (float)USECS_PER_SECOND; - // update the particles + // update the particles for (auto& particle : _cpuParticles) { if (_prevEmitterShouldTrail != particleProperties.emission.shouldTrail) { if (_prevEmitterShouldTrail) { @@ -313,7 +351,7 @@ void ParticleEffectEntityRenderer::stepSimulation() { static GpuParticles gpuParticles; gpuParticles.clear(); gpuParticles.reserve(_cpuParticles.size()); // Reserve space - std::transform(_cpuParticles.begin(), _cpuParticles.end(), std::back_inserter(gpuParticles), [&particleProperties, &modelTransform](const CpuParticle& particle) { + std::transform(_cpuParticles.begin(), _cpuParticles.end(), std::back_inserter(gpuParticles), [&particleProperties, &modelTransform] (const CpuParticle& particle) { glm::vec3 position = particle.relativePosition + (particleProperties.emission.shouldTrail ? particle.basePosition : modelTransform.getTranslation()); return GpuParticle(position, glm::vec2(particle.lifetime, particle.seed)); }); @@ -358,3 +396,12 @@ void ParticleEffectEntityRenderer::doRender(RenderArgs* args) { auto numParticles = _particleBuffer->getSize() / sizeof(GpuParticle); batch.drawInstanced((gpu::uint32)numParticles, gpu::TRIANGLE_STRIP, (gpu::uint32)VERTEX_PER_PARTICLE); } + +void ParticleEffectEntityRenderer::fetchGeometryResource() { + QUrl hullURL(_compoundShapeURL); + if (hullURL.isEmpty()) { + _geometryResource.reset(); + } else { + _geometryResource = DependencyManager::get()->getCollisionGeometryResource(hullURL); + } +} \ No newline at end of file diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h index 853d5cac29..4a4e5e5cbc 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h @@ -81,7 +81,8 @@ private: glm::vec2 spare; }; - static CpuParticle createParticle(uint64_t now, const Transform& baseTransform, const particle::Properties& particleProperties); + static CpuParticle createParticle(uint64_t now, const Transform& baseTransform, const particle::Properties& particleProperties, + const ShapeType& shapeType, const GeometryResource::Pointer& geometryResource); void stepSimulation(); particle::Properties _particleProperties; @@ -90,11 +91,16 @@ private: CpuParticles _cpuParticles; bool _emitting { false }; uint64_t _timeUntilNextEmit { 0 }; - BufferPointer _particleBuffer{ std::make_shared() }; + BufferPointer _particleBuffer { std::make_shared() }; BufferView _uniformBuffer; quint64 _lastSimulated { 0 }; PulsePropertyGroup _pulseProperties; + ShapeType _shapeType; + QString _compoundShapeURL; + + void fetchGeometryResource(); + GeometryResource::Pointer _geometryResource; NetworkTexturePointer _networkTexture; ScenePointer _scene; diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 3efedf02ec..0a6875b63d 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -1114,23 +1114,28 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * default, particles emit along the entity's local z-axis, and azimuthStart and azimuthFinish * are relative to the entity's local x-axis. The default value is a rotation of -90 degrees about the local x-axis, i.e., * the particles emit vertically. - * @property {Vec3} emitDimensions=0,0,0 - The dimensions of the ellipsoid from which particles are emitted. - * @property {number} emitRadiusStart=1 - The starting radius within the ellipsoid at which particles start being emitted; - * range 0.01.0 for the ellipsoid center to the ellipsoid surface, respectively. - * Particles are emitted from the portion of the ellipsoid that lies between emitRadiusStart and the - * ellipsoid's surface. + * @property {Vec3} emitDimensions=0,0,0 - The dimensions of the shape from which particles are emitted. The shape is specified with + * shapeType. + * @property {number} emitRadiusStart=1 - The starting radius within the shape at which particles start being emitted; + * range 0.01.0 for the center to the surface, respectively. + * Particles are emitted from the portion of the shape that lies between emitRadiusStart and the + * shape's surface. * @property {number} polarStart=0 - The angle in radians from the entity's local z-axis at which particles start being emitted * within the ellipsoid; range 0Math.PI. Particles are emitted from the portion of the - * ellipsoid that lies between polarStart and polarFinish. + * ellipsoid that lies between polarStart and polarFinish. Only used if shapeType is + * ellipsoid. * @property {number} polarFinish=0 - The angle in radians from the entity's local z-axis at which particles stop being emitted * within the ellipsoid; range 0Math.PI. Particles are emitted from the portion of the - * ellipsoid that lies between polarStart and polarFinish. + * ellipsoid that lies between polarStart and polarFinish. Only used if shapeType is + * ellipsoid. * @property {number} azimuthStart=-Math.PI - The angle in radians from the entity's local x-axis about the entity's local * z-axis at which particles start being emitted; range -Math.PIMath.PI. Particles are * emitted from the portion of the ellipsoid that lies between azimuthStart and azimuthFinish. + * Only used if shapeType is ellipsoid. * @property {number} azimuthFinish=Math.PI - The angle in radians from the entity's local x-axis about the entity's local * z-axis at which particles stop being emitted; range -Math.PIMath.PI. Particles are * emitted from the portion of the ellipsoid that lies between azimuthStart and azimuthFinish. + * Only used if shapeType is ellipsoid. * * @property {string} textures="" - The URL of a JPG or PNG image file to display for each particle. If you want transparency, * use PNG format. @@ -1170,7 +1175,9 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * up in the world. If true, they will point towards the entity's up vector, based on its orientation. * @property {Entities.Pulse} pulse - The pulse-related properties. Deprecated. * - * @property {ShapeType} shapeType="none" - Currently not used. Read-only. + * @property {ShapeType} shapeType="ellipsoid" - The shape of the collision hull used if collisions are enabled. + * @property {string} compoundShapeURL="" - The model file to use for the compound shape if shapeType is + * "compound". * * @example

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 B?KS`a literal 74623 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 vsaK=d2 zU-baSWnA!h*QoQ$B^RVcxZHY-Fj;YN|1()Tfw>ub5!u*New&2bj!zIv+b1yjQ90L! z)tgruooRiPF3bN}xCE2C>Q+mq$S6ndgVJeYlmcbK&g}PO^1E!^yY52sdCRyLR@Ii z$VglE{4LFhR8sROBGakpK~QvH)tKWu-gi-ED8$yJn04M5y-G^`+*tF_OINP0nKxtK z7(}ApRqY!~N}GA*MaMLp2soq=7XCpJ8f~HPRPy*Qqhb-cER&+V1-3y}*A^~Z68GYQ zZvF<**3=qv(UzUf4=<+qd3QZfm*#bNapR7o#(xQewd($vGH<6qaYC-?_?dW*a$<9hx@Jw zAKEF~fj{(qWx~SbchRq(+qeI(z{e)JE*+GjKr@ZHC+4>92jpGAGAt1RHk7;$VKi1uNFtGp3`$`|;y7abL> zd>5xJ8unm?m)fO=TW3^1E!}-3E4VdIH!tZ>`ke|NN=fF;2YM~k2X^swc*@~4iY_8u zxavmKe$ocWZ1n{F&-dfEwos#(E5!C4>apze-M@;xt1$j&93JU_XJ=NG(enjZK> zeUEgT2%f~F=-@KxloDe86j=sik#bI)@&{r+4od{*nDQ5o_N&x@z!uX89XR>O9r?SH5i z8MMW83)h52%iiZ$+cR(PnmsMi_wA|D=~|35$A;AioJ})TYOJ2leCgKOdN1lhWnsg+ zH0-qSmk;+(;#Y{SH_<;?XnXtUOgELqomZzm3Aeq!PQ1+SgoGG2>&khLP+omQ`PeZ6 zIb+Ff^~vkx=h>}Pd!j0mXC-h2@j967CF)}^#r9n3^)nbkZair`%4B7ki`mDfnps}2 zcu&wM+6c*uqoj>4@q}zQmkg6~)Z3Aua+|M%$GWi~4B@E0k?UI2C+%$!myd=8&EM;6 zc|e!fJ)@L48heS9KKA%cRCoj#ad)DakW=u<@FTDJnX(5D9&&nptS-pNoLV})Y-i9O zWrxBID%bhPjP*otX}=3j}c6_5BxBGhFoAT1R z{$sZ!tfuV^{?v72mvpj6-lDtB%&^d6|1wGb_4~)wRd-~Tmv4J0XXnbNe}wsZfu^5m z6f$p7`<7#EfzB&*cHX#E{F>LbSn9E7q~aANp7=;L-8pkjo^S~&#BR1%maFg)Eodp1 zzT{(cW@%3GE*t7g?l7Blns$4ct>^|LD;q6EW*eX4^Jyzxw2<4SV)L^#Hm<>oUQNe6 z%q^Ec;A#G>Zo;Tp`N2X5DI)sA}hl9M6aTaPCD>DFi?v% zq0dwZM&A;aFT0)eDOy%CW^wc)x9sZ)L5XK`-kynF&MkhqFmKy-U!mKt5IIdNFo#Oy zW?!9^*{HTtcH`yB*sXzl9XLT+mHG;@$m_AE#*9KZ9?8j&X}Awvb1-jwUP`(TrS!b#!5YkJL~Mwpfw@9>o8&KcIbdwo-)P(JO8#2k~?rZdO6zW zp75FC_ggLRB|9Fv!mm43S8D5K&61kh%10iwGwSun|+Elc=@w)5+&N zIl?}b0>P74;1Q3xE+pByE)wQVt%*3oSL?yeHn(!o~~OEJh_->$-sn;R4s8>t=FeC1s>X>Cmi`)NUMV z+iD%_C5%{)cth5lJg)uqshzqHuP2;tm}ifaebBkMc~81(Q@dS6gwaen$%k13n#}CFd#5l^x zhZ@Ti+ns8ZwODT*<58s9kh|dZa;w;atVA<@0pc3D#j&wRk5w$bDeP!cF4sD3vQ4Vr zxnd(zQJkro+T77o6@z&`WNPSA&K8c+dGcm|$`K{2wK-Q_AYaLgw@!$eXDx$qd>Hp( z_EF!QJ-CgBju;-EqB|=WJHH@Y?8v>`sPoMa z&TgFTDfz?#rKomLI^*6^$C#-?E&FSmH540>J`WI1pS9P&EsMW9NrX8^C-qiK=%_CV zub-cgpoocjd3`C|K8}Zu+?i~xR_qj}5gYbiTHMsWXtdB(%ap1%RAXRBdE^_~MkR&) zvPwj9d0fgOTs6tp$yA>Uqxj|hvnL)c^tBqU&(2FOJz=)C_N|x3T9V09W$$L}{pg*q zBJYLlS$fj&s@9~!g)@+$yiLm98<0aes?Rl`dXZ!ZUY9=8mAEHJ*aLHUVSn+)-NX*~5pbF;C) zLOQp&5#>;x_bfw6w0w^i#aP3L?8LQKBsPrw+;q=0NDDLuxtlc0pX6E^qj%Tk5 zm7O&%5w2n4Ji`{2O+Pb=%YNT!Mc-{+c9cf_nrwQJo2BjoS9w0$F}!OnVh;lOEnK=} zQ%yqX2Ho12C@%ji;-^M?pK>dIKv-U!pEu8L^NeLVX>Fx4o)z~3=NTxSN_m-$@G;Gt zXd5CYrUC-T7;4b^i5~*hmU2@<}GSGUeFR1sqXa5$@sFM=E@D} zI)^X6-v7nCW{+CKhJ9Stw~y2+m^{|^4*Cy!XBiY%cLv~H776aI2`<4|9F`Avhv06( z-Q9w_yIZh8g1ZHW1P{)KyE~M2+D>Jr?T^Z|&HHa>cJAFf=bkWQHBpZF4T~Gy|zNwe+hI>$%I6iE-Pz7$o zip%bgZKORDcBq9rPCDIkpW81QNS819C819dzHP3^AB~w3%Ne)Zp>w2dk~$7Rsm1uP z@Sz=aeVXXHToValpRBfGeRZtd@RJ~zL`EwUiQf8jrfP*{7O)dV2TRsyg!nPSO80=q zP)puBKdhbG2Px4{51)(1XAz-lzMSseeNz|H%9{S}l=u_|jm)|?!P`+uhIeja1BDX| z6Fh&bM@xu`1r7bxX%ix@vU`W3-4#xq1cf{7&a$TQK>+QCyb#|28!};{Zqsu5g~+gi-ZV7fO0#`>)LOzC2iT;N|4yK4fS!w7VYk%C&Ubt)^KIU(K!wpJ0X_r z^aL(|h4i>rBrJ6p7jTUR{uXlFm`DXg=7_Fv=yu{VJsw`raR-lQgXGBP%etA@6lUYS zgWt+9&sPv+JNkg{ba+00J}vsba{Ou#XHnJbo{}K|Me*$cJTJL6vO7HvdLHIy`W{+b zc#$4G+4p|UHu)ULD>FfPNYJtt1c?}uOPvJo2^lg*@tk%$8{*r}gIiiYY@ZEobOhDu z`}_SG1ViFeS|4vz5%D~JnFUo-Y|uHsilf}bge|S6EF{lNLn7HaQc&X(U5}$(YHpyw z$QD-#%S(vYi-(lzQsj|}>k{HV2VFlxyFEb~Mr>cwl&L}yf9Y&Hw|WX48_27d#05^4p0+1~8K7Z3(UOzu0^puRFZM@J)}36X)l z(Y(_Rj7eS+6TiH4nFtFSP7?#3A3QYlc;pxKqnmZwT~leGnI3^1sm=DAFWJ`4v99ae z$XpsUDAdtRZcmci#4oP_iPPf|?xm2Q*wpnXvd7x8A+HbGnSM{^R>{2a-U+f3){LJ$ z3-Lalb1mBp=fMYJlf*Of_7j*pQ|1$~4slT#oi8I)U*D=vIMOoo5NtPQ-%@nE#wd^v z7|&Tc9p68~12hRVob_LdtGB@)jFLD$O+^@7Y380kt*U|={C<6Mb1<1wB{qpY4;N94 z7CqltB15p9#B#>lK2Bw-%bsN=L_=M%XUg4GGK;n7`eQC+}(oMOL}v2&k)W zJG&brHg{h{Q8?cdVS2^x2nMF~n{$HT)4$Z=&@X}ij@+mmK!#vxDgw{? zg`+;AZ}lr|K}x#~0fruIm-<~hJWlZIwLvPHy^9pxiQ{O2VN!&h^E#)y3r1gWs}#V; z%4}|zM_^w0d&Krz}e%U~Lnt_7cB{lrPVwg4@*gT`N z{zu6K^q7I-64jy&ISEryxUw8>nC8R~pN<_&xb5e8HnUk5H{hbeoHg9nqwYcJoaKis zJ9YFO81EH0{!_w>dE{j%@5FvTsU$-na$$I*&MN!lM}JvBtPm!Icd-v|rHZ0pQ%ode z3y$Xlitv&r3!y_t*1e~YULDK@mbVxEg?HQS-I4F6Sqd@PEOkt_K)aWwuf9sf;Ft6t za8~dG)D!n4owbGS!WQwCHPS$G?7%SAw;?DbvO&>qW{YrygdZrnv{C|i4L1dncP?vU zy!SDvcJAi}(}ek4_^6Pq_H)-re# zk*K7U0?Sw00OJopyAmCEWPOEj%IH+23~L3 znEJqPh{9vVx+rDvgqp2Su7QVEgY#a|yhv&g^dK$H;K$k@YjcWB!t$;xk0%C(p7dgR zy;_j6>J{L-Y+aA@Z_6IX)^NSE6(vl&)hQNbR_xx^zsQRsv{Xk6yu!Z=z8OFT)2RfJ zaddx#H7__olQyVSY-r=s^hZ^vglY&T!DeUYnU`HE__orr2HCL;`6au-TjmyF=Ir9I zVL90+#-y4Ta-=JGeO2knc`VRc#?q;?UsXBq&iP2jHXtL3Lt!^iJ>wOGW-bUfeX zMGcTw{+Z$NfsAg<;LX6vCI8Q3C>4tWp1-iI|Iu`^&tXuDcGI>lkgVh(a* zkE&Mh3;s+#Wv7mG*EYLx7=v}U3>5H6_I0}rHIq9&W+FUd0;I2MRX^xIUS0B{&bm`f zW1`ocxbE=m2n6rFa$pWIfte8k=VC-PgaKhxB9YiTaRR0ID%HkVAAe=Jn2Y3C7iXba zESjR$c3W{}IUx2t5%fKJ*nG@3)H^RSyWs^cAHvjA*z^fm!Rhq`_{vIVJZ3jhGzxpzeFSKhY zZWGn#td7`Z+d*6A{KW z0`Bt@I;ak06v4e?ViEO=Qgz)YkXwIsf?25KVU_91tdV@)b6w7p*R@Rq1Ii8OW=tiC zVBOWX6yGb~zMYTIslN=|ytByCS`12!=>0Wc`i8G6)i!6P8JmstXu%jbmA)5&i*x|p z!{hR+Y?ZO?9!NZ={Ix&0L?gSa_q{PXJ%Z?^maqm^zy2Xu8zG3-<@Y1cw7(spAD4bKwo{SB0{81-(wWr=l}oGo>dDctlFrSVE3*lN+YO=l1 zYqwi_B<=8h*OcQAg`*U{@BKkWzKX4b4DQ zyOm~wb^k!Lm^%XHwvSvkA}-_3qG0=Z3KA;#=`|c#LZjF1C%cF9*_9oi9V=pCZXN@K zi_8VkQNc!V;T2V<`~wYCtBwf^w?fB;5$Kj5?@rF2cA}MNN5Q?e73FLd{ErlLk}(-E6Y0AqNs(fWicv> zih43%mydF;+OZGq^*vs?_PI;AMtz*aANM{x9UzU_Ea`o|o9&oUbCViFowP>rwpO!V z(et@jL7hC8tID21XXjk5_PMOEoUPgvNw@S+6d~mmaAeeW-yUjmtD64FY`gav30t7CY8Cr2k>o^`H=ek7VAWJ8|!%%EIxyJ^qRMa`d<_({FlXx z=82t++0cNE&D7MGgPq;bz>w2`g^iON%*qZnW@G-(_#ar=SlEC1AO0QjciaEp;eTM^ z{7e4>8<^uK|NrlZ{~7Sr?IGDrd{wDK3Y=#`f}1W zmd)?E-Wz80BYtBVFf^*LJN!vWSxRGb={3;O=K${4r54vTJ>N%X&2EW1{`7%-;LAbM zTjv;ckAgQBfNE)^Dpti@E9Up0t=sc$3(#Y%Wz4w%;mP^9pt6(TyfU`Rj6{FB91gM2 z)BvCU68^=;%i}!0t!?l$6hzLwn98bXS1eFr9ht*`Ne?S_{Nf4uCDHY05ojk|in*EJ zVjA5H}vgssTlUAW#O>#PXImywf?#;uul?g6wR}-rXr@mLT z35>07zuW5ygcEn?BoYOc7rD|Jy)HNYSl!F4MxbTtH}?(@(n;rHO3;aH|MW1dUahUT|7c`#bE(Z6(Wh*!N+k ziQq2ADu3mqNFDf{u#1$H67bJD&c^?f)k5Rdt~`yPRrvwtvw zdZ1N`viiGWI+G+D?6)7006rr=_lup`>W-&$d887#tn|Db`uVLKI0f%2Z=H}@NVyJo z%MNT2Z1Le{t~o&+{p+;RR|Jg1T(cVkF}5pD##`!CC{}@$91v7*RXu5ka_*i>m_%Xx zRchs0n)LomZ7|TDM$$Nvn^>pO`~C{YvnER%?Uy_zA|8X%uNxK}|lLSm)(uG4X$dG8f%ZV%=T9YtH> z$hUWCGNSWZR2Ge`(0-_vnchA3`UT>#+4ok#!~^(j$oz}43z416&`PECn zM#Qu}>^4|Lp@=CER`}ORP;H9XD(Wd_t=(R4DB{ z6NpVBFZIXDHXsR4zX)6p2wr(E8$}iyn6wk5YN<#GVE< zQs~0wgGR0VkD@hHc6AT5N;|llvuhU(4rJ}Hu1IIjna@v)j^nraoN}G7p-g=6Dl4Ki z&}7&8KT@a)^_Y0CI17HT$;g3rB+NZdOR8#qP5OguW?0mz37N)~z=d*^0#I$(xg6xZ zWh^zp6#$xrU2Jv=aDNIkpCSLyz`v+wuO;wBh%Z&r?+4njxJkio0G*_7$Wbjwf$kg- z7xao*SjvZZY0Ce{HG? z({$WA!K;ioO?d7aM&DyqO8uD+x8(g1gEG1P2?qoh6-+n=h-`v5GAqE%1{9ub`6><; zOk4kI4L_z6=pkN+A;$=Qrn05QuJ`Dkljag9aG9I4tCx6nSZ}y=$U<^N>{69=8vP&u zY6`?A_d`!ghY0Ci&oOV3g02Ca;TsaR(PJocovk(6xwqZ25a5pvT~2#A6{cr5Knet# zRtLkpcm|-Z-D%A%5s(V}NGT#lq+?Hb-~Cv5^6PS2Ps;e7oF?+>sQkmPo8Jiv*(zwV zlb)Pm1rVeIISCs0)xKNWBNvXE!#EK5uaC3qy>9G!R#@ zWz)mR=teWxYov33m8J^Tk2XhPitDd4zOXhYFJ2TfZq0ZV^gXg{tsxaudqWmdX3DOa ze_k*}0&UeFb%-wLHTPKV;5>B<(B+)r72bGgZ{Z@mfb7y1vN|B>VEb1*N!C!|S2%@_$G{<~-#>UL}3Bxr z3U4I_Gvg)KEYSv(oEjj-Ng*yZM$h#RqJ>N9?qi6G zm!`ti4N+r3wOfi4jU1%GWi6oOi@0vyssNnIm~bpoScL3Lg=Q=Pjys#)bhdn*t;1;X z0;D=9Fj0r;UJ#tELzOn_L|KxD5Mma+1D;bA0JNh^8QP3wBE#J@R+f<20|&ZTS2U7% z$)0Z2@fA$!!4Uv<5PRv`yH||M&#Q+noRP1OPQ1-35sA%ApoEM27g$n-^@qh#kbw*cm zF#=W2Q5UL%@zu#RjjC_pq1#@JNY-wUqKhwqbx&w2>l1;JEkyObQigJWy`lum2rO1K zeO4M$>>bg>p*h|dgAi$R>IW!y@CSm8RB=3Y><Fs6EU5k#r8}{;cT@>n>B|@t?;N#<16X47`&kpY zJwrrMQFqR%>}?>o*k~2yO5yeMb5S}+K>S76ns^#(nK6t;JlNWamyE-dx`uLk zF1r|XqU-Aqa-sBN9USS5S*xYkg1?vy+4$yXM#_KJ5eC~!nZ*b+YnY3Wv{?2(+~d{u z&juP5XnI6^2d^3o&HvECrcuRL-DtUUEV934bBiu6^@5||#1CUKGkyc3q{2?Q?}82s z#V{@zBbjL0`KyFf#+VU=t|oL8BbOzurUxdUp@}R1X+T<6@7c_GkEVjtudpbQ`CJw4 zT$AXW1|b9}2BImzs=!Z6gxVxBQN&O1+lXagrL-|8VBCZcI9k|VTM>M|<;X-S{7i@Z z>NE>a`O=|wWHpICfUy`4K@rwKGV?pSTUR!@1Rfg;3VRJ13%CNnib5F{Y40Rz1%xx+ zIosv{SfxOUfDW;YWqrN)MY7BuGQk5H1QA73+IxM4^QO?MA5{z#0$z*nmFIe%=|7cCzGmH7QcFZWAyBgNrCPZ>~%8 zj(dw>13sG3C{Td6#P|lieuy}CDJ~j%xoo6A62p5@WXP{`O->8Sq;!l5IZ@eBQmh&$ zzTXQPIa51TS=f1>w`MQGE-hWQ0uw5#DKa?BBg07QG)TmcwwOnK5Q{{%<`)rKG|wR+yQL7wphlES9-A z^qPz+EjcQ%YNlM^@>nm_9cR?i52udcyGb~tb$d^3f?jJHQNx3roX9Lxzqx2InII~p?e`uzEA#@bmXKj1t`5i(2xF#4H3)NIQKJ zn@3w@(tP-8M647){W-SUy`&E(z_GA>^cb&eD?Bz3904__$8UqYO*~$su)aR zONdEe!s1IvU&)@}rUc1mk#&$wUr?gO95)C>?C^xXWPR&B_lsHSB5;?~DkftlK2CCu z3L;F8JcF?-tHL4CmRU1e$_XwF$EKIFVdCX(6#- z&kRmzc#mP&*4@HDE{vbg#xkjmod6E8GVhlqxTZGQ6O&hC^opQ&1qeL#dC?=yxh~!j z)lJct^Os;}^(ta~WnhUE#?YNsIol&jk$_6_DQ(Y`AItV;88n_b_S+-O6D_to>@g?R zWk|!JNuHNtLyyd&J4LQrGt5hBMfM)TjF})+z->nvHY1<$-n!Vm>u3^~1gOzT7jKg1 zq&9ZY(8pjkWZTIYs=sM4_ka&{aTrGE48wg;~^uNCrTv}W?jA(!ur3mIv!WNb& z@Z_msr-6-GPZ12Fp^QAqX*qb$Nh0aZ~s3T+%Tn0Cu^+t?-|CyIQkX5 zQP4N;9K*R~F`1(IG8#*b5Rb3Z@4ZypT%pSZ8rfX=y3-b_{AgYJ=s_Dj?E01jKbeD_ zJP&(eUP^EJGklui57Q=67H?2<0cviK+1QkzFSq1Ade8^X0;CF{l^l6IJZgMTj-*0* z=rZfV)?NR2=<}L8IJlI7Udj$l(2aa^ zGVRFDubWmvP(==(piv~tjEA(E%e^$|(@f&&gr{{ppi{TUb6ULIk2dAMA|OL_mZ7Ju zDV4UFwka$#5JB!N&@yt+8jm2QJhOT2j$hlr!ND1=)4g2Yjl*XninBb-MWp;) z6jajmg4z5L#>={)7jm8o!NroV2dW`)fQDQ^1?M$i~M~JK3m{uN^Y&k zv}*j}!)ExfjPK<+^d1SH_7k{+3Gb>*@Nl z88z5wmd6aa%UnSgJp7;ga zAMY-&IuuvFHkpFHmXcU)N?7;~aZ{hC77qy*2ZNbWSG?PwpFFq6vUGT-?~wH+Jx;pd zNC8gzD8_DmkKz6GW?(GEim} zbQP*q@zW>VzdBsXTj_e^EY$-GR}XQk;fS2a@Mk9K_vO0ZCsq7PP{CVR`V@s+?QOFB z3rIo))eT2hZ74JiS`Nni!*4GUMd7Hel5?H@sO7`&ZgY~O#=TdIaQ3uGSGc zWZI<`{wP0yne%m2R<(oP>stNL?Z~$B?!Y?hIrn@nPA*5ld#yY8x7%2=dcKF8{8ZYZ zwoXNBnr20tc~2K+hR=mjr`dScrK^gdlkt)Z>O|zd70)t)xZciQ zf5=`=tIHt)t^i!w(3f9+w*#w&@j1ckVp81W&1VzO>+%aemqu^fW$n9fuHv2}Oa0 z)K79b+`EP1PH=9j+2yMnj+_b}{B~73yv-HM(*Po8P(7G5<5FQgtk;p&X6I`S2^XKW z+ZNG0sDy9HhhP;v0e=MV(aCRb%!HblozE71gNrn@*K<$@T#n6_PJQ1KeFVQZ_Dg-{ zTG2I1v?xd$MpgV#fe>s3E4ux!^GP@cL^&p-kN}B=}Df{3i+iZ@Ax268xWi!=EJhPZFGwo0$U)W@i6=6+jkl zR#rA{*58%^{L_#B3`YDPoB#cr`0qc~|9hDVvi_X^{X61s@ZW#u)%eMO|1ZSfY2<$o z|IP7N{3kQ>&-LHGCH^M={pV|{pCtJIVEq05|F7o1Sy?%m!GGER*}*@@|9?yT1N`?N z>;LIkq+k!k9l}WZiF}8UYhgguM+T2YQhL^p6Zxj)W~Cx2sWY&z}9e(_L}kxS=I-V}&kVp%U5;FW8XR8Aji&|V` zmfBhb(mqmmpyf+0zh#J(y&!qM(%N4XbUElkWb#`j)$`aTvsj+HPDt{@ygnxF@Vq{_ z+%H6h(WYKH#M+up9T^{24sHR)zsJyG<-A@z{4u`Rjw~(S@kCI3)c$ZbJ`lb3zLJ>1 z=MBb5e-z=s@e=6r^GF1i1HWL*v>ZGDp@w2QSOoH679L65U)uvpD*oKxiUoOr1~nzw zhuh^~jlSBvS4a&m%`f5lm}#Zunjo4W&hReW=G&fOzdt>4e2tbquqP}2V-uSQztc@N zry%M}T^k|{?NFN~&6gvZi6nZ|@k|Xpp7wSpeb04$48UH~-tltZ!mlRlL6Vlz-W+|j znzf)#t23LbA0BxIi)Yc^C9f0gclM?)2b07HVybhv*)%lsA0OY|XZfvstZMh!=qD#R z(Px*6aT+8u@HGt>Z8a(SM6Y7 z)QFnT6AfW~E1fqSgyYGC{AjHI3XYCcqH~y|!dC;7F%2We&Hqr>@D zUfp&!_x*d!vR3&lKtGzzabm~I}n~m()&N+mcxLAW0PHasR{q`YQ zDo%kM6BJWNXq|Uj_bJEi5lP1u>@ryFuY3yPK8D+z_bp&yp~;#Kumi>8TC949F~}vk znVGV{pbXSn&MV^O*-nYbWHtiQJu6bWlKFva}Iy(k=PwK$`roF3di-OytGcYiubaxFQ_0rwar6Aog zfOIzu4T7YESoKU^T^fpNPIMZZBpBdO>(S41J94m7ta1**=5ExIc!z(f}zIxVNL~tLz+P*85am^9W?U zSE)BZjjQK6=C>0kw0+l7%KataYU$v&^ZLokqQ3oabOc4|5?55N5Hi&{ece@!4RLl1 zu^cKf35g58wZY82Sq{pxJZIE(p*FW1#b} zv((pq^#5GP`|;Q1U~P4wAH8WVWB&7pQfia0p=ib<1z8}LJjL$#6WFtk^5dWNG{5T_ z)<2tTDW{R#*rtE>>P6cpdN>JSzZULtx^~L$ZNKjpnAYg{miJG58i+h1E^PM02dyc~ zZ*-(}+kFL~jvU@$2#89fY{Z0b7A7?;S|~RlpZ7L!3$_MbSQK_4w`74jy!SA=>{ytM z$SFv239;={(Mw@+f+Y2@7qzy%{GmDONz@sJKpTL`S1i>pF1iC`z189oe%Tr~%2LhT z-!PH7SkJphI_DZ+?ypP-haA|}E(~#>`S9{uJ8tn5)EYY&c{{+5#39|Fu0n_cWURzO zIiNy%Ye)oG&Cl5~^7J5bRnsLAQhVd6rHXkR?-R}{?V9d-rq}#6KG5j&Q^=~vngbhW z+n2m25BJmW%gdO|>K^1PQljfXn%Sw1aqRrW;UkOhogP!MP`~O@2(c?9igsC!r+&PDV3>sx$OXA%>dBi$QQ*mONU6 zBjsg%OS&P9AzJcHL7rvw7t1*C05*(>62 z9NafK@3JW*#NF<{`ooVL(pbwHTKF>{2gC>aT~Cja55_xHRo<{*i&i7ZMxeX8>>Re* zeqd%Gu89q%4cMNezJd{apN<*%QJneLBn{+0dr|%>U3S?16*nX`qPIZ!XWAxB9#HS-R!qjFr>`=VDp)e!KTK(blY!a8q34CDsgT&%TeJAY*F>=`{) zGY9_DhzY{9B9w8UY4o*XZkEydu}a$neoerO#Iql+?C|vVXU72DrVax&aTncVq2n+! zF%1fX<{Xg8B@sG|>l(*J3F9+_5jo#SMN7%G!a0KcaO=CK844}RrN)+(+tuLlsakjW zIyjHair@*6m(0l0!AtMbdRE6{6Q zVY?*PsU24atRZCyKkx0VMMljd#u|5OJ1XX=)G7o{G)-byg|XlG!8MEd@h`Q*h{{sk zZu}~<%KDr*!wf6ztpms%G5}9Z17`_FB^_zJV8Z;}ReIXmahS`E7Jl#a4>B3*1j;~? z{$?(s?#Nn9rU<7uylN49f4^U~2qKJP=htfWJ1h$3Xv)}F*&}eH%(PO-n-1>6n;Yk@ zHmrNwC2pY6bDjMV=?Nr+9s5`_#c*f3)Q`d`6XWsW7JK)ySJ3I}1b=3lLX@ZGYYk%^ zhs1qtLb@ctPE-E`s`sT%X-_Wu9NY6r73cR?Z!p?q87S~AF|x!^1)gYcL!Tv3BnY-z z5LsBEQvaA$X~+|kM>*%sl1WeCTHU_}J=vlw^WUqLjz4HxkZ`6dKu!5?>8+_NmW%Xc zLg?e#97oer#p83jgz}w?>zgwpSbut8fDeC1TL0`rPcVPWFSxX8?GXU($lMTb_-Dx; zWh=TJO2DGvyRS`O`hJ*;lSp&bI6*Hc33S6{3utd@lE2U%-!h+Xmxi83h$5@aZEI{x z`<&%i)Z{pCPKS_<3rsZWS$HmC^4~~mO@xvKc<+7PtRde3mc+D~JlCW|wQvmP!qPX!^e=60l*m(5pjP@t_caR`{EN z*^9Cd%p=n*r6y`r#1+%oiZ6WCQnf(LFVGBm1OY4^YZra12dM%Fe&Tm1L zzu2m3sy6h-P&H<-eN4Uq`8rk8;Q97)E`9L1X~qYMTbA^zwaHjFqeqH;-g9c$aJW$D zWO}+J%f5+nHP!2me$#3dtmN* zVSaXpk+X~cn5oO?WAxyOR3>DWA|oRVA`AT`*MPLo)4O~dDL@7J;@a)O%75!*RAZdB*NvyicVRdFf zJ0^mvOofvYBLYPzqKlDTHrF`Lbi#$~^8UuRnwRMN#n~xMb1Fas(SZQRWm3rq5;d!( z_@PS+sJq8`LWDks*ng6hwj`@9i+YtNsT(9tR0*fULa^_o!ISB}e8pZ&$JXiOH<^au zkBKxM%P0)+0*X^$Gpehhp0#^1?$7j5a8r3N#L=(rsQgXilX@m}d#=UNko^(LjKHf3 za^kzYiAEf%p@Y)i>&s?vX}6muIt#8S?G+*njpQR>kv2a@$@KwSDoJ@{YT85W10zi_ zI<0jkzS;QT2aC#1vu*-gXM&L2*886uLBA__x$37rCxMEr$*LL9ETw9{JMGIysIdah zr55-U+cH%FwmrYczl6c5O@q}rg0al|bLHz|L=U~zkhd3RE#+|2XIB(8az#AelHBl! zqp+b)6w9z7gj8B#j&%&NLcQ!G1Z+-THesPnag2bgd?%jRor2=M55{QdANVAnpGD1-DO))np)yNp zk8^B3AWXeas+#kaoix+9n>uJ_n_XDk30BL+a)EOY-DYAvvrqMQmK8@>EuV#l_UaX| zJ~=%PC}EB$u-%vMaj_^)F|X~(MCG*|pRK3Y8eu={al?d99+!=5r3p4h7(1HkD2}i9 za%3d;mGfh|uzyr|PfknslReu)RS@#pRl5&F+2sx{$v-5aAdq_$36`Q!(wGnRJ{pTP z($SJsR1uzM8ICjx9;e?Y8ns2@`$E^M9{C~fjW$AuJ0X>gPg<^9Q>&wZ$zYFq(59#m7F2rGuN73OrfCNoF?deGQJ3Sr z#^Ig;l?$#(i@3pi>AqX+*9i(veI!e2r}qZb{M`|5%)GX5Yy=WdjkA5diR}(3mbjs? z(;=_S3!0i{7q_7vip8OMYIuW0T44LtG6x_d={!)^T1ZYgiz1keaAfxtC0>XmjmGzs z8_J#vvlr$OjR!5l$3Tp!aX`*J+KgsWCpKXs^feG6JbrqxtI2-y`n(G=ShpDO?u@=b z%&AV|{VX<2A8(_fUH_h6EYzXQ@Hwcph~tdsQnzEKrpqdg#=me~FGc2V|88qpAw8_l z9{hu!>C7mFR>D3-EldD=X>Y3v`lT4pW7wEwSVaktfm&Als@+XdbY4~@W_HmdiRx;T z^|93TJWNydP50mDb(TM1Q=R}1=!xP8?;|hf8HwA~0kkK}_L0nv`^@sM%4=KlG%#na z_4B1DhaE&_rfZq6e*1^T12mc$;W(4=V3EB}UM+hTpjzDjHZ_8~f_2h>eE0JNvgY$9 z@Oc!&X63Uv(r4GjxG3%f7crkJ^f=;7#%5T68H}vicgKvzc%MqvtT{i|96{~uRw3+R z6iwwO{g&I1R!GgaLd%1q1=B>Cy2W+D6$AV!JAS2PBsCm51v*nu^OjyCQDDdUKc!2tq|GI z>yV{Np2liykkT4qg$*qSjhwD^R%%JokCg0H&iI(4!fXCvmHeq0CY~pCm*2hi3s674 ze4_h`P~n0a5oZIDa|@7xLKG_%bs=JoDsldveXOj+n9@$)z1qlj$zphchk{hZA(w0rfgl5^MKpnCf3HZJDgFQI0are8`6kl3* z_+WGwk2DTB{+MZzH?eAGI4P5{y%{c$Q~gJawKBeUK16Y%cgoN$3FQs1wb>r2y+ARJ zZ<+6B`X1cKT~1_`6QssjAsvxS&3Dot?ywM-2Zo6dCzJ&(bVUN%cxH`xK8Cgmh&}d> z_^}psK5dXFF@j}Zen4=Rn0z*TN)AByHG^D5&%QUmu!iG^)s~&{pLr{3jWp5i6B+ZG z*Z$%)nI3jp<&(bYl$mODHY`>&FBiQJ8y~=CVu6R1q5g3SS zr|tSa2Ajpf?kXH`pSR{IGAW;P!)FR4^S7Cvm`KdOZ0T)h&7%}Y(8@_QrlV5Q)iqJG z(b3-3*+o=ywoV94Odm#GngT`%+BxL;Xr-&RU8~ni1Ps?#Ct<*Ucz5 z@GzWXwxlJ^tNAb`VwKo19K$ zbAEBLIKFu}z+6|5Q^uA=ryk-Ge<>%XJ2^=>>QUBSubaRm72j&jt+qh9QRQcOhin#U zwQfgyvZO2PrEC;Ju8qBpl+}~ny*EIT8Bz=#_nRc-;y&b@3=c{n>Wvf{eLU+mATNh} zBD@*YUjPBGYv!l{N)IeB3b6)}K?Zchw=}(uK+z2Te2&NDhN5P>vbQt2?B%#v1;kef z*Y^Z792n%Gp_NU#MTu19sWK~|vpOcDPC5;9O?G=~B1?*xz_;<;AnrF+EeA6Elkcd6 zgApToNNH*N7aEps>^6P=xp#?^Q1pB_QU8RTj7OPmNdXh#-st{@hxK(!Yi zbB71HH)UK~NFJ3~SCnIB#Mdn>dU_NMtIZe0{qA6oDe-%YooU6^j=>8|#$imu`S26& zw8srAiRd(tx&8R%FuCqUlchA(`!e)pXCmsv(j_@X*_B2K!S$`9&SekdLA*|{L}|)# z((q9YqHL0jzzj$0O>T*~D4;MdN@y-JOzcf2Wu%&q!iv)ya!9kH<*Me$JBtWUk8%2` zgVy>@71i>Lq+TOU=ya2YEh^#WnR_}+9v}9Kk=W}Z0`!WuBU#2`+MHdgPl!ri!EdmM z+LH~gzePT*@$TEY4iXl>395cdySxw_-ylcsgVU?yQLyTl>+@;7_-f_Ika8N)05Q{d z+DeJTN@xNP%@?3e!R2vt0z`ZUKg+Foh@Ng~;VRaW0xgRG{ZK}7EYN<2jWrV*vfWRm zsOG+KIrooKjsQ5>Au90Dqe4_^=rED_*F2eI03T)@j)_lgOHh#S6AAw(W;3^^yTaGH zn_0mD2=?>0s)Jb!(Q3n)jWkQiy1r-eRAY+GAEjsOq|(-yx&W+r)F_>l8BRyex#+ha ztrv8vGslU?)Cb|{D8^74`4Y^Sk1|1;yY6R8g*z|%4?alyX=$4=?9qY25ZWF3&S^j- zt#p3YX!!DHWf}N%r2Xog5+5NQ-!BeM^DO*&GW-K6FIARE?c|RZ*D2fbRrXL4R?gdy z3OjhZdKZ$fj+SR-;JP*+HtyZY)mn`CCCrq;(A~r*Jk0Sk7;kWkR_#m)^^S zZF{%r8&f6X;o2za9$3(jf4j+rSqcW7`+cPiIjK0Of#LnpdydN6@I}fM_P>4J8@oFm zc9z}@oj*16BTzh3r6s(_zEvtfPC{g7I-!MHc*ihc(g`w5Y3rh6!tl1WtD%4!FC&dw zXa3n)S4g;Tzn~h+>@pa(AIUWQgD)E6NeOWo`!I3LIX6g{RdCem-(G?Rf zcW?#EB_CXJu&6#ncjNJp?vyxM`e9;cP}PTzx-k9Ef)1u{GUnQ7c)MnTGP_NcWmF^g z=l|C3v&I8aOw2#-e-4IMK0OSmTVgn-!EAoycDf%WC(!N#ftRMYX59+ql$5(<1k6n6NZKu#EdL@z9x$qUIK0 zjcvgOrzF@xS~|_4XEYxxHf?%*HtH7M@ln>mE{4~8w29`0auL?_NsO{7i&K@Q9AX)K z^hAjfB6tVv1hiJ30L?--Q+_z;(#YjlkU(N)W|QtbzaSV>Xnr_dGh_smBxFklKgM!# z)3hnw-izT@^A5%F;vVklOu|j9MYWUk#u=OBD)oUPj;|Ic5jtqA44qS~zIcv%!~J?l z9q0k8&Ut}ySvaua@EPt0!Fv9&>SYn&-JCV%*x21fT6C^g&8;>cBbRSs2Dl{5+^m#~Dn7O*XDfS<7WkvHSDqY2-0uvJTY(+5FlJIcaY5d@%x<71vFzF5CF7Ne1}J)9VeC7lSR z_PZ)m$>7HG(R=q5@{W;o*O{H9FhkRz5e+N)wcx850*}~-%s(NGnR!6_q~Z2z zKoz%siOd#i;>F4Fq6SKR+&tf}Fp!kF!W(7@N%wrf&VjRO;Gf1*!^N{#umOT^-9gTd zr(4UJ!NcV2mc3<1t@<}mnz|`kZpM#gz^0;#6dv#Vi0VT%Ri!P^=2WKarY!Vi5xAgt zqaKsqO=TTzxDj}E@EUhX()D=viz8RC(4sUF{1c_o{Z(GxMVLuGZ|FWD{m*>~?>_ky z7WI5wy|2i^nH|YS&YweYb7-eNpiDA)>DH;5TU;mqaX>2cRv3uo@`CYJ8D;f8;O$@H+1XDI}|_ z=cU6b&y9zFb}eB{UOOGC6-fS|eM=rv3@|c=60;G0RQv5E6k#@R4CE1XYU&_3uus_Q zq-|aRTyd@g&0cm#Sv#H_oIOfWZoQ1w?>568?+z*Nah}>F3+ZWkN@(okZS%Bk z!_h2Pui<|MrDSBUR$x!nD&`>of0j8-3^wPfV*9&9a97t8+X zxJ`~Fv*R;!{c5WsN(!}+%98Bz^2)H6*AR*P)-LeKXRD_+?B|z=fsNqnEw3QMk+LCU z2%sq=OskX)OvON7;pi0F^UBY^QI8W{`og54NeV-ACEV|-YlwH~`b!}5W+MLi`VbA3 zWNZCK?gG@}H+mUmKvW(rK4xNrQC2^PK)6QSC-=>3tdZ2`=*f0Edr9@L@<_{Hejy$B zAB2_-+5gynqzP^axZLGbol6vF0_eP=5w7#N+!g#DQrRTT9f&*vKjJ@gxH(?M!1Xe6 z4o~rTJ(RNg!+g%A?e}(J9&^I?JRFI8pM(>JqD}|^@`^^h&Et4Bbu2w>X}`ZFyy#j` zAq&|%WcwW|Oa4%uz`*fwO?cy9jv7>s3ZfE4@UlW_a7^;Vc+F`-CTM{*p_G)S`uEDg zesPdt_H9_~lKSELsn%T4&u@Ca8wEUjEh>Wj@9t`8|i`=4bB6H+0A3TC&ICo`(Gr^s)hyFR(vLX=#6EUY73=T80=%Tnye+7o~%!Zo}K5ODh7 zS+j2)A91obPE-|Z*xzaJi|+UQ+fzELZtSO8^PsVkgchHzR+lYD$_E8#;7LR6kKl)+ zQpd-Cwj`e~5*9HMM((gC=4-XDp3kFxE_{2GBli%en-KCM zCO-dqm4)e?Bj))b80oJdois!8q~DTu(fC!49D(d$v9*Kny%->Ig?3#%Zfx;RbfMI1 zh{E^7qIl0!>M5R1_hbLwp}5V4PGBvtW&^$ES86H|jj|8-rqKuFWkIat3!U3}l#Z$@ zO0=8YqaH^1G)&b@gBH9<@Obt4RzJapebjl~&wk^wGpR64Rcc_@9bK(->z`9}%4?GI zBP;8@+0n>g%Lhi1!L@9a(c0zm8B_FW8#7O}8 h1*cHrU^R-;2LJ!Up8PNV#lQF;;y>dPFkt{z0|0@gpgaHo From 79dda7a6c77108d7dda6252769660f9d7a8815b2 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 15 Mar 2019 13:42:33 -0700 Subject: [PATCH 207/446] build errors --- libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp | 1 - libraries/entities-renderer/src/RenderableShapeEntityItem.cpp | 2 -- libraries/entities-renderer/src/RenderableTextEntityItem.cpp | 2 -- libraries/graphics/src/graphics/MaterialTextures.slh | 2 +- libraries/render-utils/src/DeferredBufferWrite.slh | 2 +- 5 files changed, 2 insertions(+), 7 deletions(-) diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 46a810f6a4..38108416ee 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -43,7 +43,6 @@ using namespace std; -const int NUM_BODY_CONE_SIDES = 9; const float CHAT_MESSAGE_SCALE = 0.0015f; const float CHAT_MESSAGE_HEIGHT = 0.1f; const float DISPLAYNAME_FADE_TIME = 0.5f; diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 0ba3adbe9b..b33eb619c8 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -19,8 +19,6 @@ #include "RenderPipelines.h" -#include - //#define SHAPE_ENTITY_USE_FADE_EFFECT #ifdef SHAPE_ENTITY_USE_FADE_EFFECT #include diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp index 5cd0abae68..a3e1a2f56d 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp @@ -19,8 +19,6 @@ #include "GLMHelpers.h" -#include - #include "DeferredLightingEffect.h" using namespace render; diff --git a/libraries/graphics/src/graphics/MaterialTextures.slh b/libraries/graphics/src/graphics/MaterialTextures.slh index 1cbee33238..c725aae9bb 100644 --- a/libraries/graphics/src/graphics/MaterialTextures.slh +++ b/libraries/graphics/src/graphics/MaterialTextures.slh @@ -235,7 +235,7 @@ vec3 fetchLightmapMap(vec2 uv) { <@endfunc@> <@func discardInvisible(opacity)@> { - if (<$opacity$> < 1.e-6) { + if (<$opacity$> <= 0.0) { discard; } } diff --git a/libraries/render-utils/src/DeferredBufferWrite.slh b/libraries/render-utils/src/DeferredBufferWrite.slh index 66d0aa2ddb..fc9310a520 100644 --- a/libraries/render-utils/src/DeferredBufferWrite.slh +++ b/libraries/render-utils/src/DeferredBufferWrite.slh @@ -64,7 +64,7 @@ void packDeferredFragmentUnlit(vec3 normal, float alpha, vec3 color) { } void packDeferredFragmentTranslucent(vec3 normal, float alpha, vec3 albedo, float roughness) { - if (alpha < 1.e-6) { + if (alpha <= 0.0) { discard; } _fragColor0 = vec4(albedo.rgb, alpha); From 60ed9e12a42b77e89d24d14536c0328a359221cf Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Fri, 15 Mar 2019 15:17:33 -0700 Subject: [PATCH 208/446] Attempt to fix build errors --- libraries/baking/src/MaterialBaker.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp index b2392e0cb7..47604fa7dc 100644 --- a/libraries/baking/src/MaterialBaker.cpp +++ b/libraries/baking/src/MaterialBaker.cpp @@ -27,6 +27,15 @@ std::function MaterialBaker::_getNextOvenWorkerThreadOperator; static int materialNum = 0; +namespace std { + template <> + struct hash { + size_t operator()(const graphics::Material::MapChannel& a) const { + return std::hash()((size_t)a); + } + }; +}; + MaterialBaker::MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir, const QUrl& destinationPath) : _materialData(materialData), _isURL(isURL), From 0d9403051574d6bffd3ab4343ec08d17d78dd5da Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 15 Mar 2019 15:32:31 -0700 Subject: [PATCH 209/446] use web surface alpha --- libraries/render-utils/src/simple_opaque_web_browser.slf | 2 +- libraries/render-utils/src/simple_transparent_web_browser.slf | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/render-utils/src/simple_opaque_web_browser.slf b/libraries/render-utils/src/simple_opaque_web_browser.slf index 36b0c825ad..df789ee22b 100644 --- a/libraries/render-utils/src/simple_opaque_web_browser.slf +++ b/libraries/render-utils/src/simple_opaque_web_browser.slf @@ -28,7 +28,7 @@ layout(location=RENDER_UTILS_ATTR_TEXCOORD01) in vec4 _texCoord01; #define _texCoord1 _texCoord01.zw void main(void) { - vec4 texel = texture(originalTexture, _texCoord0.st); + vec4 texel = texture(originalTexture, _texCoord0); texel = color_sRGBAToLinear(texel); packDeferredFragmentUnlit(normalize(_normalWS), 1.0, _color.rgb * texel.rgb); } diff --git a/libraries/render-utils/src/simple_transparent_web_browser.slf b/libraries/render-utils/src/simple_transparent_web_browser.slf index 1d5aad0914..599fd3d87f 100644 --- a/libraries/render-utils/src/simple_transparent_web_browser.slf +++ b/libraries/render-utils/src/simple_transparent_web_browser.slf @@ -28,11 +28,11 @@ layout(location=RENDER_UTILS_ATTR_TEXCOORD01) in vec4 _texCoord01; #define _texCoord1 _texCoord01.zw void main(void) { - vec4 texel = texture(originalTexture, _texCoord0.st); + vec4 texel = texture(originalTexture, _texCoord0); texel = color_sRGBAToLinear(texel); packDeferredFragmentTranslucent( normalize(_normalWS), - _color.a, + _color.a * texel.a, _color.rgb * texel.rgb, DEFAULT_ROUGHNESS); } From 53429f459e2c7094239f0abf6ca3e38dfdceb65d Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Fri, 15 Mar 2019 15:32:39 -0700 Subject: [PATCH 210/446] Remove some redundancy involving model texture URL resolution --- libraries/baking/src/FBXBaker.h | 1 - libraries/baking/src/ModelBaker.cpp | 15 ++++++--------- libraries/baking/src/ModelBaker.h | 2 +- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/libraries/baking/src/FBXBaker.h b/libraries/baking/src/FBXBaker.h index 257efbe983..59ef5e349d 100644 --- a/libraries/baking/src/FBXBaker.h +++ b/libraries/baking/src/FBXBaker.h @@ -43,7 +43,6 @@ private: void replaceMeshNodeWithDraco(FBXNode& meshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList); hfm::Model::Pointer _hfmModel; - QHash _remappedTexturePaths; bool _pendingErrorEmission { false }; }; diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index 0a5341cce4..b1f6e1d51b 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -408,7 +408,7 @@ QString ModelBaker::compressTexture(QString modelTextureFileName, image::Texture if (!modelTextureFileInfo.filePath().isEmpty()) { textureContent = _textureContentMap.value(modelTextureFileName.toLocal8Bit()); } - auto urlToTexture = getTextureURL(modelTextureFileInfo, modelTextureFileName, !textureContent.isNull()); + auto urlToTexture = getTextureURL(modelTextureFileInfo, !textureContent.isNull()); QString baseTextureFileName; if (_remappedTexturePaths.contains(urlToTexture)) { @@ -559,14 +559,11 @@ void ModelBaker::handleAbortedTexture() { checkIfTexturesFinished(); } -QUrl ModelBaker::getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded) { +QUrl ModelBaker::getTextureURL(const QFileInfo& textureFileInfo, bool isEmbedded) { QUrl urlToTexture; - // use QFileInfo to easily split up the existing texture filename into its components - auto apparentRelativePath = QFileInfo(relativeFileName.replace("\\", "/")); - if (isEmbedded) { - urlToTexture = _modelURL.toString() + "/" + apparentRelativePath.filePath(); + urlToTexture = _modelURL.toString() + "/" + textureFileInfo.filePath(); } else { if (textureFileInfo.exists() && textureFileInfo.isFile()) { // set the texture URL to the local texture that we have confirmed exists @@ -576,14 +573,14 @@ QUrl ModelBaker::getTextureURL(const QFileInfo& textureFileInfo, QString relativ // this is a relative file path which will require different handling // depending on the location of the original model - if (_modelURL.isLocalFile() && apparentRelativePath.exists() && apparentRelativePath.isFile()) { + if (_modelURL.isLocalFile() && textureFileInfo.exists() && textureFileInfo.isFile()) { // the absolute path we ran into for the texture in the model exists on this machine // so use that file - urlToTexture = QUrl::fromLocalFile(apparentRelativePath.absoluteFilePath()); + urlToTexture = QUrl::fromLocalFile(textureFileInfo.absoluteFilePath()); } else { // we didn't find the texture on this machine at the absolute path // so assume that it is right beside the model to match the behaviour of interface - urlToTexture = _modelURL.resolved(apparentRelativePath.fileName()); + urlToTexture = _modelURL.resolved(textureFileInfo.fileName()); } } } diff --git a/libraries/baking/src/ModelBaker.h b/libraries/baking/src/ModelBaker.h index 17af2604a2..45b0f4c6ca 100644 --- a/libraries/baking/src/ModelBaker.h +++ b/libraries/baking/src/ModelBaker.h @@ -98,7 +98,7 @@ private slots: void handleAbortedTexture(); private: - QUrl getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded = false); + QUrl getTextureURL(const QFileInfo& textureFileInfo, bool isEmbedded = false); void bakeTexture(const QUrl & textureURL, image::TextureUsage::Type textureType, const QDir & outputDir, const QString & bakedFilename, const QByteArray & textureContent); QString texturePathRelativeToModel(QUrl modelURL, QUrl textureURL); From b6c44ea4436e95ccbace9dad8e06fd7bd3c64ee0 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Fri, 15 Mar 2019 15:56:36 -0700 Subject: [PATCH 211/446] Remove unused variables when iterating through mesh nodes in FBXBaker --- libraries/baking/src/FBXBaker.cpp | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/libraries/baking/src/FBXBaker.cpp b/libraries/baking/src/FBXBaker.cpp index 371a492964..2189e7bdc3 100644 --- a/libraries/baking/src/FBXBaker.cpp +++ b/libraries/baking/src/FBXBaker.cpp @@ -123,21 +123,7 @@ void FBXBaker::rewriteAndBakeSceneModels(const QVector& meshes, const } } else if (object.name == "Model") { for (FBXNode& modelChild : object.children) { - bool properties = false; - hifi::ByteArray propertyName; - int index; - if (modelChild.name == "Properties60") { - properties = true; - propertyName = "Property"; - index = 3; - - } else if (modelChild.name == "Properties70") { - properties = true; - propertyName = "P"; - index = 4; - } - - if (properties) { + if (modelChild.name == "Properties60" || modelChild.name == "Properties70") { // This is a properties node // Remove the geometric transform because that has been applied directly to the vertices in FBXSerializer static const QVariant GEOMETRIC_TRANSLATION = hifi::ByteArray("GeometricTranslation"); From d4e1ec97418614d9a88393352c41ce71d8aa5eb2 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 15 Mar 2019 15:50:48 -0700 Subject: [PATCH 212/446] fix emitScriptEvent --- interface/src/Application.cpp | 7 +++++++ libraries/entities-renderer/src/RenderableEntityItem.h | 1 + .../entities-renderer/src/RenderableWebEntityItem.h | 2 +- libraries/entities/src/EntityItem.h | 2 -- libraries/entities/src/EntityScriptingInterface.cpp | 9 +-------- libraries/entities/src/EntityScriptingInterface.h | 1 - libraries/entities/src/EntityTree.cpp | 7 +++++++ libraries/entities/src/EntityTree.h | 4 ++++ 8 files changed, 21 insertions(+), 12 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index de4a6bb167..21ef706dfc 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1985,6 +1985,13 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo return nullptr; }); + EntityTree::setEmitScriptEventOperator([this](const QUuid& id, const QVariant& message) { + auto entities = getEntities(); + if (auto entity = entities->renderableForEntityId(id)) { + entity->emitScriptEvent(message); + } + }); + EntityTree::setTextSizeOperator([this](const QUuid& id, const QString& text) { auto entities = getEntities(); if (auto entity = entities->renderableForEntityId(id)) { diff --git a/libraries/entities-renderer/src/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h index e9a6035e3d..39f9ad091e 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.h +++ b/libraries/entities-renderer/src/RenderableEntityItem.h @@ -40,6 +40,7 @@ public: virtual bool wantsKeyboardFocus() const { return false; } virtual void setProxyWindow(QWindow* proxyWindow) {} virtual QObject* getEventHandler() { return nullptr; } + virtual void emitScriptEvent(const QVariant& message) {} const EntityItemPointer& getEntity() const { return _entity; } const ItemID& getRenderItemID() const { return _renderItemID; } diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.h b/libraries/entities-renderer/src/RenderableWebEntityItem.h index 0345898b62..7118774d30 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.h +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.h @@ -106,7 +106,7 @@ private: static std::function&, bool&, std::vector&)> _releaseWebSurfaceOperator; public slots: - void emitScriptEvent(const QVariant& scriptMessage); + void emitScriptEvent(const QVariant& scriptMessage) override; signals: void scriptEventReceived(const QVariant& message); diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index a9a8baa413..fae871a124 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -511,8 +511,6 @@ public: virtual void setProxyWindow(QWindow* proxyWindow) {} virtual QObject* getEventHandler() { return nullptr; } - virtual void emitScriptEvent(const QVariant& message) {} - QUuid getLastEditedBy() const { return _lastEditedBy; } void setLastEditedBy(QUuid value) { _lastEditedBy = value; } diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 22cd26eac6..55a36202a8 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -2202,14 +2202,7 @@ bool EntityScriptingInterface::wantsHandControllerPointerEvents(const QUuid& id) } void EntityScriptingInterface::emitScriptEvent(const EntityItemID& entityID, const QVariant& message) { - if (_entityTree) { - _entityTree->withReadLock([&] { - EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(entityID)); - if (entity) { - entity->emitScriptEvent(message); - } - }); - } + EntityTree::emitScriptEvent(entityID, message); } // TODO move this someplace that makes more sense... diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 0cf9070b08..f6aedac3fc 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -1529,7 +1529,6 @@ public slots: * @function Entities.emitScriptEvent * @param {Uuid} entityID - The ID of the {@link Entities.EntityType|Web} entity. * @param {string} message - The message to send. - * @todo This function is currently not implemented. */ Q_INVOKABLE void emitScriptEvent(const EntityItemID& entityID, const QVariant& message); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index d64c8870eb..8bf7c92b1f 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -2978,6 +2978,7 @@ QStringList EntityTree::getJointNames(const QUuid& entityID) const { std::function EntityTree::_getEntityObjectOperator = nullptr; std::function EntityTree::_textSizeOperator = nullptr; std::function EntityTree::_areEntityClicksCapturedOperator = nullptr; +std::function EntityTree::_emitScriptEventOperator = nullptr; QObject* EntityTree::getEntityObject(const QUuid& id) { if (_getEntityObjectOperator) { @@ -3000,6 +3001,12 @@ bool EntityTree::areEntityClicksCaptured() { return false; } +void EntityTree::emitScriptEvent(const QUuid& id, const QVariant& message) { + if (_emitScriptEventOperator) { + _emitScriptEventOperator(id, message); + } +} + void EntityTree::updateEntityQueryAACubeWorker(SpatiallyNestablePointer object, EntityEditPacketSender* packetSender, MovingEntitiesOperator& moveOperator, bool force, bool tellServer) { // if the queryBox has changed, tell the entity-server diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 39b3dc57c7..e627a07d13 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -272,6 +272,9 @@ public: static void setEntityClicksCapturedOperator(std::function areEntityClicksCapturedOperator) { _areEntityClicksCapturedOperator = areEntityClicksCapturedOperator; } static bool areEntityClicksCaptured(); + static void setEmitScriptEventOperator(std::function emitScriptEventOperator) { _emitScriptEventOperator = emitScriptEventOperator; } + static void emitScriptEvent(const QUuid& id, const QVariant& message); + std::map getNamedPaths() const { return _namedPaths; } void updateEntityQueryAACube(SpatiallyNestablePointer object, EntityEditPacketSender* packetSender, @@ -383,6 +386,7 @@ private: static std::function _getEntityObjectOperator; static std::function _textSizeOperator; static std::function _areEntityClicksCapturedOperator; + static std::function _emitScriptEventOperator; std::vector _staleProxies; From e1358fd5f5059d44074aeb3e54836ec06f6d6e17 Mon Sep 17 00:00:00 2001 From: David Back Date: Fri, 15 Mar 2019 16:28:46 -0700 Subject: [PATCH 213/446] prevent export from humanoid config, cleaner scene switch --- .../Editor/AvatarExporter/AvatarExporter.cs | 28 ++++++++++-------- .../AvatarExporter/HeightReference.prefab | 10 +++---- tools/unity-avatar-exporter/Assets/README.txt | 4 +-- .../avatarExporter.unitypackage | Bin 74591 -> 74615 bytes 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs index 87f401d478..11d83a52e8 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs @@ -17,7 +17,7 @@ using System.Text.RegularExpressions; class AvatarExporter : MonoBehaviour { // update version number for every PR that changes this file, also set updated version in README file - static readonly string AVATAR_EXPORTER_VERSION = "0.3.6"; + static readonly string AVATAR_EXPORTER_VERSION = "0.3.7"; static readonly float HIPS_GROUND_MIN_Y = 0.01f; static readonly float HIPS_SPINE_CHEST_MIN_SEPARATION = 0.001f; @@ -332,8 +332,7 @@ class AvatarExporter : MonoBehaviour { static List alternateStandardShaderMaterials = new List(); static List unsupportedShaderMaterials = new List(); - static Scene previewScene; - static string previousScene = ""; + static SceneSetup[] previousSceneSetup; static Vector3 previousScenePivot = Vector3.zero; static Quaternion previousSceneRotation = Quaternion.identity; static float previousSceneSize = 0.0f; @@ -1223,14 +1222,22 @@ class AvatarExporter : MonoBehaviour { } static bool OpenPreviewScene() { + // store the current scene setup to restore when closing the preview scene + previousSceneSetup = EditorSceneManager.GetSceneManagerSetup(); + + // if the user is currently in the Humanoid Avatar Configuration then inform them to close it first + if (EditorSceneManager.GetActiveScene().name == "Avatar Configuration" && previousSceneSetup.Length == 0) { + EditorUtility.DisplayDialog("Error", "Please exit the Avatar Configuration before exporting.", "Ok"); + return false; + } + // see if the user wants to save their current scene before opening preview avatar scene in place of user's scene if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) { return false; } - // store the user's current scene to re-open when done and open a new default scene in place of the user's scene - previousScene = EditorSceneManager.GetActiveScene().path; - previewScene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene); + // open a new empty scene in place of the user's scene + EditorSceneManager.NewScene(NewSceneSetup.EmptyScene); // instantiate a game object to preview the avatar and a game object for the height reference prefab at 0, 0, 0 UnityEngine.Object heightReferenceResource = AssetDatabase.LoadAssetAtPath(HEIGHT_REFERENCE_PREFAB, typeof(UnityEngine.Object)); @@ -1259,13 +1266,8 @@ class AvatarExporter : MonoBehaviour { DestroyImmediate(avatarPreviewObject); DestroyImmediate(heightReferenceObject); - // re-open the scene the user had open before switching to the preview scene - if (!string.IsNullOrEmpty(previousScene)) { - EditorSceneManager.OpenScene(previousScene); - } - - // close the preview scene and flag it to be removed - EditorSceneManager.CloseScene(previewScene, true); + // restore to the previous scene setup that the user had open before exporting + EditorSceneManager.RestoreSceneManagerSetup(previousSceneSetup); // restore the camera pivot and rotation to the user's previous scene settings var sceneView = SceneView.lastActiveSceneView; diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/HeightReference.prefab b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/HeightReference.prefab index 3a6b6b21fa..4464617387 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/HeightReference.prefab +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/HeightReference.prefab @@ -599,7 +599,7 @@ MeshRenderer: m_ReflectionProbeUsage: 1 m_RenderingLayerMask: 4294967295 m_Materials: - - {fileID: 2100000, guid: d1133891b03286946b3b0c63c1a57d08, type: 2} + - {fileID: 2100000, guid: 83f430ba33ffd544bab7a13796246d30, type: 2} m_StaticBatchInfo: firstSubMesh: 0 subMeshCount: 0 @@ -774,7 +774,7 @@ MeshRenderer: m_ReflectionProbeUsage: 1 m_RenderingLayerMask: 4294967295 m_Materials: - - {fileID: 2100000, guid: d1133891b03286946b3b0c63c1a57d08, type: 2} + - {fileID: 2100000, guid: 83f430ba33ffd544bab7a13796246d30, type: 2} m_StaticBatchInfo: firstSubMesh: 0 subMeshCount: 0 @@ -809,7 +809,7 @@ MeshRenderer: m_ReflectionProbeUsage: 1 m_RenderingLayerMask: 4294967295 m_Materials: - - {fileID: 2100000, guid: d1133891b03286946b3b0c63c1a57d08, type: 2} + - {fileID: 2100000, guid: 83f430ba33ffd544bab7a13796246d30, type: 2} m_StaticBatchInfo: firstSubMesh: 0 subMeshCount: 0 @@ -844,7 +844,7 @@ MeshRenderer: m_ReflectionProbeUsage: 1 m_RenderingLayerMask: 4294967295 m_Materials: - - {fileID: 2100000, guid: d1133891b03286946b3b0c63c1a57d08, type: 2} + - {fileID: 2100000, guid: 83f430ba33ffd544bab7a13796246d30, type: 2} m_StaticBatchInfo: firstSubMesh: 0 subMeshCount: 0 @@ -879,7 +879,7 @@ MeshRenderer: m_ReflectionProbeUsage: 1 m_RenderingLayerMask: 4294967295 m_Materials: - - {fileID: 2100000, guid: d1133891b03286946b3b0c63c1a57d08, type: 2} + - {fileID: 2100000, guid: 83f430ba33ffd544bab7a13796246d30, type: 2} m_StaticBatchInfo: firstSubMesh: 0 subMeshCount: 0 diff --git a/tools/unity-avatar-exporter/Assets/README.txt b/tools/unity-avatar-exporter/Assets/README.txt index 5b228ebf75..0b5cb49117 100644 --- a/tools/unity-avatar-exporter/Assets/README.txt +++ b/tools/unity-avatar-exporter/Assets/README.txt @@ -1,8 +1,8 @@ High Fidelity, Inc. Avatar Exporter -Version 0.3.6 +Version 0.3.7 -Note: It is recommended to use Unity versions between 2017.4.17f1 and 2018.2.12f1 for this Avatar Exporter. +Note: It is recommended to use Unity versions between 2017.4.15f1 and 2018.2.12f1 for this Avatar Exporter. To create a new avatar project: 1. Import your .fbx avatar model into your Unity project's Assets by either dragging and dropping the file into the Assets window or by using Assets menu > Import New Assets. diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage index 05ad49baa6bfa1696cb12f659750246f2a6d2d9e..d906cfc0a45055b79a0ed8667441799fea682af3 100644 GIT binary patch literal 74615 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

* particles = Entities.addEntity({ @@ -1658,6 +1665,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool // Particles only if (_type == EntityTypes::ParticleEffect) { COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_SHAPE_TYPE, shapeType, getShapeTypeAsString()); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COMPOUND_SHAPE_URL, compoundShapeURL); COPY_PROPERTY_TO_QSCRIPTVALUE_TYPED(PROP_COLOR, color, u8vec3Color); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ALPHA, alpha); _pulse.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties); @@ -3104,6 +3112,7 @@ OctreeElement::AppendState EntityItemProperties::encodeEntityEditPacket(PacketTy if (properties.getType() == EntityTypes::ParticleEffect) { APPEND_ENTITY_PROPERTY(PROP_SHAPE_TYPE, (uint32_t)(properties.getShapeType())); + APPEND_ENTITY_PROPERTY(PROP_COMPOUND_SHAPE_URL, properties.getCompoundShapeURL()); APPEND_ENTITY_PROPERTY(PROP_COLOR, properties.getColor()); APPEND_ENTITY_PROPERTY(PROP_ALPHA, properties.getAlpha()); _staticPulse.setProperties(properties); @@ -3584,6 +3593,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int if (properties.getType() == EntityTypes::ParticleEffect) { READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SHAPE_TYPE, ShapeType, setShapeType); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_COMPOUND_SHAPE_URL, QString, setCompoundShapeURL); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_COLOR, u8vec3Color, setColor); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ALPHA, float, setAlpha); properties.getPulse().decodeFromEditPacket(propertyFlags, dataAt, processedBytes); diff --git a/libraries/entities/src/ParticleEffectEntityItem.cpp b/libraries/entities/src/ParticleEffectEntityItem.cpp index b916ecc3de..801cf56b32 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.cpp +++ b/libraries/entities/src/ParticleEffectEntityItem.cpp @@ -410,6 +410,7 @@ EntityItemProperties ParticleEffectEntityItem::getProperties(const EntityPropert EntityItemProperties properties = EntityItem::getProperties(desiredProperties, allowEmptyDesiredProperties); // get the properties from our base class COPY_ENTITY_PROPERTY_TO_PROPERTIES(shapeType, getShapeType); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(compoundShapeURL, getCompoundShapeURL); COPY_ENTITY_PROPERTY_TO_PROPERTIES(color, getColor); COPY_ENTITY_PROPERTY_TO_PROPERTIES(alpha, getAlpha); withReadLock([&] { @@ -464,6 +465,7 @@ bool ParticleEffectEntityItem::setProperties(const EntityItemProperties& propert bool somethingChanged = EntityItem::setProperties(properties); // set the properties in our base class SET_ENTITY_PROPERTY_FROM_PROPERTIES(shapeType, setShapeType); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(compoundShapeURL, setCompoundShapeURL); SET_ENTITY_PROPERTY_FROM_PROPERTIES(color, setColor); SET_ENTITY_PROPERTY_FROM_PROPERTIES(alpha, setAlpha); withWriteLock([&] { @@ -540,6 +542,7 @@ int ParticleEffectEntityItem::readEntitySubclassDataFromBuffer(const unsigned ch const unsigned char* dataAt = data; READ_ENTITY_PROPERTY(PROP_SHAPE_TYPE, ShapeType, setShapeType); + READ_ENTITY_PROPERTY(PROP_COMPOUND_SHAPE_URL, QString, setCompoundShapeURL); READ_ENTITY_PROPERTY(PROP_COLOR, u8vec3Color, setColor); READ_ENTITY_PROPERTY(PROP_ALPHA, float, setAlpha); withWriteLock([&] { @@ -598,6 +601,7 @@ EntityPropertyFlags ParticleEffectEntityItem::getEntityProperties(EncodeBitstrea EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); requestedProperties += PROP_SHAPE_TYPE; + requestedProperties += PROP_COMPOUND_SHAPE_URL; requestedProperties += PROP_COLOR; requestedProperties += PROP_ALPHA; requestedProperties += _pulseProperties.getEntityProperties(params); @@ -656,6 +660,7 @@ void ParticleEffectEntityItem::appendSubclassData(OctreePacketData* packetData, bool successPropertyFits = true; APPEND_ENTITY_PROPERTY(PROP_SHAPE_TYPE, (uint32_t)getShapeType()); + APPEND_ENTITY_PROPERTY(PROP_COMPOUND_SHAPE_URL, getCompoundShapeURL()); APPEND_ENTITY_PROPERTY(PROP_COLOR, getColor()); APPEND_ENTITY_PROPERTY(PROP_ALPHA, getAlpha()); withReadLock([&] { @@ -726,6 +731,24 @@ void ParticleEffectEntityItem::setShapeType(ShapeType type) { }); } +ShapeType ParticleEffectEntityItem::getShapeType() const { + return resultWithReadLock([&] { + return _shapeType; + }); +} + +void ParticleEffectEntityItem::setCompoundShapeURL(const QString& compoundShapeURL) { + withWriteLock([&] { + _compoundShapeURL = compoundShapeURL; + }); +} + +QString ParticleEffectEntityItem::getCompoundShapeURL() const { + return resultWithReadLock([&] { + return _compoundShapeURL; + }); +} + void ParticleEffectEntityItem::setMaxParticles(quint32 maxParticles) { withWriteLock([&] { _particleProperties.maxParticles = glm::clamp(maxParticles, MINIMUM_MAX_PARTICLES, MAXIMUM_MAX_PARTICLES); diff --git a/libraries/entities/src/ParticleEffectEntityItem.h b/libraries/entities/src/ParticleEffectEntityItem.h index 0755d7868b..52f229201e 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.h +++ b/libraries/entities/src/ParticleEffectEntityItem.h @@ -79,6 +79,7 @@ namespace particle { static const QString DEFAULT_TEXTURES = ""; static const bool DEFAULT_EMITTER_SHOULD_TRAIL = false; static const bool DEFAULT_ROTATE_WITH_ENTITY = false; + static const ShapeType DEFAULT_SHAPE_TYPE = ShapeType::SHAPE_TYPE_ELLIPSOID; template struct Range { @@ -255,7 +256,10 @@ public: float getAlphaSpread() const { return _particleProperties.alpha.gradient.spread; } void setShapeType(ShapeType type) override; - virtual ShapeType getShapeType() const override { return _shapeType; } + virtual ShapeType getShapeType() const override; + + QString getCompoundShapeURL() const; + virtual void setCompoundShapeURL(const QString& url); virtual void debugDump() const override; @@ -349,7 +353,8 @@ protected: PulsePropertyGroup _pulseProperties; bool _isEmitting { true }; - ShapeType _shapeType { SHAPE_TYPE_NONE }; + ShapeType _shapeType{ particle::DEFAULT_SHAPE_TYPE }; + QString _compoundShapeURL { "" }; }; #endif // hifi_ParticleEffectEntityItem_h diff --git a/libraries/entities/src/ZoneEntityItem.cpp b/libraries/entities/src/ZoneEntityItem.cpp index 98b18869fc..f243d59da0 100644 --- a/libraries/entities/src/ZoneEntityItem.cpp +++ b/libraries/entities/src/ZoneEntityItem.cpp @@ -13,7 +13,6 @@ #include #include -#include #include @@ -463,9 +462,6 @@ void ZoneEntityItem::fetchCollisionGeometryResource() { if (hullURL.isEmpty()) { _shapeResource.reset(); } else { - QUrlQuery queryArgs(hullURL); - queryArgs.addQueryItem("collision-hull", ""); - hullURL.setQuery(queryArgs); _shapeResource = DependencyManager::get()->getCollisionGeometryResource(hullURL); } } From 7a5bbb8f6f42b59e85f8678e066cca7b3dfcfa4c Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 18 Mar 2019 14:43:32 -0700 Subject: [PATCH 224/446] fix laser ghosting --- .../src/RenderablePolyLineEntityItem.cpp | 90 +++++++++++-------- .../src/RenderablePolyLineEntityItem.h | 2 +- 2 files changed, 56 insertions(+), 36 deletions(-) diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp index 98f79780be..df52934b87 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp @@ -95,19 +95,18 @@ bool PolyLineEntityRenderer::needsRenderUpdate() const { } bool PolyLineEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const { - return ( - entity->pointsChanged() || - entity->widthsChanged() || - entity->normalsChanged() || - entity->texturesChanged() || - entity->colorsChanged() || - _isUVModeStretch != entity->getIsUVModeStretch() || - _glow != entity->getGlow() || - _faceCamera != entity->getFaceCamera() - ); + if (entity->pointsChanged() || entity->widthsChanged() || entity->normalsChanged() || entity->texturesChanged() || entity->colorsChanged()) { + return true; + } + + if (_isUVModeStretch != entity->getIsUVModeStretch() || _glow != entity->getGlow() || _faceCamera != entity->getFaceCamera()) { + return true; + } + + return Parent::needsRenderUpdateFromTypedEntity(entity); } -void PolyLineEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) { +void PolyLineEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) { auto pointsChanged = entity->pointsChanged(); auto widthsChanged = entity->widthsChanged(); auto normalsChanged = entity->normalsChanged(); @@ -119,10 +118,6 @@ void PolyLineEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo entity->resetPolyLineChanged(); - // Transform - updateModelTransformAndBound(); - _renderTransform = getModelTransform(); - // Textures if (entity->texturesChanged()) { entity->resetTexturesChanged(); @@ -131,7 +126,9 @@ void PolyLineEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo if (!textures.isEmpty()) { entityTextures = QUrl(textures); } - _texture = DependencyManager::get()->getTexture(entityTextures); + withWriteLock([&] { + _texture = DependencyManager::get()->getTexture(entityTextures); + }); _textureAspectRatio = 1.0f; _textureLoaded = false; } @@ -145,11 +142,13 @@ void PolyLineEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo // Data bool faceCameraChanged = faceCamera != _faceCamera; - if (faceCameraChanged || glow != _glow) { - _faceCamera = faceCamera; - _glow = glow; - updateData(); - } + withWriteLock([&] { + if (faceCameraChanged || glow != _glow) { + _faceCamera = faceCamera; + _glow = glow; + updateData(); + } + }); // Geometry if (pointsChanged) { @@ -165,10 +164,21 @@ void PolyLineEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo _colors = entity->getStrokeColors(); _color = toGlm(entity->getColor()); } - if (_isUVModeStretch != isUVModeStretch || pointsChanged || widthsChanged || normalsChanged || colorsChanged || textureChanged || faceCameraChanged) { - _isUVModeStretch = isUVModeStretch; - updateGeometry(); - } + + bool uvModeStretchChanged = _isUVModeStretch != isUVModeStretch; + _isUVModeStretch = isUVModeStretch; + + void* key = (void*)this; + AbstractViewStateInterface::instance()->pushPostUpdateLambda(key, [=]() { + withWriteLock([&] { + updateModelTransformAndBound(); + _renderTransform = getModelTransform(); + + if (uvModeStretchChanged || pointsChanged || widthsChanged || normalsChanged || colorsChanged || textureChanged || faceCameraChanged) { + updateGeometry(); + } + }); + }); } void PolyLineEntityRenderer::updateGeometry() { @@ -267,22 +277,32 @@ void PolyLineEntityRenderer::updateData() { } void PolyLineEntityRenderer::doRender(RenderArgs* args) { - if (_numVertices < 2) { - return; - } - PerformanceTimer perfTimer("RenderablePolyLineEntityItem::render"); Q_ASSERT(args->_batch); gpu::Batch& batch = *args->_batch; - if (!_pipeline || !_glowPipeline) { + size_t numVertices; + Transform transform; + gpu::TexturePointer texture; + withReadLock([&] { + numVertices = _numVertices; + transform = _renderTransform; + texture = _textureLoaded ? _texture->getGPUTexture() : DependencyManager::get()->getWhiteTexture(); + + batch.setResourceBuffer(0, _polylineGeometryBuffer); + batch.setUniformBuffer(0, _polylineDataBuffer); + }); + + if (numVertices < 2) { + return; + } + + if (!_pipeline) { buildPipeline(); } batch.setPipeline(_glow ? _glowPipeline : _pipeline); - batch.setModelTransform(_renderTransform); - batch.setResourceTexture(0, _textureLoaded ? _texture->getGPUTexture() : DependencyManager::get()->getWhiteTexture()); - batch.setResourceBuffer(0, _polylineGeometryBuffer); - batch.setUniformBuffer(0, _polylineDataBuffer); - batch.draw(gpu::TRIANGLE_STRIP, (gpu::uint32)(2 * _numVertices), 0); + batch.setModelTransform(transform); + batch.setResourceTexture(0, texture); + batch.draw(gpu::TRIANGLE_STRIP, (gpu::uint32)(2 * numVertices), 0); } diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h index b8a3ad3b3e..3815b57671 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h @@ -31,7 +31,7 @@ public: protected: virtual bool needsRenderUpdate() const override; virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override; - virtual void doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) override; + virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override; virtual ItemKey getKey() override; virtual ShapeKey getShapeKey() override; From 0990b56952da701f65c47e7160004aebcab00c97 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Fri, 15 Mar 2019 16:46:37 -0700 Subject: [PATCH 225/446] Better avoid overwriting textures in ModelBaker::compressTexture --- libraries/baking/src/ModelBaker.cpp | 47 ++++++++++++++--------------- libraries/baking/src/ModelBaker.h | 8 ++--- libraries/baking/src/TextureBaker.h | 2 ++ 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index b1f6e1d51b..977f773337 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -410,43 +410,40 @@ QString ModelBaker::compressTexture(QString modelTextureFileName, image::Texture } auto urlToTexture = getTextureURL(modelTextureFileInfo, !textureContent.isNull()); - QString baseTextureFileName; - if (_remappedTexturePaths.contains(urlToTexture)) { - baseTextureFileName = _remappedTexturePaths[urlToTexture]; - } else { + TextureKey textureKey { urlToTexture, textureType }; + auto bakingTextureIt = _bakingTextures.find(textureKey); + if (bakingTextureIt == _bakingTextures.cend()) { // construct the new baked texture file name and file path // ensuring that the baked texture will have a unique name // even if there was another texture with the same name at a different path - baseTextureFileName = _textureFileNamer.createBaseTextureFileName(modelTextureFileInfo, textureType); - _remappedTexturePaths[urlToTexture] = baseTextureFileName; - } + QString baseTextureFileName = _textureFileNamer.createBaseTextureFileName(modelTextureFileInfo, textureType); - qCDebug(model_baking).noquote() << "Re-mapping" << modelTextureFileName - << "to" << baseTextureFileName; + QString bakedTextureFilePath { + _bakedOutputDir + "/" + baseTextureFileName + BAKED_META_TEXTURE_SUFFIX + }; - QString bakedTextureFilePath { - _bakedOutputDir + "/" + baseTextureFileName + BAKED_META_TEXTURE_SUFFIX - }; + textureChild = baseTextureFileName + BAKED_META_TEXTURE_SUFFIX; - textureChild = baseTextureFileName + BAKED_META_TEXTURE_SUFFIX; - - if (!_bakingTextures.contains(urlToTexture)) { _outputFiles.push_back(bakedTextureFilePath); // bake this texture asynchronously - bakeTexture(urlToTexture, textureType, _bakedOutputDir, baseTextureFileName, textureContent); + bakeTexture(textureKey, _bakedOutputDir, baseTextureFileName, textureContent); + } else { + // Fetch existing texture meta name + textureChild = (*bakingTextureIt)->getBaseFilename() + BAKED_META_TEXTURE_SUFFIX; } } + + qCDebug(model_baking).noquote() << "Re-mapping" << modelTextureFileName + << "to" << textureChild; return textureChild; } -void ModelBaker::bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, - const QDir& outputDir, const QString& bakedFilename, const QByteArray& textureContent) { - +void ModelBaker::bakeTexture(const TextureKey& textureKey, const QDir& outputDir, const QString& bakedFilename, const QByteArray& textureContent) { // start a bake for this texture and add it to our list to keep track of QSharedPointer bakingTexture{ - new TextureBaker(textureURL, textureType, outputDir, "../", bakedFilename, textureContent), + new TextureBaker(textureKey.first, textureKey.second, outputDir, "../", bakedFilename, textureContent), &TextureBaker::deleteLater }; @@ -455,7 +452,7 @@ void ModelBaker::bakeTexture(const QUrl& textureURL, image::TextureUsage::Type t connect(bakingTexture.data(), &TextureBaker::aborted, this, &ModelBaker::handleAbortedTexture); // keep a shared pointer to the baking texture - _bakingTextures.insert(textureURL, bakingTexture); + _bakingTextures.insert(textureKey, bakingTexture); // start baking the texture on one of our available worker threads bakingTexture->moveToThread(_textureThreadGetter()); @@ -507,7 +504,7 @@ void ModelBaker::handleBakedTexture() { // now that this texture has been baked and handled, we can remove that TextureBaker from our hash - _bakingTextures.remove(bakedTexture->getTextureURL()); + _bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() }); checkIfTexturesFinished(); } else { @@ -518,7 +515,7 @@ void ModelBaker::handleBakedTexture() { _pendingErrorEmission = true; // now that this texture has been baked, even though it failed, we can remove that TextureBaker from our list - _bakingTextures.remove(bakedTexture->getTextureURL()); + _bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() }); // abort any other ongoing texture bakes since we know we'll end up failing for (auto& bakingTexture : _bakingTextures) { @@ -531,7 +528,7 @@ void ModelBaker::handleBakedTexture() { // we have errors to attend to, so we don't do extra processing for this texture // but we do need to remove that TextureBaker from our list // and then check if we're done with all textures - _bakingTextures.remove(bakedTexture->getTextureURL()); + _bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() }); checkIfTexturesFinished(); } @@ -545,7 +542,7 @@ void ModelBaker::handleAbortedTexture() { qDebug() << "Texture aborted: " << bakedTexture->getTextureURL(); if (bakedTexture) { - _bakingTextures.remove(bakedTexture->getTextureURL()); + _bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() }); } // since a texture we were baking aborted, our status is also aborted diff --git a/libraries/baking/src/ModelBaker.h b/libraries/baking/src/ModelBaker.h index 45b0f4c6ca..6b69d4d0ec 100644 --- a/libraries/baking/src/ModelBaker.h +++ b/libraries/baking/src/ModelBaker.h @@ -42,6 +42,8 @@ class ModelBaker : public Baker { Q_OBJECT public: + using TextureKey = QPair; + ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); virtual ~ModelBaker(); @@ -99,13 +101,11 @@ private slots: private: QUrl getTextureURL(const QFileInfo& textureFileInfo, bool isEmbedded = false); - void bakeTexture(const QUrl & textureURL, image::TextureUsage::Type textureType, const QDir & outputDir, - const QString & bakedFilename, const QByteArray & textureContent); + void bakeTexture(const TextureKey& textureKey, const QDir& outputDir, const QString& bakedFilename, const QByteArray& textureContent); QString texturePathRelativeToModel(QUrl modelURL, QUrl textureURL); - QMultiHash> _bakingTextures; + QMultiHash> _bakingTextures; QHash _textureNameMatchCount; - QHash _remappedTexturePaths; bool _pendingErrorEmission { false }; bool _hasBeenBaked { false }; diff --git a/libraries/baking/src/TextureBaker.h b/libraries/baking/src/TextureBaker.h index 9b86d875e9..4fc9680653 100644 --- a/libraries/baking/src/TextureBaker.h +++ b/libraries/baking/src/TextureBaker.h @@ -39,6 +39,8 @@ public: QUrl getTextureURL() const { return _textureURL; } + QString getBaseFilename() const { return _baseFilename; } + QString getMetaTextureFileName() const { return _metaTextureFileName; } virtual void setWasAborted(bool wasAborted) override; From bc3b35aad337938dad06bc908877c5eda560053a Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Mon, 18 Mar 2019 09:42:45 -0700 Subject: [PATCH 226/446] Do not consolidate source images by file for now, since they may have the same filename but different paths --- libraries/baking/src/TextureBaker.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/libraries/baking/src/TextureBaker.cpp b/libraries/baking/src/TextureBaker.cpp index dfc684ddee..d097b4765b 100644 --- a/libraries/baking/src/TextureBaker.cpp +++ b/libraries/baking/src/TextureBaker.cpp @@ -48,18 +48,24 @@ TextureBaker::TextureBaker(const QUrl& textureURL, image::TextureUsage::Type tex _baseFilename = originalFilename.left(originalFilename.lastIndexOf('.')); } - _originalCopyFilePath = _outputDirectory.absoluteFilePath(_textureURL.fileName()); + auto textureFilename = _textureURL.fileName(); + QString originalExtension; + int extensionStart = textureFilename.indexOf("."); + if (extensionStart != -1) { + originalExtension = textureFilename.mid(extensionStart); + } + _originalCopyFilePath = _outputDirectory.absoluteFilePath(_baseFilename + originalExtension); } void TextureBaker::bake() { // once our texture is loaded, kick off a the processing connect(this, &TextureBaker::originalTextureLoaded, this, &TextureBaker::processTexture); - if (_originalTexture.isEmpty() && !QFile(_originalCopyFilePath.toString()).exists()) { + if (_originalTexture.isEmpty()) { // first load the texture (either locally or remotely) loadTexture(); } else { - // we already have a texture passed to us, or the texture is already saved, so use that + // we already have a texture passed to us, use that emit originalTextureLoaded(); } } @@ -135,7 +141,7 @@ void TextureBaker::processTexture() { // Copy the original file into the baked output directory if it doesn't exist yet { QFile file { originalCopyFilePath }; - if (!file.exists() && (!file.open(QIODevice::WriteOnly) || file.write(_originalTexture) == -1)) { + if (!file.open(QIODevice::WriteOnly) || file.write(_originalTexture) == -1) { handleError("Could not write original texture for " + _textureURL.toString()); return; } From c29b3a8c351aae118df25e918bcccee9a4620976 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Mon, 18 Mar 2019 10:20:10 -0700 Subject: [PATCH 227/446] Bypass signals in JSBaker/MaterialBaker when resource is already loaded --- libraries/baking/src/JSBaker.cpp | 2 +- libraries/baking/src/MaterialBaker.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/baking/src/JSBaker.cpp b/libraries/baking/src/JSBaker.cpp index e5682cde20..96d7247a82 100644 --- a/libraries/baking/src/JSBaker.cpp +++ b/libraries/baking/src/JSBaker.cpp @@ -36,7 +36,7 @@ void JSBaker::bake() { loadScript(); } else { // we already have a script passed to us, use that - emit originalScriptLoaded(); + processScript(); } } diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp index 47604fa7dc..458e8ad482 100644 --- a/libraries/baking/src/MaterialBaker.cpp +++ b/libraries/baking/src/MaterialBaker.cpp @@ -57,7 +57,7 @@ void MaterialBaker::bake() { } else { // we already have a material passed to us, use that if (_materialResource->isLoaded()) { - emit originalMaterialLoaded(); + processMaterial(); } else { connect(_materialResource.data(), &Resource::finished, this, &MaterialBaker::originalMaterialLoaded); } From cf40ed953bf6aa6cc7413cefb065d371f546b16c Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Mon, 18 Mar 2019 11:56:34 -0700 Subject: [PATCH 228/446] Do not create temporary directory in ModelBaker and copy model directly to the original output folder --- libraries/baking/src/ModelBaker.cpp | 73 ++++++++---------------- libraries/baking/src/ModelBaker.h | 4 +- libraries/baking/src/baking/FSTBaker.cpp | 12 ++-- 3 files changed, 30 insertions(+), 59 deletions(-) diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index 977f773337..9568a81578 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -50,38 +50,12 @@ ModelBaker::ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter input _textureThreadGetter(inputTextureThreadGetter), _hasBeenBaked(hasBeenBaked) { - auto tempDir = PathUtils::generateTemporaryDir(); - - if (tempDir.isEmpty()) { - handleError("Failed to create a temporary directory."); - return; - } - - _modelTempDir = tempDir; - - _originalModelFilePath = _modelTempDir.filePath(_modelURL.fileName()); - qDebug() << "Made temporary dir " << _modelTempDir; - qDebug() << "Origin file path: " << _originalModelFilePath; - - { - auto bakedFilename = _modelURL.fileName(); - if (!hasBeenBaked) { - bakedFilename = bakedFilename.left(bakedFilename.lastIndexOf('.')); - bakedFilename += BAKED_FBX_EXTENSION; - } - _bakedModelURL = _bakedOutputDir + "/" + bakedFilename; - } -} - -ModelBaker::~ModelBaker() { - if (_modelTempDir.exists()) { - if (!_modelTempDir.remove(_originalModelFilePath)) { - qCWarning(model_baking) << "Failed to remove temporary copy of model file:" << _originalModelFilePath; - } - if (!_modelTempDir.rmdir(".")) { - qCWarning(model_baking) << "Failed to remove temporary directory:" << _modelTempDir; - } + auto bakedFilename = _modelURL.fileName(); + if (!hasBeenBaked) { + bakedFilename = bakedFilename.left(bakedFilename.lastIndexOf('.')); + bakedFilename += BAKED_FBX_EXTENSION; } + _bakedModelURL = _bakedOutputDir + "/" + bakedFilename; } void ModelBaker::setOutputURLSuffix(const QUrl& outputURLSuffix) { @@ -136,7 +110,8 @@ void ModelBaker::initializeOutputDirs() { } } - if (QDir(_originalOutputDir).exists()) { + QDir originalOutputDir { _originalOutputDir }; + if (originalOutputDir.exists()) { if (_mappingURL.isEmpty()) { qWarning() << "Output path" << _originalOutputDir << "already exists. Continuing."; } @@ -144,8 +119,16 @@ void ModelBaker::initializeOutputDirs() { qCDebug(model_baking) << "Creating original output folder" << _originalOutputDir; if (!QDir().mkpath(_originalOutputDir)) { handleError("Failed to create original output folder " + _originalOutputDir); + return; } } + + if (originalOutputDir.isReadable()) { + // The output directory is available. Use that to write/read the original model file + _originalOutputModelPath = originalOutputDir.filePath(_modelURL.fileName()); + } else { + handleError("Unable to write to original output folder " + _originalOutputDir); + } } void ModelBaker::saveSourceModel() { @@ -154,7 +137,7 @@ void ModelBaker::saveSourceModel() { // load up the local file QFile localModelURL { _modelURL.toLocalFile() }; - qDebug() << "Local file url: " << _modelURL << _modelURL.toString() << _modelURL.toLocalFile() << ", copying to: " << _originalModelFilePath; + qDebug() << "Local file url: " << _modelURL << _modelURL.toString() << _modelURL.toLocalFile() << ", copying to: " << _originalOutputModelPath; if (!localModelURL.exists()) { //QMessageBox::warning(this, "Could not find " + _modelURL.toString(), ""); @@ -162,13 +145,7 @@ void ModelBaker::saveSourceModel() { return; } - // make a copy in the output folder - if (!_originalOutputDir.isEmpty()) { - qDebug() << "Copying to: " << _originalOutputDir << "/" << _modelURL.fileName(); - localModelURL.copy(_originalOutputDir + "/" + _modelURL.fileName()); - } - - localModelURL.copy(_originalModelFilePath); + localModelURL.copy(_originalOutputModelPath); // emit our signal to start the import of the model source copy emit modelLoaded(); @@ -199,13 +176,13 @@ void ModelBaker::handleModelNetworkReply() { qCDebug(model_baking) << "Downloaded" << _modelURL; // grab the contents of the reply and make a copy in the output folder - QFile copyOfOriginal(_originalModelFilePath); + QFile copyOfOriginal(_originalOutputModelPath); - qDebug(model_baking) << "Writing copy of original model file to" << _originalModelFilePath << copyOfOriginal.fileName(); + qDebug(model_baking) << "Writing copy of original model file to" << _originalOutputModelPath << copyOfOriginal.fileName(); if (!copyOfOriginal.open(QIODevice::WriteOnly)) { // add an error to the error list for this model stating that a duplicate of the original model could not be made - handleError("Could not create copy of " + _modelURL.toString() + " (Failed to open " + _originalModelFilePath + ")"); + handleError("Could not create copy of " + _modelURL.toString() + " (Failed to open " + _originalOutputModelPath + ")"); return; } if (copyOfOriginal.write(requestReply->readAll()) == -1) { @@ -216,10 +193,6 @@ void ModelBaker::handleModelNetworkReply() { // close that file now that we are done writing to it copyOfOriginal.close(); - if (!_originalOutputDir.isEmpty()) { - copyOfOriginal.copy(_originalOutputDir + "/" + _modelURL.fileName()); - } - // emit our signal to start the import of the model source copy emit modelLoaded(); } else { @@ -229,9 +202,9 @@ void ModelBaker::handleModelNetworkReply() { } void ModelBaker::bakeSourceCopy() { - QFile modelFile(_originalModelFilePath); + QFile modelFile(_originalOutputModelPath); if (!modelFile.open(QIODevice::ReadOnly)) { - handleError("Error opening " + _originalModelFilePath + " for reading"); + handleError("Error opening " + _originalOutputModelPath + " for reading"); return; } hifi::ByteArray modelData = modelFile.readAll(); @@ -243,7 +216,7 @@ void ModelBaker::bakeSourceCopy() { { auto serializer = DependencyManager::get()->getSerializerForMediaType(modelData, _modelURL, ""); if (!serializer) { - handleError("Could not recognize file type of model file " + _originalModelFilePath); + handleError("Could not recognize file type of model file " + _originalOutputModelPath); return; } hifi::VariantHash serializerMapping = _mapping; diff --git a/libraries/baking/src/ModelBaker.h b/libraries/baking/src/ModelBaker.h index 6b69d4d0ec..d9a559392f 100644 --- a/libraries/baking/src/ModelBaker.h +++ b/libraries/baking/src/ModelBaker.h @@ -46,7 +46,6 @@ public: ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); - virtual ~ModelBaker(); void setOutputURLSuffix(const QUrl& urlSuffix); void setMappingURL(const QUrl& mappingURL); @@ -86,10 +85,9 @@ protected: QString _bakedOutputDir; QString _originalOutputDir; TextureBakerThreadGetter _textureThreadGetter; + QString _originalOutputModelPath; QString _outputMappingURL; QUrl _bakedModelURL; - QDir _modelTempDir; - QString _originalModelFilePath; protected slots: void handleModelNetworkReply(); diff --git a/libraries/baking/src/baking/FSTBaker.cpp b/libraries/baking/src/baking/FSTBaker.cpp index f76180bb58..acf3bfe1c7 100644 --- a/libraries/baking/src/baking/FSTBaker.cpp +++ b/libraries/baking/src/baking/FSTBaker.cpp @@ -49,9 +49,9 @@ void FSTBaker::bakeSourceCopy() { return; } - QFile fstFile(_originalModelFilePath); + QFile fstFile(_originalOutputModelPath); if (!fstFile.open(QIODevice::ReadOnly)) { - handleError("Error opening " + _originalModelFilePath + " for reading"); + handleError("Error opening " + _originalOutputModelPath + " for reading"); return; } @@ -60,25 +60,25 @@ void FSTBaker::bakeSourceCopy() { auto filenameField = _mapping[FILENAME_FIELD].toString(); if (filenameField.isEmpty()) { - handleError("The '" + FILENAME_FIELD + "' property in the FST file '" + _originalModelFilePath + "' could not be found"); + handleError("The '" + FILENAME_FIELD + "' property in the FST file '" + _originalOutputModelPath + "' could not be found"); return; } auto modelURL = _mappingURL.adjusted(QUrl::RemoveFilename).resolved(filenameField); auto bakeableModelURL = getBakeableModelURL(modelURL); if (bakeableModelURL.isEmpty()) { - handleError("The '" + FILENAME_FIELD + "' property in the FST file '" + _originalModelFilePath + "' could not be resolved to a valid bakeable model url"); + handleError("The '" + FILENAME_FIELD + "' property in the FST file '" + _originalOutputModelPath + "' could not be resolved to a valid bakeable model url"); return; } auto baker = getModelBakerWithOutputDirectories(bakeableModelURL, _textureThreadGetter, _bakedOutputDir, _originalOutputDir); _modelBaker = std::unique_ptr(dynamic_cast(baker.release())); if (!_modelBaker) { - handleError("The model url '" + bakeableModelURL.toString() + "' from the FST file '" + _originalModelFilePath + "' (property: '" + FILENAME_FIELD + "') could not be used to initialize a valid model baker"); + handleError("The model url '" + bakeableModelURL.toString() + "' from the FST file '" + _originalOutputModelPath + "' (property: '" + FILENAME_FIELD + "') could not be used to initialize a valid model baker"); return; } if (dynamic_cast(_modelBaker.get())) { // Could be interesting, but for now let's just prevent infinite FST loops in the most straightforward way possible - handleError("The FST file '" + _originalModelFilePath + "' (property: '" + FILENAME_FIELD + "') references another FST file. FST chaining is not supported."); + handleError("The FST file '" + _originalOutputModelPath + "' (property: '" + FILENAME_FIELD + "') references another FST file. FST chaining is not supported."); return; } _modelBaker->setMappingURL(_mappingURL); From 266f3a8ad8cbe90c5a87dcce3fb19ef9b781e45c Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Mon, 18 Mar 2019 12:09:35 -0700 Subject: [PATCH 229/446] Warn if handleFinishedTextureBaker doesn't recognize the sender --- libraries/baking/src/MaterialBaker.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp index 458e8ad482..dd1ba55e54 100644 --- a/libraries/baking/src/MaterialBaker.cpp +++ b/libraries/baking/src/MaterialBaker.cpp @@ -193,6 +193,8 @@ void MaterialBaker::handleFinishedTextureBaker() { if (_textureBakers.empty()) { outputMaterial(); } + } else { + handleWarning("Unidentified baker finished and signaled to material baker to handle texture. Material: " + _materialData); } } From 64fbf51ac25d8708e9e3410ac2ac4b45195a8601 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Mon, 18 Mar 2019 13:13:12 -0700 Subject: [PATCH 230/446] Warn if baked mesh is empty for OBJBaker --- libraries/baking/src/OBJBaker.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/baking/src/OBJBaker.cpp b/libraries/baking/src/OBJBaker.cpp index ebc24201f4..70bdeb2071 100644 --- a/libraries/baking/src/OBJBaker.cpp +++ b/libraries/baking/src/OBJBaker.cpp @@ -119,7 +119,7 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, const hfm::Model::Pointer& h // Store the draco node containing the compressed mesh information, along with the per-meshPart material IDs the draco node references // Because we redefine the material IDs when initializing the material nodes above, we pass that in for the material list // The nth mesh part gets the nth material - { + if (!dracoMesh.isEmpty()) { std::vector newMaterialList; newMaterialList.reserve(_materialIDs.size()); for (auto materialID : _materialIDs) { @@ -128,6 +128,8 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, const hfm::Model::Pointer& h FBXNode dracoNode; buildDracoMeshNode(dracoNode, dracoMesh, newMaterialList); geometryNode.children.append(dracoNode); + } else { + handleWarning("Baked mesh for OBJ model '" + _modelURL.toString() + "' is empty"); } // Generating Texture Node From e4cafced2ad7afb0421538a3d6e64b42b492466b Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Mon, 18 Mar 2019 13:42:07 -0700 Subject: [PATCH 231/446] Re-name GRAP_KEY to GRAB_KEY in DomainBaker.cpp --- tools/oven/src/DomainBaker.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 544786f03e..3970238ab5 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -302,7 +302,7 @@ const QString TYPE_KEY = "type"; // Models const QString MODEL_URL_KEY = "modelURL"; const QString COMPOUND_SHAPE_URL_KEY = "compoundShapeURL"; -const QString GRAP_KEY = "grab"; +const QString GRAB_KEY = "grab"; const QString EQUIPPABLE_INDICATOR_URL_KEY = "equippableIndicatorURL"; const QString ANIMATION_KEY = "animation"; const QString ANIMATION_URL_KEY = "url"; @@ -354,10 +354,10 @@ void DomainBaker::enumerateEntities() { addModelBaker(ANIMATION_KEY + "." + ANIMATION_URL_KEY, animationObject[ANIMATION_URL_KEY].toString(), *it); } } - if (entity.contains(GRAP_KEY)) { - auto grabObject = entity[GRAP_KEY].toObject(); + if (entity.contains(GRAB_KEY)) { + auto grabObject = entity[GRAB_KEY].toObject(); if (grabObject.contains(EQUIPPABLE_INDICATOR_URL_KEY)) { - addModelBaker(GRAP_KEY + "." + EQUIPPABLE_INDICATOR_URL_KEY, grabObject[EQUIPPABLE_INDICATOR_URL_KEY].toString(), *it); + addModelBaker(GRAB_KEY + "." + EQUIPPABLE_INDICATOR_URL_KEY, grabObject[EQUIPPABLE_INDICATOR_URL_KEY].toString(), *it); } } From e6487332e8c5c458067e0c3943b64c4269585045 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Mon, 18 Mar 2019 14:25:34 -0700 Subject: [PATCH 232/446] Attempt to fix linker error with Android and draco in BuildDracoMeshTask.cpp --- libraries/model-baker/src/model-baker/Baker.cpp | 2 ++ .../model-baker/src/model-baker/BuildDracoMeshTask.cpp | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp index 7bb53376ed..536255a841 100644 --- a/libraries/model-baker/src/model-baker/Baker.cpp +++ b/libraries/model-baker/src/model-baker/Baker.cpp @@ -164,6 +164,8 @@ namespace baker { // Build Draco meshes // NOTE: This task is disabled by default and must be enabled through configuration // TODO: Tangent support (Needs changes to FBXSerializer_Mesh as well) + // NOTE: Due to an unresolved linker error, BuildDracoMeshTask is not functional on Android + // TODO: Figure out why BuildDracoMeshTask.cpp won't link with draco on Android const auto buildDracoMeshInputs = BuildDracoMeshTask::Input(meshesIn, normalsPerMesh, tangentsPerMesh).asVarying(); const auto buildDracoMeshOutputs = model.addJob("BuildDracoMesh", buildDracoMeshInputs); const auto dracoMeshes = buildDracoMeshOutputs.getN(0); diff --git a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp index 46b170fd25..2e378965de 100644 --- a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp +++ b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp @@ -22,8 +22,11 @@ #pragma GCC diagnostic ignored "-Wsign-compare" #endif + +#ifndef Q_OS_ANDROID #include #include +#endif #ifdef _WIN32 #pragma warning( pop ) @@ -35,6 +38,7 @@ #include "ModelBakerLogging.h" #include "ModelMath.h" +#ifndef Q_OS_ANDROID std::vector createMaterialList(const hfm::Mesh& mesh) { std::vector materialList; for (const auto& meshPart : mesh.parts) { @@ -199,6 +203,7 @@ std::unique_ptr createDracoMesh(const hfm::Mesh& mesh, const std::v return dracoMesh; } +#endif // not Q_OS_ANDROID void BuildDracoMeshTask::configure(const Config& config) { _encodeSpeed = config.encodeSpeed; @@ -206,6 +211,9 @@ void BuildDracoMeshTask::configure(const Config& config) { } void BuildDracoMeshTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) { +#ifdef Q_OS_ANDROID + qCWarning(model_baker) << "BuildDracoMesh is disabled on Android. Output meshes will be empty."; +#else const auto& meshes = input.get0(); const auto& normalsPerMesh = input.get1(); const auto& tangentsPerMesh = input.get2(); @@ -239,4 +247,5 @@ void BuildDracoMeshTask::run(const baker::BakeContextPointer& context, const Inp dracoBytes = hifi::ByteArray(buffer.data(), (int)buffer.size()); } } +#endif // not Q_OS_ANDROID } From b0b4307f27cfae415552067e0c2870b526abf376 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Mon, 18 Mar 2019 15:58:25 -0700 Subject: [PATCH 233/446] Fix unsupported model types preventing DomainBaker from finishing --- tools/oven/src/DomainBaker.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 3970238ab5..e21b1e4435 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -176,12 +176,12 @@ void DomainBaker::addModelBaker(const QString& property, const QString& url, con // keep track of the total number of baking entities ++_totalNumberOfSubBakes; + + // add this QJsonValueRef to our multi hash so that we can easily re-write + // the model URL to the baked version once the baker is complete + _entitiesNeedingRewrite.insert(bakeableModelURL, { property, jsonRef }); } } - - // add this QJsonValueRef to our multi hash so that we can easily re-write - // the model URL to the baked version once the baker is complete - _entitiesNeedingRewrite.insert(bakeableModelURL, { property, jsonRef }); } } From fa6a94f16a4c65e817dde0a04fa099345e51d1b8 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Mon, 18 Mar 2019 17:21:05 -0700 Subject: [PATCH 234/446] Fix not mapping identical URLs in DomainBaker --- tools/oven/src/DomainBaker.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index e21b1e4435..05745aad24 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -150,7 +150,8 @@ void DomainBaker::addModelBaker(const QString& property, const QString& url, con QUrl bakeableModelURL = getBakeableModelURL(url); if (!bakeableModelURL.isEmpty() && (_shouldRebakeOriginals || !isModelBaked(bakeableModelURL))) { // setup a ModelBaker for this URL, as long as we don't already have one - if (!_modelBakers.contains(bakeableModelURL)) { + bool haveBaker = _modelBakers.contains(bakeableModelURL); + if (!haveBaker) { auto getWorkerThreadCallback = []() -> QThread* { return Oven::instance().getNextWorkerThread(); }; @@ -168,6 +169,7 @@ void DomainBaker::addModelBaker(const QString& property, const QString& url, con // insert it into our bakers hash so we hold a strong pointer to it _modelBakers.insert(bakeableModelURL, baker); + haveBaker = true; // move the baker to the baker thread // and kickoff the bake @@ -176,12 +178,14 @@ void DomainBaker::addModelBaker(const QString& property, const QString& url, con // keep track of the total number of baking entities ++_totalNumberOfSubBakes; - - // add this QJsonValueRef to our multi hash so that we can easily re-write - // the model URL to the baked version once the baker is complete - _entitiesNeedingRewrite.insert(bakeableModelURL, { property, jsonRef }); } } + + if (haveBaker) { + // add this QJsonValueRef to our multi hash so that we can easily re-write + // the model URL to the baked version once the baker is complete + _entitiesNeedingRewrite.insert(bakeableModelURL, { property, jsonRef }); + } } } From b6e583087e16a768f389c22f414acd35ad9382e2 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Mon, 18 Mar 2019 17:40:01 -0700 Subject: [PATCH 235/446] Stand-alone Tagging: tagged places not coming first in GOTO list --- interface/resources/qml/hifi/Feed.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/Feed.qml b/interface/resources/qml/hifi/Feed.qml index 718ebc9331..a9fde05d8d 100644 --- a/interface/resources/qml/hifi/Feed.qml +++ b/interface/resources/qml/hifi/Feed.qml @@ -54,7 +54,7 @@ Column { 'require_online=true', 'protocol=' + encodeURIComponent(Window.protocolSignature()) ]; - endpoint: '/api/v1/user_stories?' + options.join('&'); + endpoint: '/api/v1/user_stories?' + options.join('&') + (PlatformInfo.isStandalone() ? '&standalone_optimized=true' : '') itemsPerPage: 4; processPage: function (data) { return data.user_stories.map(makeModelData); From 0e4d3b2aeb07b93729d9b55ad5ea485ac89c5b9a Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 18 Mar 2019 17:40:54 -0700 Subject: [PATCH 236/446] allow baseURL to be specified in fst --- libraries/model-networking/src/model-networking/ModelCache.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index a48f96eb1b..d588b711c9 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -120,6 +120,8 @@ void GeometryMappingResource::downloadFinished(const QByteArray& data) { if (filename.isNull()) { finishedLoading(false); } else { + const QString baseURL = _mapping.value("baseURL").toString(); + _url = _effectiveBaseURL.resolved(baseURL); QUrl url = _url.resolved(filename); QString texdir = _mapping.value(TEXDIR_FIELD).toString(); From ad2c4718a396c6c5065be5736269b76b44bc7919 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 18 Mar 2019 12:13:36 -0700 Subject: [PATCH 237/446] Fix loading relative files --- libraries/gpu/src/gpu/FrameReader.cpp | 2 +- tools/noramlizeFrame.py | 62 --------------------------- tools/normalizeFrame.py | 7 ++- 3 files changed, 6 insertions(+), 65 deletions(-) delete mode 100644 tools/noramlizeFrame.py diff --git a/libraries/gpu/src/gpu/FrameReader.cpp b/libraries/gpu/src/gpu/FrameReader.cpp index 2fe143ee90..812f4db7cc 100644 --- a/libraries/gpu/src/gpu/FrameReader.cpp +++ b/libraries/gpu/src/gpu/FrameReader.cpp @@ -40,7 +40,7 @@ public: auto lastSlash = filename.rfind('/'); result = filename.substr(0, lastSlash + 1); } else { - std::string result = QFileInfo(filename.c_str()).absoluteDir().canonicalPath().toStdString(); + result = QFileInfo(filename.c_str()).absoluteDir().canonicalPath().toStdString(); if (*result.rbegin() != '/') { result += '/'; } diff --git a/tools/noramlizeFrame.py b/tools/noramlizeFrame.py deleted file mode 100644 index f1012f6428..0000000000 --- a/tools/noramlizeFrame.py +++ /dev/null @@ -1,62 +0,0 @@ -import os -import json -import shutil -import sys - -def scriptRelative(*paths): - scriptdir = os.path.dirname(os.path.realpath(sys.argv[0])) - result = os.path.join(scriptdir, *paths) - result = os.path.realpath(result) - result = os.path.normcase(result) - return result - - - -class FrameProcessor: - def __init__(self, filename): - self.filename = filename - dir, name = os.path.split(self.filename) - self.dir = dir - self.ktxDir = os.path.join(self.dir, 'ktx') - os.makedirs(self.ktxDir, exist_ok=True) - self.resDir = scriptRelative("../interface/resources") - - if (name.endswith(".json")): - self.name = name[0:-5] - else: - self.name = name - self.filename = self.filename + '.json' - - with open(self.filename, 'r') as f: - self.json = json.load(f) - - - def processKtx(self, texture): - if texture is None: return - if not 'ktxFile' in texture: return - sourceKtx = texture['ktxFile'] - if sourceKtx.startswith(':'): - sourceKtx = sourceKtx[1:] - while sourceKtx.startswith('/'): - sourceKtx = sourceKtx[1:] - sourceKtx = os.path.join(self.resDir, sourceKtx) - sourceKtxDir, sourceKtxName = os.path.split(sourceKtx) - destKtx = os.path.join(self.ktxDir, sourceKtxName) - if not os.path.isfile(destKtx): - shutil.copy(sourceKtx, destKtx) - newValue = 'ktx/' + sourceKtxName - texture['ktxFile'] = newValue - - - def process(self): - for texture in self.json['textures']: - self.processKtx(texture) - - with open(self.filename, 'w') as f: - json.dump(self.json, f, indent=2) - -fp = FrameProcessor("D:/Frames/20190114_1629.json") -fp.process() - - -#C:\Users\bdavi\git\hifi\interface\resources\meshes \ No newline at end of file diff --git a/tools/normalizeFrame.py b/tools/normalizeFrame.py index 36b2038d5c..892609bbf0 100644 --- a/tools/normalizeFrame.py +++ b/tools/normalizeFrame.py @@ -36,7 +36,10 @@ class FrameProcessor: if not 'ktxFile' in texture: return sourceKtx = texture['ktxFile'] if sourceKtx.startswith(':'): - sourceKtx = os.path.join(self.resDir, sourceKtx[3:]) + sourceKtx = sourceKtx[1:] + while sourceKtx.startswith('/'): + sourceKtx = sourceKtx[1:] + sourceKtx = os.path.join(self.resDir, sourceKtx) sourceKtxDir, sourceKtxName = os.path.split(sourceKtx) destKtx = os.path.join(self.ktxDir, sourceKtxName) if not os.path.isfile(destKtx): @@ -52,7 +55,7 @@ class FrameProcessor: with open(self.filename, 'w') as f: json.dump(self.json, f, indent=2) -fp = FrameProcessor("D:/Frames/20190110_1635.json") +fp = FrameProcessor("D:/Frames/20190112_1647.json") fp.process() From 718eed8d5b8494046b970ded5f5fb36a18be64f2 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Tue, 19 Mar 2019 09:58:44 -0700 Subject: [PATCH 238/446] Corrected typo. --- cmake/macros/TargetPython.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/macros/TargetPython.cmake b/cmake/macros/TargetPython.cmake index cd0ea0f34c..2c055cf8bc 100644 --- a/cmake/macros/TargetPython.cmake +++ b/cmake/macros/TargetPython.cmake @@ -1,7 +1,7 @@ macro(TARGET_PYTHON) if (NOT HIFI_PYTHON_EXEC) # Find the python interpreter - if (CAME_VERSION VERSION_LESS 3.12) + if (CMAKE_VERSION VERSION_LESS 3.12) # this logic is deprecated in CMake after 3.12 # FIXME eventually we should make 3.12 the min cmake verion and just use the Python3 find_package path set(Python_ADDITIONAL_VERSIONS 3) From 61fb65b5a42209b2baf378715f8b71af43777738 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 19 Mar 2019 12:41:58 -0700 Subject: [PATCH 239/446] don't set _url, so that cache_clearing works --- .../src/model-networking/ModelCache.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index d588b711c9..6f4cbfa253 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -121,20 +121,20 @@ void GeometryMappingResource::downloadFinished(const QByteArray& data) { finishedLoading(false); } else { const QString baseURL = _mapping.value("baseURL").toString(); - _url = _effectiveBaseURL.resolved(baseURL); - QUrl url = _url.resolved(filename); + const QUrl base = _effectiveBaseURL.resolved(baseURL); + QUrl url = base.resolved(filename); QString texdir = _mapping.value(TEXDIR_FIELD).toString(); if (!texdir.isNull()) { if (!texdir.endsWith('/')) { texdir += '/'; } - _textureBaseUrl = resolveTextureBaseUrl(url, _url.resolved(texdir)); + _textureBaseUrl = resolveTextureBaseUrl(url, base.resolved(texdir)); } else { _textureBaseUrl = url.resolved(QUrl(".")); } - auto scripts = FSTReader::getScripts(_url, _mapping); + auto scripts = FSTReader::getScripts(base, _mapping); if (scripts.size() > 0) { _mapping.remove(SCRIPT_FIELD); for (auto &scriptPath : scripts) { @@ -147,7 +147,7 @@ void GeometryMappingResource::downloadFinished(const QByteArray& data) { if (animGraphVariant.isValid()) { QUrl fstUrl(animGraphVariant.toString()); if (fstUrl.isValid()) { - _animGraphOverrideUrl = _url.resolved(fstUrl); + _animGraphOverrideUrl = base.resolved(fstUrl); } else { _animGraphOverrideUrl = QUrl(); } @@ -156,7 +156,7 @@ void GeometryMappingResource::downloadFinished(const QByteArray& data) { } auto modelCache = DependencyManager::get(); - GeometryExtra extra { GeometryMappingPair(_url, _mapping), _textureBaseUrl, false }; + GeometryExtra extra { GeometryMappingPair(base, _mapping), _textureBaseUrl, false }; // Get the raw GeometryResource _geometryResource = modelCache->getResource(url, QUrl(), &extra, std::hash()(extra)).staticCast(); From 432a3f1610d0ccbe0004a0ab2fa14a3880727c56 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 19 Mar 2019 12:59:03 -0700 Subject: [PATCH 240/446] uniform sampling of ellipsoid points --- .../src/RenderableParticleEffectEntityItem.cpp | 9 +++++---- libraries/entities/src/ParticleEffectEntityItem.cpp | 13 +++++++++++++ libraries/networking/src/udt/PacketHeaders.h | 1 + 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp index 2168347554..df393e691c 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp @@ -239,7 +239,7 @@ ParticleEffectEntityRenderer::CpuParticle ParticleEffectEntityRenderer::createPa azimuth = azimuthStart + (TWO_PI + azimuthFinish - azimuthStart) * randFloat(); } - if (emitDimensions == Vectors::ZERO || shapeType == ShapeType::SHAPE_TYPE_NONE) { + if (emitDimensions == Vectors::ZERO) { // Point emitDirection = glm::quat(glm::vec3(PI_OVER_TWO - elevation, 0.0f, azimuth)) * Vectors::UNIT_Z; } else { @@ -265,9 +265,10 @@ ParticleEffectEntityRenderer::CpuParticle ParticleEffectEntityRenderer::createPa default: { float radiusScale = 1.0f; if (emitRadiusStart < 1.0f) { - float randRadius = - emitRadiusStart + randFloatInRange(0.0f, particle::MAXIMUM_EMIT_RADIUS_START - emitRadiusStart); - radiusScale = 1.0f - std::pow(1.0f - randRadius, 3.0f); + float innerRadiusCubed = emitRadiusStart * emitRadiusStart * emitRadiusStart; + float outerRadiusCubed = 1.0f; // pow(particle::MAXIMUM_EMIT_RADIUS_START, 3); + float randRadiusCubed = randFloatInRange(innerRadiusCubed, outerRadiusCubed); + radiusScale = std::cbrt(randRadiusCubed); } glm::vec3 radii = radiusScale * 0.5f * emitDimensions; diff --git a/libraries/entities/src/ParticleEffectEntityItem.cpp b/libraries/entities/src/ParticleEffectEntityItem.cpp index 801cf56b32..b9f723d880 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.cpp +++ b/libraries/entities/src/ParticleEffectEntityItem.cpp @@ -723,6 +723,19 @@ void ParticleEffectEntityItem::debugDump() const { } void ParticleEffectEntityItem::setShapeType(ShapeType type) { + switch (type) { + case SHAPE_TYPE_NONE: + case SHAPE_TYPE_HULL: + case SHAPE_TYPE_SIMPLE_HULL: + case SHAPE_TYPE_SIMPLE_COMPOUND: + case SHAPE_TYPE_STATIC_MESH: + // these types are unsupported for ParticleEffectEntity + type = particle::DEFAULT_SHAPE_TYPE; + break; + default: + break; + } + withWriteLock([&] { if (type != _shapeType) { _shapeType = type; diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 0ec7c40ca4..48aecbc791 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -266,6 +266,7 @@ enum class EntityVersion : PacketVersion { ModelScale, ReOrderParentIDProperties, CertificateTypeProperty, + ParticleShapeType, // Add new versions above here NUM_PACKET_TYPE, From 51ab8880e93eeaa927d0267417e731615a47c139 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Tue, 19 Mar 2019 13:25:40 -0700 Subject: [PATCH 241/446] Corrected labels. --- tools/nitpick/ui/Nitpick.ui | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/nitpick/ui/Nitpick.ui b/tools/nitpick/ui/Nitpick.ui index a0f368863d..e1d7699f22 100644 --- a/tools/nitpick/ui/Nitpick.ui +++ b/tools/nitpick/ui/Nitpick.ui @@ -46,7 +46,7 @@ - 5 + 0 @@ -620,7 +620,7 @@ <html><head/><body><p>If unchecked, will not show results during evaluation</p></body></html> - usePreviousInstallation + Use Previous Installation false @@ -895,7 +895,7 @@ <html><head/><body><p>If unchecked, will not show results during evaluation</p></body></html> - usePreviousInstallation + Use Previous Installation false From 94739aa7c7377f7c3cf5a4ade17082e7e2fa8f16 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Tue, 19 Mar 2019 13:32:44 -0700 Subject: [PATCH 242/446] Case 21769 - updatable items not showing up as updatable after clicking 'show updates' button. --- .../resources/qml/hifi/commerce/purchases/PurchasedItem.qml | 2 +- interface/resources/qml/hifi/commerce/purchases/Purchases.qml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml index a7b36eae36..0e3402a6a9 100644 --- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml +++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml @@ -49,7 +49,7 @@ Item { property string wornEntityID; property string updatedItemId; property string upgradeTitle; - property bool updateAvailable: root.updateItemId && root.updateItemId !== ""; + property bool updateAvailable: root.updateItemId !== ""; property bool valid; property bool standaloneOptimized; property bool standaloneIncompatible; diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 46bbb626c6..9a2bf62e08 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -523,9 +523,9 @@ Rectangle { item.cardBackVisible = false; item.isInstalled = root.installedApps.indexOf(item.id) > -1; item.wornEntityID = ''; + item.upgrade_id = item.upgrade_id ? item.upgrade_id : ""; }); sendToScript({ method: 'purchases_updateWearables' }); - return data.assets; } } @@ -545,7 +545,7 @@ Rectangle { delegate: PurchasedItem { itemName: title; itemId: id; - updateItemId: model.upgrade_id ? model.upgrade_id : ""; + updateItemId: model.upgrade_id itemPreviewImageUrl: preview; itemHref: download_url; certificateId: certificate_id; From 194008c77a745e56b50128474a3baadebd231cd3 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 19 Mar 2019 14:14:37 -0700 Subject: [PATCH 243/446] box shapeType --- .../RenderableParticleEffectEntityItem.cpp | 83 ++++++++++++------- .../entities/src/ParticleEffectEntityItem.cpp | 3 + 2 files changed, 57 insertions(+), 29 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp index df393e691c..1d384cc3b5 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp @@ -98,12 +98,6 @@ void ParticleEffectEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePoi _timeUntilNextEmit = 0; withWriteLock([&] { _particleProperties = newParticleProperties; - _shapeType = entity->getShapeType(); - QString compoundShapeURL = entity->getCompoundShapeURL(); - if (_compoundShapeURL != compoundShapeURL) { - _compoundShapeURL = compoundShapeURL; - fetchGeometryResource(); - } if (!_prevEmitterShouldTrailInitialized) { _prevEmitterShouldTrailInitialized = true; _prevEmitterShouldTrail = _particleProperties.emission.shouldTrail; @@ -113,6 +107,12 @@ void ParticleEffectEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePoi withWriteLock([&] { _pulseProperties = entity->getPulseProperties(); + _shapeType = entity->getShapeType(); + QString compoundShapeURL = entity->getCompoundShapeURL(); + if (_compoundShapeURL != compoundShapeURL) { + _compoundShapeURL = compoundShapeURL; + fetchGeometryResource(); + } }); _emitting = entity->getIsEmitting(); @@ -193,7 +193,28 @@ Item::Bound ParticleEffectEntityRenderer::getBound() { return _bound; } -static const size_t VERTEX_PER_PARTICLE = 4; +// FIXME: these methods assume uniform emitDimensions, need to importance sample based on dimensions +float importanceSample2Dimension(float startDim) { + float dimension = 1.0f; + if (startDim < 1.0f) { + float innerDimensionSquared = startDim * startDim; + float outerDimensionSquared = 1.0f; // pow(particle::MAXIMUM_EMIT_RADIUS_START, 2); + float randDimensionSquared = randFloatInRange(innerDimensionSquared, outerDimensionSquared); + dimension = std::cbrt(randDimensionSquared); + } + return dimension; +} + +float importanceSample3DDimension(float startDim) { + float dimension = 1.0f; + if (startDim < 1.0f) { + float innerDimensionCubed = startDim * startDim * startDim; + float outerDimensionCubed = 1.0f; // pow(particle::MAXIMUM_EMIT_RADIUS_START, 3); + float randDimensionCubed = randFloatInRange(innerDimensionCubed, outerDimensionCubed); + dimension = std::cbrt(randDimensionCubed); + } + return dimension; +} ParticleEffectEntityRenderer::CpuParticle ParticleEffectEntityRenderer::createParticle(uint64_t now, const Transform& baseTransform, const particle::Properties& particleProperties, const ShapeType& shapeType, const GeometryResource::Pointer& geometryResource) { @@ -245,33 +266,35 @@ ParticleEffectEntityRenderer::CpuParticle ParticleEffectEntityRenderer::createPa } else { glm::vec3 emitPosition; switch (shapeType) { - case ShapeType::SHAPE_TYPE_BOX: + case SHAPE_TYPE_BOX: { + glm::vec3 dim = importanceSample3DDimension(emitRadiusStart) * 0.5f * emitDimensions; - case ShapeType::SHAPE_TYPE_CAPSULE_X: - case ShapeType::SHAPE_TYPE_CAPSULE_Y: - case ShapeType::SHAPE_TYPE_CAPSULE_Z: + int side = randIntInRange(0, 5); + int axis = side % 3; + float direction = side > 2 ? 1.0f : -1.0f; - case ShapeType::SHAPE_TYPE_CYLINDER_X: - case ShapeType::SHAPE_TYPE_CYLINDER_Y: - case ShapeType::SHAPE_TYPE_CYLINDER_Z: + emitDirection[axis] = direction; + emitPosition[axis] = direction * dim[axis]; + axis = (axis + 1) % 3; + emitPosition[axis] = dim[axis] * randFloatInRange(-1.0f, 1.0f); + axis = (axis + 1) % 3; + emitPosition[axis] = dim[axis] * randFloatInRange(-1.0f, 1.0f); + break; + } - case ShapeType::SHAPE_TYPE_CIRCLE: - case ShapeType::SHAPE_TYPE_PLANE: + case SHAPE_TYPE_CYLINDER_X: + case SHAPE_TYPE_CYLINDER_Y: + case SHAPE_TYPE_CYLINDER_Z: - case ShapeType::SHAPE_TYPE_COMPOUND: + case SHAPE_TYPE_CIRCLE: + case SHAPE_TYPE_PLANE: - case ShapeType::SHAPE_TYPE_SPHERE: - case ShapeType::SHAPE_TYPE_ELLIPSOID: + case SHAPE_TYPE_COMPOUND: + + case SHAPE_TYPE_SPHERE: + case SHAPE_TYPE_ELLIPSOID: default: { - float radiusScale = 1.0f; - if (emitRadiusStart < 1.0f) { - float innerRadiusCubed = emitRadiusStart * emitRadiusStart * emitRadiusStart; - float outerRadiusCubed = 1.0f; // pow(particle::MAXIMUM_EMIT_RADIUS_START, 3); - float randRadiusCubed = randFloatInRange(innerRadiusCubed, outerRadiusCubed); - radiusScale = std::cbrt(randRadiusCubed); - } - - glm::vec3 radii = radiusScale * 0.5f * emitDimensions; + glm::vec3 radii = importanceSample3DDimension(emitRadiusStart) * 0.5f * emitDimensions; float x = radii.x * glm::cos(elevation) * glm::cos(azimuth); float y = radii.y * glm::cos(elevation) * glm::sin(azimuth); float z = radii.z * glm::sin(elevation); @@ -279,6 +302,7 @@ ParticleEffectEntityRenderer::CpuParticle ParticleEffectEntityRenderer::createPa emitDirection = glm::normalize(glm::vec3(radii.x > 0.0f ? x / (radii.x * radii.x) : 0.0f, radii.y > 0.0f ? y / (radii.y * radii.y) : 0.0f, radii.z > 0.0f ? z / (radii.z * radii.z) : 0.0f)); + break; } } @@ -313,7 +337,7 @@ void ParticleEffectEntityRenderer::stepSimulation() { const auto& modelTransform = getModelTransform(); if (_emitting && particleProperties.emitting() && - (_shapeType != ShapeType::SHAPE_TYPE_COMPOUND || (_geometryResource && _geometryResource->isLoaded()))) { + (_shapeType != SHAPE_TYPE_COMPOUND || (_geometryResource && _geometryResource->isLoaded()))) { uint64_t emitInterval = particleProperties.emitIntervalUsecs(); if (emitInterval > 0 && interval >= _timeUntilNextEmit) { auto timeRemaining = interval; @@ -395,6 +419,7 @@ void ParticleEffectEntityRenderer::doRender(RenderArgs* args) { batch.setInputBuffer(0, _particleBuffer, 0, sizeof(GpuParticle)); auto numParticles = _particleBuffer->getSize() / sizeof(GpuParticle); + static const size_t VERTEX_PER_PARTICLE = 4; batch.drawInstanced((gpu::uint32)numParticles, gpu::TRIANGLE_STRIP, (gpu::uint32)VERTEX_PER_PARTICLE); } diff --git a/libraries/entities/src/ParticleEffectEntityItem.cpp b/libraries/entities/src/ParticleEffectEntityItem.cpp index b9f723d880..5f285ca91b 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.cpp +++ b/libraries/entities/src/ParticleEffectEntityItem.cpp @@ -725,6 +725,9 @@ void ParticleEffectEntityItem::debugDump() const { void ParticleEffectEntityItem::setShapeType(ShapeType type) { switch (type) { case SHAPE_TYPE_NONE: + case SHAPE_TYPE_CAPSULE_X: + case SHAPE_TYPE_CAPSULE_Y: + case SHAPE_TYPE_CAPSULE_Z: case SHAPE_TYPE_HULL: case SHAPE_TYPE_SIMPLE_HULL: case SHAPE_TYPE_SIMPLE_COMPOUND: From 775eddc2657df459e76b365cade537fc2252ee0f Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Tue, 19 Mar 2019 16:16:53 -0700 Subject: [PATCH 244/446] Agent requires the ModelCache singleton for zone entities w/ meshes --- assignment-client/src/Agent.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 5c644cb132..3937d5f799 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -52,6 +52,8 @@ #include #include // TODO: consider moving to scriptengine.h +#include + #include "entities/AssignmentParentFinder.h" #include "AssignmentDynamicFactory.h" #include "RecordingScriptingInterface.h" @@ -99,6 +101,9 @@ Agent::Agent(ReceivedMessage& message) : DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + // Needed to ensure the creation of the DebugDraw instance on the main thread DebugDraw::getInstance(); @@ -819,6 +824,9 @@ void Agent::aboutToFinish() { DependencyManager::get()->cleanup(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); // cleanup the AudioInjectorManager (and any still running injectors) From 19c51b25d1d28878fbe692322774e62905d2fea9 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Tue, 19 Mar 2019 22:38:16 +0100 Subject: [PATCH 245/446] don't ignore updates that originate from entityPropertiesTool itself --- scripts/system/edit.js | 7 ++++--- scripts/system/html/js/entityProperties.js | 2 +- scripts/system/libraries/entitySelectionTool.js | 9 ++++++--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 2c3785217c..ca2918a108 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -2286,14 +2286,15 @@ var PropertiesTool = function (opts) { }) }; - function updateSelections(selectionUpdated) { + function updateSelections(selectionUpdated, caller) { if (blockPropertyUpdates) { return; } var data = { type: 'update', - spaceMode: selectionDisplay.getSpaceMode() + spaceMode: selectionDisplay.getSpaceMode(), + isPropertiesToolUpdate: caller === this, }; if (selectionUpdated) { @@ -2339,7 +2340,7 @@ var PropertiesTool = function (opts) { emitScriptEvent(data); } - selectionManager.addEventListener(updateSelections); + selectionManager.addEventListener(updateSelections, this); var onWebEventReceived = function(data) { diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index f501df7933..f259b0a017 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -3326,7 +3326,7 @@ function loaded() { let hasSelectedEntityChanged = lastEntityID !== '"' + selectedEntityProperties.id + '"'; - if (!hasSelectedEntityChanged && document.hasFocus()) { + if (!data.isPropertiesToolUpdate && !hasSelectedEntityChanged && document.hasFocus()) { // in case the selection has not changed and we still have focus on the properties page, // we will ignore the event. return; diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 269283ea6d..064dafec06 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -128,8 +128,11 @@ SelectionManager = (function() { } }; - that.addEventListener = function(func) { - listeners.push(func); + that.addEventListener = function(func, thisContext) { + listeners.push({ + callback: func, + thisContext: thisContext + }); }; that.hasSelection = function() { @@ -572,7 +575,7 @@ SelectionManager = (function() { for (var j = 0; j < listeners.length; j++) { try { - listeners[j](selectionUpdated === true, caller); + listeners[j].callback.call(listeners[j].thisContext, selectionUpdated === true, caller); } catch (e) { print("ERROR: entitySelectionTool.update got exception: " + JSON.stringify(e)); } From 640b05304a8a72abe2591e157da6470edf9f7931 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Tue, 19 Mar 2019 16:24:07 -0700 Subject: [PATCH 246/446] Case 21392 - Excise unneeded code from marketplacesInject.js --- scripts/system/html/js/marketplacesInject.js | 365 ------------------- 1 file changed, 365 deletions(-) diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index 8d408169ba..56075a514e 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -28,10 +28,6 @@ var xmlHttpRequest = null; var isPreparing = false; // Explicitly track download request status. - var limitedCommerce = false; - var commerceMode = false; - var userIsLoggedIn = false; - var walletNeedsSetup = false; var marketplaceBaseURL = "https://highfidelity.com"; var messagesWaiting = false; @@ -109,356 +105,6 @@ }); } - emitWalletSetupEvent = function () { - EventBridge.emitWebEvent(JSON.stringify({ - type: "WALLET_SETUP" - })); - }; - - function maybeAddSetupWalletButton() { - if (!$('body').hasClass("walletsetup-injected") && userIsLoggedIn && walletNeedsSetup) { - $('body').addClass("walletsetup-injected"); - - var resultsElement = document.getElementById('results'); - var setupWalletElement = document.createElement('div'); - setupWalletElement.classList.add("row"); - setupWalletElement.id = "setupWalletDiv"; - setupWalletElement.style = "height:60px;margin:20px 10px 10px 10px;padding:12px 5px;" + - "background-color:#D6F4D8;border-color:#aee9b2;border-width:2px;border-style:solid;border-radius:5px;"; - - var span = document.createElement('span'); - span.style = "margin:10px 5px;color:#1b6420;font-size:15px;"; - span.innerHTML = "Activate your Wallet to get money and shop in Marketplace."; - - var xButton = document.createElement('a'); - xButton.id = "xButton"; - xButton.setAttribute('href', "#"); - xButton.style = "width:50px;height:100%;margin:0;color:#ccc;font-size:20px;"; - xButton.innerHTML = "X"; - xButton.onclick = function () { - setupWalletElement.remove(); - dummyRow.remove(); - }; - - setupWalletElement.appendChild(span); - setupWalletElement.appendChild(xButton); - - resultsElement.insertBefore(setupWalletElement, resultsElement.firstChild); - - // Dummy row for padding - var dummyRow = document.createElement('div'); - dummyRow.classList.add("row"); - dummyRow.style = "height:15px;"; - resultsElement.insertBefore(dummyRow, resultsElement.firstChild); - } - } - - function maybeAddLogInButton() { - if (!$('body').hasClass("login-injected") && !userIsLoggedIn) { - $('body').addClass("login-injected"); - var resultsElement = document.getElementById('results'); - if (!resultsElement) { // If we're on the main page, this will evaluate to `true` - resultsElement = document.getElementById('item-show'); - resultsElement.style = 'margin-top:0;'; - } - var logInElement = document.createElement('div'); - logInElement.classList.add("row"); - logInElement.id = "logInDiv"; - logInElement.style = "height:60px;margin:20px 10px 10px 10px;padding:5px;" + - "background-color:#D6F4D8;border-color:#aee9b2;border-width:2px;border-style:solid;border-radius:5px;"; - - var button = document.createElement('a'); - button.classList.add("btn"); - button.classList.add("btn-default"); - button.id = "logInButton"; - button.setAttribute('href', "#"); - button.innerHTML = "LOG IN"; - button.style = "width:80px;height:100%;margin-top:0;margin-left:10px;padding:13px;font-weight:bold;background:linear-gradient(white, #ccc);"; - button.onclick = function () { - EventBridge.emitWebEvent(JSON.stringify({ - type: "LOGIN" - })); - }; - - var span = document.createElement('span'); - span.style = "margin:10px;color:#1b6420;font-size:15px;"; - span.innerHTML = "to get items from the Marketplace."; - - var xButton = document.createElement('a'); - xButton.id = "xButton"; - xButton.setAttribute('href', "#"); - xButton.style = "width:50px;height:100%;margin:0;color:#ccc;font-size:20px;"; - xButton.innerHTML = "X"; - xButton.onclick = function () { - logInElement.remove(); - dummyRow.remove(); - }; - - logInElement.appendChild(button); - logInElement.appendChild(span); - logInElement.appendChild(xButton); - - resultsElement.insertBefore(logInElement, resultsElement.firstChild); - - // Dummy row for padding - var dummyRow = document.createElement('div'); - dummyRow.classList.add("row"); - dummyRow.style = "height:15px;"; - resultsElement.insertBefore(dummyRow, resultsElement.firstChild); - } - } - - function changeDropdownMenu() { - var logInOrOutButton = document.createElement('a'); - logInOrOutButton.id = "logInOrOutButton"; - logInOrOutButton.setAttribute('href', "#"); - logInOrOutButton.innerHTML = userIsLoggedIn ? "Log Out" : "Log In"; - logInOrOutButton.onclick = function () { - EventBridge.emitWebEvent(JSON.stringify({ - type: "LOGIN" - })); - }; - - $($('.dropdown-menu').find('li')[0]).append(logInOrOutButton); - - $('a[href="/marketplace?view=mine"]').each(function () { - $(this).attr('href', '#'); - $(this).on('click', function () { - EventBridge.emitWebEvent(JSON.stringify({ - type: "MY_ITEMS" - })); - }); - }); - } - - function buyButtonClicked(id, referrer, edition) { - EventBridge.emitWebEvent(JSON.stringify({ - type: "CHECKOUT", - itemId: id, - referrer: referrer, - itemEdition: edition - })); - } - - function injectBuyButtonOnMainPage() { - var cost; - - // Unbind original mouseenter and mouseleave behavior - $('body').off('mouseenter', '#price-or-edit .price'); - $('body').off('mouseleave', '#price-or-edit .price'); - - $('.grid-item').find('#price-or-edit').each(function () { - $(this).css({ "margin-top": "0" }); - }); - - $('.grid-item').find('#price-or-edit').find('a').each(function() { - if ($(this).attr('href') !== '#') { // Guard necessary because of the AJAX nature of Marketplace site - $(this).attr('data-href', $(this).attr('href')); - $(this).attr('href', '#'); - } - cost = $(this).closest('.col-xs-3').find('.item-cost').text(); - var costInt = parseInt(cost, 10); - - $(this).closest('.col-xs-3').prev().attr("class", 'col-xs-6'); - $(this).closest('.col-xs-3').attr("class", 'col-xs-6'); - - var priceElement = $(this).find('.price'); - var available = true; - - if (priceElement.text() === 'invalidated' || - priceElement.text() === 'sold out' || - priceElement.text() === 'not for sale') { - available = false; - priceElement.css({ - "padding": "3px 5px 10px 5px", - "height": "40px", - "background": "linear-gradient(#a2a2a2, #fefefe)", - "color": "#000", - "font-weight": "600", - "line-height": "34px" - }); - } else { - priceElement.css({ - "padding": "3px 5px", - "height": "40px", - "background": "linear-gradient(#00b4ef, #0093C5)", - "color": "#FFF", - "font-weight": "600", - "line-height": "34px" - }); - } - - if (parseInt(cost) > 0) { - priceElement.css({ "width": "auto" }); - - if (available) { - priceElement.html(' ' + cost); - } - - priceElement.css({ "min-width": priceElement.width() + 30 }); - } - }); - - // change pricing to GET/BUY on button hover - $('body').on('mouseenter', '#price-or-edit .price', function () { - var $this = $(this); - var buyString = "BUY"; - var getString = "GET"; - // Protection against the button getting stuck in the "BUY"/"GET" state. - // That happens when the browser gets two MOUSEENTER events before getting a - // MOUSELEAVE event. Also, if not available for sale, just return. - if ($this.text() === buyString || - $this.text() === getString || - $this.text() === 'invalidated' || - $this.text() === 'sold out' || - $this.text() === 'not for sale' ) { - return; - } - $this.data('initialHtml', $this.html()); - - var cost = $(this).parent().siblings().text(); - if (parseInt(cost) > 0) { - $this.text(buyString); - } - if (parseInt(cost) == 0) { - $this.text(getString); - } - }); - - $('body').on('mouseleave', '#price-or-edit .price', function () { - var $this = $(this); - $this.html($this.data('initialHtml')); - }); - - - $('.grid-item').find('#price-or-edit').find('a').on('click', function () { - var price = $(this).closest('.grid-item').find('.price').text(); - if (price === 'invalidated' || - price === 'sold out' || - price === 'not for sale') { - return false; - } - buyButtonClicked($(this).closest('.grid-item').attr('data-item-id'), - "mainPage", - -1); - }); - } - - function injectUnfocusOnSearch() { - // unfocus input field on search, thus hiding virtual keyboard - $('#search-box').on('submit', function () { - if (document.activeElement) { - document.activeElement.blur(); - } - }); - } - - // fix for 10108 - marketplace category cannot scroll - function injectAddScrollbarToCategories() { - $('#categories-dropdown').on('show.bs.dropdown', function () { - $('body > div.container').css('display', 'none') - $('#categories-dropdown > ul.dropdown-menu').css({ 'overflow': 'auto', 'height': 'calc(100vh - 110px)' }); - }); - - $('#categories-dropdown').on('hide.bs.dropdown', function () { - $('body > div.container').css('display', ''); - $('#categories-dropdown > ul.dropdown-menu').css({ 'overflow': '', 'height': '' }); - }); - } - - function injectHiFiCode() { - if (commerceMode) { - maybeAddLogInButton(); - maybeAddSetupWalletButton(); - - if (!$('body').hasClass("code-injected")) { - - $('body').addClass("code-injected"); - changeDropdownMenu(); - - var target = document.getElementById('templated-items'); - // MutationObserver is necessary because the DOM is populated after the page is loaded. - // We're searching for changes to the element whose ID is '#templated-items' - this is - // the element that gets filled in by the AJAX. - var observer = new MutationObserver(function (mutations) { - mutations.forEach(function (mutation) { - injectBuyButtonOnMainPage(); - }); - }); - var config = { attributes: true, childList: true, characterData: true }; - observer.observe(target, config); - - // Try this here in case it works (it will if the user just pressed the "back" button, - // since that doesn't trigger another AJAX request. - injectBuyButtonOnMainPage(); - } - } - - injectUnfocusOnSearch(); - injectAddScrollbarToCategories(); - } - - function injectHiFiItemPageCode() { - if (commerceMode) { - maybeAddLogInButton(); - - if (!$('body').hasClass("code-injected")) { - - $('body').addClass("code-injected"); - changeDropdownMenu(); - - var purchaseButton = $('#side-info').find('.btn').first(); - - var href = purchaseButton.attr('href'); - purchaseButton.attr('href', '#'); - var cost = $('.item-cost').text(); - var costInt = parseInt(cost, 10); - var availability = $.trim($('.item-availability').text()); - if (limitedCommerce && (costInt > 0)) { - availability = ''; - } - if (availability === 'available') { - purchaseButton.css({ - "background": "linear-gradient(#00b4ef, #0093C5)", - "color": "#FFF", - "font-weight": "600", - "padding-bottom": "10px" - }); - } else { - purchaseButton.css({ - "background": "linear-gradient(#a2a2a2, #fefefe)", - "color": "#000", - "font-weight": "600", - "padding-bottom": "10px" - }); - } - - var type = $('.item-type').text(); - var isUpdating = window.location.href.indexOf('edition=') > -1; - var urlParams = new URLSearchParams(window.location.search); - if (isUpdating) { - purchaseButton.html('UPDATE FOR FREE'); - } else if (availability !== 'available') { - purchaseButton.html('UNAVAILABLE ' + (availability ? ('(' + availability + ')') : '')); - } else if (parseInt(cost) > 0 && $('#side-info').find('#buyItemButton').size() === 0) { - purchaseButton.html('PURCHASE ' + cost); - } - - purchaseButton.on('click', function () { - if ('available' === availability || isUpdating) { - buyButtonClicked(window.location.pathname.split("/")[3], - "itemPage", - urlParams.get('edition')); - } - }); - } - } - - injectUnfocusOnSearch(); - } - function updateClaraCode() { // Have to repeatedly update Clara page because its content can change dynamically without location.href changing. @@ -695,16 +341,9 @@ case DIRECTORY: injectDirectoryCode(); break; - case HIFI: - injectHiFiCode(); - break; case CLARA: injectClaraCode(); break; - case HIFI_ITEM_PAGE: - injectHiFiItemPageCode(); - break; - } } @@ -717,10 +356,6 @@ cancelClaraDownload(); } else if (message.type === "marketplaces") { if (message.action === "commerceSetting") { - limitedCommerce = !!message.data.limitedCommerce; - commerceMode = !!message.data.commerceMode; - userIsLoggedIn = !!message.data.userIsLoggedIn; - walletNeedsSetup = !!message.data.walletNeedsSetup; marketplaceBaseURL = message.data.metaverseServerURL; if (marketplaceBaseURL.indexOf('metaverse.') !== -1) { marketplaceBaseURL = marketplaceBaseURL.replace('metaverse.', ''); From ab61f65ea2897bec5ed2973c6a5825e73603fbbc Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Tue, 19 Mar 2019 17:08:04 -0700 Subject: [PATCH 247/446] CR fix --- interface/resources/qml/hifi/NameCard.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 4e578f8274..141ddf0077 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -368,7 +368,7 @@ Item { enabled: selected hoverEnabled: true onClicked: { - if(has3DHTML) { + if (has3DHTML) { userInfoViewer.url = Account.metaverseServerURL + "/users/" + userName; userInfoViewer.visible = true; } From 27fa0dc4c683d699233085a0d67d62a0729e83bc Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Tue, 19 Mar 2019 17:24:19 -0700 Subject: [PATCH 248/446] CR fixes --- .../resources/qml/hifi/commerce/marketplace/Marketplace.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index a9f058fce1..3402f919fb 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -359,7 +359,7 @@ Rectangle { } onAccepted: { - if(root.searchString !== searchField.text) { + if (root.searchString !== searchField.text) { root.searchString = searchField.text; getMarketplaceItems(); searchField.forceActiveFocus(); From 38864e5b6e581917cb1f172f39f6fde36ddcfd77 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 19 Mar 2019 18:04:32 -0700 Subject: [PATCH 249/446] plane, circle, cylinders --- .../RenderableParticleEffectEntityItem.cpp | 47 +++++++++++++++++-- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp index 1d384cc3b5..8883108f75 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp @@ -194,13 +194,13 @@ Item::Bound ParticleEffectEntityRenderer::getBound() { } // FIXME: these methods assume uniform emitDimensions, need to importance sample based on dimensions -float importanceSample2Dimension(float startDim) { +float importanceSample2DDimension(float startDim) { float dimension = 1.0f; if (startDim < 1.0f) { float innerDimensionSquared = startDim * startDim; float outerDimensionSquared = 1.0f; // pow(particle::MAXIMUM_EMIT_RADIUS_START, 2); float randDimensionSquared = randFloatInRange(innerDimensionSquared, outerDimensionSquared); - dimension = std::cbrt(randDimensionSquared); + dimension = std::sqrt(randDimensionSquared); } return dimension; } @@ -259,6 +259,7 @@ ParticleEffectEntityRenderer::CpuParticle ParticleEffectEntityRenderer::createPa } else { azimuth = azimuthStart + (TWO_PI + azimuthFinish - azimuthStart) * randFloat(); } + // TODO: azimuth and elevation are only used for ellipsoids, but could be used for other shapes too if (emitDimensions == Vectors::ZERO) { // Point @@ -284,10 +285,46 @@ ParticleEffectEntityRenderer::CpuParticle ParticleEffectEntityRenderer::createPa case SHAPE_TYPE_CYLINDER_X: case SHAPE_TYPE_CYLINDER_Y: - case SHAPE_TYPE_CYLINDER_Z: + case SHAPE_TYPE_CYLINDER_Z: { + glm::vec3 radii = importanceSample2DDimension(emitRadiusStart) * 0.5f * emitDimensions; + int axis = shapeType - SHAPE_TYPE_CYLINDER_X; - case SHAPE_TYPE_CIRCLE: - case SHAPE_TYPE_PLANE: + emitPosition[axis] = emitDimensions[axis] * randFloatInRange(-0.5f, 0.5f); + emitDirection[axis] = 0.0f; + axis = (axis + 1) % 3; + emitPosition[axis] = radii[axis] * glm::cos(azimuth); + emitDirection[axis] = radii[axis] > 0.0f ? emitPosition[axis] / (radii[axis] * radii[axis]) : 0.0f; + axis = (axis + 1) % 3; + emitPosition[axis] = radii[axis] * glm::sin(azimuth); + emitDirection[axis] = radii[axis] > 0.0f ? emitPosition[axis] / (radii[axis] * radii[axis]) : 0.0f; + emitDirection = glm::normalize(emitDirection); + break; + } + + case SHAPE_TYPE_CIRCLE: { // FIXME: SHAPE_TYPE_CIRCLE is not exposed to scripts in buildStringToShapeTypeLookup() + glm::vec2 radii = importanceSample2DDimension(emitRadiusStart) * 0.5f * glm::vec2(emitDimensions.x, emitDimensions.z); + float x = radii.x * glm::cos(azimuth); + float z = radii.y * glm::sin(azimuth); + emitPosition = glm::vec3(x, 0.0f, z); + emitDirection = Vectors::UP; + break; + } + case SHAPE_TYPE_PLANE: { + glm::vec2 dim = importanceSample2DDimension(emitRadiusStart) * 0.5f * glm::vec2(emitDimensions.x, emitDimensions.z); + + int side = randIntInRange(0, 3); + int axis = side % 2; + float direction = side > 1 ? 1.0f : -1.0f; + + glm::vec2 pos; + pos[axis] = direction * dim[axis]; + axis = (axis + 1) % 2; + pos[axis] = dim[axis] * randFloatInRange(-1.0f, 1.0f); + + emitPosition = glm::vec3(pos.x, 0.0f, pos.y); + emitDirection = Vectors::UP; + break; + } case SHAPE_TYPE_COMPOUND: From 9d11e44b4bed1ed0ea32b2c62f28f452719336da Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 20 Mar 2019 11:37:16 -0700 Subject: [PATCH 250/446] update AvatarEntity trait when parentID changes --- interface/src/avatar/MyAvatar.cpp | 23 +++++++++++-------- interface/src/avatar/MyAvatar.h | 2 +- .../src/avatars-renderer/Avatar.cpp | 2 +- .../src/avatars-renderer/Avatar.h | 2 +- .../entities/src/EntityEditPacketSender.cpp | 11 ++------- .../entities/src/EntityEditPacketSender.h | 4 ++-- 6 files changed, 21 insertions(+), 23 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 02ef91cdba..ddedc270f8 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1570,7 +1570,7 @@ void MyAvatar::handleChangedAvatarEntityData() { entityTree->withWriteLock([&] { EntityItemPointer entity = entityTree->addEntity(id, properties); if (entity) { - packetSender->queueEditEntityMessage(PacketType::EntityAdd, entityTree, id, properties); + packetSender->queueEditAvatarEntityMessage(entityTree, id); } }); } @@ -3451,10 +3451,10 @@ float MyAvatar::getGravity() { } void MyAvatar::setSessionUUID(const QUuid& sessionUUID) { - QUuid oldID = getSessionUUID(); + QUuid oldSessionID = getSessionUUID(); Avatar::setSessionUUID(sessionUUID); - QUuid id = getSessionUUID(); - if (id != oldID) { + QUuid newSessionID = getSessionUUID(); + if (newSessionID != oldSessionID) { auto treeRenderer = DependencyManager::get(); EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; if (entityTree) { @@ -3462,15 +3462,20 @@ void MyAvatar::setSessionUUID(const QUuid& sessionUUID) { _avatarEntitiesLock.withReadLock([&] { avatarEntityIDs = _packedAvatarEntityData.keys(); }); + EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender(); entityTree->withWriteLock([&] { for (const auto& entityID : avatarEntityIDs) { auto entity = entityTree->findEntityByID(entityID); if (!entity) { continue; } - entity->setOwningAvatarID(id); - if (entity->getParentID() == oldID) { - entity->setParentID(id); + entity->setOwningAvatarID(newSessionID); + // NOTE: each attached AvatarEntity should already have the correct updated parentID + // via magic in SpatiallyNestable, but when an AvatarEntity IS parented to MyAvatar + // we need to update the "packedAvatarEntityData" we send to the avatar-mixer + // so that others will get the updated state. + if (entity->getParentID() == newSessionID) { + packetSender->queueEditAvatarEntityMessage(entityTree, entityID); } } }); @@ -5523,14 +5528,14 @@ void MyAvatar::initFlowFromFST() { } } -void MyAvatar::sendPacket(const QUuid& entityID, const EntityItemProperties& properties) const { +void MyAvatar::sendPacket(const QUuid& entityID) const { auto treeRenderer = DependencyManager::get(); EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; if (entityTree) { entityTree->withWriteLock([&] { // force an update packet EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender(); - packetSender->queueEditEntityMessage(PacketType::EntityEdit, entityTree, entityID, properties); + packetSender->queueEditAvatarEntityMessage(entityTree, entityID); }); } } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index aadc8ee268..905216cfba 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1918,7 +1918,7 @@ private: bool didTeleport(); bool getIsAway() const { return _isAway; } void setAway(bool value); - void sendPacket(const QUuid& entityID, const EntityItemProperties& properties) const override; + void sendPacket(const QUuid& entityID) const override; std::mutex _pinnedJointsMutex; std::vector _pinnedJoints; diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 38108416ee..992ee5db96 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -376,7 +376,7 @@ bool Avatar::applyGrabChanges() { const EntityItemPointer& entity = std::dynamic_pointer_cast(target); if (entity && entity->getEntityHostType() == entity::HostType::AVATAR && entity->getSimulationOwner().getID() == getID()) { EntityItemProperties properties = entity->getProperties(); - sendPacket(entity->getID(), properties); + sendPacket(entity->getID()); } } } else { diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index d81b04d4b2..6026367440 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -605,7 +605,7 @@ protected: // protected methods... bool isLookingAtMe(AvatarSharedPointer avatar) const; - virtual void sendPacket(const QUuid& entityID, const EntityItemProperties& properties) const { } + virtual void sendPacket(const QUuid& entityID) const { } bool applyGrabChanges(); void relayJointDataToChildren(); diff --git a/libraries/entities/src/EntityEditPacketSender.cpp b/libraries/entities/src/EntityEditPacketSender.cpp index af0e34303b..0491bdedae 100644 --- a/libraries/entities/src/EntityEditPacketSender.cpp +++ b/libraries/entities/src/EntityEditPacketSender.cpp @@ -39,9 +39,7 @@ void EntityEditPacketSender::adjustEditPacketForClockSkew(PacketType type, QByte } } -void EntityEditPacketSender::queueEditAvatarEntityMessage(EntityTreePointer entityTree, - EntityItemID entityItemID, - const EntityItemProperties& properties) { +void EntityEditPacketSender::queueEditAvatarEntityMessage(EntityTreePointer entityTree, EntityItemID entityItemID) { assert(_myAvatar); if (!entityTree) { qCDebug(entities) << "EntityEditPacketSender::queueEditAvatarEntityMessage null entityTree."; @@ -54,11 +52,6 @@ void EntityEditPacketSender::queueEditAvatarEntityMessage(EntityTreePointer enti } entity->setLastBroadcast(usecTimestampNow()); - // serialize ALL properties in an "AvatarEntity" packet - // rather than just the ones being edited. - EntityItemProperties entityProperties = entity->getProperties(); - entityProperties.merge(properties); - OctreePacketData packetData(false, AvatarTraits::MAXIMUM_TRAIT_SIZE); EncodeBitstreamParams params; EntityTreeElementExtraEncodeDataPointer extra { nullptr }; @@ -82,7 +75,7 @@ void EntityEditPacketSender::queueEditEntityMessage(PacketType type, qCWarning(entities) << "Suppressing entity edit message: cannot send avatar entity edit with no myAvatar"; } else if (properties.getOwningAvatarID() == _myAvatar->getID()) { // this is an avatar-based entity --> update our avatar-data rather than sending to the entity-server - queueEditAvatarEntityMessage(entityTree, entityItemID, properties); + queueEditAvatarEntityMessage(entityTree, entityItemID); } else { qCWarning(entities) << "Suppressing entity edit message: cannot send avatar entity edit for another avatar"; } diff --git a/libraries/entities/src/EntityEditPacketSender.h b/libraries/entities/src/EntityEditPacketSender.h index 99a5202986..3cc2f016f0 100644 --- a/libraries/entities/src/EntityEditPacketSender.h +++ b/libraries/entities/src/EntityEditPacketSender.h @@ -50,8 +50,8 @@ public slots: void processEntityEditNackPacket(QSharedPointer message, SharedNodePointer sendingNode); private: - void queueEditAvatarEntityMessage(EntityTreePointer entityTree, - EntityItemID entityItemID, const EntityItemProperties& properties); + friend class MyAvatar; + void queueEditAvatarEntityMessage(EntityTreePointer entityTree, EntityItemID entityItemID); private: std::mutex _mutex; From 3775633cc3b61f8391d7dc7088265fd3118fd8a1 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Wed, 20 Mar 2019 15:29:13 -0700 Subject: [PATCH 251/446] Case 20407 - Pay-In API doesn't display error when no username specified --- .../resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml index 626ac4da65..e159344d5c 100644 --- a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml +++ b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml @@ -2248,6 +2248,7 @@ Item { if (sendAssetStep.selectedRecipientUserName === "") { console.log("SendAsset: Script didn't specify a recipient username!"); sendAssetHome.visible = false; + root.nextActiveView = 'paymentFailure'; return; } From 8947d4d133320b44001433d9330b274cc85ec96e Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 20 Mar 2019 15:45:06 -0700 Subject: [PATCH 252/446] When adding new Node clear any dangling Connection objects to its address --- libraries/networking/src/LimitedNodeList.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index a9dbc12b09..18a180ad79 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -677,6 +677,9 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t // If there is a new node with the same socket, this is a reconnection, kill the old node removeOldNode(findNodeWithAddr(publicSocket)); removeOldNode(findNodeWithAddr(localSocket)); + // If there is an old Connection to the new node's address kill it + _nodeSocket.cleanupConnection(publicSocket); + _nodeSocket.cleanupConnection(localSocket); auto it = _connectionIDs.find(uuid); if (it == _connectionIDs.end()) { From 3186a9468231daff21687c76999469c012910824 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Wed, 20 Mar 2019 15:48:05 -0700 Subject: [PATCH 253/446] Add Users.setInjectorGain() and Users.getInjectorGain() to the scripting interface --- .../script-engine/src/UsersScriptingInterface.cpp | 9 +++++++++ .../script-engine/src/UsersScriptingInterface.h | 15 +++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp index fef11c12e9..a0593d3ff8 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.cpp +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -51,6 +51,15 @@ float UsersScriptingInterface::getAvatarGain(const QUuid& nodeID) { return DependencyManager::get()->getAvatarGain(nodeID); } +void UsersScriptingInterface::setInjectorGain(float gain) { + // ask the NodeList to set the audio injector gain + DependencyManager::get()->setInjectorGain(gain); +} + +float UsersScriptingInterface::getInjectorGain() { + return DependencyManager::get()->getInjectorGain(); +} + void UsersScriptingInterface::kick(const QUuid& nodeID) { // ask the NodeList to kick the user with the given session ID DependencyManager::get()->kickNodeBySessionID(nodeID); diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index 57de205066..17a84248a1 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -90,6 +90,21 @@ public slots: */ float getAvatarGain(const QUuid& nodeID); + /**jsdoc + * Sets the audio injector gain at the server. + * Units are Decibels (dB) + * @function Users.setInjectorGain + * @param {number} gain (in dB) + */ + void setInjectorGain(float gain); + + /**jsdoc + * Gets the audio injector gain at the server. + * @function Users.getInjectorGain + * @returns {number} gain (in dB) + */ + float getInjectorGain(); + /**jsdoc * Kick/ban another user. Removes them from the server and prevents them from returning. Bans by either user name (if * available) or machine fingerprint otherwise. This will only do anything if you're an admin of the domain you're in. From b12a2684649c7ab2382dbb958452869f3e03e1f6 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Wed, 20 Mar 2019 16:25:59 -0700 Subject: [PATCH 254/446] Fixed copy-paste-drink too much error --- tools/nitpick/src/AWSInterface.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tools/nitpick/src/AWSInterface.cpp b/tools/nitpick/src/AWSInterface.cpp index 16c0a220d8..19697d51dc 100644 --- a/tools/nitpick/src/AWSInterface.cpp +++ b/tools/nitpick/src/AWSInterface.cpp @@ -53,10 +53,6 @@ void AWSInterface::createWebPageFromResults(const QString& testResults, _testResults = testResults; - _urlLineEdit = urlLineEdit; - _urlLineEdit->setEnabled(false); - - _urlLineEdit = urlLineEdit; _urlLineEdit->setEnabled(false); From 5888adfd9035090cfc84737d6d5917f371b87d08 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 20 Mar 2019 17:08:42 -0700 Subject: [PATCH 255/446] update terms of service links --- interface/resources/qml/LoginDialog/CompleteProfileBody.qml | 4 ++-- interface/resources/qml/LoginDialog/SignUpBody.qml | 2 +- interface/resources/qml/LoginDialog/UsernameCollisionBody.qml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml index 65f8a8c1dc..17d6a7d3b3 100644 --- a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml +++ b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml @@ -379,9 +379,9 @@ Item { Component.onCompleted: { // with the link. if (completeProfileBody.withOculus) { - termsText.text = qsTr("By signing up, you agree to High Fidelity's Terms of Service") + termsText.text = qsTr("By signing up, you agree to High Fidelity's Terms of Service") } else { - termsText.text = qsTr("By creating this user profile, you agree to High Fidelity's Terms of Service") + termsText.text = qsTr("By creating this user profile, you agree to High Fidelity's Terms of Service") } } } diff --git a/interface/resources/qml/LoginDialog/SignUpBody.qml b/interface/resources/qml/LoginDialog/SignUpBody.qml index 64df9089a1..69ac2f5a6c 100644 --- a/interface/resources/qml/LoginDialog/SignUpBody.qml +++ b/interface/resources/qml/LoginDialog/SignUpBody.qml @@ -395,7 +395,7 @@ Item { text: signUpBody.termsContainerText Component.onCompleted: { // with the link. - termsText.text = qsTr("By signing up, you agree to High Fidelity's Terms of Service") + termsText.text = qsTr("By signing up, you agree to High Fidelity's Terms of Service") } } diff --git a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml index 2c8e61a29a..d450b1e7bc 100644 --- a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml +++ b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml @@ -218,7 +218,7 @@ Item { text: usernameCollisionBody.termsContainerText Component.onCompleted: { // with the link. - termsText.text = qsTr("By creating this user profile, you agree to High Fidelity's Terms of Service") + termsText.text = qsTr("By creating this user profile, you agree to High Fidelity's Terms of Service") } } From 3ff0770441a2ce24063063eff9b07264715bc5f9 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 20 Mar 2019 21:14:54 -0700 Subject: [PATCH 256/446] model emitters! --- .../RenderableParticleEffectEntityItem.cpp | 173 +++++++++++++++++- .../src/RenderableParticleEffectEntityItem.h | 12 +- .../entities/src/EntityItemProperties.cpp | 21 ++- libraries/entities/src/LineEntityItem.h | 2 - libraries/entities/src/ModelEntityItem.h | 2 +- libraries/entities/src/ShapeEntityItem.h | 2 +- libraries/entities/src/ZoneEntityItem.cpp | 2 +- libraries/entities/src/ZoneEntityItem.h | 2 +- libraries/shared/src/GeometryUtil.cpp | 6 + libraries/shared/src/GeometryUtil.h | 1 + .../system/assets/data/createAppTooltips.json | 8 + scripts/system/html/js/entityProperties.js | 15 ++ 12 files changed, 222 insertions(+), 24 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp index 8883108f75..d517ecd026 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp @@ -1,4 +1,4 @@ -// +// // RenderableParticleEffectEntityItem.cpp // interface/src // @@ -14,6 +14,8 @@ #include #include +#include + using namespace render; using namespace render::entities; @@ -111,6 +113,7 @@ void ParticleEffectEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePoi QString compoundShapeURL = entity->getCompoundShapeURL(); if (_compoundShapeURL != compoundShapeURL) { _compoundShapeURL = compoundShapeURL; + _hasComputedTriangles = false; fetchGeometryResource(); } }); @@ -217,7 +220,8 @@ float importanceSample3DDimension(float startDim) { } ParticleEffectEntityRenderer::CpuParticle ParticleEffectEntityRenderer::createParticle(uint64_t now, const Transform& baseTransform, const particle::Properties& particleProperties, - const ShapeType& shapeType, const GeometryResource::Pointer& geometryResource) { + const ShapeType& shapeType, const GeometryResource::Pointer& geometryResource, + const TriangleInfo& triangleInfo) { CpuParticle particle; const auto& accelerationSpread = particleProperties.emission.acceleration.spread; @@ -259,7 +263,7 @@ ParticleEffectEntityRenderer::CpuParticle ParticleEffectEntityRenderer::createPa } else { azimuth = azimuthStart + (TWO_PI + azimuthFinish - azimuthStart) * randFloat(); } - // TODO: azimuth and elevation are only used for ellipsoids, but could be used for other shapes too + // TODO: azimuth and elevation are only used for ellipsoids/circles, but could be used for other shapes too if (emitDimensions == Vectors::ZERO) { // Point @@ -301,7 +305,7 @@ ParticleEffectEntityRenderer::CpuParticle ParticleEffectEntityRenderer::createPa break; } - case SHAPE_TYPE_CIRCLE: { // FIXME: SHAPE_TYPE_CIRCLE is not exposed to scripts in buildStringToShapeTypeLookup() + case SHAPE_TYPE_CIRCLE: { glm::vec2 radii = importanceSample2DDimension(emitRadiusStart) * 0.5f * glm::vec2(emitDimensions.x, emitDimensions.z); float x = radii.x * glm::cos(azimuth); float z = radii.y * glm::sin(azimuth); @@ -326,7 +330,43 @@ ParticleEffectEntityRenderer::CpuParticle ParticleEffectEntityRenderer::createPa break; } - case SHAPE_TYPE_COMPOUND: + case SHAPE_TYPE_COMPOUND: { + // if we get here we know that geometryResource is loaded + + size_t index = randFloat() * triangleInfo.totalSamples; + Triangle triangle; + for (size_t i = 0; i < triangleInfo.samplesPerTriangle.size(); i++) { + size_t numSamples = triangleInfo.samplesPerTriangle[i]; + if (index < numSamples) { + triangle = triangleInfo.triangles[i]; + break; + } + index -= numSamples; + } + + float edgeLength1 = glm::length(triangle.v1 - triangle.v0); + float edgeLength2 = glm::length(triangle.v2 - triangle.v1); + float edgeLength3 = glm::length(triangle.v0 - triangle.v2); + + float perimeter = edgeLength1 + edgeLength2 + edgeLength3; + float fraction1 = randFloatInRange(0.0f, 1.0f); + float fractionEdge1 = glm::min(fraction1 * perimeter / edgeLength1, 1.0f); + float fraction2 = fraction1 - edgeLength1 / perimeter; + float fractionEdge2 = glm::clamp(fraction2 * perimeter / edgeLength2, 0.0f, 1.0f); + float fraction3 = fraction2 - edgeLength2 / perimeter; + float fractionEdge3 = glm::clamp(fraction3 * perimeter / edgeLength3, 0.0f, 1.0f); + + float dim = importanceSample2DDimension(emitRadiusStart); + triangle = triangle * (glm::scale(emitDimensions) * triangleInfo.transform); + glm::vec3 center = (triangle.v0 + triangle.v1 + triangle.v2) / 3.0f; + glm::vec3 v0 = (dim * (triangle.v0 - center)) + center; + glm::vec3 v1 = (dim * (triangle.v1 - center)) + center; + glm::vec3 v2 = (dim * (triangle.v2 - center)) + center; + + emitPosition = glm::mix(v0, glm::mix(v1, glm::mix(v2, v0, fractionEdge3), fractionEdge2), fractionEdge1); + emitDirection = triangle.getNormal(); + break; + } case SHAPE_TYPE_SPHERE: case SHAPE_TYPE_ELLIPSOID: @@ -374,13 +414,16 @@ void ParticleEffectEntityRenderer::stepSimulation() { const auto& modelTransform = getModelTransform(); if (_emitting && particleProperties.emitting() && - (_shapeType != SHAPE_TYPE_COMPOUND || (_geometryResource && _geometryResource->isLoaded()))) { + (shapeType != SHAPE_TYPE_COMPOUND || (geometryResource && geometryResource->isLoaded()))) { uint64_t emitInterval = particleProperties.emitIntervalUsecs(); if (emitInterval > 0 && interval >= _timeUntilNextEmit) { auto timeRemaining = interval; while (timeRemaining > _timeUntilNextEmit) { + if (_shapeType == SHAPE_TYPE_COMPOUND && !_hasComputedTriangles) { + computeTriangles(geometryResource->getHFMModel()); + } // emit particle - _cpuParticles.push_back(createParticle(now, modelTransform, particleProperties, shapeType, geometryResource)); + _cpuParticles.push_back(createParticle(now, modelTransform, particleProperties, shapeType, geometryResource, _triangleInfo)); _timeUntilNextEmit = emitInterval; if (emitInterval < timeRemaining) { timeRemaining -= emitInterval; @@ -467,4 +510,120 @@ void ParticleEffectEntityRenderer::fetchGeometryResource() { } else { _geometryResource = DependencyManager::get()->getCollisionGeometryResource(hullURL); } +} + +// FIXME: this is very similar to Model::calculateTriangleSets +void ParticleEffectEntityRenderer::computeTriangles(const hfm::Model& hfmModel) { + PROFILE_RANGE(render, __FUNCTION__); + + int numberOfMeshes = hfmModel.meshes.size(); + + _hasComputedTriangles = true; + _triangleInfo.triangles.clear(); + _triangleInfo.samplesPerTriangle.clear(); + + std::vector areas; + float minArea = FLT_MAX; + AABox bounds; + + for (int i = 0; i < numberOfMeshes; i++) { + const HFMMesh& mesh = hfmModel.meshes.at(i); + + const int numberOfParts = mesh.parts.size(); + for (int j = 0; j < numberOfParts; j++) { + const HFMMeshPart& part = mesh.parts.at(j); + + const int INDICES_PER_TRIANGLE = 3; + const int INDICES_PER_QUAD = 4; + const int TRIANGLES_PER_QUAD = 2; + + // tell our triangleSet how many triangles to expect. + int numberOfQuads = part.quadIndices.size() / INDICES_PER_QUAD; + int numberOfTris = part.triangleIndices.size() / INDICES_PER_TRIANGLE; + int totalTriangles = (numberOfQuads * TRIANGLES_PER_QUAD) + numberOfTris; + _triangleInfo.triangles.reserve(_triangleInfo.triangles.size() + totalTriangles); + areas.reserve(areas.size() + totalTriangles); + + auto meshTransform = hfmModel.offset * mesh.modelTransform; + + if (part.quadIndices.size() > 0) { + int vIndex = 0; + for (int q = 0; q < numberOfQuads; q++) { + int i0 = part.quadIndices[vIndex++]; + int i1 = part.quadIndices[vIndex++]; + int i2 = part.quadIndices[vIndex++]; + int i3 = part.quadIndices[vIndex++]; + + // track the model space version... these points will be transformed by the FST's offset, + // which includes the scaling, rotation, and translation specified by the FST/FBX, + // this can't change at runtime, so we can safely store these in our TriangleSet + glm::vec3 v0 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i0], 1.0f)); + glm::vec3 v1 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i1], 1.0f)); + glm::vec3 v2 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i2], 1.0f)); + glm::vec3 v3 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i3], 1.0f)); + + Triangle tri1 = { v0, v1, v3 }; + Triangle tri2 = { v1, v2, v3 }; + _triangleInfo.triangles.push_back(tri1); + _triangleInfo.triangles.push_back(tri2); + + float area1 = tri1.getArea(); + areas.push_back(area1); + if (area1 > EPSILON) { + minArea = std::min(minArea, area1); + } + + float area2 = tri2.getArea(); + areas.push_back(area2); + if (area2 > EPSILON) { + minArea = std::min(minArea, area2); + } + + bounds += v0; + bounds += v1; + bounds += v2; + bounds += v3; + } + } + + if (part.triangleIndices.size() > 0) { + int vIndex = 0; + for (int t = 0; t < numberOfTris; t++) { + int i0 = part.triangleIndices[vIndex++]; + int i1 = part.triangleIndices[vIndex++]; + int i2 = part.triangleIndices[vIndex++]; + + // track the model space version... these points will be transformed by the FST's offset, + // which includes the scaling, rotation, and translation specified by the FST/FBX, + // this can't change at runtime, so we can safely store these in our TriangleSet + glm::vec3 v0 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i0], 1.0f)); + glm::vec3 v1 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i1], 1.0f)); + glm::vec3 v2 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i2], 1.0f)); + + Triangle tri = { v0, v1, v2 }; + _triangleInfo.triangles.push_back(tri); + + float area = tri.getArea(); + areas.push_back(area); + if (area > EPSILON) { + minArea = std::min(minArea, area); + } + + bounds += v0; + bounds += v1; + bounds += v2; + } + } + } + } + + _triangleInfo.totalSamples = 0; + for (auto& area : areas) { + size_t numSamples = area / minArea; + _triangleInfo.samplesPerTriangle.push_back(numSamples); + _triangleInfo.totalSamples += numSamples; + } + + glm::vec3 scale = bounds.getScale(); + _triangleInfo.transform = glm::scale(1.0f / scale) * glm::translate(-bounds.calcCenter()); } \ No newline at end of file diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h index 4a4e5e5cbc..d13c966e96 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h @@ -81,8 +81,18 @@ private: glm::vec2 spare; }; + void computeTriangles(const hfm::Model& hfmModel); + bool _hasComputedTriangles{ false }; + struct TriangleInfo { + std::vector triangles; + std::vector samplesPerTriangle; + size_t totalSamples; + glm::mat4 transform; + } _triangleInfo; + static CpuParticle createParticle(uint64_t now, const Transform& baseTransform, const particle::Properties& particleProperties, - const ShapeType& shapeType, const GeometryResource::Pointer& geometryResource); + const ShapeType& shapeType, const GeometryResource::Pointer& geometryResource, + const TriangleInfo& triangleInfo); void stepSimulation(); particle::Properties _particleProperties; diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 0a6875b63d..5958af66dd 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -127,6 +127,7 @@ void buildStringToShapeTypeLookup() { addShapeType(SHAPE_TYPE_SIMPLE_COMPOUND); addShapeType(SHAPE_TYPE_STATIC_MESH); addShapeType(SHAPE_TYPE_ELLIPSOID); + addShapeType(SHAPE_TYPE_CIRCLE); } QHash stringToMaterialMappingModeLookup; @@ -1121,21 +1122,21 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * Particles are emitted from the portion of the shape that lies between emitRadiusStart and the * shape's surface. * @property {number} polarStart=0 - The angle in radians from the entity's local z-axis at which particles start being emitted - * within the ellipsoid; range 0Math.PI. Particles are emitted from the portion of the - * ellipsoid that lies between polarStart and polarFinish. Only used if shapeType is - * ellipsoid. + * within the shape; range 0Math.PI. Particles are emitted from the portion of the + * shape that lies between polarStart and polarFinish. Only used if shapeType is + * ellipsoid or sphere. * @property {number} polarFinish=0 - The angle in radians from the entity's local z-axis at which particles stop being emitted - * within the ellipsoid; range 0Math.PI. Particles are emitted from the portion of the - * ellipsoid that lies between polarStart and polarFinish. Only used if shapeType is - * ellipsoid. + * within the shape; range 0Math.PI. Particles are emitted from the portion of the + * shape that lies between polarStart and polarFinish. Only used if shapeType is + * ellipsoid or sphere. * @property {number} azimuthStart=-Math.PI - The angle in radians from the entity's local x-axis about the entity's local * z-axis at which particles start being emitted; range -Math.PIMath.PI. Particles are - * emitted from the portion of the ellipsoid that lies between azimuthStart and azimuthFinish. - * Only used if shapeType is ellipsoid. + * emitted from the portion of the shape that lies between azimuthStart and azimuthFinish. + * Only used if shapeType is ellipsoid, sphere, or circle. * @property {number} azimuthFinish=Math.PI - The angle in radians from the entity's local x-axis about the entity's local * z-axis at which particles stop being emitted; range -Math.PIMath.PI. Particles are - * emitted from the portion of the ellipsoid that lies between azimuthStart and azimuthFinish. - * Only used if shapeType is ellipsoid. + * emitted from the portion of the shape that lies between azimuthStart and azimuthFinish. + * Only used if shapeType is ellipsoid, sphere, or circle.. * * @property {string} textures="" - The URL of a JPG or PNG image file to display for each particle. If you want transparency, * use PNG format. diff --git a/libraries/entities/src/LineEntityItem.h b/libraries/entities/src/LineEntityItem.h index 86ca6065bb..098183299f 100644 --- a/libraries/entities/src/LineEntityItem.h +++ b/libraries/entities/src/LineEntityItem.h @@ -49,8 +49,6 @@ class LineEntityItem : public EntityItem { QVector getLinePoints() const; - virtual ShapeType getShapeType() const override { return SHAPE_TYPE_NONE; } - // never have a ray intersection pick a LineEntityItem. virtual bool supportsDetailedIntersection() const override { return true; } virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, diff --git a/libraries/entities/src/ModelEntityItem.h b/libraries/entities/src/ModelEntityItem.h index 08468617ba..8f5e24ad76 100644 --- a/libraries/entities/src/ModelEntityItem.h +++ b/libraries/entities/src/ModelEntityItem.h @@ -175,7 +175,7 @@ protected: QString _textures; - ShapeType _shapeType = SHAPE_TYPE_NONE; + ShapeType _shapeType { SHAPE_TYPE_NONE } ; private: uint64_t _lastAnimated{ 0 }; diff --git a/libraries/entities/src/ShapeEntityItem.h b/libraries/entities/src/ShapeEntityItem.h index 363a7f39d1..fc590e06a4 100644 --- a/libraries/entities/src/ShapeEntityItem.h +++ b/libraries/entities/src/ShapeEntityItem.h @@ -112,7 +112,7 @@ protected: //! This is SHAPE_TYPE_ELLIPSOID rather than SHAPE_TYPE_NONE to maintain //! prior functionality where new or unsupported shapes are treated as //! ellipsoids. - ShapeType _collisionShapeType{ ShapeType::SHAPE_TYPE_ELLIPSOID }; + ShapeType _collisionShapeType { ShapeType::SHAPE_TYPE_ELLIPSOID }; }; #endif // hifi_ShapeEntityItem_h diff --git a/libraries/entities/src/ZoneEntityItem.cpp b/libraries/entities/src/ZoneEntityItem.cpp index f243d59da0..0771d9ad54 100644 --- a/libraries/entities/src/ZoneEntityItem.cpp +++ b/libraries/entities/src/ZoneEntityItem.cpp @@ -352,7 +352,7 @@ bool ZoneEntityItem::contains(const glm::vec3& point) const { Extents meshExtents = hfmModel.getMeshExtents(); glm::vec3 meshExtentsDiagonal = meshExtents.maximum - meshExtents.minimum; - glm::vec3 offset = -meshExtents.minimum- (meshExtentsDiagonal * getRegistrationPoint()); + glm::vec3 offset = -meshExtents.minimum - (meshExtentsDiagonal * getRegistrationPoint()); glm::vec3 scale(getScaledDimensions() / meshExtentsDiagonal); glm::mat4 hfmToEntityMatrix = glm::scale(scale) * glm::translate(offset); diff --git a/libraries/entities/src/ZoneEntityItem.h b/libraries/entities/src/ZoneEntityItem.h index df6ce50fd6..69e3227135 100644 --- a/libraries/entities/src/ZoneEntityItem.h +++ b/libraries/entities/src/ZoneEntityItem.h @@ -133,7 +133,7 @@ protected: KeyLightPropertyGroup _keyLightProperties; AmbientLightPropertyGroup _ambientLightProperties; - ShapeType _shapeType = DEFAULT_SHAPE_TYPE; + ShapeType _shapeType { DEFAULT_SHAPE_TYPE }; QString _compoundShapeURL; // The following 3 values are the defaults for zone creation diff --git a/libraries/shared/src/GeometryUtil.cpp b/libraries/shared/src/GeometryUtil.cpp index 3f2f7cd7fb..b6fca03403 100644 --- a/libraries/shared/src/GeometryUtil.cpp +++ b/libraries/shared/src/GeometryUtil.cpp @@ -358,6 +358,12 @@ glm::vec3 Triangle::getNormal() const { return glm::normalize(glm::cross(edge1, edge2)); } +float Triangle::getArea() const { + glm::vec3 edge1 = v1 - v0; + glm::vec3 edge2 = v2 - v0; + return 0.5f * glm::length(glm::cross(edge1, edge2)); +} + Triangle Triangle::operator*(const glm::mat4& transform) const { return { glm::vec3(transform * glm::vec4(v0, 1.0f)), diff --git a/libraries/shared/src/GeometryUtil.h b/libraries/shared/src/GeometryUtil.h index 8ec75f71bd..04c54fc32e 100644 --- a/libraries/shared/src/GeometryUtil.h +++ b/libraries/shared/src/GeometryUtil.h @@ -125,6 +125,7 @@ public: glm::vec3 v1; glm::vec3 v2; glm::vec3 getNormal() const; + float getArea() const; Triangle operator*(const glm::mat4& transform) const; }; diff --git a/scripts/system/assets/data/createAppTooltips.json b/scripts/system/assets/data/createAppTooltips.json index 7201cdecad..73c7504089 100644 --- a/scripts/system/assets/data/createAppTooltips.json +++ b/scripts/system/assets/data/createAppTooltips.json @@ -227,6 +227,14 @@ "speedSpread": { "tooltip": "The spread in speeds at which particles are emitted at, resulting in a variety of speeds." }, + "particleShapeType": { + "tooltip": "The shape of the surface from which to emit particles.", + "jsPropertyName": "shapeType" + }, + "particleCompoundShapeURL": { + "tooltip": "The model file to use for the particle emitter if Shape Type is \"Use Compound Shape URL\".", + "jsPropertyName": "compoundShapeURL" + }, "emitDimensions": { "tooltip": "The outer limit radius in dimensions that the particles can be emitted from." }, diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index f501df7933..15ab40e5ea 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -807,6 +807,21 @@ const GROUPS = [ decimals: 2, propertyID: "speedSpread", }, + { + label: "Shape Type", + type: "dropdown", + options: { "box": "Box", "ellipsoid": "Ellipsoid", + "cylinder-y": "Cylinder", "circle": "Circle", "plane": "Plane", + "compound": "Use Compound Shape URL" }, + propertyID: "particleShapeType", + propertyName: "shapeType", + }, + { + label: "Compound Shape URL", + type: "string", + propertyID: "particleCompoundShapeURL", + propertyName: "compoundShapeURL", + }, { label: "Emit Dimensions", type: "vec3", From 61b7b8b66963a5f1fbae027e81fcc0705d074e34 Mon Sep 17 00:00:00 2001 From: Sam Gondelman Date: Thu, 21 Mar 2019 08:36:32 -0700 Subject: [PATCH 257/446] reduce lambda copies --- .../entities-renderer/src/RenderablePolyLineEntityItem.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp index df52934b87..2430643ce2 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp @@ -167,14 +167,16 @@ void PolyLineEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& bool uvModeStretchChanged = _isUVModeStretch != isUVModeStretch; _isUVModeStretch = isUVModeStretch; + + bool geometryChanged = uvModeStretchChanged || pointsChanged || widthsChanged || normalsChanged || colorsChanged || textureChanged || faceCameraChanged; void* key = (void*)this; - AbstractViewStateInterface::instance()->pushPostUpdateLambda(key, [=]() { + AbstractViewStateInterface::instance()->pushPostUpdateLambda(key, [this, geometryChanged] () { withWriteLock([&] { updateModelTransformAndBound(); _renderTransform = getModelTransform(); - if (uvModeStretchChanged || pointsChanged || widthsChanged || normalsChanged || colorsChanged || textureChanged || faceCameraChanged) { + if (geometryChanged) { updateGeometry(); } }); From c777f94231045cfc35de064fa8d79f6d47f9f1ee Mon Sep 17 00:00:00 2001 From: Robin Wilson Date: Tue, 19 Mar 2019 16:14:55 -0700 Subject: [PATCH 258/446] remove AvatarBookmarks.deleteBookmark function --- interface/src/AvatarBookmarks.cpp | 3 +++ interface/src/AvatarBookmarks.h | 3 +++ interface/src/Bookmarks.h | 5 +---- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/interface/src/AvatarBookmarks.cpp b/interface/src/AvatarBookmarks.cpp index 5fe35bd23f..54c67daab8 100644 --- a/interface/src/AvatarBookmarks.cpp +++ b/interface/src/AvatarBookmarks.cpp @@ -149,6 +149,9 @@ void AvatarBookmarks::removeBookmark(const QString& bookmarkName) { emit bookmarkDeleted(bookmarkName); } +void AvatarBookmarks::deleteBookmark() { +} + void AvatarBookmarks::updateAvatarEntities(const QVariantList &avatarEntities) { auto myAvatar = DependencyManager::get()->getMyAvatar(); auto currentAvatarEntities = myAvatar->getAvatarEntityData(); diff --git a/interface/src/AvatarBookmarks.h b/interface/src/AvatarBookmarks.h index 4623e7d929..df75fec865 100644 --- a/interface/src/AvatarBookmarks.h +++ b/interface/src/AvatarBookmarks.h @@ -76,6 +76,9 @@ protected: void readFromFile() override; QVariantMap getAvatarDataToBookmark(); +protected slots: + void deleteBookmark() override; + private: const QString AVATARBOOKMARKS_FILENAME = "avatarbookmarks.json"; const QString ENTRY_AVATAR_URL = "avatarUrl"; diff --git a/interface/src/Bookmarks.h b/interface/src/Bookmarks.h index 88510e4eda..56d26b55c6 100644 --- a/interface/src/Bookmarks.h +++ b/interface/src/Bookmarks.h @@ -51,13 +51,10 @@ protected: bool _isMenuSorted; protected slots: - /**jsdoc - * @function AvatarBookmarks.deleteBookmark - */ /**jsdoc * @function LocationBookmarks.deleteBookmark */ - void deleteBookmark(); + virtual void deleteBookmark(); private: static bool sortOrder(QAction* a, QAction* b); From 7311c3ac06aa3e1425c169afc22f0d0c2842900e Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 21 Mar 2019 11:51:49 -0700 Subject: [PATCH 259/446] Move the new audio volume API from Users scripting interface to Audio scripting interface --- interface/src/scripting/Audio.cpp | 36 ++++++++++++++++--- interface/src/scripting/Audio.h | 30 ++++++++++++++++ .../src/UsersScriptingInterface.cpp | 9 ----- .../src/UsersScriptingInterface.h | 15 -------- 4 files changed, 61 insertions(+), 29 deletions(-) diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index bf43db3044..e0474b7bba 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -374,6 +374,18 @@ void Audio::handlePushedToTalk(bool enabled) { } } +void Audio::setInputDevice(const QAudioDeviceInfo& device, bool isHMD) { + withWriteLock([&] { + _devices.chooseInputDevice(device, isHMD); + }); +} + +void Audio::setOutputDevice(const QAudioDeviceInfo& device, bool isHMD) { + withWriteLock([&] { + _devices.chooseOutputDevice(device, isHMD); + }); +} + void Audio::setReverb(bool enable) { withWriteLock([&] { DependencyManager::get()->setReverb(enable); @@ -386,14 +398,28 @@ void Audio::setReverbOptions(const AudioEffectOptions* options) { }); } -void Audio::setInputDevice(const QAudioDeviceInfo& device, bool isHMD) { +void Audio::setAvatarGain(float gain) { withWriteLock([&] { - _devices.chooseInputDevice(device, isHMD); + // ask the NodeList to set the master avatar gain + DependencyManager::get()->setAvatarGain("", gain); }); } -void Audio::setOutputDevice(const QAudioDeviceInfo& device, bool isHMD) { - withWriteLock([&] { - _devices.chooseOutputDevice(device, isHMD); +float Audio::getAvatarGain() { + return resultWithReadLock([&] { + return DependencyManager::get()->getAvatarGain(""); + }); +} + +void Audio::setInjectorGain(float gain) { + withWriteLock([&] { + // ask the NodeList to set the audio injector gain + DependencyManager::get()->setInjectorGain(gain); + }); +} + +float Audio::getInjectorGain() { + return resultWithReadLock([&] { + return DependencyManager::get()->getInjectorGain(); }); } diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index 9ee230fc29..14a75d5ffe 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -170,6 +170,36 @@ public: */ Q_INVOKABLE void setReverbOptions(const AudioEffectOptions* options); + /**jsdoc + * Sets the master avatar gain at the server. + * Units are Decibels (dB) + * @function Audio.setAvatarGain + * @param {number} gain (in dB) + */ + Q_INVOKABLE void setAvatarGain(float gain); + + /**jsdoc + * Gets the master avatar gain at the server. + * @function Audio.getAvatarGain + * @returns {number} gain (in dB) + */ + Q_INVOKABLE float getAvatarGain(); + + /**jsdoc + * Sets the audio injector gain at the server. + * Units are Decibels (dB) + * @function Audio.setInjectorGain + * @param {number} gain (in dB) + */ + Q_INVOKABLE void setInjectorGain(float gain); + + /**jsdoc + * Gets the audio injector gain at the server. + * @function Audio.getInjectorGain + * @returns {number} gain (in dB) + */ + Q_INVOKABLE float getInjectorGain(); + /**jsdoc * Starts making an audio recording of the audio being played in-world (i.e., not local-only audio) to a file in WAV format. * @function Audio.startRecording diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp index a0593d3ff8..fef11c12e9 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.cpp +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -51,15 +51,6 @@ float UsersScriptingInterface::getAvatarGain(const QUuid& nodeID) { return DependencyManager::get()->getAvatarGain(nodeID); } -void UsersScriptingInterface::setInjectorGain(float gain) { - // ask the NodeList to set the audio injector gain - DependencyManager::get()->setInjectorGain(gain); -} - -float UsersScriptingInterface::getInjectorGain() { - return DependencyManager::get()->getInjectorGain(); -} - void UsersScriptingInterface::kick(const QUuid& nodeID) { // ask the NodeList to kick the user with the given session ID DependencyManager::get()->kickNodeBySessionID(nodeID); diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index 17a84248a1..57de205066 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -90,21 +90,6 @@ public slots: */ float getAvatarGain(const QUuid& nodeID); - /**jsdoc - * Sets the audio injector gain at the server. - * Units are Decibels (dB) - * @function Users.setInjectorGain - * @param {number} gain (in dB) - */ - void setInjectorGain(float gain); - - /**jsdoc - * Gets the audio injector gain at the server. - * @function Users.getInjectorGain - * @returns {number} gain (in dB) - */ - float getInjectorGain(); - /**jsdoc * Kick/ban another user. Removes them from the server and prevents them from returning. Bans by either user name (if * available) or machine fingerprint otherwise. This will only do anything if you're an admin of the domain you're in. From 87d75ec75ceee653cfb974ec49d92547c99409ca Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Thu, 21 Mar 2019 12:08:42 -0700 Subject: [PATCH 260/446] Case 20617 - People app filter bar breaks when deleting connections --- interface/resources/qml/hifi/Pal.qml | 8 ++++++++ scripts/system/pal.js | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 1c190a2b79..55f2bb80b1 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -1261,6 +1261,14 @@ Rectangle { case 'refreshConnections': refreshConnections(); break; + case 'connectionRemoved': + for (var i=0; i Date: Thu, 21 Mar 2019 12:15:03 -0700 Subject: [PATCH 261/446] new procedural uniforms for time --- .../src/RenderableShapeEntityItem.cpp | 2 +- .../src/RenderableZoneEntityItem.cpp | 2 +- .../procedural/src/procedural/Procedural.cpp | 13 +++++++++++-- .../procedural/src/procedural/Procedural.h | 19 ++++++++++++------- .../src/procedural/ProceduralCommon.slh | 8 ++++++-- .../src/procedural/ProceduralSkybox.cpp | 4 ++-- .../src/procedural/ProceduralSkybox.h | 5 ++++- 7 files changed, 37 insertions(+), 16 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index b33eb619c8..0375e236a4 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -261,7 +261,7 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) { if (_procedural.isReady()) { outColor = _procedural.getColor(outColor); outColor.a *= _procedural.isFading() ? Interpolate::calculateFadeRatio(_procedural.getFadeStartTime()) : 1.0f; - _procedural.prepare(batch, _position, _dimensions, _orientation, ProceduralProgramKey(outColor.a < 1.0f)); + _procedural.prepare(batch, _position, _dimensions, _orientation, _created, ProceduralProgramKey(outColor.a < 1.0f)); proceduralRender = true; } }); diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp index 631148c27a..8a7fa3f8e7 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp @@ -33,7 +33,7 @@ using namespace render::entities; ZoneEntityRenderer::ZoneEntityRenderer(const EntityItemPointer& entity) : Parent(entity) { - _background->setSkybox(std::make_shared()); + _background->setSkybox(std::make_shared(entity->getCreated())); } void ZoneEntityRenderer::onRemoveFromSceneTyped(const TypedEntityPointer& entity) { diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index ff8c270371..c5bfa43e75 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -225,11 +225,13 @@ void Procedural::prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation, + const quint64& created, const ProceduralProgramKey key) { std::lock_guard lock(_mutex); _entityDimensions = size; _entityPosition = position; _entityOrientation = glm::mat3_cast(orientation); + _entityCreated = created; if (!_shaderPath.isEmpty()) { auto lastModified = (quint64)QFileInfo(_shaderPath).lastModified().toMSecsSinceEpoch(); if (lastModified > _shaderModified) { @@ -278,7 +280,10 @@ void Procedural::prepare(gpu::Batch& batch, _proceduralPipelines[key] = gpu::Pipeline::create(program, key.isTransparent() ? _transparentState : _opaqueState); - _start = usecTimestampNow(); + _lastCompile = usecTimestampNow(); + if (_firstCompile == 0) { + _firstCompile = _lastCompile; + } _frameCount = 0; recompiledShader = true; } @@ -371,7 +376,11 @@ void Procedural::setupUniforms() { _uniforms.push_back([=](gpu::Batch& batch) { _standardInputs.position = vec4(_entityPosition, 1.0f); // Minimize floating point error by doing an integer division to milliseconds, before the floating point division to seconds - _standardInputs.time = (float)((usecTimestampNow() - _start) / USECS_PER_MSEC) / MSECS_PER_SECOND; + auto now = usecTimestampNow(); + _standardInputs.timeSinceLastCompile = (float)((now - _lastCompile) / USECS_PER_MSEC) / MSECS_PER_SECOND; + _standardInputs.timeSinceFirstCompile = (float)((now - _firstCompile) / USECS_PER_MSEC) / MSECS_PER_SECOND; + _standardInputs.timeSinceEntityCreation = (float)((now - _entityCreated) / USECS_PER_MSEC) / MSECS_PER_SECOND; + // Date { diff --git a/libraries/procedural/src/procedural/Procedural.h b/libraries/procedural/src/procedural/Procedural.h index 8477e69afc..b8fd77b052 100644 --- a/libraries/procedural/src/procedural/Procedural.h +++ b/libraries/procedural/src/procedural/Procedural.h @@ -82,7 +82,8 @@ public: bool isReady() const; bool isEnabled() const { return _enabled; } - void prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation, const ProceduralProgramKey key = ProceduralProgramKey()); + void prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation, + const quint64& created, const ProceduralProgramKey key = ProceduralProgramKey()); glm::vec4 getColor(const glm::vec4& entityColor) const; quint64 getFadeStartTime() const { return _fadeStartTime; } @@ -106,9 +107,10 @@ protected: vec4 date; vec4 position; vec4 scale; - float time; + float timeSinceLastCompile; + float timeSinceFirstCompile; + float timeSinceEntityCreation; int frameCount; - vec2 _spare1; vec4 resolution[4]; mat4 orientation; }; @@ -116,9 +118,10 @@ protected: static_assert(0 == offsetof(StandardInputs, date), "ProceduralOffsets"); static_assert(16 == offsetof(StandardInputs, position), "ProceduralOffsets"); static_assert(32 == offsetof(StandardInputs, scale), "ProceduralOffsets"); - static_assert(48 == offsetof(StandardInputs, time), "ProceduralOffsets"); - static_assert(52 == offsetof(StandardInputs, frameCount), "ProceduralOffsets"); - static_assert(56 == offsetof(StandardInputs, _spare1), "ProceduralOffsets"); + static_assert(48 == offsetof(StandardInputs, timeSinceLastCompile), "ProceduralOffsets"); + static_assert(52 == offsetof(StandardInputs, timeSinceFirstCompile), "ProceduralOffsets"); + static_assert(56 == offsetof(StandardInputs, timeSinceEntityCreation), "ProceduralOffsets"); + static_assert(60 == offsetof(StandardInputs, frameCount), "ProceduralOffsets"); static_assert(64 == offsetof(StandardInputs, resolution), "ProceduralOffsets"); static_assert(128 == offsetof(StandardInputs, orientation), "ProceduralOffsets"); @@ -126,7 +129,8 @@ protected: ProceduralData _data; bool _enabled { false }; - uint64_t _start { 0 }; + uint64_t _lastCompile { 0 }; + uint64_t _firstCompile { 0 }; int32_t _frameCount { 0 }; // Rendering object descriptions, from userData @@ -152,6 +156,7 @@ protected: glm::vec3 _entityDimensions; glm::vec3 _entityPosition; glm::mat3 _entityOrientation; + quint64 _entityCreated; private: void setupUniforms(); diff --git a/libraries/procedural/src/procedural/ProceduralCommon.slh b/libraries/procedural/src/procedural/ProceduralCommon.slh index bd894a9e92..6e73534440 100644 --- a/libraries/procedural/src/procedural/ProceduralCommon.slh +++ b/libraries/procedural/src/procedural/ProceduralCommon.slh @@ -36,9 +36,11 @@ LAYOUT_STD140(binding=0) uniform standardInputsBuffer { // Offset 48 float globalTime; // Offset 52 - int frameCount; + float localCreatedTime; // Offset 56 - vec2 _spare1; + float entityTime; + // Offset 60 + int frameCount; // Offset 64, acts as vec4[4] for alignment purposes vec3 channelResolution[4]; // Offset 128, acts as vec4[3] for alignment purposes @@ -52,6 +54,8 @@ LAYOUT_STD140(binding=0) uniform standardInputsBuffer { #define iWorldPosition standardInputs.worldPosition #define iWorldScale standardInputs.worldScale #define iGlobalTime standardInputs.globalTime +#define iLocalCreatedTime standardInputs.localCreatedTime +#define iEntityTime standardInputs.entityTime #define iFrameCount standardInputs.frameCount #define iChannelResolution standardInputs.channelResolution #define iWorldOrientation standardInputs.worldOrientation diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.cpp b/libraries/procedural/src/procedural/ProceduralSkybox.cpp index 6eb6d531e1..bf8e408e70 100644 --- a/libraries/procedural/src/procedural/ProceduralSkybox.cpp +++ b/libraries/procedural/src/procedural/ProceduralSkybox.cpp @@ -17,7 +17,7 @@ #include #include -ProceduralSkybox::ProceduralSkybox() : graphics::Skybox() { +ProceduralSkybox::ProceduralSkybox(quint64 created) : graphics::Skybox(), _created(created) { _procedural._vertexSource = gpu::Shader::createVertex(shader::graphics::vertex::skybox)->getSource(); _procedural._opaqueFragmentSource = shader::Source::get(shader::procedural::fragment::proceduralSkybox); // Adjust the pipeline state for background using the stencil test @@ -59,7 +59,7 @@ void ProceduralSkybox::render(gpu::Batch& batch, const ViewFrustum& viewFrustum, batch.setModelTransform(Transform()); // only for Mac auto& procedural = skybox._procedural; - procedural.prepare(batch, glm::vec3(0), glm::vec3(1), glm::quat()); + procedural.prepare(batch, glm::vec3(0), glm::vec3(1), glm::quat(), skybox.getCreated()); skybox.prepare(batch); batch.draw(gpu::TRIANGLE_STRIP, 4); } diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.h b/libraries/procedural/src/procedural/ProceduralSkybox.h index 5db1078a5f..1b01b891d3 100644 --- a/libraries/procedural/src/procedural/ProceduralSkybox.h +++ b/libraries/procedural/src/procedural/ProceduralSkybox.h @@ -19,7 +19,7 @@ class ProceduralSkybox: public graphics::Skybox { public: - ProceduralSkybox(); + ProceduralSkybox(quint64 created = 0); void parse(const QString& userData) { _procedural.setProceduralData(ProceduralData::parse(userData)); } @@ -29,8 +29,11 @@ public: void render(gpu::Batch& batch, const ViewFrustum& frustum) const override; static void render(gpu::Batch& batch, const ViewFrustum& frustum, const ProceduralSkybox& skybox); + quint64 getCreated() const { return _created; } + protected: mutable Procedural _procedural; + quint64 _created; }; typedef std::shared_ptr< ProceduralSkybox > ProceduralSkyboxPointer; From b0e2b5fde1aebf9c13689bb885c6791808a060b4 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Thu, 21 Mar 2019 14:47:48 -0700 Subject: [PATCH 262/446] Remove unneeded variable. --- tools/nitpick/src/TestRunnerMobile.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 1ab3ed7737..105d30fca4 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -197,10 +197,6 @@ void TestRunnerMobile::installAPK() { return; } - // Remove the path - QStringList parts = installerPathname.split('/'); - _installerFilename = parts[parts.length() - 1]; - _statusLabel->setText("Installing"); QString command = _adbInterface->getAdbCommand() + " install -r -d " + installerPathname + " >" + _workingFolder + "/installOutput.txt"; appendLog(command); From 206792f851fb288ff7c96ddcfff0bdb25a9ad8fa Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 21 Mar 2019 15:00:11 -0700 Subject: [PATCH 263/446] don't queue AvatarEntity messages when not in domain --- interface/src/avatar/MyAvatar.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index ddedc270f8..298e661f24 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3454,6 +3454,11 @@ void MyAvatar::setSessionUUID(const QUuid& sessionUUID) { QUuid oldSessionID = getSessionUUID(); Avatar::setSessionUUID(sessionUUID); QUuid newSessionID = getSessionUUID(); + if (DependencyManager::get()->getSessionUUID().isNull()) { + // we don't actually have a connection to a domain right now + // so there is no need to queue AvatarEntity messages --> bail early + return; + } if (newSessionID != oldSessionID) { auto treeRenderer = DependencyManager::get(); EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; From 9a14cfc7dfbeb63a56796bd55dad82579a87b613 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Thu, 21 Mar 2019 20:50:59 +0100 Subject: [PATCH 264/446] make sure that onWebEventReceived has the correct context in VR --- scripts/system/edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index ca2918a108..894ea2b696 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -2524,7 +2524,7 @@ var PropertiesTool = function (opts) { createToolsWindow.webEventReceived.addListener(this, onWebEventReceived); - webView.webEventReceived.connect(onWebEventReceived); + webView.webEventReceived.connect(this, onWebEventReceived); return that; }; From 4c7d5c7da7cd876933be0fbe45a247021245f7ba Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 22 Mar 2019 12:19:20 +1300 Subject: [PATCH 265/446] Detect signal functions based on their return type --- tools/jsdoc/plugins/hifi.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/jsdoc/plugins/hifi.js b/tools/jsdoc/plugins/hifi.js index b4350ddbdb..bd77204347 100644 --- a/tools/jsdoc/plugins/hifi.js +++ b/tools/jsdoc/plugins/hifi.js @@ -121,6 +121,11 @@ exports.handlers = { e.doclet.description = (e.doclet.description ? e.doclet.description : "") + availableIn; } } + + if (e.doclet.kind === "function" && e.doclet.returns && e.doclet.returns[0].type + && e.doclet.returns[0].type.names[0] === "Signal") { + e.doclet.kind = "signal"; + } } }; @@ -178,4 +183,4 @@ exports.defineTags = function (dictionary) { } }); -}; \ No newline at end of file +}; From 025326b85f2a85924386033acd486f15a2a2eb61 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 22 Mar 2019 12:20:10 +1300 Subject: [PATCH 266/446] Remove custom @signal tag --- tools/jsdoc/plugins/hifi.js | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/tools/jsdoc/plugins/hifi.js b/tools/jsdoc/plugins/hifi.js index bd77204347..b2b91de1c8 100644 --- a/tools/jsdoc/plugins/hifi.js +++ b/tools/jsdoc/plugins/hifi.js @@ -129,20 +129,6 @@ exports.handlers = { } }; -// Functions for adding @signal custom tag -/** @private */ -function setDocletKindToTitle(doclet, tag) { - doclet.addTag( 'kind', tag.title ); -} - -function setDocletNameToValue(doclet, tag) { - if (tag.value && tag.value.description) { // as in a long tag - doclet.addTag('name', tag.value.description); - } else if (tag.text) { // or a short tag - doclet.addTag('name', tag.text); - } -} - // Define custom hifi tags here exports.defineTags = function (dictionary) { @@ -173,14 +159,5 @@ exports.defineTags = function (dictionary) { doclet.hifiServerEntity = true; } }); - - // @signal - dictionary.defineTag("signal", { - mustHaveValue: true, - onTagged: function(doclet, tag) { - setDocletKindToTitle(doclet, tag); - setDocletNameToValue(doclet, tag); - } - }); }; From ba0923a3ad7a44550257b5196e3f793a1a8898b0 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 22 Mar 2019 12:20:22 +1300 Subject: [PATCH 267/446] Fix signal summary text --- tools/jsdoc/hifi-jsdoc-template/tmpl/signalList.tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/jsdoc/hifi-jsdoc-template/tmpl/signalList.tmpl b/tools/jsdoc/hifi-jsdoc-template/tmpl/signalList.tmpl index b9a0e0ca86..c5fdefc7d8 100644 --- a/tools/jsdoc/hifi-jsdoc-template/tmpl/signalList.tmpl +++ b/tools/jsdoc/hifi-jsdoc-template/tmpl/signalList.tmpl @@ -13,8 +13,8 @@ var self = this; * (function () { * Agent.setIsAvatar(true); @@ -76,9 +76,14 @@ public slots: void setIsAvatar(bool isAvatar) const { _agent->setIsAvatar(isAvatar); } /**jsdoc - * Checks whether or not the script is emulating an avatar. + * Checks whether the script is emulating an avatar. * @function Agent.isAvatar - * @returns {boolean} true if the script is acting as if an avatar, otherwise false. + * @returns {boolean} true if the script is emulating an avatar, otherwise false. + * @example + * (function () { + * print("Agent is avatar: " + Agent.isAvatar()); + * print("Agent is avatar: " + Agent.isAvatar); // Same result. + * }()); */ bool isAvatar() const { return _agent->isAvatar(); } @@ -86,7 +91,15 @@ public slots: * Plays a sound from the position and with the orientation of the emulated avatar's head. No sound is played unless * isAvatar == true. * @function Agent.playAvatarSound - * @param {SoundObject} avatarSound - The sound to play. + * @param {SoundObject} avatarSound - The sound played. + * @example + * (function () { + * Agent.isAvatar = true; + * var sound = SoundCache.getSound(Script.resourcesPath() + "sounds/sample.wav"); + * Script.setTimeout(function () { // Give the sound time to load. + * Agent.playAvatarSound(sound); + * }, 1000); + * }()); */ void playAvatarSound(SharedSoundPointer avatarSound) const { _agent->playAvatarSound(avatarSound); } diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h index 37e82947ae..fbe5675bd8 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.h +++ b/assignment-client/src/avatars/ScriptableAvatar.h @@ -20,7 +20,7 @@ /**jsdoc * The Avatar API is used to manipulate scriptable avatars on the domain. This API is a subset of the - * {@link MyAvatar} API. To enable this API, set {@link Agent|Agent.isAvatatr} to true. + * {@link MyAvatar} API. To enable this API, set {@link Agent|Agent.isAvatar} to true. * *

For Interface, client entity, and avatar scripts, see {@link MyAvatar}.

* @@ -30,13 +30,13 @@ * * @comment IMPORTANT: This group of properties is copied from AvatarData.h; they should NOT be edited here. * @property {Vec3} position - The position of the avatar. - * @property {number} scale=1.0 - The scale of the avatar. When setting, the value is limited to between 0.005 - * and 1000.0. When getting, the value may temporarily be further limited by the domain's settings. + * @property {number} scale=1.0 - The scale of the avatar. The value can be set to anything between 0.005 and + * 1000.0. When the scale value is fetched, it may temporarily be further limited by the domain's settings. * @property {number} density - The density of the avatar in kg/m3. The density is used to work out its mass in * the application of physics. Read-only. * @property {Vec3} handPosition - A user-defined hand position, in world coordinates. The position moves with the avatar * but is otherwise not used or changed by Interface. - * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. + * @property {number} bodyYaw - The left or right rotation about an axis running from the head to the feet of the avatar. * Yaw is sometimes called "heading". * @property {number} bodyPitch - The rotation about an axis running from shoulder to shoulder of the avatar. Pitch is * sometimes called "elevation". @@ -57,13 +57,12 @@ * @property {number} audioAverageLoudness - The rolling average loudness of the audio input that the avatar is injecting * into the domain. * @property {string} displayName - The avatar's display name. - * @property {string} sessionDisplayName - Sanitized, defaulted version of displayName that is defined by the - * avatar mixer rather than by Interface clients. The result is unique among all avatars present in the domain at the - * time. - * @property {boolean} lookAtSnappingEnabled=true - If true, the avatar's eyes snap to look at another avatar's - * eyes if generally in the line of sight and the other avatar also has lookAtSnappingEnabled == true. - * @property {string} skeletonModelURL - The URL of the avatar model's .fst file. - * @property {AttachmentData[]} attachmentData - Information on the attachments worn by the avatar.
+ * @property {string} sessionDisplayName - displayName's sanitized and default version defined by the avatar mixer + * rather than Interface clients. The result is unique among all avatars present in the domain at the time. + * @property {boolean} lookAtSnappingEnabled=true - true if the avatar's eyes snap to look at another avatar's + * eyes when the other avatar is in the line of sight and also has lookAtSnappingEnabled == true. + * @property {string} skeletonModelURL - The avatar's FST file. + * @property {AttachmentData[]} attachmentData - Information on the avatar's attachments.
* Deprecated: Use avatar entities instead. * @property {string[]} jointNames - The list of joints in the current avatar model. Read-only. * @property {Uuid} sessionUUID - Unique ID of the avatar in the domain. Read-only. @@ -75,6 +74,12 @@ * avatar. Read-only. * @property {number} sensorToWorldScale - The scale that transforms dimensions in the user's real world to the avatar's * size in the virtual world. Read-only. + * + * @example
+ * (function () { + * Agent.setIsAvatar(true); + * print("Position: " + JSON.stringify(Avatar.position)); // 0, 0, 0 + * }()); */ class ScriptableAvatar : public AvatarData, public Dependency { @@ -90,14 +95,14 @@ public: /**jsdoc * Starts playing an animation on the avatar. * @function Avatar.startAnimation - * @param {string} url - The URL to the animation file. Animation files need to be .FBX format but only need to contain + * @param {string} url - The animation file's URL. Animation files need to be in the FBX format but only need to contain * the avatar skeleton and animation data. * @param {number} [fps=30] - The frames per second (FPS) rate for the animation playback. 30 FPS is normal speed. * @param {number} [priority=1] - Not used. * @param {boolean} [loop=false] - true if the animation should loop, false if it shouldn't. * @param {boolean} [hold=false] - Not used. - * @param {number} [firstFrame=0] - The frame the animation should start at. - * @param {number} [lastFrame=3.403e+38] - The frame the animation should stop at. + * @param {number} [firstFrame=0] - The frame at which the animation starts. + * @param {number} [lastFrame=3.403e+38] - The frame at which the animation stops. * @param {string[]} [maskedJoints=[]] - The names of joints that should not be animated. */ /// Allows scripts to run animations. @@ -115,6 +120,9 @@ public: * Gets the details of the current avatar animation that is being or was recently played. * @function Avatar.getAnimationDetails * @returns {Avatar.AnimationDetails} The current or recent avatar animation. + * @example + * var animationDetails = Avatar.getAnimationDetails(); + * print("Animation details: " + JSON.stringify(animationDetails)); */ Q_INVOKABLE AnimationDetails getAnimationDetails(); @@ -146,18 +154,21 @@ public: bool getHasAudioEnabledFaceMovement() const override { return _headData->getHasAudioEnabledFaceMovement(); } /**jsdoc - * Gets the avatar entities as binary data. - *

Warning: Potentially a very expensive call. Do not use if possible.

+ * Gets details of all avatar entities. + *

Warning: Potentially an expensive call. Do not use if possible.

* @function Avatar.getAvatarEntityData - * @returns {AvatarEntityMap} The avatar entities as binary data. + * @returns {AvatarEntityMap} Details of the avatar entities. + * @example
+ * var avatarEntityData = Avatar.getAvatarEntityData(); + * print("Avatar entities: " + JSON.stringify(avatarEntityData)); */ Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const override; /**jsdoc - * Sets the avatar entities from binary data. + * Sets all avatar entities from an object. *

Warning: Potentially an expensive call. Do not use if possible.

* @function Avatar.setAvatarEntityData - * @param {AvatarEntityMap} avatarEntityData - The avatar entities as binary data. + * @param {AvatarEntityMap} avatarEntityData - Details of the avatar entities. */ Q_INVOKABLE void setAvatarEntityData(const AvatarEntityMap& avatarEntityData) override; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index e0e9b5b648..12c260ccde 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3542,8 +3542,8 @@ void MyAvatar::clearScaleRestriction() { /**jsdoc * A teleport target. * @typedef {object} MyAvatar.GoToProperties - * @property {Vec3} position - The new position for the avatar, in world coordinates. - * @property {Quat} [orientation] - The new orientation for the avatar. + * @property {Vec3} position - The avatar's new position. + * @property {Quat} [orientation] - The avatar's new orientation. */ void MyAvatar::goToLocation(const QVariant& propertiesVar) { qCDebug(interfaceapp, "MyAvatar QML goToLocation"); @@ -3902,8 +3902,7 @@ void MyAvatar::setCollisionWithOtherAvatarsFlags() { } /**jsdoc - * A collision capsule is a cylinder with hemispherical ends. It is used, in particular, to approximate the extents of an - * avatar. + * A collision capsule is a cylinder with hemispherical ends. It is often used to approximate the extents of an avatar. * @typedef {object} MyAvatar.CollisionCapsule * @property {Vec3} start - The bottom end of the cylinder, excluding the bottom hemisphere. * @property {Vec3} end - The top end of the cylinder, excluding the top hemisphere. @@ -5361,12 +5360,12 @@ void MyAvatar::addAvatarHandsToFlow(const std::shared_ptr& otherAvatar) /**jsdoc * Physics options to use in the flow simulation of a joint. * @typedef {object} MyAvatar.FlowPhysicsOptions - * @property {boolean} [active=true] - true to enable flow on the joint, false if it isn't., - * @property {number} [radius=0.01] - The thickness of segments and knots. (Needed for collisions.) + * @property {boolean} [active=true] - true to enable flow on the joint, otherwise false. + * @property {number} [radius=0.01] - The thickness of segments and knots (needed for collisions). * @property {number} [gravity=-0.0096] - Y-value of the gravity vector. * @property {number} [inertia=0.8] - Rotational inertia multiplier. * @property {number} [damping=0.85] - The amount of damping on joint oscillation. - * @property {number} [stiffness=0.0] - How stiff each thread is. + * @property {number} [stiffness=0.0] - The stiffness of each thread. * @property {number} [delta=0.55] - Delta time for every integration step. */ /**jsdoc @@ -5454,18 +5453,18 @@ void MyAvatar::useFlow(bool isActive, bool isCollidable, const QVariantMap& phys * that has been configured. * @property {Object} collisions - The collisions configuration for each joint that * has collisions configured. - * @property {Object} threads - The threads hat have been configured, with the name of the first joint as - * the ThreadName and an array of the indexes of all the joints in the thread as the value. + * @property {Object} threads - The threads that have been configured, with the first joint's name as the + * ThreadName and value as an array of the indexes of all the joints in the thread. */ /**jsdoc * A set of physics options currently used in flow simulation. * @typedef {object} MyAvatar.FlowPhysicsData - * @property {boolean} active - true to enable flow on the joint, false if it isn't., + * @property {boolean} active - true to enable flow on the joint, otherwise false. * @property {number} radius - The thickness of segments and knots. (Needed for collisions.) * @property {number} gravity - Y-value of the gravity vector. * @property {number} inertia - Rotational inertia multiplier. * @property {number} damping - The amount of damping on joint oscillation. - * @property {number} stiffness - How stiff each thread is. + * @property {number} stiffness - The stiffness of each thread. * @property {number} delta - Delta time for every integration step. * @property {number[]} jointIndices - The indexes of the joints the options are applied to. */ diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 8951bc7fed..3159348871 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -72,13 +72,13 @@ class MyAvatar : public Avatar { * * @comment IMPORTANT: This group of properties is copied from AvatarData.h; they should NOT be edited here. * @property {Vec3} position - The position of the avatar. - * @property {number} scale=1.0 - The scale of the avatar. When setting, the value is limited to between 0.005 - * and 1000.0. When getting, the value may temporarily be further limited by the domain's settings. + * @property {number} scale=1.0 - The scale of the avatar. The value can be set to anything between 0.005 and + * 1000.0. When the scale value is fetched, it may temporarily be further limited by the domain's settings. * @property {number} density - The density of the avatar in kg/m3. The density is used to work out its mass in * the application of physics. Read-only. * @property {Vec3} handPosition - A user-defined hand position, in world coordinates. The position moves with the avatar * but is otherwise not used or changed by Interface. - * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. + * @property {number} bodyYaw - The left or right rotation about an axis running from the head to the feet of the avatar. * Yaw is sometimes called "heading". * @property {number} bodyPitch - The rotation about an axis running from shoulder to shoulder of the avatar. Pitch is * sometimes called "elevation". @@ -99,13 +99,12 @@ class MyAvatar : public Avatar { * @property {number} audioAverageLoudness - The rolling average loudness of the audio input that the avatar is injecting * into the domain. * @property {string} displayName - The avatar's display name. - * @property {string} sessionDisplayName - Sanitized, defaulted version of displayName that is defined by the - * avatar mixer rather than by Interface clients. The result is unique among all avatars present in the domain at the - * time. - * @property {boolean} lookAtSnappingEnabled=true - If true, the avatar's eyes snap to look at another avatar's - * eyes if generally in the line of sight and the other avatar also has lookAtSnappingEnabled == true. - * @property {string} skeletonModelURL - The URL of the avatar model's .fst file. - * @property {AttachmentData[]} attachmentData - Information on the attachments worn by the avatar.
+ * @property {string} sessionDisplayName - displayName's sanitized and default version defined by the avatar + * mixer rather than Interface clients. The result is unique among all avatars present in the domain at the time. + * @property {boolean} lookAtSnappingEnabled=true - true if the avatar's eyes snap to look at another avatar's + * eyes when the other avatar is in the line of sight and also has lookAtSnappingEnabled == true. + * @property {string} skeletonModelURL - The avatar's FST file. + * @property {AttachmentData[]} attachmentData - Information on the avatar's attachments.
* Deprecated: Use avatar entities instead. * @property {string[]} jointNames - The list of joints in the current avatar model. Read-only. * @property {Uuid} sessionUUID - Unique ID of the avatar in the domain. Read-only. @@ -149,12 +148,12 @@ class MyAvatar : public Avatar { * property value is audioListenerModeCustom. * @property {Quat} customListenOrientation=Quat.IDENTITY - The listening orientation used when the * audioListenerMode property value is audioListenerModeCustom. - * @property {boolean} hasScriptedBlendshapes=false - Blendshapes will be transmitted over the network if set to true.
+ * @property {boolean} hasScriptedBlendshapes=false - true to transmit blendshapes over the network.
* Note: Currently doesn't work. Use {@link MyAvatar.setForceFaceTrackerConnected} instead. - * @property {boolean} hasProceduralBlinkFaceMovement=true - If true then procedural blinking is turned on. - * @property {boolean} hasProceduralEyeFaceMovement=true - If true then procedural eye movement is turned on. - * @property {boolean} hasAudioEnabledFaceMovement=true - If true then voice audio will move the mouth - * blendshapes while MyAvatar.hasScriptedBlendshapes is enabled. + * @property {boolean} hasProceduralBlinkFaceMovement=true - true if procedural blinking is turned on. + * @property {boolean} hasProceduralEyeFaceMovement=true - true if procedural eye movement is turned on. + * @property {boolean} hasAudioEnabledFaceMovement=true - true to move the mouth blendshapes with voice audio + * when MyAvatar.hasScriptedBlendshapes is enabled. * @property {number} rotationRecenterFilterLength - Configures how quickly the avatar root rotates to recenter its facing * direction to match that of the user's torso based on head and hands orientation. A smaller value makes the * recentering happen more quickly. The minimum value is 0.01. @@ -178,23 +177,23 @@ class MyAvatar : public Avatar { * the palm, in avatar coordinates. If the hand isn't being positioned by a controller, the value is * {@link Vec3(0)|Vec3.ZERO}. Read-only. * - * @property {Pose} leftHandPose - The pose of the left hand as determined by the hand controllers, relative to the avatar. + * @property {Pose} leftHandPose - The left hand's pose as determined by the hand controllers, relative to the avatar. * Read-only. - * @property {Pose} rightHandPose - The pose right hand position as determined by the hand controllers, relative to the - * avatar. Read-only. - * @property {Pose} leftHandTipPose - The pose of the left hand as determined by the hand controllers, relative to the - * avatar, with the position adjusted to be 0.3m along the direction of the palm. Read-only. - * @property {Pose} rightHandTipPose - The pose of the right hand as determined by the hand controllers, relative to the - * avatar, with the position adjusted by 0.3m along the direction of the palm. Read-only. + * @property {Pose} rightHandPose - The right hand's pose as determined by the hand controllers, relative to the avatar. + * Read-only. + * @property {Pose} leftHandTipPose - The left hand's pose as determined by the hand controllers, relative to the avatar, + * with the position adjusted by 0.3m along the direction of the palm. Read-only. + * @property {Pose} rightHandTipPose - The right hand's pose as determined by the hand controllers, relative to the avatar, + * with the position adjusted by 0.3m along the direction of the palm. Read-only. * * @property {number} energy - Deprecated: This property will be removed from the API. * @property {boolean} isAway - true if your avatar is away (i.e., inactive), false if it is * active. * - * @property {boolean} centerOfGravityModelEnabled=true - If true then the avatar hips are placed according to - * the center of gravity model that balances the center of gravity over the base of support of the feet. Setting the - * value false results in the default behavior where the hips are positioned under the head. - * @property {boolean} hmdLeanRecenterEnabled=true - If true then the avatar is re-centered to be under the + * @property {boolean} centerOfGravityModelEnabled=true - true if the avatar hips are placed according to + * the center of gravity model that balances the center of gravity over the base of support of the feet. Set the + * value to false for default behavior where the hips are positioned under the head. + * @property {boolean} hmdLeanRecenterEnabled=true - true IF the avatar is re-centered to be under the * head's position. In room-scale VR, this behavior is what causes your avatar to follow your HMD as you walk around * the room. Setting the value false is useful if you want to pin the avatar to a fixed position. * @property {boolean} collisionsEnabled - Set to true to enable the avatar to collide with the environment, @@ -227,9 +226,9 @@ class MyAvatar : public Avatar { * where MyAvatar.sessionUUID is not available (e.g., if not connected to a domain). Note: Likely to be deprecated. * Read-only. * - * @property {number} walkSpeed - Adjusts the walk speed of your avatar. - * @property {number} walkBackwardSpeed - Adjusts the walk backward speed of your avatar. - * @property {number} sprintSpeed - Adjusts the sprint speed of your avatar. + * @property {number} walkSpeed - The walk speed of your avatar. + * @property {number} walkBackwardSpeed - The walk backward speed of your avatar. + * @property {number} sprintSpeed - The sprint speed of your avatar. * @property {MyAvatar.SitStandModelType} userRecenterModel - Controls avatar leaning and recentering behavior. * @property {number} isInSittingState - true if your avatar is sitting (avatar leaning is disabled, * recenntering is enabled), false if it is standing (avatar leaning is enabled, and avatar recenters if it @@ -237,8 +236,8 @@ class MyAvatar : public Avatar { * user sits or stands, unless isSitStandStateLocked == true. Setting the property value overrides the * current siting / standing state, which is updated when the user next sits or stands unless * isSitStandStateLocked == true. - * @property {boolean} isSitStandStateLocked - true locks the avatar sitting / standing state, i.e., disables - * automatically changing it based on the user sitting or standing. + * @property {boolean} isSitStandStateLocked - true to lock the avatar sitting/standing state, i.e., use this + * to disable automatically changing state. * @property {boolean} allowTeleporting - true if teleporting is enabled in the Interface settings, * false if it isn't. Read-only. * @@ -396,7 +395,7 @@ public: *
- * + * * * * function animStateHandler(dictionary) { * print("Anim state dictionary: " + JSON.stringify(dictionary)); @@ -715,7 +715,7 @@ public: /**jsdoc - * Gets whether or not you do snap turns in HMD mode. + * Gets whether you do snap turns in HMD mode. * @function MyAvatar.getSnapTurn * @returns {boolean} true if you do snap turns in HMD mode; false if you do smooth turns in HMD * mode. @@ -761,7 +761,7 @@ public: Q_INVOKABLE QString getHmdAvatarAlignmentType() const; /**jsdoc - * Sets whether the avatar hips are balanced over the feet or positioned under the head. + * Sets whether the avatar's hips are balanced over the feet or positioned under the head. * @function MyAvatar.setCenterOfGravityModelEnabled * @param {boolean} enabled - true to balance the hips over the feet, false to position the hips * under the head. @@ -777,7 +777,7 @@ public: Q_INVOKABLE bool getCenterOfGravityModelEnabled() const { return _centerOfGravityModelEnabled; } /**jsdoc - * Sets whether or not the avatar's position updates to recenter the avatar under the head. In room-scale VR, recentering + * Sets whether the avatar's position updates to recenter the avatar under the head. In room-scale VR, recentering * causes your avatar to follow your HMD as you walk around the room. Disabling recentering is useful if you want to pin * the avatar to a fixed position. * @function MyAvatar.setHMDLeanRecenterEnabled @@ -787,7 +787,7 @@ public: Q_INVOKABLE void setHMDLeanRecenterEnabled(bool value) { _hmdLeanRecenterEnabled = value; } /**jsdoc - * Gets whether or not the avatar's position updates to recenter the avatar under the head. In room-scale VR, recentering + * Gets whether the avatar's position updates to recenter the avatar under the head. In room-scale VR, recentering * causes your avatar to follow your HMD as you walk around the room. * @function MyAvatar.getHMDLeanRecenterEnabled * @returns {boolean} true if recentering is enabled, false if not. @@ -864,7 +864,7 @@ public: float getDriveKey(DriveKeys key) const; /**jsdoc - * Gets the value of a drive key, regardless of whether or not it is disabled. + * Gets the value of a drive key, regardless of whether it is disabled. * @function MyAvatar.getRawDriveKey * @param {MyAvatar.DriveKeys} key - The drive key. * @returns {number} The value of the drive key. @@ -889,14 +889,15 @@ public: Q_INVOKABLE void disableDriveKey(DriveKeys key); /**jsdoc - * Enables the acction associated with a drive key. + * Enables the action associated with a drive key. The action may have been disabled with + * {@link MyAvatar.disableDriveKey|disableDriveKey}. * @function MyAvatar.enableDriveKey * @param {MyAvatar.DriveKeys} key - The drive key to enable. */ Q_INVOKABLE void enableDriveKey(DriveKeys key); /**jsdoc - * Checks whether or not a drive key is disabled. + * Checks whether a drive key is disabled. * @function MyAvatar.isDriveKeyDisabled * @param {DriveKeys} key - The drive key to check. * @returns {boolean} true if the drive key is disabled, false if it isn't. @@ -925,7 +926,7 @@ public: Q_INVOKABLE void triggerRotationRecenter(); /**jsdoc - * Gets whether or not the avatar is configured to keep its center of gravity under its head. + * Gets whether the avatar is configured to keep its center of gravity under its head. * @function MyAvatar.isRecenteringHorizontally * @returns {boolean} true if the avatar is keeping its center of gravity under its head position, * false if not. @@ -967,8 +968,8 @@ public: Q_INVOKABLE float getHeadFinalPitch() const { return getHead()->getFinalPitch(); } /**jsdoc - * If a face tracker is connected and being used, gets the estimated pitch of the user's head scaled such that the avatar - * looks at the edge of the view frustum when the user looks at the edge of their screen. + * If a face tracker is connected and being used, gets the estimated pitch of the user's head scaled. This is scale such + * that the avatar looks at the edge of the view frustum when the user looks at the edge of their screen. * @function MyAvatar.getHeadDeltaPitch * @returns {number} The pitch that the avatar's head should be if a face tracker is connected and being used, otherwise * 0, in degrees. @@ -1080,8 +1081,8 @@ public: /**jsdoc * Gets the pose (position, rotation, velocity, and angular velocity) of the avatar's left hand, relative to the avatar, as * positioned by a hand controller (e.g., Oculus Touch or Vive), and translated 0.3m along the palm. - *

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints - * for hand animation.) If you are using the Leap Motion, the return value's valid property will be + *

Note: Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints + * for hand animation.) If you are using Leap Motion, the return value's valid property will be * false and any pose values returned will not be meaningful.

* @function MyAvatar.getLeftHandTipPose * @returns {Pose} The pose of the avatar's left hand, relative to the avatar, as positioned by a hand controller, and @@ -1092,8 +1093,8 @@ public: /**jsdoc * Gets the pose (position, rotation, velocity, and angular velocity) of the avatar's right hand, relative to the avatar, as * positioned by a hand controller (e.g., Oculus Touch or Vive), and translated 0.3m along the palm. - *

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints - * for hand animation.) If you are using the Leap Motion, the return value's valid property will be + *

Note: Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints + * for hand animation.) If you are using Leap Motion, the return value's valid property will be * false and any pose values returned will not be meaningful.

* @function MyAvatar.getRightHandTipPose * @returns {Pose} The pose of the avatar's right hand, relative to the avatar, as positioned by a hand controller, and @@ -1247,14 +1248,14 @@ public: void clearWornAvatarEntities(); /**jsdoc - * Checks whether your avatar is flying or not. + * Checks whether your avatar is flying. * @function MyAvatar.isFlying * @returns {boolean} true if your avatar is flying and not taking off or falling, false if not. */ Q_INVOKABLE bool isFlying(); /**jsdoc - * Checks whether your avatar is in the air or not. + * Checks whether your avatar is in the air. * @function MyAvatar.isInAir * @returns {boolean} true if your avatar is taking off, flying, or falling, otherwise false * because your avatar is on the ground. @@ -1332,9 +1333,9 @@ public: Q_INVOKABLE void setAvatarScale(float scale); /**jsdoc - * Sets whether or not the avatar should collide with entities. + * Sets whether the avatar should collide with entities. *

Note: A false value won't disable collisions if the avatar is in a zone that disallows - * collisionless avatars, however the false value will be set so that collisions are disabled as soon as the + * collisionless avatars. However, the false value will be set so that collisions are disabled as soon as the * avatar moves to a position where collisionless avatars are allowed. * @function MyAvatar.setCollisionsEnabled * @param {boolean} enabled - true to enable the avatar to collide with entities, false to @@ -1343,7 +1344,7 @@ public: Q_INVOKABLE void setCollisionsEnabled(bool enabled); /**jsdoc - * Gets whether or not the avatar will currently collide with entities. + * Gets whether the avatar will currently collide with entities. *

Note: The avatar will always collide with entities if in a zone that disallows collisionless avatars. * @function MyAvatar.getCollisionsEnabled * @returns {boolean} true if the avatar will currently collide with entities, false if it won't. @@ -1351,7 +1352,7 @@ public: Q_INVOKABLE bool getCollisionsEnabled(); /**jsdoc - * Sets whether or not the avatar should collide with other avatars. + * Sets whether the avatar should collide with other avatars. * @function MyAvatar.setOtherAvatarsCollisionsEnabled * @param {boolean} enabled - true to enable the avatar to collide with other avatars, false * to disable. @@ -1359,7 +1360,7 @@ public: Q_INVOKABLE void setOtherAvatarsCollisionsEnabled(bool enabled); /**jsdoc - * Gets whether or not the avatar will collide with other avatars. + * Gets whether the avatar will collide with other avatars. * @function MyAvatar.getOtherAvatarsCollisionsEnabled * @returns {boolean} true if the avatar will collide with other avatars, false if it won't. */ @@ -1395,6 +1396,10 @@ public: * @function MyAvatar.getAbsoluteJointRotationInObjectFrame * @param {number} index - The index of the joint. * @returns {Quat} The rotation of the joint relative to the avatar. + * @example

+ * var headIndex = MyAvatar.getJointIndex("Head"); + * var headRotation = MyAvatar.getAbsoluteJointRotationInObjectFrame(headIndex); + * print("Head rotation: " + JSON.stringify(Quat.safeEulerAngles(headRotation))); // Degrees */ virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override; @@ -1404,6 +1409,10 @@ public: * @function MyAvatar.getAbsoluteJointTranslationInObjectFrame * @param {number} index - The index of the joint. * @returns {Vec3} The translation of the joint relative to the avatar. + * @example + * var headIndex = MyAvatar.getJointIndex("Head"); + * var headTranslation = MyAvatar.getAbsoluteJointTranslationInObjectFrame(headIndex); + * print("Head translation: " + JSON.stringify(headTranslation)); */ virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override; @@ -1500,36 +1509,56 @@ public: void prepareAvatarEntityDataForReload(); /**jsdoc - * Creates a new grab, that grabs an entity. + * Creates a new grab that grabs an entity. * @function MyAvatar.grab * @param {Uuid} targetID - The ID of the entity to grab. * @param {number} parentJointIndex - The avatar joint to use to grab the entity. - * @param {Vec3} offset - The target's local positional relative to the joint. + * @param {Vec3} offset - The target's local position relative to the joint. * @param {Quat} rotationalOffset - The target's local rotation relative to the joint. * @returns {Uuid} The ID of the new grab. + * @example + * var entityPosition = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -5 })); + * var entityRotation = MyAvatar.orientation; + * var entityID = Entities.addEntity({ + * type: "Box", + * position: entityPosition, + * rotation: entityRotation, + * dimensions: { x: 0.5, y: 0.5, z: 0.5 } + * }); + * var rightHandJoint = MyAvatar.getJointIndex("RightHand"); + * var relativePosition = Entities.worldToLocalPosition(entityPosition, MyAvatar.SELF_ID, rightHandJoint); + * var relativeRotation = Entities.worldToLocalRotation(entityRotation, MyAvatar.SELF_ID, rightHandJoint); + * var grabID = MyAvatar.grab(entityID, rightHandJoint, relativePosition, relativeRotation); + * + * Script.setTimeout(function () { + * MyAvatar.releaseGrab(grabID); + * Entities.deleteEntity(entityID); + * }, 10000); */ Q_INVOKABLE const QUuid grab(const QUuid& targetID, int parentJointIndex, glm::vec3 positionalOffset, glm::quat rotationalOffset); /**jsdoc - * Releases (deletes) a grab, to stop grabbing an entity. + * Releases (deletes) a grab to stop grabbing an entity. * @function MyAvatar.releaseGrab * @param {Uuid} grabID - The ID of the grab to release. */ Q_INVOKABLE void releaseGrab(const QUuid& grabID); /**jsdoc - * Gets the avatar entities as binary data. + * Gets details of all avatar entities. * @function MyAvatar.getAvatarEntityData - * @override - * @returns {AvatarEntityMap} The avatar entities as binary data. + * @returns {AvatarEntityMap} Details of the avatar entities. + * @example + * var avatarEntityData = MyAvatar.getAvatarEntityData(); + * print("Avatar entities: " + JSON.stringify(avatarEntityData)); */ AvatarEntityMap getAvatarEntityData() const override; /**jsdoc - * Sets the avatar entities from binary data. + * Sets all avatar entities from an object. * @function MyAvatar.setAvatarEntityData - * @param {AvatarEntityMap} avatarEntityData - The avatar entities as binary data. + * @param {AvatarEntityMap} avatarEntityData - Details of the avatar entities. */ void setAvatarEntityData(const AvatarEntityMap& avatarEntityData) override; @@ -1549,7 +1578,7 @@ public: /**jsdoc * Enables and disables flow simulation of physics on the avatar's hair, clothes, and body parts. See - * {@link https://docs.highfidelity.com/create/avatars/create-avatars/add-flow.html|Add Flow to Your Avatar} for more + * {@link https://docs.highfidelity.com/create/avatars/add-flow.html|Add Flow to Your Avatar} for more * information. * @function MyAvatar.useFlow * @param {boolean} isActive - true if flow simulation is enabled on the joint, false if it isn't. @@ -1585,7 +1614,7 @@ public slots: * MyAvatar.resetSize(); * * for (var i = 0; i < 5; i++){ - * print ("Growing by 5 percent"); + * print("Growing by 5 percent"); * MyAvatar.increaseSize(); * } */ @@ -1598,7 +1627,7 @@ public slots: * MyAvatar.resetSize(); * * for (var i = 0; i < 5; i++){ - * print ("Shrinking by 5 percent"); + * print("Shrinking by 5 percent"); * MyAvatar.decreaseSize(); * } */ @@ -1695,7 +1724,7 @@ public slots: /**jsdoc - * Adds a thrust to your avatar's current thrust, to be applied for a short while. + * Adds a thrust to your avatar's current thrust to be applied for a short while. * @function MyAvatar.addThrust * @param {Vec3} thrust - The thrust direction and magnitude. * @deprecated Use {@link MyAvatar|MyAvatar.motorVelocity} and related properties instead. @@ -1735,7 +1764,9 @@ public slots: void setToggleHips(bool followHead); /**jsdoc - * Displays base of support of feet debug graphics. + * Displays the base of support area debug graphics if in HMD mode. If your head goes outside this area your avatar's hips + * are moved to counterbalance your avatar, and if your head moves too far then your avatar's position is moved (i.e., a + * step happens). * @function MyAvatar.setEnableDebugDrawBaseOfSupport * @param {boolean} enabled - true to show the debug graphics, false to hide. */ @@ -1805,7 +1836,7 @@ public slots: void setEnableDebugDrawDetailedCollision(bool isEnabled); /**jsdoc - * Gets whether or not your avatar mesh is visible. + * Gets whether your avatar mesh is visible. * @function MyAvatar.getEnableMeshVisible * @returns {boolean} true if your avatar's mesh is visible, otherwise false. */ @@ -1830,7 +1861,7 @@ public slots: void sanitizeAvatarEntityProperties(EntityItemProperties& properties) const; /**jsdoc - * Sets whether or not your avatar mesh is visible to you. + * Sets whether your avatar mesh is visible to you. * @function MyAvatar.setEnableMeshVisible * @param {boolean} enabled - true to show your avatar mesh, false to hide. * @example @@ -1842,7 +1873,7 @@ public slots: virtual void setEnableMeshVisible(bool isEnabled) override; /**jsdoc - * Sets whether or not inverse kinematics (IK) for your avatar. + * Sets whether inverse kinematics (IK) is enabled for your avatar. * @function MyAvatar.setEnableInverseKinematics * @param {boolean} enabled - true to enable IK, false to disable. */ @@ -1854,7 +1885,8 @@ public slots: *

See {@link https://docs.highfidelity.com/create/avatars/custom-animations.html|Custom Avatar Animations} for * information on animation graphs.

* @function MyAvatar.getAnimGraphOverrideUrl - * @returns {string} The URL of the override animation graph. "" if there is no override animation graph. + * @returns {string} The URL of the override animation graph JSON file. "" if there is no override animation + * graph. */ QUrl getAnimGraphOverrideUrl() const; // thread-safe @@ -1863,25 +1895,27 @@ public slots: *

See {@link https://docs.highfidelity.com/create/avatars/custom-animations.html|Custom Avatar Animations} for * information on animation graphs.

* @function MyAvatar.setAnimGraphOverrideUrl - * @param {string} url - The URL of the animation graph to use. Set to "" to clear an override. + * @param {string} url - The URL of the animation graph JSON file to use. Set to "" to clear an override. */ void setAnimGraphOverrideUrl(QUrl value); // thread-safe /**jsdoc - * Gets the URL of animation graph that's currently being used for avatar animations. + * Gets the URL of animation graph (i.e., the avatar animation JSON) that's currently being used for avatar animations. *

See {@link https://docs.highfidelity.com/create/avatars/custom-animations.html|Custom Avatar Animations} for * information on animation graphs.

* @function MyAvatar.getAnimGraphUrl - * @returns {string} The URL of the current animation graph. + * @returns {string} The URL of the current animation graph JSON file. + * @Example
+ * print(MyAvatar.getAnimGraphUrl()); */ QUrl getAnimGraphUrl() const; // thread-safe /**jsdoc - * Sets the current animation graph to use for avatar animations and makes it the default. + * Sets the current animation graph (i.e., the avatar animation JSON) to use for avatar animations and makes it the default. *

See {@link https://docs.highfidelity.com/create/avatars/custom-animations.html|Custom Avatar Animations} for * information on animation graphs.

* @function MyAvatar.setAnimGraphUrl - * @param {string} url - The URL of the animation graph to use. + * @param {string} url - The URL of the animation graph JSON file to use. */ void setAnimGraphUrl(const QUrl& url); // thread-safe @@ -1963,11 +1997,14 @@ signals: void otherAvatarsCollisionsEnabledChanged(bool enabled); /**jsdoc - * Triggered when the avatar's animation graph URL changes. + * Triggered when the avatar's animation graph being used changes. * @function MyAvatar.animGraphUrlChanged - * @param {string} url - The URL of the new animation graph. + * @param {string} url - The URL of the new animation graph JSON file. * @returns {Signal} - */ + * @example
+ * MyAvatar.animGraphUrlChanged.connect(function (url) { + * print("Avatar animation JSON changed to: " + url); + * }); */ void animGraphUrlChanged(const QUrl& url); /**jsdoc diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h index d5a110ea76..6d58ecedec 100644 --- a/libraries/animation/src/AnimInverseKinematics.h +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -66,24 +66,23 @@ public: * * * - * - * + * + * * - * * diff --git a/libraries/animation/src/AnimOverlay.h b/libraries/animation/src/AnimOverlay.h index 623672143e..1ad4e100db 100644 --- a/libraries/animation/src/AnimOverlay.h +++ b/libraries/animation/src/AnimOverlay.h @@ -34,17 +34,21 @@ public: * * * - * - * - * - * + * + * * * * - * - * + * + * * * * diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index cec440eb1a..87e6af5bb3 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -203,7 +203,7 @@ public: /**jsdoc * Gets the default rotation of a joint (in the current avatar) relative to its parent. *

For information on the joint hierarchy used, see - * Avatar Standards.

+ * Avatar Standards.

* @function MyAvatar.getDefaultJointRotation * @param {number} index - The joint index. * @returns {Quat} The default rotation of the joint if the joint index is valid, otherwise {@link Quat(0)|Quat.IDENTITY}. @@ -214,7 +214,7 @@ public: * Gets the default translation of a joint (in the current avatar) relative to its parent, in model coordinates. *

Warning: These coordinates are not necessarily in meters.

*

For information on the joint hierarchy used, see - * Avatar Standards.

+ * Avatar Standards.

* @function MyAvatar.getDefaultJointTranslation * @param {number} index - The joint index. * @returns {Vec3} The default translation of the joint (in model coordinates) if the joint index is valid, otherwise @@ -223,22 +223,30 @@ public: Q_INVOKABLE virtual glm::vec3 getDefaultJointTranslation(int index) const; /**jsdoc - * Provides read-only access to the default joint rotations in avatar coordinates. + * Gets the default joint rotations in avatar coordinates. * The default pose of the avatar is defined by the position and orientation of all bones * in the avatar's model file. Typically this is a T-pose. * @function MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame * @param index {number} - The joint index. - * @returns {Quat} The rotation of this joint in avatar coordinates. + * @returns {Quat} The default rotation of the joint in avatar coordinates. + * @example
+ * var headIndex = MyAvatar.getJointIndex("Head"); + * var defaultHeadRotation = MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame(headIndex); + * print("Default head rotation: " + JSON.stringify(Quat.safeEulerAngles(defaultHeadRotation))); // Degrees */ Q_INVOKABLE virtual glm::quat getAbsoluteDefaultJointRotationInObjectFrame(int index) const; /**jsdoc - * Provides read-only access to the default joint translations in avatar coordinates. + * Gets the default joint translations in avatar coordinates. * The default pose of the avatar is defined by the position and orientation of all bones * in the avatar's model file. Typically this is a T-pose. * @function MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame * @param index {number} - The joint index. - * @returns {Vec3} The position of this joint in avatar coordinates. + * @returns {Vec3} The default position of the joint in avatar coordinates. + * @example + * var headIndex = MyAvatar.getJointIndex("Head"); + * var defaultHeadTranslation = MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(headIndex); + * print("Default head translation: " + JSON.stringify(defaultHeadTranslation)); */ Q_INVOKABLE virtual glm::vec3 getAbsoluteDefaultJointTranslationInObjectFrame(int index) const; @@ -459,7 +467,7 @@ public: Q_INVOKABLE virtual quint16 getParentJointIndex() const override { return SpatiallyNestable::getParentJointIndex(); } /**jsdoc - * sets the joint of the entity or avatar that the avatar is parented to. + * Sets the joint of the entity or avatar that the avatar is parented to. * @function MyAvatar.setParentJointIndex * @param {number} parentJointIndex - he joint of the entity or avatar that the avatar should be parented to. Use * 65535 or -1 to parent to the entity or avatar's position and orientation rather than a diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 317d9ec903..39dfaa8a1a 100755 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -2802,7 +2802,7 @@ glm::vec3 AvatarData::getAbsoluteJointTranslationInObjectFrame(int index) const /**jsdoc * Information on an attachment worn by the avatar. * @typedef {object} AttachmentData - * @property {string} modelUrl - The URL of the model file. Models can be .FBX or .OBJ format. + * @property {string} modelUrl - The URL of the model file. Models can be FBX or OBJ format. * @property {string} jointName - The offset to apply to the model relative to the joint position. * @property {Vec3} translation - The offset from the joint that the attachment is positioned at. * @property {Vec3} rotation - The rotation applied to the model relative to the joint orientation. @@ -3015,6 +3015,10 @@ float AvatarData::_avatarSortCoefficientSize { 8.0f }; float AvatarData::_avatarSortCoefficientCenter { 0.25f }; float AvatarData::_avatarSortCoefficientAge { 1.0f }; +/**jsdoc + * An object with the UUIDs of avatar entities as keys and avatar entity properties objects as values. + * @typedef {Object.} AvatarEntityMap + */ QScriptValue AvatarEntityMapToScriptValue(QScriptEngine* engine, const AvatarEntityMap& value) { QScriptValue obj = engine->newObject(); for (auto entityID : value.keys()) { diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index e2d24465cb..00e7e67923 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -61,10 +61,6 @@ using AvatarSharedPointer = std::shared_ptr; using AvatarWeakPointer = std::weak_ptr; using AvatarHash = QHash; -/**jsdoc - * An object with the UUIDs of avatar entities as keys and binary blobs, being the entity properties, as values. - * @typedef {Object.>} AvatarEntityMap - */ using AvatarEntityMap = QMap; using PackedAvatarEntityMap = QMap; // similar to AvatarEntityMap, but different internal format using AvatarEntityIDs = QSet; @@ -439,13 +435,13 @@ class AvatarData : public QObject, public SpatiallyNestable { // IMPORTANT: The JSDoc for the following properties should be copied to MyAvatar.h and ScriptableAvatar.h. /* * @property {Vec3} position - The position of the avatar. - * @property {number} scale=1.0 - The scale of the avatar. When setting, the value is limited to between 0.005 - * and 1000.0. When getting, the value may temporarily be further limited by the domain's settings. - * @property {number} density - The density of the avatar in kg/m3. The density is used to work out its mass in + * @property {number} scale=1.0 - The scale of the avatar. The value can be set to anything between 0.005 and + * 1000.0. When the scale value is fetched, it may temporarily be further limited by the domain's settings. + * @property {number} density - The density of the avatar in kg/m3. The density is used to work out its mass in * the application of physics. Read-only. - * @property {Vec3} handPosition - A user-defined hand position, in world coordinates. The position moves with the avatar + * @property {Vec3} handPosition - A user-defined hand position, in world coordinates. The position moves with the avatar * but is otherwise not used or changed by Interface. - * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. + * @property {number} bodyYaw - The left or right rotation about an axis running from the head to the feet of the avatar. * Yaw is sometimes called "heading". * @property {number} bodyPitch - The rotation about an axis running from shoulder to shoulder of the avatar. Pitch is * sometimes called "elevation". @@ -461,28 +457,27 @@ class AvatarData : public QObject, public SpatiallyNestable { * sometimes called "bank". * @property {Vec3} velocity - The current velocity of the avatar. * @property {Vec3} angularVelocity - The current angular velocity of the avatar. - * @property {number} audioLoudness - The instantaneous loudness of the audio input that the avatar is injecting into the + * @property {number} audioLoudness - The instantaneous loudness of the audio input that the avatar is injecting into the * domain. - * @property {number} audioAverageLoudness - The rolling average loudness of the audio input that the avatar is injecting + * @property {number} audioAverageLoudness - The rolling average loudness of the audio input that the avatar is injecting * into the domain. * @property {string} displayName - The avatar's display name. - * @property {string} sessionDisplayName - Sanitized, defaulted version of displayName that is defined by the - * avatar mixer rather than by Interface clients. The result is unique among all avatars present in the domain at the - * time. - * @property {boolean} lookAtSnappingEnabled=true - If true, the avatar's eyes snap to look at another avatar's - * eyes if generally in the line of sight and the other avatar also has lookAtSnappingEnabled == true. - * @property {string} skeletonModelURL - The URL of the avatar model's .fst file. - * @property {AttachmentData[]} attachmentData - Information on the attachments worn by the avatar.
+ * @property {string} sessionDisplayName - displayName's sanitized and default version defined by the avatar + * mixer rather than Interface clients. The result is unique among all avatars present in the domain at the time. + * @property {boolean} lookAtSnappingEnabled=true - true if the avatar's eyes snap to look at another avatar's + * eyes when the other avatar is in the line of sight and also has lookAtSnappingEnabled == true. + * @property {string} skeletonModelURL - The avatar's FST file. + * @property {AttachmentData[]} attachmentData - Information on the avatar's attachments.
* Deprecated: Use avatar entities instead. * @property {string[]} jointNames - The list of joints in the current avatar model. Read-only. * @property {Uuid} sessionUUID - Unique ID of the avatar in the domain. Read-only. - * @property {Mat4} sensorToWorldMatrix - The scale, rotation, and translation transform from the user's real world to the + * @property {Mat4} sensorToWorldMatrix - The scale, rotation, and translation transform from the user's real world to the * avatar's size, orientation, and position in the virtual world. Read-only. - * @property {Mat4} controllerLeftHandMatrix - The rotation and translation of the left hand controller relative to the + * @property {Mat4} controllerLeftHandMatrix - The rotation and translation of the left hand controller relative to the * avatar. Read-only. - * @property {Mat4} controllerRightHandMatrix - The rotation and translation of the right hand controller relative to the + * @property {Mat4} controllerRightHandMatrix - The rotation and translation of the right hand controller relative to the * avatar. Read-only. - * @property {number} sensorToWorldScale - The scale that transforms dimensions in the user's real world to the avatar's + * @property {number} sensorToWorldScale - The scale that transforms dimensions in the user's real world to the avatar's * size in the virtual world. Read-only. */ Q_PROPERTY(glm::vec3 position READ getWorldPosition WRITE setPositionViaScript) @@ -785,7 +780,7 @@ public: /**jsdoc * Gets the rotation of a joint relative to its parent. For information on the joint hierarchy used, see - * Avatar Standards. + * Avatar Standards. * @function Avatar.getJointRotation * @param {number} index - The index of the joint. * @returns {Quat} The rotation of the joint relative to its parent. @@ -796,7 +791,7 @@ public: * Gets the translation of a joint relative to its parent, in model coordinates. *

Warning: These coordinates are not necessarily in meters.

*

For information on the joint hierarchy used, see - * Avatar Standards.

+ * Avatar Standards.

* @function Avatar.getJointTranslation * @param {number} index - The index of the joint. * @returns {Vec3} The translation of the joint relative to its parent, in model coordinates. @@ -897,7 +892,7 @@ public: Q_INVOKABLE virtual void clearJointData(const QString& name); /**jsdoc - * Checks that the data for a joint are valid. + * Checks if the data for a joint are valid. * @function Avatar.isJointDataValid * @param {string} name - The name of the joint. * @returns {boolean} true if the joint data are valid, false if not. @@ -906,7 +901,7 @@ public: /**jsdoc * Gets the rotation of a joint relative to its parent. For information on the joint hierarchy used, see - * Avatar Standards. + * Avatar Standards. * @function Avatar.getJointRotation * @param {string} name - The name of the joint. * @returns {Quat} The rotation of the joint relative to its parent. @@ -921,7 +916,7 @@ public: * Gets the translation of a joint relative to its parent, in model coordinates. *

Warning: These coordinates are not necessarily in meters.

*

For information on the joint hierarchy used, see - * Avatar Standards.

+ * Avatar Standards.

* @function Avatar.getJointTranslation * @param {number} name - The name of the joint. * @returns {Vec3} The translation of the joint relative to its parent, in model coordinates. @@ -1062,8 +1057,13 @@ public: * your avatar's face, use {@link Avatar.setForceFaceTrackerConnected} or {@link MyAvatar.setForceFaceTrackerConnected}. * @function Avatar.setBlendshape * @param {string} name - The name of the blendshape, per the - * {@link https://docs.highfidelity.com/create/avatars/create-avatars/avatar-standards.html#blendshapes Avatar Standards}. + * {@link https://docs.highfidelity.com/create/avatars/avatar-standards.html#blendshapes Avatar Standards}. * @param {number} value - A value between 0.0 and 1.0. + * @example
+ * MyAvatar.setForceFaceTrackerConnected(true); + * MyAvatar.setBlendshape("JawOpen", 1.0); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ Q_INVOKABLE void setBlendshape(QString name, float val) { _headData->setBlendshape(name, val); } @@ -1170,7 +1170,7 @@ public: * @example * var attachments = MyAvatar.getaAttachmentData(); * for (var i = 0; i < attachments.length; i++) { - * print (attachments[i].modelURL); + * print(attachments[i].modelURL); * } * * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". @@ -1318,6 +1318,14 @@ public: * @function Avatar.getSensorToWorldMatrix * @returns {Mat4} The scale, rotation, and translation transform from the user's real world to the avatar's size, * orientation, and position in the virtual world. + * @example + * var sensorToWorldMatrix = MyAvatar.getSensorToWorldMatrix(); + * print("Sensor to woprld matrix: " + JSON.stringify(sensorToWorldMatrix)); + * print("Rotation: " + JSON.stringify(Mat4.extractRotation(sensorToWorldMatrix))); + * print("Translation: " + JSON.stringify(Mat4.extractTranslation(sensorToWorldMatrix))); + * print("Scale: " + JSON.stringify(Mat4.extractScale(sensorToWorldMatrix))); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ // thread safe Q_INVOKABLE glm::mat4 getSensorToWorldMatrix() const; @@ -1335,6 +1343,14 @@ public: * Gets the rotation and translation of the left hand controller relative to the avatar. * @function Avatar.getControllerLeftHandMatrix * @returns {Mat4} The rotation and translation of the left hand controller relative to the avatar. + * @example + * var leftHandMatrix = MyAvatar.getControllerLeftHandMatrix(); + * print("Controller left hand matrix: " + JSON.stringify(leftHandMatrix)); + * print("Rotation: " + JSON.stringify(Mat4.extractRotation(leftHandMatrix))); + * print("Translation: " + JSON.stringify(Mat4.extractTranslation(leftHandMatrix))); + * print("Scale: " + JSON.stringify(Mat4.extractScale(leftHandMatrix))); // Always 1,1,1. + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ // thread safe Q_INVOKABLE glm::mat4 getControllerLeftHandMatrix() const; @@ -1409,6 +1425,12 @@ signals: * Triggered when the avatar's displayName property value changes. * @function Avatar.displayNameChanged * @returns {Signal} + * @example + * MyAvatar.displayNameChanged.connect(function () { + * print("Avatar display name changed to: " + MyAvatar.displayName); + * }); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ void displayNameChanged(); @@ -1416,6 +1438,12 @@ signals: * Triggered when the avatar's sessionDisplayName property value changes. * @function Avatar.sessionDisplayNameChanged * @returns {Signal} + * @example + * MyAvatar.sessionDisplayNameChanged.connect(function () { + * print("Avatar session display name changed to: " + MyAvatar.sessionDisplayName); + * }); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ void sessionDisplayNameChanged(); @@ -1423,6 +1451,12 @@ signals: * Triggered when the avatar's model (i.e., skeletonModelURL property value) is changed. * @function Avatar.skeletonModelURLChanged * @returns {Signal} + * @example + * MyAvatar.skeletonModelURLChanged.connect(function () { + * print("Skeleton model changed to: " + MyAvatar.skeletonModelURL); + * }); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ void skeletonModelURLChanged(); @@ -1431,6 +1465,12 @@ signals: * @function Avatar.lookAtSnappingChanged * @param {boolean} enabled - true if look-at snapping is enabled, false if not. * @returns {Signal} + * @example + * MyAvatar.lookAtSnappingChanged.connect(function () { + * print("Avatar look-at snapping changed to: " + MyAvatar.lookAtSnappingEnabled); + * }); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ void lookAtSnappingChanged(bool enabled); @@ -1438,6 +1478,12 @@ signals: * Triggered when the avatar's sessionUUID property value changes. * @function Avatar.sessionUUIDChanged * @returns {Signal} + * @example + * MyAvatar.sessionUUIDChanged.connect(function () { + * print("Avatar session UUID changed to: " + MyAvatar.sessionUUID); + * }); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ void sessionUUIDChanged(); From 839a03ebe6daa68be3ca5c34155fc3e8109a57d0 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 26 Mar 2019 09:41:07 +1300 Subject: [PATCH 338/446] Miscellaneous JSDoc fixes noticed in passing --- interface/src/ui/overlays/Overlays.cpp | 22 +++++++++++----------- libraries/shared/src/shared/Camera.h | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index eec6eddf44..2ade5edda5 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -1876,7 +1876,7 @@ QVector Overlays::findOverlays(const glm::vec3& center, float radius) { * @property {Vec3} localPosition - The local position of the overlay relative to its parent if the overlay has a * parentID set, otherwise the same value as position. * @property {Quat} localRotation - The orientation of the overlay relative to its parent if the overlay has a - * parentID set, otherwise the same value as rotation. Synonym: localOrientation. + * parentID set, otherwise the same value as rotation. Synonym: localOrientation. * @property {boolean} ignorePickIntersection=false - If true, picks ignore the overlay. ignoreRayIntersection is a synonym. * @property {boolean} drawInFront=false - If true, the overlay is rendered in front of objects in the world, but behind the HUD. * @property {boolean} drawHUDLayer=false - If true, the overlay is rendered in front of everything, including the HUD. @@ -1916,7 +1916,7 @@ QVector Overlays::findOverlays(const glm::vec3& center, float radius) { * @property {Vec3} localPosition - The local position of the overlay relative to its parent if the overlay has a * parentID set, otherwise the same value as position. * @property {Quat} localRotation - The orientation of the overlay relative to its parent if the overlay has a - * parentID set, otherwise the same value as rotation. Synonym: localOrientation. + * parentID set, otherwise the same value as rotation. Synonym: localOrientation. * @property {boolean} isSolid=false - Synonyms: solid, isFilled, and filled. * Antonyms: isWire and wire. * @property {boolean} ignorePickIntersection=false - If true, picks ignore the overlay. ignoreRayIntersection is a synonym. @@ -1927,11 +1927,11 @@ QVector Overlays::findOverlays(const glm::vec3& center, float radius) { * @property {number} parentJointIndex=65535 - Integer value specifying the skeleton joint that the overlay is attached to if * parentID is an avatar skeleton. A value of 65535 means "no joint". * - * @property {number} startAt = 0 - The counter - clockwise angle from the overlay's x-axis that drawing starts at, in degrees. - * @property {number} endAt = 360 - The counter - clockwise angle from the overlay's x-axis that drawing ends at, in degrees. - * @property {number} outerRadius = 1 - The outer radius of the overlay, in meters.Synonym: radius. - * @property {number} innerRadius = 0 - The inner radius of the overlay, in meters. - * @property {Color} color = 255, 255, 255 - The color of the overlay.Setting this value also sets the values of + * @property {number} startAt = 0 - The counter - clockwise angle from the overlay's x-axis that drawing starts at in degrees. + * @property {number} endAt = 360 - The counter - clockwise angle from the overlay's x-axis that drawing ends at in degrees. + * @property {number} outerRadius = 1 - The outer radius of the overlay in meters. Synonym: radius. + * @property {number} innerRadius = 0 - The inner radius of the overlay in meters. + * @property {Color} color = 255, 255, 255 - The color of the overlay. Setting this value also sets the values of * innerStartColor, innerEndColor, outerStartColor, and outerEndColor. * @property {Color} startColor - Sets the values of innerStartColor and outerStartColor. * Write - only. @@ -1945,9 +1945,9 @@ QVector Overlays::findOverlays(const glm::vec3& center, float radius) { * @property {Color} innerEndColor - The color at the inner end point of the overlay. * @property {Color} outerStartColor - The color at the outer start point of the overlay. * @property {Color} outerEndColor - The color at the outer end point of the overlay. - * @property {number} alpha = 0.5 - The opacity of the overlay, 0.0 -1.0.Setting this value also sets + * @property {number} alpha = 0.5 - The opacity of the overlay, 0.0 -1.0. Setting this value also sets * the values of innerStartAlpha, innerEndAlpha, outerStartAlpha, and - * outerEndAlpha.Synonym: Alpha; write - only. + * outerEndAlpha. Synonym: Alpha; write - only. * @property {number} startAlpha - Sets the values of innerStartAlpha and outerStartAlpha. * Write - only. * @property {number} endAlpha - Sets the values of innerEndAlpha and outerEndAlpha. @@ -1964,9 +1964,9 @@ QVector Overlays::findOverlays(const glm::vec3& center, float radius) { * @property {boolean} hasTickMarks = false - If true, tick marks are drawn. * @property {number} majorTickMarksAngle = 0 - The angle between major tick marks, in degrees. * @property {number} minorTickMarksAngle = 0 - The angle between minor tick marks, in degrees. - * @property {number} majorTickMarksLength = 0 - The length of the major tick marks, in meters.A positive value draws tick marks + * @property {number} majorTickMarksLength = 0 - The length of the major tick marks, in meters. A positive value draws tick marks * outwards from the inner radius; a negative value draws tick marks inwards from the outer radius. - * @property {number} minorTickMarksLength = 0 - The length of the minor tick marks, in meters.A positive value draws tick marks + * @property {number} minorTickMarksLength = 0 - The length of the minor tick marks, in meters. A positive value draws tick marks * outwards from the inner radius; a negative value draws tick marks inwards from the outer radius. * @property {Color} majorTickMarksColor = 0, 0, 0 - The color of the major tick marks. * @property {Color} minorTickMarksColor = 0, 0, 0 - The color of the minor tick marks. diff --git a/libraries/shared/src/shared/Camera.h b/libraries/shared/src/shared/Camera.h index 31e6228bb9..0132e58d18 100644 --- a/libraries/shared/src/shared/Camera.h +++ b/libraries/shared/src/shared/Camera.h @@ -138,7 +138,7 @@ public slots: * var pickRay = Camera.computePickRay(event.x, event.y); * var intersection = Entities.findRayIntersection(pickRay); * if (intersection.intersects) { - * print ("You clicked on entity " + intersection.entityID); + * print("You clicked on entity " + intersection.entityID); * } * } * From 4a832be8c65efb9bc5504a802702427bed58c34b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 26 Mar 2019 10:03:30 +1300 Subject: [PATCH 339/446] Fix JSDoc post-merge --- interface/src/avatar/MyAvatar.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index f95ea898ba..edb686a6a6 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1609,8 +1609,7 @@ public: public slots: /**jsdoc - * @function MyAvatar.setSessionUUID - * @param {Uuid} sessionUUID + * @comment Uses the base class's JSDoc. */ virtual void setSessionUUID(const QUuid& sessionUUID) override; From 1608b24be15df3fffeac3b074cfc4d977c3f9ec9 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Mon, 25 Mar 2019 14:28:54 -0700 Subject: [PATCH 340/446] ase 20832 - Inventory app login and cancel buttons don't work on logout --- .../resources/qml/hifi/commerce/marketplace/Marketplace.qml | 2 +- interface/src/Application.cpp | 4 ---- scripts/system/commerce/wallet.js | 3 +++ scripts/system/marketplaces/marketplaces.js | 3 +-- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index 7dcdf9b434..619547ef43 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -664,7 +664,7 @@ Rectangle { text: "LOG IN" onClicked: { - sendToScript({method: 'needsLogIn_loginClicked'}); + sendToScript({method: 'marketplace_loginClicked'}); } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 0c46404b39..1852c0007e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1206,10 +1206,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(&domainHandler, SIGNAL(connectedToDomain(QUrl)), SLOT(updateWindowTitle())); connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle())); connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, [this]() { - auto tabletScriptingInterface = DependencyManager::get(); - if (tabletScriptingInterface) { - tabletScriptingInterface->setQmlTabletRoot(SYSTEM_TABLET, nullptr); - } auto entityScriptingInterface = DependencyManager::get(); entityScriptingInterface->deleteEntity(getTabletScreenID()); entityScriptingInterface->deleteEntity(getTabletHomeButtonID()); diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 17ff918243..86806fd8b4 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -377,6 +377,9 @@ function deleteSendMoneyParticleEffect() { } function onUsernameChanged() { + if (ui.checkIsOpen()) { + ui.open(WALLET_QML_SOURCE); + } } var MARKETPLACE_QML_PATH = "hifi/commerce/marketplace/Marketplace.qml"; diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index e059081741..38287e3af3 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -615,11 +615,10 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { openMarketplace(message.itemId, message.itemEdition); break; case 'passphrasePopup_cancelClicked': - case 'needsLogIn_cancelClicked': // Should/must NOT check for wallet setup. openMarketplace(); break; - case 'needsLogIn_loginClicked': + case 'marketplace_loginClicked': openLoginWindow(); break; case 'disableHmdPreview': From 0173c87695f60b5430d205e6b703348f6d356091 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Fri, 22 Mar 2019 02:43:06 +0100 Subject: [PATCH 341/446] Fix clashing hyperlinks in AvatarPackager project page --- interface/resources/qml/hifi/avatarPackager/AvatarProject.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index bf8c06d1b3..e5bffa7829 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -339,8 +339,8 @@ Item { visible: AvatarPackagerCore.currentAvatarProject && AvatarPackagerCore.currentAvatarProject.hasErrors anchors { - top: notForSaleMessage.bottom - topMargin: 16 + top: notForSaleMessage.visible ? notForSaleMessage.bottom : infoMessage .bottom + bottom: showFilesText.top horizontalCenter: parent.horizontalCenter } From 49ce30d536443dc989aa98b71f78d2293531d2a0 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 25 Mar 2019 15:35:05 -0700 Subject: [PATCH 342/446] adding changes to design + truly separating muted/ptt --- .../qml/controlsUit/CheckBoxQQC2.qml | 3 +- .../resources/qml/controlsUit/Switch.qml | 5 +- interface/resources/qml/hifi/audio/Audio.qml | 57 ++++++++++++------- .../resources/qml/hifi/audio/InputPeak.qml | 20 +++---- .../qml/hifi/audio/LoopbackAudio.qml | 7 ++- .../qml/hifi/audio/PlaySampleSound.qml | 5 +- 6 files changed, 59 insertions(+), 38 deletions(-) diff --git a/interface/resources/qml/controlsUit/CheckBoxQQC2.qml b/interface/resources/qml/controlsUit/CheckBoxQQC2.qml index 91d35ecd58..bd71025aa9 100644 --- a/interface/resources/qml/controlsUit/CheckBoxQQC2.qml +++ b/interface/resources/qml/controlsUit/CheckBoxQQC2.qml @@ -24,6 +24,7 @@ CheckBox { leftPadding: 0 property int colorScheme: hifi.colorSchemes.light property string color: hifi.colors.lightGrayText + property int fontSize: hifi.fontSizes.inputLabel readonly property bool isLightColorScheme: colorScheme === hifi.colorSchemes.light property bool isRedCheck: false property bool isRound: false @@ -109,7 +110,7 @@ CheckBox { contentItem: Text { id: root - font.pixelSize: hifi.fontSizes.inputLabel + font.pixelSize: fontSize; font.family: "Raleway" font.weight: Font.DemiBold text: checkBox.text diff --git a/interface/resources/qml/controlsUit/Switch.qml b/interface/resources/qml/controlsUit/Switch.qml index 4e1c21c456..422b08b4eb 100644 --- a/interface/resources/qml/controlsUit/Switch.qml +++ b/interface/resources/qml/controlsUit/Switch.qml @@ -21,6 +21,7 @@ Item { property int switchWidth: 70; readonly property int switchRadius: height/2; property string labelTextOff: ""; + property int labelTextSize: hifi.fontSizes.inputLabel; property string labelGlyphOffText: ""; property int labelGlyphOffSize: 32; property string labelTextOn: ""; @@ -89,7 +90,7 @@ Item { RalewaySemiBold { id: labelOff; text: labelTextOff; - size: hifi.fontSizes.inputLabel; + size: labelTextSize; color: originalSwitch.checked ? hifi.colors.lightGrayText : "#FFFFFF"; anchors.top: parent.top; anchors.right: parent.right; @@ -130,7 +131,7 @@ Item { RalewaySemiBold { id: labelOn; text: labelTextOn; - size: hifi.fontSizes.inputLabel; + size: labelTextSize; color: originalSwitch.checked ? "#FFFFFF" : hifi.colors.lightGrayText; anchors.top: parent.top; anchors.left: parent.left; diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index cd0f290da4..79222ea792 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -31,6 +31,8 @@ Rectangle { property string title: "Audio Settings" property int switchHeight: 16 property int switchWidth: 40 + property bool pushToTalk: (bar.currentIndex === 0) ? AudioScriptingInterface.pushToTalkDesktop : AudioScriptingInterface.pushToTalkHMD; + property bool muted: (bar.currentIndex === 0) ? AudioScriptingInterface.desktopMuted : AudioScriptingInterface.hmdMuted; readonly property real verticalScrollWidth: 10 readonly property real verticalScrollShaft: 8 signal sendToScript(var message); @@ -44,7 +46,7 @@ Rectangle { property bool isVR: AudioScriptingInterface.context === "VR" - property real rightMostInputLevelPos: 440 + property real rightMostInputLevelPos: root.width //placeholder for control sizes and paddings //recalculates dynamically in case of UI size is changed QtObject { @@ -92,7 +94,9 @@ Rectangle { } } - Component.onCompleted: enablePeakValues(); + Component.onCompleted: { + enablePeakValues(); + } Flickable { id: flickView; @@ -167,15 +171,25 @@ Rectangle { height: root.switchHeight; switchWidth: root.switchWidth; labelTextOn: "Mute microphone"; + labelTextSize: 16; backgroundOnColor: "#E3E3E3"; - checked: AudioScriptingInterface.muted; + checked: muted; onClicked: { - if (AudioScriptingInterface.pushToTalk && !checked) { + if (pushToTalk && !checked) { // disable push to talk if unmuting - AudioScriptingInterface.pushToTalk = false; + if ((bar.currentIndex === 0)) { + AudioScriptingInterface.pushToTalkDesktop = false; + } + else { + AudioScriptingInterface.pushToTalkHMD = false; + } + } + if ((bar.currentIndex === 0)) { + AudioScriptingInterface.desktopMuted = checked; + } + else { + AudioScriptingInterface.hmdMuted = checked; } - AudioScriptingInterface.muted = checked; - checked = Qt.binding(function() { return AudioScriptingInterface.muted; }); // restore binding } } @@ -187,6 +201,7 @@ Rectangle { anchors.topMargin: 24 anchors.left: parent.left labelTextOn: "Noise Reduction"; + labelTextSize: 16; backgroundOnColor: "#E3E3E3"; checked: AudioScriptingInterface.noiseReduction; onCheckedChanged: { @@ -203,6 +218,7 @@ Rectangle { anchors.topMargin: 24 anchors.left: parent.left labelTextOn: qsTr("Push To Talk (T)"); + labelTextSize: 16; backgroundOnColor: "#E3E3E3"; checked: (bar.currentIndex === 0) ? AudioScriptingInterface.pushToTalkDesktop : AudioScriptingInterface.pushToTalkHMD; onCheckedChanged: { @@ -211,13 +227,6 @@ Rectangle { } else { AudioScriptingInterface.pushToTalkHMD = checked; } - checked = Qt.binding(function() { - if (bar.currentIndex === 0) { - return AudioScriptingInterface.pushToTalkDesktop; - } else { - return AudioScriptingInterface.pushToTalkHMD; - } - }); // restore binding } } } @@ -235,6 +244,7 @@ Rectangle { anchors.top: parent.top anchors.left: parent.left labelTextOn: qsTr("Warn when muted"); + labelTextSize: 16; backgroundOnColor: "#E3E3E3"; checked: AudioScriptingInterface.warnWhenMuted; onClicked: { @@ -252,6 +262,7 @@ Rectangle { anchors.topMargin: 24 anchors.left: parent.left labelTextOn: qsTr("Audio Level Meter"); + labelTextSize: 16; backgroundOnColor: "#E3E3E3"; checked: AvatarInputs.showAudioTools; onCheckedChanged: { @@ -268,6 +279,7 @@ Rectangle { anchors.topMargin: 24 anchors.left: parent.left labelTextOn: qsTr("Stereo input"); + labelTextSize: 16; backgroundOnColor: "#E3E3E3"; checked: AudioScriptingInterface.isStereoInput; onCheckedChanged: { @@ -281,6 +293,7 @@ Rectangle { Item { id: pttTextContainer + visible: pushToTalk; anchors.top: switchesContainer.bottom; anchors.topMargin: 10; anchors.left: parent.left; @@ -303,7 +316,7 @@ Rectangle { Separator { id: secondSeparator; - anchors.top: pttTextContainer.bottom; + anchors.top: pttTextContainer.visible ? pttTextContainer.bottom : switchesContainer.bottom; anchors.topMargin: 10; } @@ -330,7 +343,7 @@ Rectangle { width: margins.sizeText + margins.sizeLevel; anchors.left: parent.left; anchors.leftMargin: margins.sizeCheckBox; - size: 16; + size: 22; color: hifi.colors.white; text: qsTr("Choose input device"); } @@ -338,7 +351,7 @@ Rectangle { ListView { id: inputView; - width: parent.width - margins.paddings*2; + width: rightMostInputLevelPos; anchors.top: inputDeviceHeader.bottom; anchors.topMargin: 10; x: margins.paddings @@ -347,7 +360,7 @@ Rectangle { clip: true; model: AudioScriptingInterface.devices.input; delegate: Item { - width: rightMostInputLevelPos + width: rightMostInputLevelPos - margins.paddings*2 height: margins.sizeCheckBox > checkBoxInput.implicitHeight ? margins.sizeCheckBox : checkBoxInput.implicitHeight @@ -363,6 +376,7 @@ Rectangle { boxSize: margins.sizeCheckBox / 2 isRound: true text: devicename + fontSize: 16; onPressed: { if (!checked) { stereoInput.checked = false; @@ -395,7 +409,7 @@ Rectangle { Separator { id: thirdSeparator; - anchors.top: loopbackAudio.bottom; + anchors.top: loopbackAudio.visible ? loopbackAudio.bottom : inputView.bottom; anchors.topMargin: 10; } @@ -422,7 +436,7 @@ Rectangle { anchors.left: parent.left anchors.leftMargin: margins.sizeCheckBox anchors.verticalCenter: parent.verticalCenter; - size: 16; + size: 22; color: hifi.colors.white; text: qsTr("Choose output device"); } @@ -452,6 +466,7 @@ Rectangle { checked: bar.currentIndex === 0 ? selectedDesktop : selectedHMD; checkable: !checked text: devicename + fontSize: 16 onPressed: { if (!checked) { AudioScriptingInterface.setOutputDevice(info, bar.currentIndex === 1); @@ -514,7 +529,7 @@ Rectangle { RalewayRegular { // The slider for my card is special, it controls the master gain id: gainSliderText; - text: "Avatar volume"; + text: "People volume"; size: 16; anchors.left: parent.left; color: hifi.colors.white; diff --git a/interface/resources/qml/hifi/audio/InputPeak.qml b/interface/resources/qml/hifi/audio/InputPeak.qml index 00f7e63528..d8b166cee4 100644 --- a/interface/resources/qml/hifi/audio/InputPeak.qml +++ b/interface/resources/qml/hifi/audio/InputPeak.qml @@ -12,24 +12,26 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 -Rectangle { +Item { property var peak; width: 70; height: 8; - color: "transparent"; - - Item { + QtObject { id: colors; + readonly property string unmuted: "#FFF"; readonly property string muted: "#E2334D"; readonly property string gutter: "#575757"; readonly property string greenStart: "#39A38F"; readonly property string greenEnd: "#1FC6A6"; + readonly property string yellow: "#C0C000"; readonly property string red: colors.muted; + readonly property string fill: "#55000000"; } + Text { id: status; @@ -79,23 +81,19 @@ Rectangle { anchors { fill: mask } source: mask start: Qt.point(0, 0); - end: Qt.point(70, 0); + end: Qt.point(bar.width, 0); gradient: Gradient { GradientStop { position: 0; color: colors.greenStart; } GradientStop { - position: 0.8; + position: 0.5; color: colors.greenEnd; } - GradientStop { - position: 0.801; - color: colors.red; - } GradientStop { position: 1; - color: colors.red; + color: colors.yellow; } } } diff --git a/interface/resources/qml/hifi/audio/LoopbackAudio.qml b/interface/resources/qml/hifi/audio/LoopbackAudio.qml index 8ec0ffc496..b668568035 100644 --- a/interface/resources/qml/hifi/audio/LoopbackAudio.qml +++ b/interface/resources/qml/hifi/audio/LoopbackAudio.qml @@ -44,8 +44,11 @@ RowLayout { } HifiControlsUit.Button { - text: audioLoopedBack ? qsTr("STOP TESTING YOUR VOICE") : qsTr("TEST YOUR VOICE"); + text: audioLoopedBack ? qsTr("STOP TESTING VOICE") : qsTr("TEST YOUR VOICE"); color: audioLoopedBack ? hifi.buttons.red : hifi.buttons.blue; + fontSize: 15; + width: 200; + height: 32; onClicked: { if (audioLoopedBack) { loopbackTimer.stop(); @@ -59,7 +62,7 @@ RowLayout { RalewayRegular { Layout.leftMargin: 2; - size: 14; + size: 18; color: "white"; font.italic: true text: audioLoopedBack ? qsTr("Speak in your input") : ""; diff --git a/interface/resources/qml/hifi/audio/PlaySampleSound.qml b/interface/resources/qml/hifi/audio/PlaySampleSound.qml index b9d9727dab..8565512837 100644 --- a/interface/resources/qml/hifi/audio/PlaySampleSound.qml +++ b/interface/resources/qml/hifi/audio/PlaySampleSound.qml @@ -59,11 +59,14 @@ RowLayout { text: isPlaying ? qsTr("STOP TESTING YOUR SOUND") : qsTr("TEST YOUR SOUND"); color: isPlaying ? hifi.buttons.red : hifi.buttons.blue; onClicked: isPlaying ? stopSound() : playSound(); + fontSize: 15; + width: 200; + height: 32; } RalewayRegular { Layout.leftMargin: 2; - size: 14; + size: 18; color: "white"; font.italic: true text: isPlaying ? qsTr("Listen to your output") : ""; From 6aebd000d6be1a364d26725262d48ab121be7798 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 25 Mar 2019 15:41:03 -0700 Subject: [PATCH 343/446] changing api to match verbiage of overall code --- interface/resources/qml/hifi/audio/Audio.qml | 6 ++--- interface/src/scripting/Audio.cpp | 24 ++++++++++---------- interface/src/scripting/Audio.h | 21 +++++++++-------- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index 79222ea792..3266f3ef46 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -32,7 +32,7 @@ Rectangle { property int switchHeight: 16 property int switchWidth: 40 property bool pushToTalk: (bar.currentIndex === 0) ? AudioScriptingInterface.pushToTalkDesktop : AudioScriptingInterface.pushToTalkHMD; - property bool muted: (bar.currentIndex === 0) ? AudioScriptingInterface.desktopMuted : AudioScriptingInterface.hmdMuted; + property bool muted: (bar.currentIndex === 0) ? AudioScriptingInterface.mutedDesktop : AudioScriptingInterface.mutedHMD; readonly property real verticalScrollWidth: 10 readonly property real verticalScrollShaft: 8 signal sendToScript(var message); @@ -185,10 +185,10 @@ Rectangle { } } if ((bar.currentIndex === 0)) { - AudioScriptingInterface.desktopMuted = checked; + AudioScriptingInterface.mutedDesktop = checked; } else { - AudioScriptingInterface.hmdMuted = checked; + AudioScriptingInterface.mutedHMD = checked; } } } diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 0e0d13ae45..df88538724 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -88,44 +88,44 @@ void Audio::setMuted(bool isMuted) { void Audio::setMutedDesktop(bool isMuted) { bool changed = false; withWriteLock([&] { - if (_desktopMuted != isMuted) { + if (_mutedDesktop != isMuted) { changed = true; - _desktopMuted = isMuted; + _mutedDesktop = isMuted; auto client = DependencyManager::get().data(); QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); } }); if (changed) { emit mutedChanged(isMuted); - emit desktopMutedChanged(isMuted); + emit mutedDesktopChanged(isMuted); } } bool Audio::getMutedDesktop() const { return resultWithReadLock([&] { - return _desktopMuted; + return _mutedDesktop; }); } void Audio::setMutedHMD(bool isMuted) { bool changed = false; withWriteLock([&] { - if (_hmdMuted != isMuted) { + if (_mutedHMD != isMuted) { changed = true; - _hmdMuted = isMuted; + _mutedHMD = isMuted; auto client = DependencyManager::get().data(); QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); } }); if (changed) { emit mutedChanged(isMuted); - emit hmdMutedChanged(isMuted); + emit mutedHMDChanged(isMuted); } } bool Audio::getMutedHMD() const { return resultWithReadLock([&] { - return _hmdMuted; + return _mutedHMD; }); } @@ -217,15 +217,15 @@ void Audio::setPTTHMD(bool enabled) { } void Audio::saveData() { - _desktopMutedSetting.set(getMutedDesktop()); - _hmdMutedSetting.set(getMutedHMD()); + _mutedDesktopSetting.set(getMutedDesktop()); + _mutedHMDSetting.set(getMutedHMD()); _pttDesktopSetting.set(getPTTDesktop()); _pttHMDSetting.set(getPTTHMD()); } void Audio::loadData() { - setMutedDesktop(_desktopMutedSetting.get()); - setMutedHMD(_hmdMutedSetting.get()); + setMutedDesktop(_mutedDesktopSetting.get()); + setMutedHMD(_mutedHMDSetting.get()); setPTTDesktop(_pttDesktopSetting.get()); setPTTHMD(_pttHMDSetting.get()); diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index 9ee230fc29..dba3af0730 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -41,6 +41,7 @@ class Audio : public AudioScriptingInterface, protected ReadWriteLockable { * @hifi-assignment-client * * @property {boolean} muted - true if the audio input is muted, otherwise false. + * @property {boolean} mutedDesktop - true if the audio input is muted, otherwise false. * @property {boolean} noiseReduction - true if noise reduction is enabled, otherwise false. When * enabled, the input audio signal is blocked (fully attenuated) when it falls below an adaptive threshold set just * above the noise floor. @@ -68,8 +69,8 @@ class Audio : public AudioScriptingInterface, protected ReadWriteLockable { Q_PROPERTY(bool clipping READ isClipping NOTIFY clippingChanged) Q_PROPERTY(QString context READ getContext NOTIFY contextChanged) Q_PROPERTY(AudioDevices* devices READ getDevices NOTIFY nop) - Q_PROPERTY(bool desktopMuted READ getMutedDesktop WRITE setMutedDesktop NOTIFY desktopMutedChanged) - Q_PROPERTY(bool hmdMuted READ getMutedHMD WRITE setMutedHMD NOTIFY hmdMutedChanged) + Q_PROPERTY(bool mutedDesktop READ getMutedDesktop WRITE setMutedDesktop NOTIFY mutedDesktopChanged) + Q_PROPERTY(bool mutedHMD READ getMutedHMD WRITE setMutedHMD NOTIFY mutedHMDChanged) Q_PROPERTY(bool pushToTalk READ getPTT WRITE setPTT NOTIFY pushToTalkChanged); Q_PROPERTY(bool pushToTalkDesktop READ getPTTDesktop WRITE setPTTDesktop NOTIFY pushToTalkDesktopChanged) Q_PROPERTY(bool pushToTalkHMD READ getPTTHMD WRITE setPTTHMD NOTIFY pushToTalkHMDChanged) @@ -227,19 +228,19 @@ signals: /**jsdoc * Triggered when desktop audio input is muted or unmuted. - * @function Audio.desktopMutedChanged + * @function Audio.mutedDesktopChanged * @param {boolean} isMuted - true if the audio input is muted for desktop mode, otherwise false. * @returns {Signal} */ - void desktopMutedChanged(bool isMuted); + void mutedDesktopChanged(bool isMuted); /**jsdoc * Triggered when HMD audio input is muted or unmuted. - * @function Audio.hmdMutedChanged + * @function Audio.mutedHMDChanged * @param {boolean} isMuted - true if the audio input is muted for HMD mode, otherwise false. * @returns {Signal} */ - void hmdMutedChanged(bool isMuted); + void mutedHMDChanged(bool isMuted); /** * Triggered when Push-to-Talk has been enabled or disabled. @@ -356,12 +357,12 @@ private: bool _contextIsHMD { false }; AudioDevices* getDevices() { return &_devices; } AudioDevices _devices; - Setting::Handle _desktopMutedSetting{ QStringList { Audio::AUDIO, "desktopMuted" }, true }; - Setting::Handle _hmdMutedSetting{ QStringList { Audio::AUDIO, "hmdMuted" }, true }; + Setting::Handle _mutedDesktopSetting{ QStringList { Audio::AUDIO, "mutedDesktop" }, true }; + Setting::Handle _mutedHMD{ QStringList { Audio::AUDIO, "mutedHMD" }, true }; Setting::Handle _pttDesktopSetting{ QStringList { Audio::AUDIO, "pushToTalkDesktop" }, false }; Setting::Handle _pttHMDSetting{ QStringList { Audio::AUDIO, "pushToTalkHMD" }, false }; - bool _desktopMuted{ true }; - bool _hmdMuted{ false }; + bool _mutedDesktop{ true }; + bool _mutedHMD{ false }; bool _pttDesktop{ false }; bool _pttHMD{ false }; bool _pushingToTalk{ false }; From 46f897b69330e92e1a41d9130b414afd2a0eceea Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Mon, 25 Mar 2019 15:42:30 -0700 Subject: [PATCH 344/446] Better estimate of avatar centre for zone membership --- assignment-client/src/avatars/AvatarMixerClientData.cpp | 9 +++------ assignment-client/src/avatars/AvatarMixerClientData.h | 1 - assignment-client/src/avatars/MixerAvatar.h | 3 +++ 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index 4880f73226..2175018824 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -23,9 +23,6 @@ #include "AvatarMixerSlave.h" -// Offset from reported position for priority-zone purposes: -const glm::vec3 AvatarMixerClientData::AVATAR_CENTER_OFFSET { 0.0f, 1.0f, 0.0 }; - AvatarMixerClientData::AvatarMixerClientData(const QUuid& nodeID, Node::LocalID nodeLocalID) : NodeData(nodeID, nodeLocalID) { // in case somebody calls getSessionUUID on the AvatarData instance, make sure it has the right ID @@ -132,7 +129,7 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared incrementNumOutOfOrderSends(); } _lastReceivedSequenceNumber = sequenceNumber; - glm::vec3 oldPosition = getPosition(); + glm::vec3 oldPosition = _avatar->getCentroidPosition(); bool oldHasPriority = _avatar->getHasPriority(); // compute the offset to the data payload @@ -143,10 +140,10 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared // Regardless of what the client says, restore the priority as we know it without triggering any update. _avatar->setHasPriorityWithoutTimestampReset(oldHasPriority); - auto newPosition = getPosition(); + auto newPosition = _avatar->getCentroidPosition(); if (newPosition != oldPosition || _avatar->getNeedsHeroCheck()) { EntityTree& entityTree = *slaveSharedData.entityTree; - FindPriorityZone findPriorityZone { newPosition + AVATAR_CENTER_OFFSET } ; + FindPriorityZone findPriorityZone { newPosition } ; entityTree.recurseTreeWithOperation(&FindPriorityZone::operation, &findPriorityZone); _avatar->setHasPriority(findPriorityZone.isInPriorityZone); _avatar->setNeedsHeroCheck(false); diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index 492dfc4720..98c8d7e15b 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -223,7 +223,6 @@ private: PerNodeTraitVersions _perNodeSentTraitVersions; std::atomic_bool _isIgnoreRadiusEnabled { false }; - static const glm::vec3 AVATAR_CENTER_OFFSET; }; #endif // hifi_AvatarMixerClientData_h diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h index 01e5e91b44..f812917614 100644 --- a/assignment-client/src/avatars/MixerAvatar.h +++ b/assignment-client/src/avatars/MixerAvatar.h @@ -22,6 +22,9 @@ public: bool getNeedsHeroCheck() const { return _needsHeroCheck; } void setNeedsHeroCheck(bool needsHeroCheck = true) { _needsHeroCheck = needsHeroCheck; } + // Bounding-box World centre: + glm::vec3 getCentroidPosition() const + { return getWorldPosition() + _globalBoundingBoxOffset; } private: bool _needsHeroCheck { false }; From 02129e0543919831cbfa26b44008f7ccac1a3035 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Mon, 25 Mar 2019 16:14:48 -0700 Subject: [PATCH 345/446] no-op refactor in prep for multiple entities per cert --- .../src/entities/EntityServer.cpp | 99 ++++++++----------- .../ui/overlays/ContextOverlayInterface.cpp | 3 +- libraries/entities/src/EntityTree.cpp | 21 ++-- libraries/entities/src/EntityTree.h | 4 +- 4 files changed, 54 insertions(+), 73 deletions(-) diff --git a/assignment-client/src/entities/EntityServer.cpp b/assignment-client/src/entities/EntityServer.cpp index 581d854909..f2cad1e400 100644 --- a/assignment-client/src/entities/EntityServer.cpp +++ b/assignment-client/src/entities/EntityServer.cpp @@ -474,73 +474,58 @@ void EntityServer::startDynamicDomainVerification() { QHash localMap(tree->getEntityCertificateIDMap()); QHashIterator i(localMap); - qCDebug(entities) << localMap.size() << "entities in _entityCertificateIDMap"; + qCDebug(entities) << localMap.size() << "certificates present."; while (i.hasNext()) { i.next(); const auto& certificateID = i.key(); const auto& entityID = i.value(); - EntityItemPointer entity = tree->findEntityByEntityItemID(entityID); + // Examine each cert: + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkRequest networkRequest; + networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL(); + requestURL.setPath("/api/v1/commerce/proof_of_purchase_status/location"); + QJsonObject request; + request["certificate_id"] = certificateID; + networkRequest.setUrl(requestURL); - if (entity) { - if (!entity->getProperties().verifyStaticCertificateProperties()) { - qCDebug(entities) << "During Dynamic Domain Verification, a certified entity with ID" << entityID << "failed" - << "static certificate verification."; - // Delete the entity if it doesn't pass static certificate verification - tree->withWriteLock([&] { - tree->deleteEntity(entityID, true); - }); - } else { - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkRequest networkRequest; - networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); - networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL(); - requestURL.setPath("/api/v1/commerce/proof_of_purchase_status/location"); - QJsonObject request; - request["certificate_id"] = certificateID; - networkRequest.setUrl(requestURL); + QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson()); - QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson()); + connect(networkReply, &QNetworkReply::finished, this, [this, entityID, networkReply] { + EntityTreePointer tree = std::static_pointer_cast(_tree); - connect(networkReply, &QNetworkReply::finished, this, [this, entityID, networkReply] { - EntityTreePointer tree = std::static_pointer_cast(_tree); + QJsonObject jsonObject = QJsonDocument::fromJson(networkReply->readAll()).object(); + jsonObject = jsonObject["data"].toObject(); + networkReply->deleteLater(); - QJsonObject jsonObject = QJsonDocument::fromJson(networkReply->readAll()).object(); - jsonObject = jsonObject["data"].toObject(); - - if (networkReply->error() == QNetworkReply::NoError) { - QString thisDomainID = DependencyManager::get()->getDomainID().remove(QRegExp("\\{|\\}")); - if (jsonObject["domain_id"].toString() != thisDomainID) { - EntityItemPointer entity = tree->findEntityByEntityItemID(entityID); - if (!entity) { - qCDebug(entities) << "Entity undergoing dynamic domain verification is no longer available:" << entityID; - networkReply->deleteLater(); - return; - } - if (entity->getAge() > (_MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS/MSECS_PER_SECOND)) { - qCDebug(entities) << "Entity's cert's domain ID" << jsonObject["domain_id"].toString() - << "doesn't match the current Domain ID" << thisDomainID << "; deleting entity" << entityID; - tree->withWriteLock([&] { - tree->deleteEntity(entityID, true); - }); - } else { - qCDebug(entities) << "Entity failed dynamic domain verification, but was created too recently to necessitate deletion:" << entityID; - } - } else { - qCDebug(entities) << "Entity passed dynamic domain verification:" << entityID; - } - } else { - qCDebug(entities) << "Call to" << networkReply->url() << "failed with error" << networkReply->error() << "; NOT deleting entity" << entityID - << "More info:" << jsonObject; - } - - networkReply->deleteLater(); - }); + if (networkReply->error() != QNetworkReply::NoError) { + qCDebug(entities) << "Call to" << networkReply->url() << "failed with error" << networkReply->error() << "; NOT deleting entity" << entityID + << "More info:" << jsonObject; + return; } - } else { - qCWarning(entities) << "During DDV, an entity with ID" << entityID << "was NOT found in the Entity Tree!"; - } + QString thisDomainID = DependencyManager::get()->getDomainID().remove(QRegExp("\\{|\\}")); + if (jsonObject["domain_id"].toString() == thisDomainID) { + // Entity belongs here. Nothing to do. + return; + } + // Entity does not belong here: + EntityItemPointer entity = tree->findEntityByEntityItemID(entityID); + if (!entity) { + qCDebug(entities) << "Entity undergoing dynamic domain verification is no longer available:" << entityID; + return; + } + if (entity->getAge() <= (_MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS / MSECS_PER_SECOND)) { + qCDebug(entities) << "Entity failed dynamic domain verification, but was created too recently to necessitate deletion:" << entityID; + return; + } + qCDebug(entities) << "Entity's cert's domain ID" << jsonObject["domain_id"].toString() + << "doesn't match the current Domain ID" << thisDomainID << "; deleting entity" << entityID; + tree->withWriteLock([&] { + tree->deleteEntity(entityID, true); + }); + }); } int nextInterval = qrand() % ((_MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS + 1) - _MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS) + _MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS; diff --git a/interface/src/ui/overlays/ContextOverlayInterface.cpp b/interface/src/ui/overlays/ContextOverlayInterface.cpp index e5cec70f64..794feddd8a 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.cpp +++ b/interface/src/ui/overlays/ContextOverlayInterface.cpp @@ -422,8 +422,7 @@ void ContextOverlayInterface::handleChallengeOwnershipReplyPacket(QSharedPointer QString certID(packet->read(certIDByteArraySize)); QString text(packet->read(textByteArraySize)); - EntityItemID id; - bool verificationSuccess = DependencyManager::get()->getTree()->verifyNonce(certID, text, id); + bool verificationSuccess = DependencyManager::get()->getTree()->verifyNonce(certID, text); if (verificationSuccess) { emit ledger->updateCertificateStatus(certID, (uint)(ledger->CERTIFICATE_STATUS_VERIFICATION_SUCCESS)); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 8bf7c92b1f..11e392f590 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1423,9 +1423,7 @@ bool EntityTree::isScriptInWhitelist(const QString& scriptProperty) { void EntityTree::startChallengeOwnershipTimer(const EntityItemID& entityItemID) { QTimer* _challengeOwnershipTimeoutTimer = new QTimer(this); - connect(this, &EntityTree::killChallengeOwnershipTimeoutTimer, this, [=](const QString& certID) { - QReadLocker locker(&_entityCertificateIDMapLock); - EntityItemID id = _entityCertificateIDMap.value(certID); + connect(this, &EntityTree::killChallengeOwnershipTimeoutTimer, this, [=](const EntityItemID& id) { if (entityItemID == id && _challengeOwnershipTimeoutTimer) { _challengeOwnershipTimeoutTimer->stop(); _challengeOwnershipTimeoutTimer->deleteLater(); @@ -1455,12 +1453,7 @@ QByteArray EntityTree::computeNonce(const QString& certID, const QString ownerKe return nonceBytes; } -bool EntityTree::verifyNonce(const QString& certID, const QString& nonce, EntityItemID& id) { - { - QReadLocker certIdMapLocker(&_entityCertificateIDMapLock); - id = _entityCertificateIDMap.value(certID); - } - +bool EntityTree::verifyNonce(const QString& certID, const QString& nonce) { QString actualNonce, key; { QWriteLocker locker(&_certNonceMapLock); @@ -1645,10 +1638,14 @@ void EntityTree::processChallengeOwnershipPacket(ReceivedMessage& message, const QString certID(message.read(certIDByteArraySize)); QString text(message.read(textByteArraySize)); - emit killChallengeOwnershipTimeoutTimer(certID); + EntityItemID id; + { + QReadLocker certIdMapLocker(&_entityCertificateIDMapLock); + id = _entityCertificateIDMap.value(certID); + } + emit killChallengeOwnershipTimeoutTimer(id); - EntityItemID id; - if (!verifyNonce(certID, text, id)) { + if (!verifyNonce(certID, text)) { if (!id.isNull()) { deleteEntity(id, true); } diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index e627a07d13..63e1197970 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -253,7 +253,7 @@ public: static const float DEFAULT_MAX_TMP_ENTITY_LIFETIME; QByteArray computeNonce(const QString& certID, const QString ownerKey); - bool verifyNonce(const QString& certID, const QString& nonce, EntityItemID& id); + bool verifyNonce(const QString& certID, const QString& nonce); QUuid getMyAvatarSessionUUID() { return _myAvatar ? _myAvatar->getSessionUUID() : QUuid(); } void setMyAvatar(std::shared_ptr myAvatar) { _myAvatar = myAvatar; } @@ -290,7 +290,7 @@ signals: void entityServerScriptChanging(const EntityItemID& entityItemID, const bool reload); void newCollisionSoundURL(const QUrl& url, const EntityItemID& entityID); void clearingEntities(); - void killChallengeOwnershipTimeoutTimer(const QString& certID); + void killChallengeOwnershipTimeoutTimer(const EntityItemID& certID); protected: From c54e8f55693be7175839700c6e00d04b368bbc77 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 25 Mar 2019 17:32:03 -0700 Subject: [PATCH 346/446] showing ptt text always + hmd mode switch fix --- interface/resources/qml/BubbleIcon.qml | 10 +++++++++- interface/resources/qml/hifi/audio/Audio.qml | 5 ++--- interface/resources/qml/hifi/audio/MicBar.qml | 15 +++++++++------ .../qml/hifi/audio/MicBarApplication.qml | 13 +++++++++---- scripts/system/audio.js | 2 ++ 5 files changed, 31 insertions(+), 14 deletions(-) diff --git a/interface/resources/qml/BubbleIcon.qml b/interface/resources/qml/BubbleIcon.qml index 1ad73f6179..430eb19860 100644 --- a/interface/resources/qml/BubbleIcon.qml +++ b/interface/resources/qml/BubbleIcon.qml @@ -22,7 +22,7 @@ Rectangle { property var dragTarget: null; property bool ignoreRadiusEnabled: AvatarInputs.ignoreRadiusEnabled; - onIgnoreRadiusEnabledChanged: { + function updateOpacity() { if (ignoreRadiusEnabled) { bubbleRect.opacity = 0.7; } else { @@ -30,6 +30,14 @@ Rectangle { } } + Component.onCompleted: { + updateOpacity(); + } + + onIgnoreRadiusEnabledChanged: { + updateOpacity(); + } + color: "#00000000"; border { width: mouseArea.containsMouse || mouseArea.containsPress ? 2 : 0; diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index 3266f3ef46..015a9542e9 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -177,14 +177,14 @@ Rectangle { onClicked: { if (pushToTalk && !checked) { // disable push to talk if unmuting - if ((bar.currentIndex === 0)) { + if (bar.currentIndex === 0) { AudioScriptingInterface.pushToTalkDesktop = false; } else { AudioScriptingInterface.pushToTalkHMD = false; } } - if ((bar.currentIndex === 0)) { + if (bar.currentIndex === 0) { AudioScriptingInterface.mutedDesktop = checked; } else { @@ -293,7 +293,6 @@ Rectangle { Item { id: pttTextContainer - visible: pushToTalk; anchors.top: switchesContainer.bottom; anchors.topMargin: 10; anchors.left: parent.left; diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index 4b243e033a..b6254b168c 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -22,14 +22,18 @@ Rectangle { property var muted: AudioScriptingInterface.muted; readonly property var level: AudioScriptingInterface.inputLevel; readonly property var clipping: AudioScriptingInterface.clipping; - readonly property var pushToTalk: AudioScriptingInterface.pushToTalk; - readonly property var pushingToTalk: AudioScriptingInterface.pushingToTalk; + property var pushToTalk: AudioScriptingInterface.pushToTalk; + property var pushingToTalk: AudioScriptingInterface.pushingToTalk; readonly property var userSpeakingLevel: 0.4; property bool gated: false; Component.onCompleted: { AudioScriptingInterface.noiseGateOpened.connect(function() { gated = false; }); AudioScriptingInterface.noiseGateClosed.connect(function() { gated = true; }); + HMD.displayModeChanged.connect(function() { + muted = AudioScriptingInterface.muted; + pushToTalk = AudioScriptingInterface.pushToTalk; + }); } property bool standalone: false; @@ -147,7 +151,6 @@ Rectangle { Item { id: status; - readonly property string color: colors.icon; visible: (pushToTalk && !pushingToTalk) || muted; @@ -166,7 +169,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - color: parent.color; + color: colors.icon; text: (pushToTalk && !pushingToTalk) ? (HMD.active ? "MUTED PTT" : "MUTED PTT-(T)") : (muted ? "MUTED" : "MUTE"); font.pointSize: 12; @@ -180,7 +183,7 @@ Rectangle { width: pushToTalk && !pushingToTalk ? (HMD.active ? 27 : 25) : 50; height: 4; - color: parent.color; + color: colors.icon; } Rectangle { @@ -191,7 +194,7 @@ Rectangle { width: pushToTalk && !pushingToTalk ? (HMD.active ? 27 : 25) : 50; height: 4; - color: parent.color; + color: colors.icon; } } diff --git a/interface/resources/qml/hifi/audio/MicBarApplication.qml b/interface/resources/qml/hifi/audio/MicBarApplication.qml index f89ada6e49..509517063d 100644 --- a/interface/resources/qml/hifi/audio/MicBarApplication.qml +++ b/interface/resources/qml/hifi/audio/MicBarApplication.qml @@ -19,14 +19,18 @@ Rectangle { id: micBar; readonly property var level: AudioScriptingInterface.inputLevel; readonly property var clipping: AudioScriptingInterface.clipping; - readonly property var muted: AudioScriptingInterface.muted; - readonly property var pushToTalk: AudioScriptingInterface.pushToTalk; - readonly property var pushingToTalk: AudioScriptingInterface.pushingToTalk; + property var muted: AudioScriptingInterface.muted; + property var pushToTalk: AudioScriptingInterface.pushToTalk; + property var pushingToTalk: AudioScriptingInterface.pushingToTalk; readonly property var userSpeakingLevel: 0.4; property bool gated: false; Component.onCompleted: { AudioScriptingInterface.noiseGateOpened.connect(function() { gated = false; }); AudioScriptingInterface.noiseGateClosed.connect(function() { gated = true; }); + HMD.displayModeChanged.connect(function() { + muted = AudioScriptingInterface.muted; + pushToTalk = AudioScriptingInterface.pushToTalk; + }); } readonly property string unmutedIcon: "../../../icons/tablet-icons/mic-unmute-i.svg"; @@ -86,8 +90,9 @@ Rectangle { hoverEnabled: true; scrollGestureEnabled: false; onClicked: { - AudioScriptingInterface.muted = !AudioScriptingInterface.muted; + AudioScriptingInterface.muted = !muted; Tablet.playSound(TabletEnums.ButtonClick); + muted = Qt.binding(function() { return AudioScriptingInterface.muted; }); // restore binding } drag.target: dragTarget; onContainsMouseChanged: { diff --git a/scripts/system/audio.js b/scripts/system/audio.js index 19ed3faef2..a161b40ffd 100644 --- a/scripts/system/audio.js +++ b/scripts/system/audio.js @@ -75,6 +75,7 @@ button.clicked.connect(onClicked); tablet.screenChanged.connect(onScreenChanged); Audio.mutedChanged.connect(onMuteToggled); Audio.pushToTalkChanged.connect(onMuteToggled); +HMD.displayModeChanged.connect(onMuteToggled); Script.scriptEnding.connect(function () { if (onAudioScreen) { @@ -84,6 +85,7 @@ Script.scriptEnding.connect(function () { tablet.screenChanged.disconnect(onScreenChanged); Audio.mutedChanged.disconnect(onMuteToggled); Audio.pushToTalkChanged.disconnect(onMuteToggled); + HMD.displayModeChanged.disconnect(onMuteToggled); tablet.removeButton(button); }); From 060932ad4bcd609e4c434fcc18ee77d056b06bd7 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Tue, 26 Mar 2019 09:13:10 -0700 Subject: [PATCH 347/446] fixing typo --- interface/src/scripting/Audio.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index dba3af0730..90687e220e 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -358,7 +358,7 @@ private: AudioDevices* getDevices() { return &_devices; } AudioDevices _devices; Setting::Handle _mutedDesktopSetting{ QStringList { Audio::AUDIO, "mutedDesktop" }, true }; - Setting::Handle _mutedHMD{ QStringList { Audio::AUDIO, "mutedHMD" }, true }; + Setting::Handle _mutedHMDSetting{ QStringList { Audio::AUDIO, "mutedHMD" }, true }; Setting::Handle _pttDesktopSetting{ QStringList { Audio::AUDIO, "pushToTalkDesktop" }, false }; Setting::Handle _pttHMDSetting{ QStringList { Audio::AUDIO, "pushToTalkHMD" }, false }; bool _mutedDesktop{ true }; From 1057166418e4c0526c2caa6e7a02c6b06b4fd63c Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 15 Mar 2019 10:44:59 -0700 Subject: [PATCH 348/446] Add master injector gain to audio-mixer --- .../src/audio/AudioMixerClientData.h | 3 + .../src/audio/AudioMixerSlave.cpp | 56 ++++++++++++------- assignment-client/src/audio/AudioMixerSlave.h | 7 ++- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index 653749f619..f9d113c53d 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -84,6 +84,8 @@ public: float getMasterAvatarGain() const { return _masterAvatarGain; } void setMasterAvatarGain(float gain) { _masterAvatarGain = gain; } + float getMasterInjectorGain() const { return _masterInjectorGain; } + void setMasterInjectorGain(float gain) { _masterInjectorGain = gain; } AudioLimiter audioLimiter; @@ -189,6 +191,7 @@ private: int _frameToSendStats { 0 }; float _masterAvatarGain { 1.0f }; // per-listener mixing gain, applied only to avatars + float _masterInjectorGain { 1.0f }; // per-listener mixing gain, applied only to injectors CodecPluginPointer _codec; QString _selectedCodecName; diff --git a/assignment-client/src/audio/AudioMixerSlave.cpp b/assignment-client/src/audio/AudioMixerSlave.cpp index a920b45161..f7f8e8a9c1 100644 --- a/assignment-client/src/audio/AudioMixerSlave.cpp +++ b/assignment-client/src/audio/AudioMixerSlave.cpp @@ -50,7 +50,7 @@ void sendEnvironmentPacket(const SharedNodePointer& node, AudioMixerClientData& // mix helpers inline float approximateGain(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd); -inline float computeGain(float masterListenerGain, const AvatarAudioStream& listeningNodeStream, +inline float computeGain(float masterAvatarGain, float masterInjectorGain, const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, float distance, bool isEcho); inline float computeAzimuth(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition); @@ -338,8 +338,8 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { } if (!isThrottling) { - updateHRTFParameters(stream, *listenerAudioStream, - listenerData->getMasterAvatarGain()); + updateHRTFParameters(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), + listenerData->getMasterInjectorGain()); } return false; }); @@ -363,8 +363,8 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { } if (!isThrottling) { - updateHRTFParameters(stream, *listenerAudioStream, - listenerData->getMasterAvatarGain()); + updateHRTFParameters(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), + listenerData->getMasterInjectorGain()); } return false; }); @@ -381,13 +381,13 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { stream.approximateVolume = approximateVolume(stream, listenerAudioStream); } else { if (shouldBeSkipped(stream, *listener, *listenerAudioStream, *listenerData)) { - addStream(stream, *listenerAudioStream, 0.0f, isSoloing); + addStream(stream, *listenerAudioStream, 0.0f, 0.0f, isSoloing); streams.skipped.push_back(move(stream)); ++stats.activeToSkipped; return true; } - addStream(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), + addStream(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), listenerData->getMasterInjectorGain(), isSoloing); if (shouldBeInactive(stream)) { @@ -423,7 +423,7 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { return true; } - addStream(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), + addStream(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), listenerData->getMasterInjectorGain(), isSoloing); if (shouldBeInactive(stream)) { @@ -491,7 +491,9 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { void AudioMixerSlave::addStream(AudioMixerClientData::MixableStream& mixableStream, AvatarAudioStream& listeningNodeStream, - float masterListenerGain, bool isSoloing) { + float masterAvatarGain, + float masterInjectorGain, + bool isSoloing) { ++stats.totalMixes; auto streamToAdd = mixableStream.positionalStream; @@ -504,9 +506,10 @@ void AudioMixerSlave::addStream(AudioMixerClientData::MixableStream& mixableStre float distance = glm::max(glm::length(relativePosition), EPSILON); float azimuth = isEcho ? 0.0f : computeAzimuth(listeningNodeStream, listeningNodeStream, relativePosition); - float gain = masterListenerGain; + float gain = masterAvatarGain; if (!isSoloing) { - gain = computeGain(masterListenerGain, listeningNodeStream, *streamToAdd, relativePosition, distance, isEcho); + gain = computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd, relativePosition, + distance, isEcho); } const int HRTF_DATASET_INDEX = 1; @@ -585,8 +588,9 @@ void AudioMixerSlave::addStream(AudioMixerClientData::MixableStream& mixableStre } void AudioMixerSlave::updateHRTFParameters(AudioMixerClientData::MixableStream& mixableStream, - AvatarAudioStream& listeningNodeStream, - float masterListenerGain) { + AvatarAudioStream& listeningNodeStream, + float masterAvatarGain, + float masterInjectorGain) { auto streamToAdd = mixableStream.positionalStream; // check if this is a server echo of a source back to itself @@ -595,7 +599,8 @@ void AudioMixerSlave::updateHRTFParameters(AudioMixerClientData::MixableStream& glm::vec3 relativePosition = streamToAdd->getPosition() - listeningNodeStream.getPosition(); float distance = glm::max(glm::length(relativePosition), EPSILON); - float gain = computeGain(masterListenerGain, listeningNodeStream, *streamToAdd, relativePosition, distance, isEcho); + float gain = computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd, relativePosition, + distance, isEcho); float azimuth = isEcho ? 0.0f : computeAzimuth(listeningNodeStream, listeningNodeStream, relativePosition); mixableStream.hrtf->setParameterHistory(azimuth, distance, gain); @@ -720,6 +725,7 @@ float approximateGain(const AvatarAudioStream& listeningNodeStream, const Positi // injector: apply attenuation if (streamToAdd.getType() == PositionalAudioStream::Injector) { gain *= reinterpret_cast(&streamToAdd)->getAttenuationRatio(); + // injector: skip master gain } // avatar: skip attenuation - it is too costly to approximate @@ -729,16 +735,23 @@ float approximateGain(const AvatarAudioStream& listeningNodeStream, const Positi float distance = glm::length(relativePosition); return gain / distance; - // avatar: skip master gain - it is constant for all streams + // avatar: skip master gain } -float computeGain(float masterListenerGain, const AvatarAudioStream& listeningNodeStream, - const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, float distance, bool isEcho) { +float computeGain(float masterAvatarGain, + float masterInjectorGain, + const AvatarAudioStream& listeningNodeStream, + const PositionalAudioStream& streamToAdd, + const glm::vec3& relativePosition, + float distance, + bool isEcho) { float gain = 1.0f; // injector: apply attenuation if (streamToAdd.getType() == PositionalAudioStream::Injector) { gain *= reinterpret_cast(&streamToAdd)->getAttenuationRatio(); + // apply master gain + gain *= masterInjectorGain; // avatar: apply fixed off-axis attenuation to make them quieter as they turn away } else if (!isEcho && (streamToAdd.getType() == PositionalAudioStream::Microphone)) { @@ -754,8 +767,8 @@ float computeGain(float masterListenerGain, const AvatarAudioStream& listeningNo gain *= offAxisCoefficient; - // apply master gain, only to avatars - gain *= masterListenerGain; + // apply master gain + gain *= masterAvatarGain; } auto& audioZones = AudioMixer::getAudioZones(); @@ -797,8 +810,9 @@ float computeGain(float masterListenerGain, const AvatarAudioStream& listeningNo return gain; } -float computeAzimuth(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, - const glm::vec3& relativePosition) { +float computeAzimuth(const AvatarAudioStream& listeningNodeStream, + const PositionalAudioStream& streamToAdd, + const glm::vec3& relativePosition) { glm::quat inverseOrientation = glm::inverse(listeningNodeStream.getOrientation()); glm::vec3 rotatedSourcePosition = inverseOrientation * relativePosition; diff --git a/assignment-client/src/audio/AudioMixerSlave.h b/assignment-client/src/audio/AudioMixerSlave.h index 3d979da1fc..9765ea8639 100644 --- a/assignment-client/src/audio/AudioMixerSlave.h +++ b/assignment-client/src/audio/AudioMixerSlave.h @@ -57,10 +57,13 @@ private: bool prepareMix(const SharedNodePointer& listener); void addStream(AudioMixerClientData::MixableStream& mixableStream, AvatarAudioStream& listeningNodeStream, - float masterListenerGain, bool isSoloing); + float masterAvatarGain, + float masterInjectorGain, + bool isSoloing); void updateHRTFParameters(AudioMixerClientData::MixableStream& mixableStream, AvatarAudioStream& listeningNodeStream, - float masterListenerGain); + float masterAvatarGain, + float masterInjectorGain); void resetHRTFState(AudioMixerClientData::MixableStream& mixableStream); void addStreams(Node& listener, AudioMixerClientData& listenerData); From b15651f1ebcf161fb50253bc9333e906cd27454b Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 15 Mar 2019 12:05:51 -0700 Subject: [PATCH 349/446] Handle InjectorGainSet packet at the audio-mixer --- assignment-client/src/audio/AudioMixer.cpp | 1 + .../src/audio/AudioMixerClientData.cpp | 14 ++++++++++++++ assignment-client/src/audio/AudioMixerClientData.h | 1 + libraries/networking/src/udt/PacketHeaders.h | 2 +- 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index f67c54239e..201e24d4b9 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -97,6 +97,7 @@ AudioMixer::AudioMixer(ReceivedMessage& message) : PacketType::RadiusIgnoreRequest, PacketType::RequestsDomainListData, PacketType::PerAvatarGainSet, + PacketType::InjectorGainSet, PacketType::AudioSoloRequest }, this, "queueAudioPacket"); diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 90698bfac8..b8d3ec62a6 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -92,6 +92,9 @@ int AudioMixerClientData::processPackets(ConcurrentAddedStreams& addedStreams) { case PacketType::PerAvatarGainSet: parsePerAvatarGainSet(*packet, node); break; + case PacketType::InjectorGainSet: + parseInjectorGainSet(*packet, node); + break; case PacketType::NodeIgnoreRequest: parseNodeIgnoreRequest(packet, node); break; @@ -205,6 +208,17 @@ void AudioMixerClientData::parsePerAvatarGainSet(ReceivedMessage& message, const } } +void AudioMixerClientData::parseInjectorGainSet(ReceivedMessage& message, const SharedNodePointer& node) { + QUuid uuid = node->getUUID(); + + uint8_t packedGain; + message.readPrimitive(&packedGain); + float gain = unpackFloatGainFromByte(packedGain); + + setMasterInjectorGain(gain); + qCDebug(audio) << "Setting MASTER injector gain for " << uuid << " to " << gain; +} + void AudioMixerClientData::setGainForAvatar(QUuid nodeID, float gain) { auto it = std::find_if(_streams.active.cbegin(), _streams.active.cend(), [nodeID](const MixableStream& mixableStream){ return mixableStream.nodeStreamID.nodeID == nodeID && mixableStream.nodeStreamID.streamID.isNull(); diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index f9d113c53d..4a1ca7f9b5 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -63,6 +63,7 @@ public: void negotiateAudioFormat(ReceivedMessage& message, const SharedNodePointer& node); void parseRequestsDomainListData(ReceivedMessage& message); void parsePerAvatarGainSet(ReceivedMessage& message, const SharedNodePointer& node); + void parseInjectorGainSet(ReceivedMessage& message, const SharedNodePointer& node); void parseNodeIgnoreRequest(QSharedPointer message, const SharedNodePointer& node); void parseRadiusIgnoreRequest(QSharedPointer message, const SharedNodePointer& node); void parseSoloRequest(QSharedPointer message, const SharedNodePointer& node); diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 0ec7c40ca4..413ff14b17 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -57,7 +57,7 @@ public: ICEServerQuery, OctreeStats, SetAvatarTraits, - UNUSED_PACKET_TYPE, + InjectorGainSet, AssignmentClientStatus, NoisyMute, AvatarIdentity, From 755762e8ecf7a7a1d70f21ba6a6c15557f5f2914 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 15 Mar 2019 17:24:50 -0700 Subject: [PATCH 350/446] Send InjectorGainSet packet to the audio-mixer --- libraries/networking/src/NodeList.cpp | 25 +++++++++++++++++++++++++ libraries/networking/src/NodeList.h | 4 ++++ 2 files changed, 29 insertions(+) diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index e6eb6087b0..eec710322e 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -265,6 +265,8 @@ void NodeList::reset(bool skipDomainHandlerReset) { _avatarGainMap.clear(); _avatarGainMapLock.unlock(); + _injectorGain = 0.0f; + if (!skipDomainHandlerReset) { // clear the domain connection information, unless they're the ones that asked us to reset _domainHandler.softReset(); @@ -1087,6 +1089,29 @@ float NodeList::getAvatarGain(const QUuid& nodeID) { return 0.0f; } +void NodeList::setInjectorGain(float gain) { + auto audioMixer = soloNodeOfType(NodeType::AudioMixer); + if (audioMixer) { + // setup the packet + auto setInjectorGainPacket = NLPacket::create(PacketType::InjectorGainSet, sizeof(float), true); + + // We need to convert the gain in dB (from the script) to an amplitude before packing it. + setInjectorGainPacket->writePrimitive(packFloatGainToByte(fastExp2f(gain / 6.02059991f))); + + qCDebug(networking) << "Sending Set Injector Gain packet with Gain:" << gain; + + sendPacket(std::move(setInjectorGainPacket), *audioMixer); + _injectorGain = gain; + + } else { + qWarning() << "Couldn't find audio mixer to send set gain request"; + } +} + +float NodeList::getInjectorGain() { + return _injectorGain; +} + void NodeList::kickNodeBySessionID(const QUuid& nodeID) { // send a request to domain-server to kick the node with the given session ID // the domain-server will handle the persistence of the kick (via username or IP) diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index e135bc937d..d2a1212d64 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -83,6 +83,8 @@ public: bool isPersonalMutingNode(const QUuid& nodeID) const; void setAvatarGain(const QUuid& nodeID, float gain); float getAvatarGain(const QUuid& nodeID); + void setInjectorGain(float gain); + float getInjectorGain(); void kickNodeBySessionID(const QUuid& nodeID); void muteNodeBySessionID(const QUuid& nodeID); @@ -181,6 +183,8 @@ private: mutable QReadWriteLock _avatarGainMapLock; tbb::concurrent_unordered_map _avatarGainMap; + std::atomic _injectorGain { 0.0f }; + void sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode); #if defined(Q_OS_ANDROID) Setting::Handle _ignoreRadiusEnabled { "IgnoreRadiusEnabled", false }; From 4a6e495f5fa96eaa4772aaeb615035a050ec3a0f Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Wed, 20 Mar 2019 15:48:05 -0700 Subject: [PATCH 351/446] Add Users.setInjectorGain() and Users.getInjectorGain() to the scripting interface --- .../script-engine/src/UsersScriptingInterface.cpp | 9 +++++++++ .../script-engine/src/UsersScriptingInterface.h | 15 +++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp index 9beb52f20a..7b30e087e5 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.cpp +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -51,6 +51,15 @@ float UsersScriptingInterface::getAvatarGain(const QUuid& nodeID) { return DependencyManager::get()->getAvatarGain(nodeID); } +void UsersScriptingInterface::setInjectorGain(float gain) { + // ask the NodeList to set the audio injector gain + DependencyManager::get()->setInjectorGain(gain); +} + +float UsersScriptingInterface::getInjectorGain() { + return DependencyManager::get()->getInjectorGain(); +} + void UsersScriptingInterface::kick(const QUuid& nodeID) { if (_kickConfirmationOperator) { diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index f8ca974b8b..d6750b263d 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -97,6 +97,21 @@ public slots: */ float getAvatarGain(const QUuid& nodeID); + /**jsdoc + * Sets the audio injector gain at the server. + * Units are Decibels (dB) + * @function Users.setInjectorGain + * @param {number} gain (in dB) + */ + void setInjectorGain(float gain); + + /**jsdoc + * Gets the audio injector gain at the server. + * @function Users.getInjectorGain + * @returns {number} gain (in dB) + */ + float getInjectorGain(); + /**jsdoc * Kick/ban another user. Removes them from the server and prevents them from returning. Bans by either user name (if * available) or machine fingerprint otherwise. This will only do anything if you're an admin of the domain you're in. From a2d261d20ca1273b99ab90952945569baa43a153 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 21 Mar 2019 11:51:49 -0700 Subject: [PATCH 352/446] Move the new audio volume API from Users scripting interface to Audio scripting interface --- interface/src/scripting/Audio.cpp | 36 ++++++++++++++++--- interface/src/scripting/Audio.h | 30 ++++++++++++++++ .../src/UsersScriptingInterface.cpp | 9 ----- .../src/UsersScriptingInterface.h | 15 -------- 4 files changed, 61 insertions(+), 29 deletions(-) diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index b1b5077e60..f9560c84f7 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -377,6 +377,18 @@ void Audio::handlePushedToTalk(bool enabled) { } } +void Audio::setInputDevice(const QAudioDeviceInfo& device, bool isHMD) { + withWriteLock([&] { + _devices.chooseInputDevice(device, isHMD); + }); +} + +void Audio::setOutputDevice(const QAudioDeviceInfo& device, bool isHMD) { + withWriteLock([&] { + _devices.chooseOutputDevice(device, isHMD); + }); +} + void Audio::setReverb(bool enable) { withWriteLock([&] { DependencyManager::get()->setReverb(enable); @@ -389,14 +401,28 @@ void Audio::setReverbOptions(const AudioEffectOptions* options) { }); } -void Audio::setInputDevice(const QAudioDeviceInfo& device, bool isHMD) { +void Audio::setAvatarGain(float gain) { withWriteLock([&] { - _devices.chooseInputDevice(device, isHMD); + // ask the NodeList to set the master avatar gain + DependencyManager::get()->setAvatarGain("", gain); }); } -void Audio::setOutputDevice(const QAudioDeviceInfo& device, bool isHMD) { - withWriteLock([&] { - _devices.chooseOutputDevice(device, isHMD); +float Audio::getAvatarGain() { + return resultWithReadLock([&] { + return DependencyManager::get()->getAvatarGain(""); + }); +} + +void Audio::setInjectorGain(float gain) { + withWriteLock([&] { + // ask the NodeList to set the audio injector gain + DependencyManager::get()->setInjectorGain(gain); + }); +} + +float Audio::getInjectorGain() { + return resultWithReadLock([&] { + return DependencyManager::get()->getInjectorGain(); }); } diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index 9ee230fc29..14a75d5ffe 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -170,6 +170,36 @@ public: */ Q_INVOKABLE void setReverbOptions(const AudioEffectOptions* options); + /**jsdoc + * Sets the master avatar gain at the server. + * Units are Decibels (dB) + * @function Audio.setAvatarGain + * @param {number} gain (in dB) + */ + Q_INVOKABLE void setAvatarGain(float gain); + + /**jsdoc + * Gets the master avatar gain at the server. + * @function Audio.getAvatarGain + * @returns {number} gain (in dB) + */ + Q_INVOKABLE float getAvatarGain(); + + /**jsdoc + * Sets the audio injector gain at the server. + * Units are Decibels (dB) + * @function Audio.setInjectorGain + * @param {number} gain (in dB) + */ + Q_INVOKABLE void setInjectorGain(float gain); + + /**jsdoc + * Gets the audio injector gain at the server. + * @function Audio.getInjectorGain + * @returns {number} gain (in dB) + */ + Q_INVOKABLE float getInjectorGain(); + /**jsdoc * Starts making an audio recording of the audio being played in-world (i.e., not local-only audio) to a file in WAV format. * @function Audio.startRecording diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp index 7b30e087e5..9beb52f20a 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.cpp +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -51,15 +51,6 @@ float UsersScriptingInterface::getAvatarGain(const QUuid& nodeID) { return DependencyManager::get()->getAvatarGain(nodeID); } -void UsersScriptingInterface::setInjectorGain(float gain) { - // ask the NodeList to set the audio injector gain - DependencyManager::get()->setInjectorGain(gain); -} - -float UsersScriptingInterface::getInjectorGain() { - return DependencyManager::get()->getInjectorGain(); -} - void UsersScriptingInterface::kick(const QUuid& nodeID) { if (_kickConfirmationOperator) { diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index d6750b263d..f8ca974b8b 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -97,21 +97,6 @@ public slots: */ float getAvatarGain(const QUuid& nodeID); - /**jsdoc - * Sets the audio injector gain at the server. - * Units are Decibels (dB) - * @function Users.setInjectorGain - * @param {number} gain (in dB) - */ - void setInjectorGain(float gain); - - /**jsdoc - * Gets the audio injector gain at the server. - * @function Users.getInjectorGain - * @returns {number} gain (in dB) - */ - float getInjectorGain(); - /**jsdoc * Kick/ban another user. Removes them from the server and prevents them from returning. Bans by either user name (if * available) or machine fingerprint otherwise. This will only do anything if you're an admin of the domain you're in. From 95b4f954a6a1ed2912a6c47ac2c5c6d86a29d8ea Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 22 Mar 2019 10:12:31 -0700 Subject: [PATCH 353/446] Add AudioClient mixing gains for local injectors and system sounds --- libraries/audio-client/src/AudioClient.cpp | 4 +++- libraries/audio-client/src/AudioClient.h | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 9d645a1dbf..9fa9a0bc18 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1368,7 +1368,9 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { memset(_localScratchBuffer, 0, bytesToRead); if (0 < injectorBuffer->readData((char*)_localScratchBuffer, bytesToRead)) { - float gain = options.volume; + bool isSystemSound = !injector->isPositionSet() && !injector->isAmbisonic(); + + float gain = injector->getVolume() * (isSystemSound ? _systemInjectorGain : _localInjectorGain); if (options.ambisonic) { diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index a153f22bf3..7608bf5cdb 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -241,6 +241,8 @@ public slots: void setInputVolume(float volume, bool emitSignal = true); void setReverb(bool reverb); void setReverbOptions(const AudioEffectOptions* options); + void setLocalInjectorGain(float gain) { _localInjectorGain = gain; }; + void setSystemInjectorGain(float gain) { _systemInjectorGain = gain; }; void outputNotify(); @@ -395,6 +397,8 @@ private: int16_t* _outputScratchBuffer { NULL }; // for local audio (used by audio injectors thread) + std::atomic _localInjectorGain { 1.0f }; + std::atomic _systemInjectorGain { 1.0f }; float _localMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; float* _localOutputMixBuffer { NULL }; From 37429a07b8a7a2262582f4b77779d46db3debdc3 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 22 Mar 2019 10:21:54 -0700 Subject: [PATCH 354/446] Add local injector gains to the Audio scripting interface --- interface/src/scripting/Audio.cpp | 34 ++++++++++++++++++++++++++ interface/src/scripting/Audio.h | 40 +++++++++++++++++++++++++++---- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index f9560c84f7..b3c7b25745 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -426,3 +426,37 @@ float Audio::getInjectorGain() { return DependencyManager::get()->getInjectorGain(); }); } + +void Audio::setLocalInjectorGain(float gain) { + withWriteLock([&] { + if (_localInjectorGain != gain) { + _localInjectorGain = gain; + // convert dB to amplitude + gain = fastExp2f(gain / 6.02059991f); + DependencyManager::get()->setLocalInjectorGain(gain); + } + }); +} + +float Audio::getLocalInjectorGain() { + return resultWithReadLock([&] { + return _localInjectorGain; + }); +} + +void Audio::setSystemInjectorGain(float gain) { + withWriteLock([&] { + if (_systemInjectorGain != gain) { + _systemInjectorGain = gain; + // convert dB to amplitude + gain = fastExp2f(gain / 6.02059991f); + DependencyManager::get()->setSystemInjectorGain(gain); + } + }); +} + +float Audio::getSystemInjectorGain() { + return resultWithReadLock([&] { + return _systemInjectorGain; + }); +} diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index 14a75d5ffe..d6823ea452 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -171,7 +171,7 @@ public: Q_INVOKABLE void setReverbOptions(const AudioEffectOptions* options); /**jsdoc - * Sets the master avatar gain at the server. + * Sets the avatar gain at the server. * Units are Decibels (dB) * @function Audio.setAvatarGain * @param {number} gain (in dB) @@ -179,14 +179,14 @@ public: Q_INVOKABLE void setAvatarGain(float gain); /**jsdoc - * Gets the master avatar gain at the server. + * Gets the avatar gain at the server. * @function Audio.getAvatarGain * @returns {number} gain (in dB) */ Q_INVOKABLE float getAvatarGain(); /**jsdoc - * Sets the audio injector gain at the server. + * Sets the injector gain at the server. * Units are Decibels (dB) * @function Audio.setInjectorGain * @param {number} gain (in dB) @@ -194,12 +194,42 @@ public: Q_INVOKABLE void setInjectorGain(float gain); /**jsdoc - * Gets the audio injector gain at the server. + * Gets the injector gain at the server. * @function Audio.getInjectorGain * @returns {number} gain (in dB) */ Q_INVOKABLE float getInjectorGain(); + /**jsdoc + * Sets the local injector gain in the client. + * Units are Decibels (dB) + * @function Audio.setLocalInjectorGain + * @param {number} gain (in dB) + */ + Q_INVOKABLE void setLocalInjectorGain(float gain); + + /**jsdoc + * Gets the local injector gain in the client. + * @function Audio.getLocalInjectorGain + * @returns {number} gain (in dB) + */ + Q_INVOKABLE float getLocalInjectorGain(); + + /**jsdoc + * Sets the injector gain for system sounds. + * Units are Decibels (dB) + * @function Audio.setSystemInjectorGain + * @param {number} gain (in dB) + */ + Q_INVOKABLE void setSystemInjectorGain(float gain); + + /**jsdoc + * Gets the injector gain for system sounds. + * @function Audio.getSystemInjectorGain + * @returns {number} gain (in dB) + */ + Q_INVOKABLE float getSystemInjectorGain(); + /**jsdoc * Starts making an audio recording of the audio being played in-world (i.e., not local-only audio) to a file in WAV format. * @function Audio.startRecording @@ -380,6 +410,8 @@ private: float _inputVolume { 1.0f }; float _inputLevel { 0.0f }; + float _localInjectorGain { 0.0f }; // in dB + float _systemInjectorGain { 0.0f }; // in dB bool _isClipping { false }; bool _enableNoiseReduction { true }; // Match default value of AudioClient::_isNoiseGateEnabled. bool _enableWarnWhenMuted { true }; From 155bd39da6339410682a3c2bfd8b1c2bcb16947f Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 22 Mar 2019 10:24:30 -0700 Subject: [PATCH 355/446] Quantize and limit the local injector gains to match the network protocol --- interface/src/scripting/Audio.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index b3c7b25745..330ed7abfe 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -433,6 +433,8 @@ void Audio::setLocalInjectorGain(float gain) { _localInjectorGain = gain; // convert dB to amplitude gain = fastExp2f(gain / 6.02059991f); + // quantize and limit to match NodeList::setInjectorGain() + gain = unpackFloatGainFromByte(packFloatGainToByte(gain)); DependencyManager::get()->setLocalInjectorGain(gain); } }); @@ -450,6 +452,8 @@ void Audio::setSystemInjectorGain(float gain) { _systemInjectorGain = gain; // convert dB to amplitude gain = fastExp2f(gain / 6.02059991f); + // quantize and limit to match NodeList::setInjectorGain() + gain = unpackFloatGainFromByte(packFloatGainToByte(gain)); DependencyManager::get()->setSystemInjectorGain(gain); } }); From c15813b44225235a96fd507d031c6e6650d0eedb Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 22 Mar 2019 17:58:17 -0700 Subject: [PATCH 356/446] Cleanup --- assignment-client/src/audio/AudioMixerClientData.cpp | 6 +++--- interface/src/scripting/Audio.cpp | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index b8d3ec62a6..41b72c04d2 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -200,11 +200,11 @@ void AudioMixerClientData::parsePerAvatarGainSet(ReceivedMessage& message, const if (avatarUUID.isNull()) { // set the MASTER avatar gain setMasterAvatarGain(gain); - qCDebug(audio) << "Setting MASTER avatar gain for " << uuid << " to " << gain; + qCDebug(audio) << "Setting MASTER avatar gain for" << uuid << "to" << gain; } else { // set the per-source avatar gain setGainForAvatar(avatarUUID, gain); - qCDebug(audio) << "Setting avatar gain adjustment for hrtf[" << uuid << "][" << avatarUUID << "] to " << gain; + qCDebug(audio) << "Setting avatar gain adjustment for hrtf[" << uuid << "][" << avatarUUID << "] to" << gain; } } @@ -216,7 +216,7 @@ void AudioMixerClientData::parseInjectorGainSet(ReceivedMessage& message, const float gain = unpackFloatGainFromByte(packedGain); setMasterInjectorGain(gain); - qCDebug(audio) << "Setting MASTER injector gain for " << uuid << " to " << gain; + qCDebug(audio) << "Setting MASTER injector gain for" << uuid << "to" << gain; } void AudioMixerClientData::setGainForAvatar(QUuid nodeID, float gain) { diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 330ed7abfe..4f2171d451 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -404,13 +404,13 @@ void Audio::setReverbOptions(const AudioEffectOptions* options) { void Audio::setAvatarGain(float gain) { withWriteLock([&] { // ask the NodeList to set the master avatar gain - DependencyManager::get()->setAvatarGain("", gain); + DependencyManager::get()->setAvatarGain(QUuid(), gain); }); } float Audio::getAvatarGain() { return resultWithReadLock([&] { - return DependencyManager::get()->getAvatarGain(""); + return DependencyManager::get()->getAvatarGain(QUuid()); }); } From 3d7c3e7b6f6fe3d737cc86e038966617397258f5 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Sat, 23 Mar 2019 06:48:37 -0700 Subject: [PATCH 357/446] Persist the audio-mixer settings across domain changes and server resets --- libraries/networking/src/NodeList.cpp | 38 ++++++++++++++++++--------- libraries/networking/src/NodeList.h | 3 ++- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index eec710322e..0021a594bc 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -265,8 +265,6 @@ void NodeList::reset(bool skipDomainHandlerReset) { _avatarGainMap.clear(); _avatarGainMapLock.unlock(); - _injectorGain = 0.0f; - if (!skipDomainHandlerReset) { // clear the domain connection information, unless they're the ones that asked us to reset _domainHandler.softReset(); @@ -1018,6 +1016,14 @@ void NodeList::maybeSendIgnoreSetToNode(SharedNodePointer newNode) { // also send them the current ignore radius state. sendIgnoreRadiusStateToNode(newNode); + + // also send the current avatar and injector gains + if (_avatarGain != 0.0f) { + setAvatarGain(QUuid(), _avatarGain); + } + if (_injectorGain != 0.0f) { + setInjectorGain(_injectorGain); + } } if (newNode->getType() == NodeType::AvatarMixer) { // this is a mixer that we just added - it's unlikely it knows who we were previously ignoring in this session, @@ -1064,13 +1070,17 @@ void NodeList::setAvatarGain(const QUuid& nodeID, float gain) { if (nodeID.isNull()) { qCDebug(networking) << "Sending Set MASTER Avatar Gain packet with Gain:" << gain; - } else { - qCDebug(networking) << "Sending Set Avatar Gain packet with UUID: " << uuidStringWithoutCurlyBraces(nodeID) << "Gain:" << gain; - } - sendPacket(std::move(setAvatarGainPacket), *audioMixer); - QWriteLocker lock{ &_avatarGainMapLock }; - _avatarGainMap[nodeID] = gain; + sendPacket(std::move(setAvatarGainPacket), *audioMixer); + _avatarGain = gain; + + } else { + qCDebug(networking) << "Sending Set Avatar Gain packet with UUID:" << uuidStringWithoutCurlyBraces(nodeID) << "Gain:" << gain; + + sendPacket(std::move(setAvatarGainPacket), *audioMixer); + QWriteLocker lock{ &_avatarGainMapLock }; + _avatarGainMap[nodeID] = gain; + } } else { qWarning() << "Couldn't find audio mixer to send set gain request"; @@ -1081,10 +1091,14 @@ void NodeList::setAvatarGain(const QUuid& nodeID, float gain) { } float NodeList::getAvatarGain(const QUuid& nodeID) { - QReadLocker lock{ &_avatarGainMapLock }; - auto it = _avatarGainMap.find(nodeID); - if (it != _avatarGainMap.cend()) { - return it->second; + if (nodeID.isNull()) { + return _avatarGain; + } else { + QReadLocker lock{ &_avatarGainMapLock }; + auto it = _avatarGainMap.find(nodeID); + if (it != _avatarGainMap.cend()) { + return it->second; + } } return 0.0f; } diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index d2a1212d64..f871560fba 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -183,7 +183,8 @@ private: mutable QReadWriteLock _avatarGainMapLock; tbb::concurrent_unordered_map _avatarGainMap; - std::atomic _injectorGain { 0.0f }; + std::atomic _avatarGain { 0.0f }; // in dB + std::atomic _injectorGain { 0.0f }; // in dB void sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode); #if defined(Q_OS_ANDROID) From 195472bd432f75e4c53ba4355307332f8ed049f6 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 15 Mar 2019 11:42:10 -0700 Subject: [PATCH 358/446] make equipping of clonables more reliable --- .../controllers/controllerDispatcher.js | 42 ++++++++++++++++--- .../controllerModules/equipEntity.js | 4 +- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index f4c0cbb0d6..0a9fa4dce1 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -12,7 +12,7 @@ LEFT_HAND, RIGHT_HAND, NEAR_GRAB_PICK_RADIUS, DEFAULT_SEARCH_SPHERE_DISTANCE, DISPATCHER_PROPERTIES, getGrabPointSphereOffset, HMD, MyAvatar, Messages, findHandChildEntities, Picks, PickType, Pointers, PointerManager, getGrabPointSphereOffset, HMD, MyAvatar, Messages, findHandChildEntities, Picks, PickType, Pointers, - PointerManager, print, Selection, DISPATCHER_HOVERING_LIST, DISPATCHER_HOVERING_STYLE, Keyboard + PointerManager, print, Keyboard */ controllerDispatcherPlugins = {}; @@ -53,6 +53,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); this.blacklist = []; this.pointerManager = new PointerManager(); this.grabSphereOverlays = [null, null]; + this.targetIDs = {}; // a module can occupy one or more "activity" slots while it's running. If all the required slots for a module are // not set to false (not in use), a module cannot start. When a module is using a slot, that module's name @@ -225,8 +226,8 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); if (tabletIndex !== -1 && closebyOverlays.indexOf(HMD.tabletID) === -1) { nearbyOverlays.splice(tabletIndex, 1); } - if (miniTabletIndex !== -1 - && ((closebyOverlays.indexOf(HMD.miniTabletID) === -1) || h !== HMD.miniTabletHand)) { + if (miniTabletIndex !== -1 && + ((closebyOverlays.indexOf(HMD.miniTabletID) === -1) || h !== HMD.miniTabletHand)) { nearbyOverlays.splice(miniTabletIndex, 1); } } @@ -336,6 +337,23 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); }); } + // also make sure we have the properties from the current module's target + for (var tIDRunningPluginName in _this.runningPluginNames) { + if (_this.runningPluginNames.hasOwnProperty(tIDRunningPluginName)) { + var targetIDs = _this.targetIDs[tIDRunningPluginName]; + if (targetIDs) { + for (var k = 0; k < targetIDs.length; k++) { + var targetID = targetIDs[k]; + if (!nearbyEntityPropertiesByID[targetID]) { + var targetProps = Entities.getEntityProperties(targetID, DISPATCHER_PROPERTIES); + targetProps.id = targetID; + nearbyEntityPropertiesByID[targetID] = targetProps; + } + } + } + } + } + // bundle up all the data about the current situation var controllerData = { triggerValues: [_this.leftTriggerValue, _this.rightTriggerValue], @@ -402,10 +420,23 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); Script.beginProfileRange("dispatch.run." + runningPluginName); } var runningness = plugin.run(controllerData, deltaTime); + + if (DEBUG) { + if (JSON.stringify(_this.targetIDs[runningPluginName]) != JSON.stringify(runningness.targets)) { + print("controllerDispatcher targetIDs[" + runningPluginName + "] = " + + JSON.stringify(runningness.targets)); + } + } + + _this.targetIDs[runningPluginName] = runningness.targets; if (!runningness.active) { // plugin is finished running, for now. remove it from the list // of running plugins and mark its activity-slots as "not in use" delete _this.runningPluginNames[runningPluginName]; + delete _this.targetIDs[runningPluginName]; + if (DEBUG) { + print("controllerDispatcher deleted targetIDs[" + runningPluginName + "]"); + } _this.markSlots(plugin, false); _this.pointerManager.makePointerInvisible(plugin.parameters.handLaser); if (DEBUG) { @@ -527,8 +558,9 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); } if (action === "tablet") { - var tabletIDs = message.blacklist - ? [HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID, HMD.homeButtonHighlightID] : []; + var tabletIDs = message.blacklist ? + [HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID, HMD.homeButtonHighlightID] : + []; if (message.hand === LEFT_HAND) { _this.leftBlacklistTabletIDs = tabletIDs; _this.setLeftBlacklist(); diff --git a/scripts/system/controllers/controllerModules/equipEntity.js b/scripts/system/controllers/controllerModules/equipEntity.js index b1c1bc7765..efb1594172 100644 --- a/scripts/system/controllers/controllerModules/equipEntity.js +++ b/scripts/system/controllers/controllerModules/equipEntity.js @@ -595,7 +595,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa equipHotspotBuddy.update(deltaTime, timestamp, controllerData); // if the potentialHotspot is cloneable, clone it and return it - // if the potentialHotspot os not cloneable and locked return null + // if the potentialHotspot is not cloneable and locked return null if (potentialEquipHotspot && (((this.triggerSmoothedSqueezed() || this.secondarySmoothedSqueezed()) && !this.waitForTriggerRelease) || this.messageGrabEntity)) { @@ -603,7 +603,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa this.targetEntityID = this.grabbedHotspot.entityID; this.startEquipEntity(controllerData); this.equipedWithSecondary = this.secondarySmoothedSqueezed(); - return makeRunningValues(true, [potentialEquipHotspot.entityID], []); + return makeRunningValues(true, [this.targetEntityID], []); } else { return makeRunningValues(false, [], []); } From 4303dd589f756fd1ee04afe58804787b2da08b34 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 16 Mar 2019 08:42:13 -0700 Subject: [PATCH 359/446] make sure equip attachment-points pulled from settings are reasonable before using them --- .../controllerModules/equipEntity.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/scripts/system/controllers/controllerModules/equipEntity.js b/scripts/system/controllers/controllerModules/equipEntity.js index efb1594172..a10da110b3 100644 --- a/scripts/system/controllers/controllerModules/equipEntity.js +++ b/scripts/system/controllers/controllerModules/equipEntity.js @@ -7,10 +7,10 @@ /* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, Camera, print, getControllerJointIndex, - enableDispatcherModule, disableDispatcherModule, entityIsFarGrabbedByOther, Messages, makeDispatcherModuleParameters, + enableDispatcherModule, disableDispatcherModule, Messages, makeDispatcherModuleParameters, makeRunningValues, Settings, entityHasActions, Vec3, Overlays, flatten, Xform, getControllerWorldLocation, ensureDynamic, - entityIsCloneable, cloneEntity, DISPATCHER_PROPERTIES, Uuid, unhighlightTargetEntity, isInEditMode, getGrabbableData, - entityIsEquippable + entityIsCloneable, cloneEntity, DISPATCHER_PROPERTIES, Uuid, isInEditMode, getGrabbableData, + entityIsEquippable, HMD */ Script.include("/~/system/libraries/Xform.js"); @@ -183,8 +183,9 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa var TRIGGER_OFF_VALUE = 0.1; var TRIGGER_ON_VALUE = TRIGGER_OFF_VALUE + 0.05; // Squeezed just enough to activate search or near grab var BUMPER_ON_VALUE = 0.5; + var ATTACHPOINT_MAX_DISTANCE = 3.0; - var EMPTY_PARENT_ID = "{00000000-0000-0000-0000-000000000000}"; + // var EMPTY_PARENT_ID = "{00000000-0000-0000-0000-000000000000}"; var UNEQUIP_KEY = "u"; @@ -200,7 +201,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa indicatorOffset: props.grab.equippableIndicatorOffset }; } else { - return null + return null; } } @@ -231,6 +232,12 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa var jointName = (hand === RIGHT_HAND) ? "RightHand" : "LeftHand"; var joints = avatarSettingsData[hotspot.key]; if (joints) { + // make sure they are reasonable + if (joints[jointName] && joints[jointName][0] && + Vec3.length(joints[jointName][0]) > ATTACHPOINT_MAX_DISTANCE) { + print("equipEntity -- Warning: rejecting settings attachPoint " + Vec3.length(joints[jointName][0])); + return undefined; + } return joints[jointName]; } } From 84385e6061378e05ce63ff1e0a8a3998932ab13b Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 16 Mar 2019 11:01:44 -0700 Subject: [PATCH 360/446] work around fb-21767 with a timer --- .../controllerModules/equipEntity.js | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/scripts/system/controllers/controllerModules/equipEntity.js b/scripts/system/controllers/controllerModules/equipEntity.js index a10da110b3..54b56ff271 100644 --- a/scripts/system/controllers/controllerModules/equipEntity.js +++ b/scripts/system/controllers/controllerModules/equipEntity.js @@ -463,6 +463,8 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa }; this.startEquipEntity = function (controllerData) { + var _this = this; + this.dropGestureReset(); this.clearEquipHaptics(); Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); @@ -505,17 +507,23 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa if (entityIsCloneable(grabbedProperties)) { var cloneID = this.cloneHotspot(grabbedProperties, controllerData); this.targetEntityID = cloneID; - Entities.editEntity(this.targetEntityID, reparentProps); controllerData.nearbyEntityPropertiesByID[this.targetEntityID] = grabbedProperties; isClone = true; - } else if (!grabbedProperties.locked) { - Entities.editEntity(this.targetEntityID, reparentProps); - } else { + } else if (grabbedProperties.locked) { this.grabbedHotspot = null; this.targetEntityID = null; return; } + + // HACK -- when + // https://highfidelity.fogbugz.com/f/cases/21767/entity-edits-shortly-after-an-add-often-fail + // is resolved, this can just be an editEntity rather than a setTimeout. + this.editDelayTimeout = Script.setTimeout(function () { + _this.editDelayTimeout = null; + Entities.editEntity(_this.targetEntityID, reparentProps); + }, 100); + // we don't want to send startEquip message until the trigger is released. otherwise, // guns etc will fire right as they are equipped. this.shouldSendStart = true; @@ -526,7 +534,6 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" })); - var _this = this; var grabEquipCheck = function() { var args = [_this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; Entities.callEntityMethod(_this.targetEntityID, "startEquip", args); @@ -539,6 +546,12 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa }; this.endEquipEntity = function () { + + if (this.editDelayTimeout) { + Script.clearTimeout(this.editDelayTimeout); + this.editDelayTimeout = null; + } + this.storeAttachPointInSettings(); Entities.editEntity(this.targetEntityID, { parentID: Uuid.NULL, From 7b56bef83815b230cda13711165f60e15c7bfd58 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Sat, 23 Mar 2019 16:00:02 -0700 Subject: [PATCH 361/446] Prototype an updated Audio tab with 3 independent volume controls --- interface/resources/qml/hifi/audio/Audio.qml | 158 ++++++++++++++++-- .../qml/hifi/audio/LoopbackAudio.qml | 16 +- .../qml/hifi/audio/PlaySampleSound.qml | 16 +- 3 files changed, 158 insertions(+), 32 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index cd0f290da4..8fdd0368e2 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -87,8 +87,19 @@ Rectangle { } function updateMyAvatarGainFromQML(sliderValue, isReleased) { - if (Users.getAvatarGain(myAvatarUuid) != sliderValue) { - Users.setAvatarGain(myAvatarUuid, sliderValue); + if (AudioScriptingInterface.getAvatarGain() != sliderValue) { + AudioScriptingInterface.setAvatarGain(sliderValue); + } + } + function updateInjectorGainFromQML(sliderValue, isReleased) { + if (AudioScriptingInterface.getInjectorGain() != sliderValue) { + AudioScriptingInterface.setInjectorGain(sliderValue); // server side + AudioScriptingInterface.setLocalInjectorGain(sliderValue); // client side + } + } + function updateSystemInjectorGainFromQML(sliderValue, isReleased) { + if (AudioScriptingInterface.getSystemInjectorGain() != sliderValue) { + AudioScriptingInterface.setSystemInjectorGain(sliderValue); } } @@ -334,6 +345,7 @@ Rectangle { color: hifi.colors.white; text: qsTr("Choose input device"); } + } ListView { @@ -462,22 +474,22 @@ Rectangle { } Item { - id: gainContainer + id: avatarGainContainer x: margins.paddings; anchors.top: outputView.bottom; anchors.topMargin: 10; width: parent.width - margins.paddings*2 - height: gainSliderTextMetrics.height + height: avatarGainSliderTextMetrics.height HifiControlsUit.Slider { - id: gainSlider + id: avatarGainSlider anchors.right: parent.right height: parent.height width: 200 minimumValue: -60.0 maximumValue: 20.0 stepSize: 5 - value: Users.getAvatarGain(myAvatarUuid) + value: AudioScriptingInterface.getAvatarGain() onValueChanged: { updateMyAvatarGainFromQML(value, false); } @@ -493,7 +505,7 @@ Rectangle { // Do nothing. } onDoubleClicked: { - gainSlider.value = 0.0 + avatarGainSlider.value = 0.0 } onPressed: { // Pass through to Slider @@ -507,13 +519,13 @@ Rectangle { } } TextMetrics { - id: gainSliderTextMetrics - text: gainSliderText.text - font: gainSliderText.font + id: avatarGainSliderTextMetrics + text: avatarGainSliderText.text + font: avatarGainSliderText.font } RalewayRegular { // The slider for my card is special, it controls the master gain - id: gainSliderText; + id: avatarGainSliderText; text: "Avatar volume"; size: 16; anchors.left: parent.left; @@ -523,15 +535,129 @@ Rectangle { } } + Item { + id: injectorGainContainer + x: margins.paddings; + width: parent.width - margins.paddings*2 + height: injectorGainSliderTextMetrics.height + + HifiControlsUit.Slider { + id: injectorGainSlider + anchors.right: parent.right + height: parent.height + width: 200 + minimumValue: -60.0 + maximumValue: 20.0 + stepSize: 5 + value: AudioScriptingInterface.getInjectorGain() + onValueChanged: { + updateInjectorGainFromQML(value, false); + } + onPressedChanged: { + if (!pressed) { + updateInjectorGainFromQML(value, false); + } + } + + MouseArea { + anchors.fill: parent + onWheel: { + // Do nothing. + } + onDoubleClicked: { + injectorGainSlider.value = 0.0 + } + onPressed: { + // Pass through to Slider + mouse.accepted = false + } + onReleased: { + // the above mouse.accepted seems to make this + // never get called, nonetheless... + mouse.accepted = false + } + } + } + TextMetrics { + id: injectorGainSliderTextMetrics + text: injectorGainSliderText.text + font: injectorGainSliderText.font + } + RalewayRegular { + id: injectorGainSliderText; + text: "Environment volume"; + size: 16; + anchors.left: parent.left; + color: hifi.colors.white; + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignTop; + } + } + + Item { + id: systemInjectorGainContainer + x: margins.paddings; + width: parent.width - margins.paddings*2 + height: systemInjectorGainSliderTextMetrics.height + + HifiControlsUit.Slider { + id: systemInjectorGainSlider + anchors.right: parent.right + height: parent.height + width: 200 + minimumValue: -60.0 + maximumValue: 20.0 + stepSize: 5 + value: AudioScriptingInterface.getSystemInjectorGain() + onValueChanged: { + updateSystemInjectorGainFromQML(value, false); + } + onPressedChanged: { + if (!pressed) { + updateSystemInjectorGainFromQML(value, false); + } + } + + MouseArea { + anchors.fill: parent + onWheel: { + // Do nothing. + } + onDoubleClicked: { + systemInjectorGainSlider.value = 0.0 + } + onPressed: { + // Pass through to Slider + mouse.accepted = false + } + onReleased: { + // the above mouse.accepted seems to make this + // never get called, nonetheless... + mouse.accepted = false + } + } + } + TextMetrics { + id: systemInjectorGainSliderTextMetrics + text: systemInjectorGainSliderText.text + font: systemInjectorGainSliderText.font + } + RalewayRegular { + id: systemInjectorGainSliderText; + text: "System Sound volume"; + size: 16; + anchors.left: parent.left; + color: hifi.colors.white; + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignTop; + } + } + AudioControls.PlaySampleSound { id: playSampleSound x: margins.paddings - anchors.top: gainContainer.bottom; + anchors.top: systemInjectorGainContainer.bottom; anchors.topMargin: 10; - - visible: (bar.currentIndex === 1 && isVR) || - (bar.currentIndex === 0 && !isVR); - anchors { left: parent.left; leftMargin: margins.paddings } } } } diff --git a/interface/resources/qml/hifi/audio/LoopbackAudio.qml b/interface/resources/qml/hifi/audio/LoopbackAudio.qml index 8ec0ffc496..74bc0f67dc 100644 --- a/interface/resources/qml/hifi/audio/LoopbackAudio.qml +++ b/interface/resources/qml/hifi/audio/LoopbackAudio.qml @@ -44,7 +44,7 @@ RowLayout { } HifiControlsUit.Button { - text: audioLoopedBack ? qsTr("STOP TESTING YOUR VOICE") : qsTr("TEST YOUR VOICE"); + text: audioLoopedBack ? qsTr("STOP TESTING") : qsTr("TEST YOUR VOICE"); color: audioLoopedBack ? hifi.buttons.red : hifi.buttons.blue; onClicked: { if (audioLoopedBack) { @@ -57,11 +57,11 @@ RowLayout { } } - RalewayRegular { - Layout.leftMargin: 2; - size: 14; - color: "white"; - font.italic: true - text: audioLoopedBack ? qsTr("Speak in your input") : ""; - } +// RalewayRegular { +// Layout.leftMargin: 2; +// size: 14; +// color: "white"; +// font.italic: true +// text: audioLoopedBack ? qsTr("Speak in your input") : ""; +// } } diff --git a/interface/resources/qml/hifi/audio/PlaySampleSound.qml b/interface/resources/qml/hifi/audio/PlaySampleSound.qml index b9d9727dab..0eb78f3efe 100644 --- a/interface/resources/qml/hifi/audio/PlaySampleSound.qml +++ b/interface/resources/qml/hifi/audio/PlaySampleSound.qml @@ -56,16 +56,16 @@ RowLayout { HifiConstants { id: hifi; } HifiControlsUit.Button { - text: isPlaying ? qsTr("STOP TESTING YOUR SOUND") : qsTr("TEST YOUR SOUND"); + text: isPlaying ? qsTr("STOP TESTING") : qsTr("TEST YOUR SOUND"); color: isPlaying ? hifi.buttons.red : hifi.buttons.blue; onClicked: isPlaying ? stopSound() : playSound(); } - RalewayRegular { - Layout.leftMargin: 2; - size: 14; - color: "white"; - font.italic: true - text: isPlaying ? qsTr("Listen to your output") : ""; - } +// RalewayRegular { +// Layout.leftMargin: 2; +// size: 14; +// color: "white"; +// font.italic: true +// text: isPlaying ? qsTr("Listen to your output") : ""; +// } } From a2d754cebd786741ec7d714576d4462ee3618177 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Tue, 26 Mar 2019 10:26:29 -0700 Subject: [PATCH 362/446] fixing audio screen with master --- interface/resources/qml/hifi/audio/Audio.qml | 4 ++++ .../qml/hifi/audio/LoopbackAudio.qml | 19 +++++++++++-------- .../qml/hifi/audio/PlaySampleSound.qml | 17 ++++++++++------- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index 8fdd0368e2..fe86d7d930 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -540,6 +540,8 @@ Rectangle { x: margins.paddings; width: parent.width - margins.paddings*2 height: injectorGainSliderTextMetrics.height + anchors.top: avatarGainContainer.bottom; + anchors.topMargin: 10; HifiControlsUit.Slider { id: injectorGainSlider @@ -599,6 +601,8 @@ Rectangle { x: margins.paddings; width: parent.width - margins.paddings*2 height: systemInjectorGainSliderTextMetrics.height + anchors.top: injectorGainContainer.bottom; + anchors.topMargin: 10; HifiControlsUit.Slider { id: systemInjectorGainSlider diff --git a/interface/resources/qml/hifi/audio/LoopbackAudio.qml b/interface/resources/qml/hifi/audio/LoopbackAudio.qml index 74bc0f67dc..8d1099d38c 100644 --- a/interface/resources/qml/hifi/audio/LoopbackAudio.qml +++ b/interface/resources/qml/hifi/audio/LoopbackAudio.qml @@ -44,8 +44,11 @@ RowLayout { } HifiControlsUit.Button { - text: audioLoopedBack ? qsTr("STOP TESTING") : qsTr("TEST YOUR VOICE"); + text: audioLoopedBack ? qsTr("STOP TESTING VOICE") : qsTr("TEST YOUR VOICE"); color: audioLoopedBack ? hifi.buttons.red : hifi.buttons.blue; + fontSize: 15; + width: 200; + height: 32; onClicked: { if (audioLoopedBack) { loopbackTimer.stop(); @@ -57,11 +60,11 @@ RowLayout { } } -// RalewayRegular { -// Layout.leftMargin: 2; -// size: 14; -// color: "white"; -// font.italic: true -// text: audioLoopedBack ? qsTr("Speak in your input") : ""; -// } + RalewayRegular { + Layout.leftMargin: 2; + size: 14; + color: "white"; + font.italic: true + text: audioLoopedBack ? qsTr("Speak in your input") : ""; + } } diff --git a/interface/resources/qml/hifi/audio/PlaySampleSound.qml b/interface/resources/qml/hifi/audio/PlaySampleSound.qml index 0eb78f3efe..4675f6087a 100644 --- a/interface/resources/qml/hifi/audio/PlaySampleSound.qml +++ b/interface/resources/qml/hifi/audio/PlaySampleSound.qml @@ -59,13 +59,16 @@ RowLayout { text: isPlaying ? qsTr("STOP TESTING") : qsTr("TEST YOUR SOUND"); color: isPlaying ? hifi.buttons.red : hifi.buttons.blue; onClicked: isPlaying ? stopSound() : playSound(); + fontSize: 15; + width: 200; + height: 32; } -// RalewayRegular { -// Layout.leftMargin: 2; -// size: 14; -// color: "white"; -// font.italic: true -// text: isPlaying ? qsTr("Listen to your output") : ""; -// } + RalewayRegular { + Layout.leftMargin: 2; + size: 14; + color: "white"; + font.italic: true + text: isPlaying ? qsTr("Listen to your output") : ""; + } } From da6ca38282706ba272b88e47cb6f7155e2839d0c Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Tue, 26 Mar 2019 10:48:59 -0700 Subject: [PATCH 363/446] Revert to using avatar's _globalPosition for zone membership According to Tony this should be the hip position, i.e. a joint pos in T-pose, not neccesarily OK root. Also SpatiallyNestable::WorldPos may depend on parent entity and so not known by mixer. --- assignment-client/src/avatars/AvatarMixerClientData.cpp | 4 ++-- assignment-client/src/avatars/MixerAvatar.h | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index 2175018824..0dbefb0109 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -129,7 +129,7 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared incrementNumOutOfOrderSends(); } _lastReceivedSequenceNumber = sequenceNumber; - glm::vec3 oldPosition = _avatar->getCentroidPosition(); + glm::vec3 oldPosition = _avatar->getClientGlobalPosition(); bool oldHasPriority = _avatar->getHasPriority(); // compute the offset to the data payload @@ -140,7 +140,7 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared // Regardless of what the client says, restore the priority as we know it without triggering any update. _avatar->setHasPriorityWithoutTimestampReset(oldHasPriority); - auto newPosition = _avatar->getCentroidPosition(); + auto newPosition = _avatar->getClientGlobalPosition(); if (newPosition != oldPosition || _avatar->getNeedsHeroCheck()) { EntityTree& entityTree = *slaveSharedData.entityTree; FindPriorityZone findPriorityZone { newPosition } ; diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h index f812917614..01e5e91b44 100644 --- a/assignment-client/src/avatars/MixerAvatar.h +++ b/assignment-client/src/avatars/MixerAvatar.h @@ -22,9 +22,6 @@ public: bool getNeedsHeroCheck() const { return _needsHeroCheck; } void setNeedsHeroCheck(bool needsHeroCheck = true) { _needsHeroCheck = needsHeroCheck; } - // Bounding-box World centre: - glm::vec3 getCentroidPosition() const - { return getWorldPosition() + _globalBoundingBoxOffset; } private: bool _needsHeroCheck { false }; From c0b71150eafa60947b5a1390ac8c18795cb25dd8 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Tue, 26 Mar 2019 10:58:04 -0700 Subject: [PATCH 364/446] removing an extra button --- interface/resources/qml/hifi/audio/Audio.qml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index 69cbad458b..195dd78a0e 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -438,14 +438,6 @@ Rectangle { color: hifi.colors.white; text: qsTr("Choose output device"); } - - AudioControls.PlaySampleSound { - x: margins.paddings - - visible: (bar.currentIndex === 1 && isVR) || - (bar.currentIndex === 0 && !isVR); - anchors { right: parent.right } - } } ListView { From 08c6acdf99cac8ab9d743f2876386316490a1fa1 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Tue, 26 Mar 2019 11:21:13 -0700 Subject: [PATCH 365/446] Improve blendshape precision for small models For avatars authored in meters, the max component length would often be less then 1. In that case, the blendshape offset was not normalized before quantization, resulting in loss of precision. This change will normalize the offset for all cases, except when the max component length is 0. --- libraries/render-utils/src/Model.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 30c4000bc7..b62b5f92f4 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1648,7 +1648,7 @@ using packBlendshapeOffsetTo = void(glm::uvec4& packed, const BlendshapeOffsetUn void packBlendshapeOffsetTo_Pos_F32_3xSN10_Nor_3xSN10_Tan_3xSN10(glm::uvec4& packed, const BlendshapeOffsetUnpacked& unpacked) { float len = glm::compMax(glm::abs(unpacked.positionOffset)); glm::vec3 normalizedPos(unpacked.positionOffset); - if (len > 1.0f) { + if (len > 0.0f) { normalizedPos /= len; } else { len = 1.0f; From 91a165b4c3d92e958f318057ea67b67207f022a1 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 26 Mar 2019 11:49:08 -0700 Subject: [PATCH 366/446] separate out the certified entity map stuff (no-op refactor) --- .../src/entities/EntityServer.cpp | 58 +-------- libraries/entities/src/EntityTree.cpp | 119 +++++++++++++----- libraries/entities/src/EntityTree.h | 3 + 3 files changed, 95 insertions(+), 85 deletions(-) diff --git a/assignment-client/src/entities/EntityServer.cpp b/assignment-client/src/entities/EntityServer.cpp index f2cad1e400..06632dabb0 100644 --- a/assignment-client/src/entities/EntityServer.cpp +++ b/assignment-client/src/entities/EntityServer.cpp @@ -22,7 +22,6 @@ #include #include #include -#include #include #include "../AssignmentDynamicFactory.h" @@ -471,62 +470,7 @@ void EntityServer::startDynamicDomainVerification() { qCDebug(entities) << "Starting Dynamic Domain Verification..."; EntityTreePointer tree = std::static_pointer_cast(_tree); - QHash localMap(tree->getEntityCertificateIDMap()); - - QHashIterator i(localMap); - qCDebug(entities) << localMap.size() << "certificates present."; - while (i.hasNext()) { - i.next(); - const auto& certificateID = i.key(); - const auto& entityID = i.value(); - - // Examine each cert: - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkRequest networkRequest; - networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); - networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL(); - requestURL.setPath("/api/v1/commerce/proof_of_purchase_status/location"); - QJsonObject request; - request["certificate_id"] = certificateID; - networkRequest.setUrl(requestURL); - - QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson()); - - connect(networkReply, &QNetworkReply::finished, this, [this, entityID, networkReply] { - EntityTreePointer tree = std::static_pointer_cast(_tree); - - QJsonObject jsonObject = QJsonDocument::fromJson(networkReply->readAll()).object(); - jsonObject = jsonObject["data"].toObject(); - networkReply->deleteLater(); - - if (networkReply->error() != QNetworkReply::NoError) { - qCDebug(entities) << "Call to" << networkReply->url() << "failed with error" << networkReply->error() << "; NOT deleting entity" << entityID - << "More info:" << jsonObject; - return; - } - QString thisDomainID = DependencyManager::get()->getDomainID().remove(QRegExp("\\{|\\}")); - if (jsonObject["domain_id"].toString() == thisDomainID) { - // Entity belongs here. Nothing to do. - return; - } - // Entity does not belong here: - EntityItemPointer entity = tree->findEntityByEntityItemID(entityID); - if (!entity) { - qCDebug(entities) << "Entity undergoing dynamic domain verification is no longer available:" << entityID; - return; - } - if (entity->getAge() <= (_MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS / MSECS_PER_SECOND)) { - qCDebug(entities) << "Entity failed dynamic domain verification, but was created too recently to necessitate deletion:" << entityID; - return; - } - qCDebug(entities) << "Entity's cert's domain ID" << jsonObject["domain_id"].toString() - << "doesn't match the current Domain ID" << thisDomainID << "; deleting entity" << entityID; - tree->withWriteLock([&] { - tree->deleteEntity(entityID, true); - }); - }); - } + tree->startDynamicDomainVerificationOnServer((float) _MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS / MSECS_PER_SECOND); int nextInterval = qrand() % ((_MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS + 1) - _MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS) + _MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS; qCDebug(entities) << "Restarting Dynamic Domain Verification timer for" << nextInterval / 1000 << "seconds"; diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 11e392f590..55a8c42261 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include "EntitySimulation.h" #include "VariantMapToScriptValue.h" @@ -286,27 +287,7 @@ void EntityTree::postAddEntity(EntityItemPointer entity) { assert(entity); if (getIsServer()) { - QString certID(entity->getCertificateID()); - EntityItemID entityItemID = entity->getEntityItemID(); - EntityItemID existingEntityItemID; - - { - QWriteLocker locker(&_entityCertificateIDMapLock); - existingEntityItemID = _entityCertificateIDMap.value(certID); - if (!certID.isEmpty()) { - _entityCertificateIDMap.insert(certID, entityItemID); - qCDebug(entities) << "Certificate ID" << certID << "belongs to" << entityItemID; - } - } - - // Delete an already-existing entity from the tree if it has the same - // CertificateID as the entity we're trying to add. - if (!existingEntityItemID.isNull() && !entity->getCertificateType().contains(DOMAIN_UNLIMITED)) { - qCDebug(entities) << "Certificate ID" << certID << "already exists on entity with ID" - << existingEntityItemID << ". Deleting existing entity."; - deleteEntity(existingEntityItemID, true); - return; - } + addCertifiedEntityOnServer(entity); } // check to see if we need to simulate this entity.. @@ -764,13 +745,7 @@ void EntityTree::processRemovedEntities(const DeleteEntityOperator& theOperator) theEntity->die(); if (getIsServer()) { - { - QWriteLocker entityCertificateIDMapLocker(&_entityCertificateIDMapLock); - QString certID = theEntity->getCertificateID(); - if (theEntity->getEntityItemID() == _entityCertificateIDMap.value(certID)) { - _entityCertificateIDMap.remove(certID); - } - } + removeCertifiedEntityOnServer(theEntity); // set up the deleted entities ID QWriteLocker recentlyDeletedEntitiesLocker(&_recentlyDeletedEntitiesLock); @@ -1421,6 +1396,94 @@ bool EntityTree::isScriptInWhitelist(const QString& scriptProperty) { return false; } +void EntityTree::addCertifiedEntityOnServer(EntityItemPointer entity) { + QString certID(entity->getCertificateID()); + EntityItemID entityItemID = entity->getEntityItemID(); + EntityItemID existingEntityItemID; + + { + QWriteLocker locker(&_entityCertificateIDMapLock); + existingEntityItemID = _entityCertificateIDMap.value(certID); + if (!certID.isEmpty()) { + _entityCertificateIDMap.insert(certID, entityItemID); + qCDebug(entities) << "Certificate ID" << certID << "belongs to" << entityItemID; + } + } + + // Delete an already-existing entity from the tree if it has the same + // CertificateID as the entity we're trying to add. + if (!existingEntityItemID.isNull() && !entity->getCertificateType().contains(DOMAIN_UNLIMITED)) { + qCDebug(entities) << "Certificate ID" << certID << "already exists on entity with ID" + << existingEntityItemID << ". Deleting existing entity."; + deleteEntity(existingEntityItemID, true); + } +} + +void EntityTree::removeCertifiedEntityOnServer(EntityItemPointer entity) { + QWriteLocker entityCertificateIDMapLocker(&_entityCertificateIDMapLock); + QString certID = entity->getCertificateID(); + if (entity->getEntityItemID() == _entityCertificateIDMap.value(certID)) { + _entityCertificateIDMap.remove(certID); + } +} + +void EntityTree::startDynamicDomainVerificationOnServer(float minimumAgeToRemove) { + QHash localMap(getEntityCertificateIDMap()); + QHashIterator i(localMap); + qCDebug(entities) << localMap.size() << "certificates present."; + while (i.hasNext()) { + i.next(); + const auto& certificateID = i.key(); + const auto& entityID = i.value(); + + // Examine each cert: + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkRequest networkRequest; + networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL(); + requestURL.setPath("/api/v1/commerce/proof_of_purchase_status/location"); + QJsonObject request; + request["certificate_id"] = certificateID; + networkRequest.setUrl(requestURL); + + QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson()); + + connect(networkReply, &QNetworkReply::finished, this, [this, entityID, networkReply, minimumAgeToRemove] { + + QJsonObject jsonObject = QJsonDocument::fromJson(networkReply->readAll()).object(); + jsonObject = jsonObject["data"].toObject(); + networkReply->deleteLater(); + + if (networkReply->error() != QNetworkReply::NoError) { + qCDebug(entities) << "Call to" << networkReply->url() << "failed with error" << networkReply->error() << "; NOT deleting entity" << entityID + << "More info:" << jsonObject; + return; + } + QString thisDomainID = DependencyManager::get()->getDomainID().remove(QRegExp("\\{|\\}")); + if (jsonObject["domain_id"].toString() == thisDomainID) { + // Entity belongs here. Nothing to do. + return; + } + // Entity does not belong here: + EntityItemPointer entity = findEntityByEntityItemID(entityID); + if (!entity) { + qCDebug(entities) << "Entity undergoing dynamic domain verification is no longer available:" << entityID; + return; + } + if (entity->getAge() <= minimumAgeToRemove) { + qCDebug(entities) << "Entity failed dynamic domain verification, but was created too recently to necessitate deletion:" << entityID; + return; + } + qCDebug(entities) << "Entity's cert's domain ID" << jsonObject["domain_id"].toString() + << "doesn't match the current Domain ID" << thisDomainID << "; deleting entity" << entityID; + withWriteLock([&] { + deleteEntity(entityID, true); + }); + }); + } +} + void EntityTree::startChallengeOwnershipTimer(const EntityItemID& entityItemID) { QTimer* _challengeOwnershipTimeoutTimer = new QTimer(this); connect(this, &EntityTree::killChallengeOwnershipTimeoutTimer, this, [=](const EntityItemID& id) { diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 63e1197970..10809747b3 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -279,6 +279,7 @@ public: void updateEntityQueryAACube(SpatiallyNestablePointer object, EntityEditPacketSender* packetSender, bool force, bool tellServer); + void startDynamicDomainVerificationOnServer(float minimumAgeToRemove); signals: void deletingEntity(const EntityItemID& entityID); @@ -377,6 +378,8 @@ protected: Q_INVOKABLE void startChallengeOwnershipTimer(const EntityItemID& entityItemID); private: + void addCertifiedEntityOnServer(EntityItemPointer entity); + void removeCertifiedEntityOnServer(EntityItemPointer entity); void sendChallengeOwnershipPacket(const QString& certID, const QString& ownerKey, const EntityItemID& entityItemID, const SharedNodePointer& senderNode); void sendChallengeOwnershipRequestPacket(const QByteArray& certID, const QByteArray& text, const QByteArray& nodeToChallenge, const SharedNodePointer& senderNode); void validatePop(const QString& certID, const EntityItemID& entityItemID, const SharedNodePointer& senderNode); From be74218d931ec3adef8c885e935d3b1aebc55c72 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 26 Mar 2019 12:05:10 -0700 Subject: [PATCH 367/446] eliminate copy of hash table --- libraries/entities/src/EntityTree.cpp | 6 +++--- libraries/entities/src/EntityTree.h | 5 ----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 55a8c42261..63259e2c58 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1428,9 +1428,9 @@ void EntityTree::removeCertifiedEntityOnServer(EntityItemPointer entity) { } void EntityTree::startDynamicDomainVerificationOnServer(float minimumAgeToRemove) { - QHash localMap(getEntityCertificateIDMap()); - QHashIterator i(localMap); - qCDebug(entities) << localMap.size() << "certificates present."; + QReadLocker locker(&_entityCertificateIDMapLock); + QHashIterator i(_entityCertificateIDMap); + qCDebug(entities) << _entityCertificateIDMap.size() << "certificates present."; while (i.hasNext()) { i.next(); const auto& certificateID = i.key(); diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 10809747b3..fe6045f6f7 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -157,11 +157,6 @@ public: return _recentlyDeletedEntityItemIDs; } - QHash getEntityCertificateIDMap() const { - QReadLocker locker(&_entityCertificateIDMapLock); - return _entityCertificateIDMap; - } - void forgetEntitiesDeletedBefore(quint64 sinceTime); int processEraseMessage(ReceivedMessage& message, const SharedNodePointer& sourceNode); From 4dfd0fbda3fa7771437c557fe17e4692ac048053 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 26 Mar 2019 15:02:13 -0700 Subject: [PATCH 368/446] challenge ownership packet has an id that gets reflected back. It doesn't have to be a cert. --- interface/src/commerce/Wallet.cpp | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/interface/src/commerce/Wallet.cpp b/interface/src/commerce/Wallet.cpp index 0e9ad7d79a..37f28960e5 100644 --- a/interface/src/commerce/Wallet.cpp +++ b/interface/src/commerce/Wallet.cpp @@ -816,18 +816,18 @@ void Wallet::handleChallengeOwnershipPacket(QSharedPointer pack bool challengeOriginatedFromClient = packet->getType() == PacketType::ChallengeOwnershipRequest; int status; - int certIDByteArraySize; + int idByteArraySize; int textByteArraySize; int challengingNodeUUIDByteArraySize; - packet->readPrimitive(&certIDByteArraySize); + packet->readPrimitive(&idByteArraySize); packet->readPrimitive(&textByteArraySize); // returns a cast char*, size if (challengeOriginatedFromClient) { packet->readPrimitive(&challengingNodeUUIDByteArraySize); } // "encryptedText" is now a series of random bytes, a nonce - QByteArray certID = packet->read(certIDByteArraySize); + QByteArray id = packet->read(idByteArraySize); QByteArray text = packet->read(textByteArraySize); QByteArray challengingNodeUUID; if (challengeOriginatedFromClient) { @@ -853,32 +853,32 @@ void Wallet::handleChallengeOwnershipPacket(QSharedPointer pack textByteArray = sig.toUtf8(); } textByteArraySize = textByteArray.size(); - int certIDSize = certID.size(); + int idSize = id.size(); // setup the packet if (challengeOriginatedFromClient) { auto textPacket = NLPacket::create(PacketType::ChallengeOwnershipReply, - certIDSize + textByteArraySize + challengingNodeUUIDByteArraySize + 3 * sizeof(int), + idSize + textByteArraySize + challengingNodeUUIDByteArraySize + 3 * sizeof(int), true); - textPacket->writePrimitive(certIDSize); + textPacket->writePrimitive(idSize); textPacket->writePrimitive(textByteArraySize); textPacket->writePrimitive(challengingNodeUUIDByteArraySize); - textPacket->write(certID); + textPacket->write(id); textPacket->write(textByteArray); textPacket->write(challengingNodeUUID); - qCDebug(commerce) << "Sending ChallengeOwnershipReply Packet containing signed text" << textByteArray << "for CertID" << certID; + qCDebug(commerce) << "Sending ChallengeOwnershipReply Packet containing signed text" << textByteArray << "for id" << id; nodeList->sendPacket(std::move(textPacket), *sendingNode); } else { - auto textPacket = NLPacket::create(PacketType::ChallengeOwnership, certIDSize + textByteArraySize + 2 * sizeof(int), true); + auto textPacket = NLPacket::create(PacketType::ChallengeOwnership, idSize + textByteArraySize + 2 * sizeof(int), true); - textPacket->writePrimitive(certIDSize); + textPacket->writePrimitive(idSize); textPacket->writePrimitive(textByteArraySize); - textPacket->write(certID); + textPacket->write(id); textPacket->write(textByteArray); - qCDebug(commerce) << "Sending ChallengeOwnership Packet containing signed text" << textByteArray << "for CertID" << certID; + qCDebug(commerce) << "Sending ChallengeOwnership Packet containing signed text" << textByteArray << "for id" << id; nodeList->sendPacket(std::move(textPacket), *sendingNode); } From c9b79b24e3fc033949e21cd24eebd8d720090144 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 26 Mar 2019 15:23:13 -0700 Subject: [PATCH 369/446] track nonces by entity id instead of by cert --- .../ui/overlays/ContextOverlayInterface.cpp | 4 ++-- libraries/entities/src/EntityTree.cpp | 20 +++++++++---------- libraries/entities/src/EntityTree.h | 8 ++++---- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/interface/src/ui/overlays/ContextOverlayInterface.cpp b/interface/src/ui/overlays/ContextOverlayInterface.cpp index 794feddd8a..5a4ff52a84 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.cpp +++ b/interface/src/ui/overlays/ContextOverlayInterface.cpp @@ -329,7 +329,7 @@ void ContextOverlayInterface::requestOwnershipVerification(const QUuid& entityID QString ownerKey = jsonObject["transfer_recipient_key"].toString(); QByteArray certID = entityProperties.getCertificateID().toUtf8(); - QByteArray text = DependencyManager::get()->getTree()->computeNonce(certID, ownerKey); + QByteArray text = DependencyManager::get()->getTree()->computeNonce(entityID, ownerKey); QByteArray nodeToChallengeByteArray = entityProperties.getOwningAvatarID().toRfc4122(); int certIDByteArraySize = certID.length(); @@ -422,7 +422,7 @@ void ContextOverlayInterface::handleChallengeOwnershipReplyPacket(QSharedPointer QString certID(packet->read(certIDByteArraySize)); QString text(packet->read(textByteArraySize)); - bool verificationSuccess = DependencyManager::get()->getTree()->verifyNonce(certID, text); + bool verificationSuccess = DependencyManager::get()->getTree()->verifyNonce(_lastInspectedEntity, text); if (verificationSuccess) { emit ledger->updateCertificateStatus(certID, (uint)(ledger->CERTIFICATE_STATUS_VERIFICATION_SUCCESS)); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 63259e2c58..41e0cbafc4 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1506,21 +1506,21 @@ void EntityTree::startChallengeOwnershipTimer(const EntityItemID& entityItemID) _challengeOwnershipTimeoutTimer->start(5000); } -QByteArray EntityTree::computeNonce(const QString& certID, const QString ownerKey) { +QByteArray EntityTree::computeNonce(const EntityItemID& entityID, const QString ownerKey) { QUuid nonce = QUuid::createUuid(); //random, 5-hex value, separated by "-" QByteArray nonceBytes = nonce.toByteArray(); - QWriteLocker locker(&_certNonceMapLock); - _certNonceMap.insert(certID, QPair(nonce, ownerKey)); + QWriteLocker locker(&_entityNonceMapLock); + _entityNonceMap.insert(entityID, QPair(nonce, ownerKey)); return nonceBytes; } -bool EntityTree::verifyNonce(const QString& certID, const QString& nonce) { +bool EntityTree::verifyNonce(const EntityItemID& entityID, const QString& nonce) { QString actualNonce, key; { - QWriteLocker locker(&_certNonceMapLock); - QPair sent = _certNonceMap.take(certID); + QWriteLocker locker(&_entityNonceMapLock); + QPair sent = _entityNonceMap.take(entityID); actualNonce = sent.first.toString(); key = sent.second; } @@ -1530,9 +1530,9 @@ bool EntityTree::verifyNonce(const QString& certID, const QString& nonce) { bool verificationSuccess = EntityItemProperties::verifySignature(annotatedKey.toUtf8(), hashedActualNonce, QByteArray::fromBase64(nonce.toUtf8())); if (verificationSuccess) { - qCDebug(entities) << "Ownership challenge for Cert ID" << certID << "succeeded."; + qCDebug(entities) << "Ownership challenge for Entity ID" << entityID << "succeeded."; } else { - qCDebug(entities) << "Ownership challenge for Cert ID" << certID << "failed. Actual nonce:" << actualNonce << + qCDebug(entities) << "Ownership challenge for Entity ID" << entityID << "failed. Actual nonce:" << actualNonce << "\nHashed actual nonce (digest):" << hashedActualNonce << "\nSent nonce (signature)" << nonce << "\nKey" << key; } @@ -1585,7 +1585,7 @@ void EntityTree::sendChallengeOwnershipPacket(const QString& certID, const QStri // 1. Obtain a nonce auto nodeList = DependencyManager::get(); - QByteArray text = computeNonce(certID, ownerKey); + QByteArray text = computeNonce(entityItemID, ownerKey); if (text == "") { qCDebug(entities) << "CRITICAL ERROR: Couldn't compute nonce. Deleting entity..."; @@ -1708,7 +1708,7 @@ void EntityTree::processChallengeOwnershipPacket(ReceivedMessage& message, const } emit killChallengeOwnershipTimeoutTimer(id); - if (!verifyNonce(certID, text)) { + if (!verifyNonce(id, text)) { if (!id.isNull()) { deleteEntity(id, true); } diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index fe6045f6f7..327f06164e 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -247,8 +247,8 @@ public: static const float DEFAULT_MAX_TMP_ENTITY_LIFETIME; - QByteArray computeNonce(const QString& certID, const QString ownerKey); - bool verifyNonce(const QString& certID, const QString& nonce); + QByteArray computeNonce(const EntityItemID& entityID, const QString ownerKey); + bool verifyNonce(const EntityItemID& entityID, const QString& nonce); QUuid getMyAvatarSessionUUID() { return _myAvatar ? _myAvatar->getSessionUUID() : QUuid(); } void setMyAvatar(std::shared_ptr myAvatar) { _myAvatar = myAvatar; } @@ -325,8 +325,8 @@ protected: mutable QReadWriteLock _entityCertificateIDMapLock; QHash _entityCertificateIDMap; - mutable QReadWriteLock _certNonceMapLock; - QHash> _certNonceMap; + mutable QReadWriteLock _entityNonceMapLock; + QHash> _entityNonceMap; EntitySimulationPointer _simulation; From 7d212222321e8ee812801070d01aaab74edd8c86 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 26 Mar 2019 16:03:56 -0700 Subject: [PATCH 370/446] the test-audio button causes a local audio-loopback rather than a server one --- interface/resources/qml/hifi/audio/LoopbackAudio.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/audio/LoopbackAudio.qml b/interface/resources/qml/hifi/audio/LoopbackAudio.qml index 8ec0ffc496..255617824b 100644 --- a/interface/resources/qml/hifi/audio/LoopbackAudio.qml +++ b/interface/resources/qml/hifi/audio/LoopbackAudio.qml @@ -21,13 +21,13 @@ RowLayout { function startAudioLoopback() { if (!audioLoopedBack) { audioLoopedBack = true; - AudioScriptingInterface.setServerEcho(true); + AudioScriptingInterface.setLocalEcho(true); } } function stopAudioLoopback() { if (audioLoopedBack) { audioLoopedBack = false; - AudioScriptingInterface.setServerEcho(false); + AudioScriptingInterface.setLocalEcho(false); } } From 16f842b2ace237bbdfd635a8536ab8426076ff18 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 26 Mar 2019 16:07:18 -0700 Subject: [PATCH 371/446] missed one --- interface/resources/qml/hifi/audio/LoopbackAudio.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/audio/LoopbackAudio.qml b/interface/resources/qml/hifi/audio/LoopbackAudio.qml index 255617824b..578d222eb2 100644 --- a/interface/resources/qml/hifi/audio/LoopbackAudio.qml +++ b/interface/resources/qml/hifi/audio/LoopbackAudio.qml @@ -17,7 +17,7 @@ import stylesUit 1.0 import controlsUit 1.0 as HifiControlsUit RowLayout { - property bool audioLoopedBack: AudioScriptingInterface.getServerEcho(); + property bool audioLoopedBack: AudioScriptingInterface.getLocalEcho(); function startAudioLoopback() { if (!audioLoopedBack) { audioLoopedBack = true; From 5f8139a44c4f1bdf398de68915460c47cbcca629 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 26 Mar 2019 16:56:10 -0700 Subject: [PATCH 372/446] audio-loopback test ignores mute --- libraries/audio-client/src/AudioClient.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 9d645a1dbf..0bb7256bf6 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1052,7 +1052,7 @@ void AudioClient::setReverbOptions(const AudioEffectOptions* options) { void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { // If there is server echo, reverb will be applied to the recieved audio stream so no need to have it here. bool hasReverb = _reverb || _receivedAudioStream.hasReverb(); - if (_muted || !_audioOutput || (!_shouldEchoLocally && !hasReverb)) { + if ((_muted && !_shouldEchoLocally) || !_audioOutput || (!_shouldEchoLocally && !hasReverb)) { return; } From 57da21cec2beac3d62b25deca50bb3df849f20a7 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 26 Mar 2019 17:12:28 -0700 Subject: [PATCH 373/446] more about challenges use entity id as id, not cert id --- interface/src/commerce/Ledger.h | 3 +- interface/src/commerce/QmlCommerce.h | 3 +- .../ui/overlays/ContextOverlayInterface.cpp | 26 ++++----- libraries/entities/src/EntityTree.cpp | 55 ++++++++----------- libraries/entities/src/EntityTree.h | 2 +- 5 files changed, 42 insertions(+), 47 deletions(-) diff --git a/interface/src/commerce/Ledger.h b/interface/src/commerce/Ledger.h index 2e18f34c8d..64528e617d 100644 --- a/interface/src/commerce/Ledger.h +++ b/interface/src/commerce/Ledger.h @@ -17,6 +17,7 @@ #include #include #include +#include #include "AccountManager.h" @@ -65,7 +66,7 @@ signals: void availableUpdatesResult(QJsonObject result); void updateItemResult(QJsonObject result); - void updateCertificateStatus(const QString& certID, uint certStatus); + void updateCertificateStatus(const EntityItemID& entityID, uint certStatus); public slots: void buySuccess(QNetworkReply* reply); diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index 3217b8a1f9..99b3e32e8b 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -19,6 +19,7 @@ #include +#include #include class QmlCommerce : public QObject, public Dependency { @@ -49,7 +50,7 @@ signals: void availableUpdatesResult(QJsonObject result); void updateItemResult(QJsonObject result); - void updateCertificateStatus(const QString& certID, uint certStatus); + void updateCertificateStatus(const EntityItemID& entityID, uint certStatus); void transferAssetToNodeResult(QJsonObject result); void transferAssetToUsernameResult(QJsonObject result); diff --git a/interface/src/ui/overlays/ContextOverlayInterface.cpp b/interface/src/ui/overlays/ContextOverlayInterface.cpp index 5a4ff52a84..0e4fa796d8 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.cpp +++ b/interface/src/ui/overlays/ContextOverlayInterface.cpp @@ -328,21 +328,21 @@ void ContextOverlayInterface::requestOwnershipVerification(const QUuid& entityID } else { QString ownerKey = jsonObject["transfer_recipient_key"].toString(); - QByteArray certID = entityProperties.getCertificateID().toUtf8(); + QByteArray id = entityID.toByteArray(); QByteArray text = DependencyManager::get()->getTree()->computeNonce(entityID, ownerKey); QByteArray nodeToChallengeByteArray = entityProperties.getOwningAvatarID().toRfc4122(); - int certIDByteArraySize = certID.length(); + int idByteArraySize = id.length(); int textByteArraySize = text.length(); int nodeToChallengeByteArraySize = nodeToChallengeByteArray.length(); auto challengeOwnershipPacket = NLPacket::create(PacketType::ChallengeOwnershipRequest, - certIDByteArraySize + textByteArraySize + nodeToChallengeByteArraySize + 3 * sizeof(int), + idByteArraySize + textByteArraySize + nodeToChallengeByteArraySize + 3 * sizeof(int), true); - challengeOwnershipPacket->writePrimitive(certIDByteArraySize); + challengeOwnershipPacket->writePrimitive(idByteArraySize); challengeOwnershipPacket->writePrimitive(textByteArraySize); challengeOwnershipPacket->writePrimitive(nodeToChallengeByteArraySize); - challengeOwnershipPacket->write(certID); + challengeOwnershipPacket->write(id); challengeOwnershipPacket->write(text); challengeOwnershipPacket->write(nodeToChallengeByteArray); nodeList->sendPacket(std::move(challengeOwnershipPacket), *entityServer); @@ -370,12 +370,12 @@ void ContextOverlayInterface::requestOwnershipVerification(const QUuid& entityID // so they always pass Ownership Verification. It's necessary to emit this signal // so that the Inspection Certificate can continue its information-grabbing process. auto ledger = DependencyManager::get(); - emit ledger->updateCertificateStatus(entityProperties.getCertificateID(), (uint)(ledger->CERTIFICATE_STATUS_VERIFICATION_SUCCESS)); + emit ledger->updateCertificateStatus(entityID, (uint)(ledger->CERTIFICATE_STATUS_VERIFICATION_SUCCESS)); } } else { auto ledger = DependencyManager::get(); _challengeOwnershipTimeoutTimer.stop(); - emit ledger->updateCertificateStatus(entityProperties.getCertificateID(), (uint)(ledger->CERTIFICATE_STATUS_STATIC_VERIFICATION_FAILED)); + emit ledger->updateCertificateStatus(entityID, (uint)(ledger->CERTIFICATE_STATUS_STATIC_VERIFICATION_FAILED)); emit DependencyManager::get()->ownershipVerificationFailed(_lastInspectedEntity); qCDebug(context_overlay) << "Entity" << _lastInspectedEntity << "failed static certificate verification!"; } @@ -401,7 +401,7 @@ void ContextOverlayInterface::startChallengeOwnershipTimer() { connect(&_challengeOwnershipTimeoutTimer, &QTimer::timeout, this, [=]() { qCDebug(entities) << "Ownership challenge timed out for" << _lastInspectedEntity; - emit ledger->updateCertificateStatus(entityProperties.getCertificateID(), (uint)(ledger->CERTIFICATE_STATUS_VERIFICATION_TIMEOUT)); + emit ledger->updateCertificateStatus(_lastInspectedEntity, (uint)(ledger->CERTIFICATE_STATUS_VERIFICATION_TIMEOUT)); emit DependencyManager::get()->ownershipVerificationFailed(_lastInspectedEntity); }); @@ -413,22 +413,22 @@ void ContextOverlayInterface::handleChallengeOwnershipReplyPacket(QSharedPointer _challengeOwnershipTimeoutTimer.stop(); - int certIDByteArraySize; + int idByteArraySize; int textByteArraySize; - packet->readPrimitive(&certIDByteArraySize); + packet->readPrimitive(&idByteArraySize); packet->readPrimitive(&textByteArraySize); - QString certID(packet->read(certIDByteArraySize)); + EntityItemID id(packet->read(idByteArraySize)); QString text(packet->read(textByteArraySize)); bool verificationSuccess = DependencyManager::get()->getTree()->verifyNonce(_lastInspectedEntity, text); if (verificationSuccess) { - emit ledger->updateCertificateStatus(certID, (uint)(ledger->CERTIFICATE_STATUS_VERIFICATION_SUCCESS)); + emit ledger->updateCertificateStatus(id, (uint)(ledger->CERTIFICATE_STATUS_VERIFICATION_SUCCESS)); emit DependencyManager::get()->ownershipVerificationSuccess(_lastInspectedEntity); } else { - emit ledger->updateCertificateStatus(certID, (uint)(ledger->CERTIFICATE_STATUS_OWNER_VERIFICATION_FAILED)); + emit ledger->updateCertificateStatus(id, (uint)(ledger->CERTIFICATE_STATUS_OWNER_VERIFICATION_FAILED)); emit DependencyManager::get()->ownershipVerificationFailed(_lastInspectedEntity); } } diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 41e0cbafc4..4fc234122b 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1540,42 +1540,42 @@ bool EntityTree::verifyNonce(const EntityItemID& entityID, const QString& nonce) } void EntityTree::processChallengeOwnershipRequestPacket(ReceivedMessage& message, const SharedNodePointer& sourceNode) { - int certIDByteArraySize; + int idByteArraySize; int textByteArraySize; int nodeToChallengeByteArraySize; - message.readPrimitive(&certIDByteArraySize); + message.readPrimitive(&idByteArraySize); message.readPrimitive(&textByteArraySize); message.readPrimitive(&nodeToChallengeByteArraySize); - QByteArray certID(message.read(certIDByteArraySize)); + QByteArray id(message.read(idByteArraySize)); QByteArray text(message.read(textByteArraySize)); QByteArray nodeToChallenge(message.read(nodeToChallengeByteArraySize)); - sendChallengeOwnershipRequestPacket(certID, text, nodeToChallenge, sourceNode); + sendChallengeOwnershipRequestPacket(id, text, nodeToChallenge, sourceNode); } void EntityTree::processChallengeOwnershipReplyPacket(ReceivedMessage& message, const SharedNodePointer& sourceNode) { auto nodeList = DependencyManager::get(); - int certIDByteArraySize; + int idByteArraySize; int textByteArraySize; int challengingNodeUUIDByteArraySize; - message.readPrimitive(&certIDByteArraySize); + message.readPrimitive(&idByteArraySize); message.readPrimitive(&textByteArraySize); message.readPrimitive(&challengingNodeUUIDByteArraySize); - QByteArray certID(message.read(certIDByteArraySize)); + QByteArray id(message.read(idByteArraySize)); QByteArray text(message.read(textByteArraySize)); QUuid challengingNode = QUuid::fromRfc4122(message.read(challengingNodeUUIDByteArraySize)); auto challengeOwnershipReplyPacket = NLPacket::create(PacketType::ChallengeOwnershipReply, - certIDByteArraySize + text.length() + 2 * sizeof(int), + idByteArraySize + text.length() + 2 * sizeof(int), true); - challengeOwnershipReplyPacket->writePrimitive(certIDByteArraySize); + challengeOwnershipReplyPacket->writePrimitive(idByteArraySize); challengeOwnershipReplyPacket->writePrimitive(text.length()); - challengeOwnershipReplyPacket->write(certID); + challengeOwnershipReplyPacket->write(id); challengeOwnershipReplyPacket->write(text); nodeList->sendPacket(std::move(challengeOwnershipReplyPacket), *(nodeList->nodeWithUUID(challengingNode))); @@ -1595,14 +1595,14 @@ void EntityTree::sendChallengeOwnershipPacket(const QString& certID, const QStri } else { qCDebug(entities) << "Challenging ownership of Cert ID" << certID; // 2. Send the nonce to the rezzing avatar's node - QByteArray certIDByteArray = certID.toUtf8(); - int certIDByteArraySize = certIDByteArray.size(); + QByteArray idByteArray = entityItemID.toByteArray(); + int idByteArraySize = idByteArray.size(); auto challengeOwnershipPacket = NLPacket::create(PacketType::ChallengeOwnership, - certIDByteArraySize + text.length() + 2 * sizeof(int), + idByteArraySize + text.length() + 2 * sizeof(int), true); - challengeOwnershipPacket->writePrimitive(certIDByteArraySize); + challengeOwnershipPacket->writePrimitive(idByteArraySize); challengeOwnershipPacket->writePrimitive(text.length()); - challengeOwnershipPacket->write(certIDByteArray); + challengeOwnershipPacket->write(idByteArray); challengeOwnershipPacket->write(text); nodeList->sendPacket(std::move(challengeOwnershipPacket), *senderNode); @@ -1616,24 +1616,24 @@ void EntityTree::sendChallengeOwnershipPacket(const QString& certID, const QStri } } -void EntityTree::sendChallengeOwnershipRequestPacket(const QByteArray& certID, const QByteArray& text, const QByteArray& nodeToChallenge, const SharedNodePointer& senderNode) { +void EntityTree::sendChallengeOwnershipRequestPacket(const QByteArray& id, const QByteArray& text, const QByteArray& nodeToChallenge, const SharedNodePointer& senderNode) { auto nodeList = DependencyManager::get(); // In this case, Client A is challenging Client B. Client A is inspecting a certified entity that it wants // to make sure belongs to Avatar B. QByteArray senderNodeUUID = senderNode->getUUID().toRfc4122(); - int certIDByteArraySize = certID.length(); + int idByteArraySize = id.length(); int TextByteArraySize = text.length(); int senderNodeUUIDSize = senderNodeUUID.length(); auto challengeOwnershipPacket = NLPacket::create(PacketType::ChallengeOwnershipRequest, - certIDByteArraySize + TextByteArraySize + senderNodeUUIDSize + 3 * sizeof(int), + idByteArraySize + TextByteArraySize + senderNodeUUIDSize + 3 * sizeof(int), true); - challengeOwnershipPacket->writePrimitive(certIDByteArraySize); + challengeOwnershipPacket->writePrimitive(idByteArraySize); challengeOwnershipPacket->writePrimitive(TextByteArraySize); challengeOwnershipPacket->writePrimitive(senderNodeUUIDSize); - challengeOwnershipPacket->write(certID); + challengeOwnershipPacket->write(id); challengeOwnershipPacket->write(text); challengeOwnershipPacket->write(senderNodeUUID); @@ -1692,26 +1692,19 @@ void EntityTree::validatePop(const QString& certID, const EntityItemID& entityIt } void EntityTree::processChallengeOwnershipPacket(ReceivedMessage& message, const SharedNodePointer& sourceNode) { - int certIDByteArraySize; + int idByteArraySize; int textByteArraySize; - message.readPrimitive(&certIDByteArraySize); + message.readPrimitive(&idByteArraySize); message.readPrimitive(&textByteArraySize); - QString certID(message.read(certIDByteArraySize)); + EntityItemID id(message.read(idByteArraySize)); QString text(message.read(textByteArraySize)); - EntityItemID id; - { - QReadLocker certIdMapLocker(&_entityCertificateIDMapLock); - id = _entityCertificateIDMap.value(certID); - } emit killChallengeOwnershipTimeoutTimer(id); if (!verifyNonce(id, text)) { - if (!id.isNull()) { - deleteEntity(id, true); - } + deleteEntity(id, true); } } diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 327f06164e..2b3ee93ab7 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -376,7 +376,7 @@ private: void addCertifiedEntityOnServer(EntityItemPointer entity); void removeCertifiedEntityOnServer(EntityItemPointer entity); void sendChallengeOwnershipPacket(const QString& certID, const QString& ownerKey, const EntityItemID& entityItemID, const SharedNodePointer& senderNode); - void sendChallengeOwnershipRequestPacket(const QByteArray& certID, const QByteArray& text, const QByteArray& nodeToChallenge, const SharedNodePointer& senderNode); + void sendChallengeOwnershipRequestPacket(const QByteArray& id, const QByteArray& text, const QByteArray& nodeToChallenge, const SharedNodePointer& senderNode); void validatePop(const QString& certID, const EntityItemID& entityItemID, const SharedNodePointer& senderNode); std::shared_ptr _myAvatar{ nullptr }; From b8f79d33649b0fa452dd915df48ba56f512fc166 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Tue, 26 Mar 2019 17:41:22 -0700 Subject: [PATCH 374/446] Guard against Node linked-data being null --- assignment-client/src/avatars/AvatarMixer.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 32fb5b9b1a..5f7e197c8f 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -265,8 +265,11 @@ void AvatarMixer::start() { nodeList->nestedEach([&](NodeList::const_iterator cbegin, NodeList::const_iterator cend) { std::for_each(cbegin, cend, [](const SharedNodePointer& node) { if (node->getType() == NodeType::Agent) { - auto& avatar = static_cast(node->getLinkedData())->getAvatar(); - avatar.setNeedsHeroCheck(); + NodeData* nodeData = node->getLinkedData(); + if (nodeData) { + auto& avatar = static_cast(nodeData)->getAvatar(); + avatar.setNeedsHeroCheck(); + } } }); }); From bf1982564c702c0f91ce27318ee2ae5a089b2108 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 26 Mar 2019 19:26:38 -0700 Subject: [PATCH 375/446] pass id around instead of relying on latest ivar value in a singleton --- .../ui/overlays/ContextOverlayInterface.cpp | 26 +++++++++---------- .../src/ui/overlays/ContextOverlayInterface.h | 5 ++-- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/interface/src/ui/overlays/ContextOverlayInterface.cpp b/interface/src/ui/overlays/ContextOverlayInterface.cpp index 0e4fa796d8..1c8a9019ea 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.cpp +++ b/interface/src/ui/overlays/ContextOverlayInterface.cpp @@ -292,7 +292,7 @@ void ContextOverlayInterface::requestOwnershipVerification(const QUuid& entityID setLastInspectedEntity(entityID); - EntityItemProperties entityProperties = _entityScriptingInterface->getEntityProperties(_lastInspectedEntity, _entityPropertyFlags); + EntityItemProperties entityProperties = _entityScriptingInterface->getEntityProperties(entityID, _entityPropertyFlags); auto nodeList = DependencyManager::get(); @@ -349,10 +349,10 @@ void ContextOverlayInterface::requestOwnershipVerification(const QUuid& entityID // Kickoff a 10-second timeout timer that marks the cert if we don't get an ownership response in time if (thread() != QThread::currentThread()) { - QMetaObject::invokeMethod(this, "startChallengeOwnershipTimer"); + QMetaObject::invokeMethod(this, "startChallengeOwnershipTimer", Q_ARG(const EntityItemID&, entityID)); return; } else { - startChallengeOwnershipTimer(); + startChallengeOwnershipTimer(entityID); } } } else { @@ -376,8 +376,8 @@ void ContextOverlayInterface::requestOwnershipVerification(const QUuid& entityID auto ledger = DependencyManager::get(); _challengeOwnershipTimeoutTimer.stop(); emit ledger->updateCertificateStatus(entityID, (uint)(ledger->CERTIFICATE_STATUS_STATIC_VERIFICATION_FAILED)); - emit DependencyManager::get()->ownershipVerificationFailed(_lastInspectedEntity); - qCDebug(context_overlay) << "Entity" << _lastInspectedEntity << "failed static certificate verification!"; + emit DependencyManager::get()->ownershipVerificationFailed(entityID); + qCDebug(context_overlay) << "Entity" << entityID << "failed static certificate verification!"; } } @@ -395,14 +395,14 @@ void ContextOverlayInterface::deletingEntity(const EntityItemID& entityID) { } } -void ContextOverlayInterface::startChallengeOwnershipTimer() { +void ContextOverlayInterface::startChallengeOwnershipTimer(const EntityItemID& entityItemID) { auto ledger = DependencyManager::get(); - EntityItemProperties entityProperties = _entityScriptingInterface->getEntityProperties(_lastInspectedEntity, _entityPropertyFlags); + EntityItemProperties entityProperties = _entityScriptingInterface->getEntityProperties(entityItemID, _entityPropertyFlags); connect(&_challengeOwnershipTimeoutTimer, &QTimer::timeout, this, [=]() { - qCDebug(entities) << "Ownership challenge timed out for" << _lastInspectedEntity; - emit ledger->updateCertificateStatus(_lastInspectedEntity, (uint)(ledger->CERTIFICATE_STATUS_VERIFICATION_TIMEOUT)); - emit DependencyManager::get()->ownershipVerificationFailed(_lastInspectedEntity); + qCDebug(entities) << "Ownership challenge timed out for" << entityItemID; + emit ledger->updateCertificateStatus(entityItemID, (uint)(ledger->CERTIFICATE_STATUS_VERIFICATION_TIMEOUT)); + emit DependencyManager::get()->ownershipVerificationFailed(entityItemID); }); _challengeOwnershipTimeoutTimer.start(5000); @@ -422,13 +422,13 @@ void ContextOverlayInterface::handleChallengeOwnershipReplyPacket(QSharedPointer EntityItemID id(packet->read(idByteArraySize)); QString text(packet->read(textByteArraySize)); - bool verificationSuccess = DependencyManager::get()->getTree()->verifyNonce(_lastInspectedEntity, text); + bool verificationSuccess = DependencyManager::get()->getTree()->verifyNonce(id, text); if (verificationSuccess) { emit ledger->updateCertificateStatus(id, (uint)(ledger->CERTIFICATE_STATUS_VERIFICATION_SUCCESS)); - emit DependencyManager::get()->ownershipVerificationSuccess(_lastInspectedEntity); + emit DependencyManager::get()->ownershipVerificationSuccess(id); } else { emit ledger->updateCertificateStatus(id, (uint)(ledger->CERTIFICATE_STATUS_OWNER_VERIFICATION_FAILED)); - emit DependencyManager::get()->ownershipVerificationFailed(_lastInspectedEntity); + emit DependencyManager::get()->ownershipVerificationFailed(id); } } diff --git a/interface/src/ui/overlays/ContextOverlayInterface.h b/interface/src/ui/overlays/ContextOverlayInterface.h index b1b62aa0c4..e688d1c115 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.h +++ b/interface/src/ui/overlays/ContextOverlayInterface.h @@ -46,7 +46,7 @@ public: ContextOverlayInterface(); Q_INVOKABLE QUuid getCurrentEntityWithContextOverlay() { return _currentEntityWithContextOverlay; } void setCurrentEntityWithContextOverlay(const QUuid& entityID) { _currentEntityWithContextOverlay = entityID; } - void setLastInspectedEntity(const QUuid& entityID) { _challengeOwnershipTimeoutTimer.stop(); _lastInspectedEntity = entityID; } + void setLastInspectedEntity(const QUuid& entityID) { _challengeOwnershipTimeoutTimer.stop(); } void setEnabled(bool enabled); bool getEnabled() { return _enabled; } bool getIsInMarketplaceInspectionMode() { return _isInMarketplaceInspectionMode; } @@ -83,7 +83,6 @@ private: EntityItemID _mouseDownEntity; quint64 _mouseDownEntityTimestamp; EntityItemID _currentEntityWithContextOverlay; - EntityItemID _lastInspectedEntity; QString _entityMarketplaceID; bool _contextOverlayJustClicked { false }; @@ -94,7 +93,7 @@ private: void deletingEntity(const EntityItemID& entityItemID); - Q_INVOKABLE void startChallengeOwnershipTimer(); + Q_INVOKABLE void startChallengeOwnershipTimer(const EntityItemID& entityItemID); QTimer _challengeOwnershipTimeoutTimer; }; From 29af3b16126690d6b2b678dd98b682031fefc8ff Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 27 Feb 2019 10:47:42 -0800 Subject: [PATCH 376/446] add a button to Avatar panel to lock or unlock wearables. allow grabbing / adjusting others' wearables if they are unlocked. --- interface/resources/qml/hifi/AvatarApp.qml | 17 +++ .../+android_interface/HifiConstants.qml | 1 + .../resources/qml/stylesUit/HifiConstants.qml | 1 + interface/src/avatar/MyAvatar.h | 2 +- interface/src/avatar/OtherAvatar.cpp | 2 +- .../src/avatars-renderer/Avatar.cpp | 30 +++++- .../src/avatars-renderer/Avatar.h | 3 +- scripts/system/avatarapp.js | 102 ++++++++++++------ .../libraries/controllerDispatcherUtils.js | 2 - 9 files changed, 116 insertions(+), 44 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index 753b9c5a81..a57b5713ed 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -16,6 +16,8 @@ Rectangle { property bool keyboardRaised: false property bool punctuationMode: false + HifiConstants { id: hifi } + HifiControls.Keyboard { id: keyboard z: 1000 @@ -48,6 +50,7 @@ Rectangle { property var jointNames: [] property var currentAvatarSettings; + property bool wearablesLocked; function fetchAvatarModelName(marketId, avatar) { var xmlhttp = new XMLHttpRequest(); @@ -187,6 +190,8 @@ Rectangle { updateCurrentAvatarInBookmarks(currentAvatar); } else if (message.method === 'selectAvatarEntity') { adjustWearables.selectWearableByID(message.entityID); + } else if (message.method === 'wearablesLockedChanged') { + wearablesLocked = message.wearablesLocked; } } @@ -507,6 +512,7 @@ Rectangle { } SquareLabel { + id: adjustLabel anchors.right: parent.right anchors.verticalCenter: wearablesLabel.verticalCenter glyphText: "\ue02e" @@ -515,6 +521,17 @@ Rectangle { adjustWearables.open(currentAvatar); } } + + SquareLabel { + anchors.right: adjustLabel.left + anchors.verticalCenter: wearablesLabel.verticalCenter + anchors.rightMargin: 15 + glyphText: wearablesLocked ? hifi.glyphs.lock : hifi.glyphs.unlock; + + onClicked: { + emitSendToScript({'method' : 'toggleWearablesLock'}); + } + } } Rectangle { diff --git a/interface/resources/qml/stylesUit/+android_interface/HifiConstants.qml b/interface/resources/qml/stylesUit/+android_interface/HifiConstants.qml index d5fab57501..995af90f0b 100644 --- a/interface/resources/qml/stylesUit/+android_interface/HifiConstants.qml +++ b/interface/resources/qml/stylesUit/+android_interface/HifiConstants.qml @@ -344,6 +344,7 @@ Item { readonly property string stop_square: "\ue01e" readonly property string avatarTPose: "\ue01f" readonly property string lock: "\ue006" + readonly property string unlock: "\ue039" readonly property string checkmark: "\ue020" readonly property string leftRightArrows: "\ue021" readonly property string hfc: "\ue022" diff --git a/interface/resources/qml/stylesUit/HifiConstants.qml b/interface/resources/qml/stylesUit/HifiConstants.qml index 75f028cd4f..2394b36106 100644 --- a/interface/resources/qml/stylesUit/HifiConstants.qml +++ b/interface/resources/qml/stylesUit/HifiConstants.qml @@ -330,6 +330,7 @@ QtObject { readonly property string stop_square: "\ue01e" readonly property string avatarTPose: "\ue01f" readonly property string lock: "\ue006" + readonly property string unlock: "\ue039" readonly property string checkmark: "\ue020" readonly property string leftRightArrows: "\ue021" readonly property string hfc: "\ue022" diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index edb686a6a6..3f60b9cada 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -2170,7 +2170,7 @@ private: bool getEnableStepResetRotation() const { return _stepResetRotationEnabled; } void setEnableDrawAverageFacing(bool drawAverage) { _drawAverageFacingEnabled = drawAverage; } bool getEnableDrawAverageFacing() const { return _drawAverageFacingEnabled; } - bool isMyAvatar() const override { return true; } + virtual bool isMyAvatar() const override { return true; } virtual int parseDataFromBuffer(const QByteArray& buffer) override; virtual glm::vec3 getSkeletonPosition() const override; int _skeletonModelChangeCount { 0 }; diff --git a/interface/src/avatar/OtherAvatar.cpp b/interface/src/avatar/OtherAvatar.cpp index 11eb6542c4..2058408596 100755 --- a/interface/src/avatar/OtherAvatar.cpp +++ b/interface/src/avatar/OtherAvatar.cpp @@ -365,7 +365,7 @@ void OtherAvatar::handleChangedAvatarEntityData() { // AVATAR ENTITY UPDATE FLOW // - if queueEditEntityMessage() sees "AvatarEntity" HostType it calls _myAvatar->storeAvatarEntityDataPayload() // - storeAvatarEntityDataPayload() saves the payload and flags the trait instance for the entity as updated, - // - ClientTraitsHandler::sendChangedTraitsToMixea() sends the entity bytes to the mixer which relays them to other interfaces + // - ClientTraitsHandler::sendChangedTraitsToMixer() sends the entity bytes to the mixer which relays them to other interfaces // - AvatarHashMap::processBulkAvatarTraits() on other interfaces calls avatar->processTraitInstance() // - AvatarData::processTraitInstance() calls storeAvatarEntityDataPayload(), which sets _avatarEntityDataChanged = true // - (My)Avatar::simulate() calls handleChangedAvatarEntityData() every frame which checks _avatarEntityDataChanged diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 992ee5db96..8cff1cc52a 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -372,11 +372,22 @@ bool Avatar::applyGrabChanges() { target->removeGrab(grab); _avatarGrabs.erase(itr); grabAddedOrRemoved = true; - if (isMyAvatar()) { - const EntityItemPointer& entity = std::dynamic_pointer_cast(target); - if (entity && entity->getEntityHostType() == entity::HostType::AVATAR && entity->getSimulationOwner().getID() == getID()) { - EntityItemProperties properties = entity->getProperties(); - sendPacket(entity->getID()); + const EntityItemPointer& entity = std::dynamic_pointer_cast(target); + if (entity && entity->getEntityHostType() == entity::HostType::AVATAR) { + // grabs are able to move avatar-entities which belong ot other avatars (assuming + // the entities are grabbable, unlocked, etc). Regardless of who released the grab + // on this entity, the entity's owner needs to send off an update. + QUuid entityOwnerID = entity->getOwningAvatarID(); + if (entityOwnerID == getMyAvatarID() || entityOwnerID == AVATAR_SELF_ID) { + bool success; + SpatiallyNestablePointer myAvatarSN = SpatiallyNestable::findByID(entityOwnerID, success); + if (success) { + std::shared_ptr myAvatar = std::dynamic_pointer_cast(myAvatarSN); + if (myAvatar) { + EntityItemProperties properties = entity->getProperties(); + myAvatar->sendPacket(entity->getID(), properties); + } + } } } } else { @@ -2103,3 +2114,12 @@ void Avatar::updateDescendantRenderIDs() { } }); } + +QUuid Avatar::getMyAvatarID() const { + auto nodeList = DependencyManager::get(); + if (nodeList) { + return nodeList->getSessionUUID(); + } else { + return QUuid(); + } +} diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index b4683d6032..2bed13cb12 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -180,7 +180,8 @@ public: /// Returns the distance to use as a LOD parameter. float getLODDistance() const; - virtual bool isMyAvatar() const override { return false; } + QUuid getMyAvatarID() const; + virtual void createOrb() { } enum class LoadingStatus { diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index fb61b914a3..429053bb8b 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -1,6 +1,8 @@ "use strict"; /*jslint vars:true, plusplus:true, forin:true*/ -/*global Tablet, Settings, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, HMD, Controller, Account, UserActivityLogger, Messages, Window, XMLHttpRequest, print, location, getControllerWorldLocation*/ +/*global Tablet, Script, Entities, MyAvatar, Camera, Quat, HMD, Account, UserActivityLogger, Messages, print, + AvatarBookmarks, ContextOverlay, AddressManager +*/ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ // // avatarapp.js @@ -14,7 +16,7 @@ (function() { // BEGIN LOCAL_SCOPE -var request = Script.require('request').request; +// var request = Script.require('request').request; var AVATARAPP_QML_SOURCE = "hifi/AvatarApp.qml"; Script.include("/~/system/libraries/controllers.js"); @@ -22,7 +24,7 @@ Script.include("/~/system/libraries/controllers.js"); var ENTRY_AVATAR_URL = "avatarUrl"; var ENTRY_AVATAR_ENTITIES = "avatarEntites"; var ENTRY_AVATAR_SCALE = "avatarScale"; -var ENTRY_VERSION = "version"; +// var ENTRY_VERSION = "version"; function executeLater(callback) { Script.setTimeout(callback, 300); @@ -44,7 +46,7 @@ function getMyAvatarWearables() { } var localRotation = entity.properties.localRotation; - entity.properties.localRotationAngles = Quat.safeEulerAngles(localRotation) + entity.properties.localRotationAngles = Quat.safeEulerAngles(localRotation); wearablesArray.push(entity); } @@ -52,7 +54,7 @@ function getMyAvatarWearables() { } function getMyAvatar() { - var avatar = {} + var avatar = {}; avatar[ENTRY_AVATAR_URL] = MyAvatar.skeletonModelURL; avatar[ENTRY_AVATAR_SCALE] = MyAvatar.getAvatarScale(); avatar[ENTRY_AVATAR_ENTITIES] = getMyAvatarWearables(); @@ -68,7 +70,7 @@ function getMyAvatarSettings() { collisionSoundUrl : MyAvatar.collisionSoundURL, animGraphUrl: MyAvatar.getAnimGraphUrl(), animGraphOverrideUrl : MyAvatar.getAnimGraphOverrideUrl(), - } + }; } function updateAvatarWearables(avatar, callback, wearablesOverride) { @@ -76,7 +78,7 @@ function updateAvatarWearables(avatar, callback, wearablesOverride) { var wearables = wearablesOverride ? wearablesOverride : getMyAvatarWearables(); avatar[ENTRY_AVATAR_ENTITIES] = wearables; - sendToQml({'method' : 'wearablesUpdated', 'wearables' : wearables}) + sendToQml({'method' : 'wearablesUpdated', 'wearables' : wearables}); if(callback) callback(); @@ -101,7 +103,7 @@ var adjustWearables = { this.opened = value; } } -} +}; var currentAvatarWearablesBackup = null; var currentAvatar = null; @@ -112,7 +114,7 @@ function onTargetScaleChanged() { if(currentAvatar.scale !== MyAvatar.getAvatarScale()) { currentAvatar.scale = MyAvatar.getAvatarScale(); if(notifyScaleChanged) { - sendToQml({'method' : 'scaleChanged', 'value' : currentAvatar.scale}) + sendToQml({'method' : 'scaleChanged', 'value' : currentAvatar.scale}); } } } @@ -126,7 +128,7 @@ function onSkeletonModelURLChanged() { function onDominantHandChanged(dominantHand) { if(currentAvatarSettings.dominantHand !== dominantHand) { currentAvatarSettings.dominantHand = dominantHand; - sendToQml({'method' : 'settingChanged', 'name' : 'dominantHand', 'value' : dominantHand}) + sendToQml({'method' : 'settingChanged', 'name' : 'dominantHand', 'value' : dominantHand}); } } @@ -140,32 +142,33 @@ function onHmdAvatarAlignmentTypeChanged(type) { function onCollisionsEnabledChanged(enabled) { if(currentAvatarSettings.collisionsEnabled !== enabled) { currentAvatarSettings.collisionsEnabled = enabled; - sendToQml({'method' : 'settingChanged', 'name' : 'collisionsEnabled', 'value' : enabled}) + sendToQml({'method' : 'settingChanged', 'name' : 'collisionsEnabled', 'value' : enabled}); } } function onOtherAvatarsCollisionsEnabledChanged(enabled) { if (currentAvatarSettings.otherAvatarsCollisionsEnabled !== enabled) { currentAvatarSettings.otherAvatarsCollisionsEnabled = enabled; - sendToQml({ 'method': 'settingChanged', 'name': 'otherAvatarsCollisionsEnabled', 'value': enabled }) + sendToQml({ 'method': 'settingChanged', 'name': 'otherAvatarsCollisionsEnabled', 'value': enabled }); } } function onNewCollisionSoundUrl(url) { if(currentAvatarSettings.collisionSoundUrl !== url) { currentAvatarSettings.collisionSoundUrl = url; - sendToQml({'method' : 'settingChanged', 'name' : 'collisionSoundUrl', 'value' : url}) + sendToQml({'method' : 'settingChanged', 'name' : 'collisionSoundUrl', 'value' : url}); } } function onAnimGraphUrlChanged(url) { if (currentAvatarSettings.animGraphUrl !== url) { currentAvatarSettings.animGraphUrl = url; - sendToQml({ 'method': 'settingChanged', 'name': 'animGraphUrl', 'value': currentAvatarSettings.animGraphUrl }) + sendToQml({ 'method': 'settingChanged', 'name': 'animGraphUrl', 'value': currentAvatarSettings.animGraphUrl }); if (currentAvatarSettings.animGraphOverrideUrl !== MyAvatar.getAnimGraphOverrideUrl()) { currentAvatarSettings.animGraphOverrideUrl = MyAvatar.getAnimGraphOverrideUrl(); - sendToQml({ 'method': 'settingChanged', 'name': 'animGraphOverrideUrl', 'value': currentAvatarSettings.animGraphOverrideUrl }) + sendToQml({ 'method': 'settingChanged', 'name': 'animGraphOverrideUrl', + 'value': currentAvatarSettings.animGraphOverrideUrl }); } } } @@ -178,6 +181,33 @@ var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/wallet/Wallet.qml"; var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace"; var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("html/js/marketplacesInject.js"); +function getWearablesLocked() { + var wearablesLocked = true; + var wearablesArray = getMyAvatarWearables(); + wearablesArray.forEach(function(wearable) { + if (isGrabbable(wearable.id)) { + wearablesLocked = false; + } + }); + + return wearablesLocked; +} + +function lockWearables() { + var wearablesArray = getMyAvatarWearables(); + wearablesArray.forEach(function(wearable) { + setGrabbable(wearable.id, false); + }); +} + +function unlockWearables() { + var wearablesArray = getMyAvatarWearables(); + wearablesArray.forEach(function(wearable) { + setGrabbable(wearable.id, true); + }); +} + + function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml. switch (message.method) { case 'getAvatars': @@ -201,7 +231,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See } } - sendToQml(message) + sendToQml(message); break; case 'selectAvatar': Entities.addingWearable.disconnect(onAddingWearable); @@ -228,7 +258,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See message.properties.localRotationAngles = Quat.safeEulerAngles(message.properties.localRotation); } - sendToQml({'method' : 'wearableUpdated', 'entityID' : message.entityID, wearableIndex : message.wearableIndex, properties : message.properties, updateUI : false}) + sendToQml({'method' : 'wearableUpdated', 'entityID' : message.entityID, wearableIndex : message.wearableIndex, properties : message.properties, updateUI : false}); break; case 'adjustWearablesOpened': currentAvatarWearablesBackup = getMyAvatarWearables(); @@ -305,11 +335,11 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See var currentAvatarURL = MyAvatar.getFullAvatarURLFromPreferences(); if(currentAvatarURL !== message.avatarURL) { MyAvatar.useFullAvatarURL(message.avatarURL); - sendToQml({'method' : 'externalAvatarApplied', 'avatarURL' : message.avatarURL}) + sendToQml({'method' : 'externalAvatarApplied', 'avatarURL' : message.avatarURL}); } break; case 'navigate': - var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system") + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); if(message.url.indexOf('app://') === 0) { if (message.url === 'app://marketplace') { tablet.gotoWebScreen(MARKETPLACE_URL, MARKETPLACES_INJECT_SCRIPT_URL); @@ -345,7 +375,17 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See MyAvatar.collisionSoundURL = message.settings.collisionSoundUrl; MyAvatar.setAnimGraphOverrideUrl(message.settings.animGraphOverrideUrl); - settings = getMyAvatarSettings(); + currentAvatarSettings = getMyAvatarSettings(); + break; + case 'toggleWearablesLock': + var wearablesLocked = getWearablesLocked(); + wearablesLocked = !wearablesLocked; + if (wearablesLocked) { + lockWearables(); + } else { + unlockWearables(); + } + sendToQml({'method' : 'wearablesLockedChanged', 'wearablesLocked' : wearablesLocked}); break; default: print('Unrecognized message from AvatarApp.qml'); @@ -398,7 +438,7 @@ function ensureWearableSelected(entityID) { function isEntityBeingWorn(entityID) { return Entities.getEntityProperties(entityID, 'parentID').parentID === MyAvatar.sessionUUID; -}; +} function onSelectedEntity(entityID, pointerEvent) { if(selectedAvatarEntityID !== entityID && isEntityBeingWorn(entityID)) @@ -445,14 +485,14 @@ function handleWearableMessages(channel, message, sender) { // for some reasons Entities.getEntityProperties returns more than was asked.. var propertyNames = ['localPosition', 'localRotation', 'dimensions', 'naturalDimensions']; var entityProperties = Entities.getEntityProperties(selectedAvatarEntityID, propertyNames); - var properties = {} + var properties = {}; propertyNames.forEach(function(propertyName) { properties[propertyName] = entityProperties[propertyName]; - }) + }); properties.localRotationAngles = Quat.safeEulerAngles(properties.localRotation); - sendToQml({'method' : 'wearableUpdated', 'entityID' : selectedAvatarEntityID, 'wearableIndex' : -1, 'properties' : properties, updateUI : true}) + sendToQml({'method' : 'wearableUpdated', 'entityID' : selectedAvatarEntityID, 'wearableIndex' : -1, 'properties' : properties, updateUI : true}); }, 1000); } else if(parsedMessage.action === 'release') { @@ -481,8 +521,8 @@ function onBookmarkDeleted(bookmarkName) { function onBookmarkAdded(bookmarkName) { var bookmark = AvatarBookmarks.getBookmark(bookmarkName); bookmark.avatarEntites.forEach(function(avatarEntity) { - avatarEntity.properties.localRotationAngles = Quat.safeEulerAngles(avatarEntity.properties.localRotation) - }) + avatarEntity.properties.localRotationAngles = Quat.safeEulerAngles(avatarEntity.properties.localRotation); + }); sendToQml({ 'method': 'bookmarkAdded', 'bookmarkName': bookmarkName, 'bookmark': bookmark }); } @@ -601,14 +641,8 @@ function onTabletScreenChanged(type, url) { onAvatarAppScreen = onAvatarAppScreenNow; if(onAvatarAppScreenNow) { - var message = { - 'method' : 'initialize', - 'data' : { - 'jointNames' : MyAvatar.getJointNames() - } - }; - - sendToQml(message) + sendToQml({ method : 'initialize', data : { jointNames : MyAvatar.getJointNames() }}); + sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); } } diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js index 5cb95f625d..51645e5502 100644 --- a/scripts/system/libraries/controllerDispatcherUtils.js +++ b/scripts/system/libraries/controllerDispatcherUtils.js @@ -341,8 +341,6 @@ entityIsGrabbable = function (eigProps) { var grabbable = getGrabbableData(eigProps).grabbable; if (!grabbable || eigProps.locked || - isAnothersAvatarEntity(eigProps) || - isAnothersChildEntity(eigProps) || FORBIDDEN_GRAB_TYPES.indexOf(eigProps.type) >= 0) { return false; } From 4fe94a4b32404ab0e096d2274fa53046fcbcb7a4 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 21 Mar 2019 14:16:18 -0700 Subject: [PATCH 377/446] when new wearables are added or removed, rerun getWearablesLocked() and update UI --- scripts/system/avatarapp.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index 429053bb8b..eb44bc7fbd 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -79,6 +79,7 @@ function updateAvatarWearables(avatar, callback, wearablesOverride) { avatar[ENTRY_AVATAR_ENTITIES] = wearables; sendToQml({'method' : 'wearablesUpdated', 'wearables' : wearables}); + sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); if(callback) callback(); @@ -239,6 +240,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See AvatarBookmarks.loadBookmark(message.name); Entities.addingWearable.connect(onAddingWearable); Entities.deletingWearable.connect(onDeletingWearable); + sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); break; case 'deleteAvatar': AvatarBookmarks.removeBookmark(message.name); @@ -406,9 +408,11 @@ function isGrabbable(entityID) { } function setGrabbable(entityID, grabbable) { - var properties = Entities.getEntityProperties(entityID, ['avatarEntity']); - if (properties.avatarEntity) { - Entities.editEntity(entityID, { grab: { grabbable: grabbable }}); + var properties = Entities.getEntityProperties(entityID, ['avatarEntity', 'grab.grabbable']); + if (properties.avatarEntity && properties.grab.grabable != grabbable) { + var editProps = { grab: { grabbable: grabbable }}; + Entities.editEntity(entityID, editProps); + sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); } } @@ -453,12 +457,14 @@ function onAddingWearable(entityID) { updateAvatarWearables(currentAvatar, function() { sendToQml({'method' : 'updateAvatarInBookmarks'}); }); + sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); } function onDeletingWearable(entityID) { updateAvatarWearables(currentAvatar, function() { sendToQml({'method' : 'updateAvatarInBookmarks'}); }); + sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); } function handleWearableMessages(channel, message, sender) { From fa36f1214530252ad86af39da0bf2473b27822a1 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 21 Mar 2019 16:12:38 -0700 Subject: [PATCH 378/446] lock wearables when adjust-wearables page is opened --- .../src/avatars-renderer/Avatar.cpp | 3 +- scripts/system/avatarapp.js | 31 ++++++------------- 2 files changed, 10 insertions(+), 24 deletions(-) diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 8cff1cc52a..b0a8875cbc 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -384,8 +384,7 @@ bool Avatar::applyGrabChanges() { if (success) { std::shared_ptr myAvatar = std::dynamic_pointer_cast(myAvatarSN); if (myAvatar) { - EntityItemProperties properties = entity->getProperties(); - myAvatar->sendPacket(entity->getID(), properties); + myAvatar->sendPacket(entity->getID()); } } } diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index eb44bc7fbd..ee337694a2 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -16,7 +16,6 @@ (function() { // BEGIN LOCAL_SCOPE -// var request = Script.require('request').request; var AVATARAPP_QML_SOURCE = "hifi/AvatarApp.qml"; Script.include("/~/system/libraries/controllers.js"); @@ -24,7 +23,6 @@ Script.include("/~/system/libraries/controllers.js"); var ENTRY_AVATAR_URL = "avatarUrl"; var ENTRY_AVATAR_ENTITIES = "avatarEntites"; var ENTRY_AVATAR_SCALE = "avatarScale"; -// var ENTRY_VERSION = "version"; function executeLater(callback) { Script.setTimeout(callback, 300); @@ -79,7 +77,7 @@ function updateAvatarWearables(avatar, callback, wearablesOverride) { avatar[ENTRY_AVATAR_ENTITIES] = wearables; sendToQml({'method' : 'wearablesUpdated', 'wearables' : wearables}); - sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); if(callback) callback(); @@ -174,7 +172,6 @@ function onAnimGraphUrlChanged(url) { } } -var selectedAvatarEntityGrabbable = false; var selectedAvatarEntityID = null; var grabbedAvatarEntityChangeNotifier = null; @@ -240,7 +237,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See AvatarBookmarks.loadBookmark(message.name); Entities.addingWearable.connect(onAddingWearable); Entities.deletingWearable.connect(onDeletingWearable); - sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); break; case 'deleteAvatar': AvatarBookmarks.removeBookmark(message.name); @@ -265,6 +262,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See case 'adjustWearablesOpened': currentAvatarWearablesBackup = getMyAvatarWearables(); adjustWearables.setOpened(true); + lockWearables(); Entities.mousePressOnEntity.connect(onSelectedEntity); Messages.subscribe('Hifi-Object-Manipulation'); @@ -409,10 +407,10 @@ function isGrabbable(entityID) { function setGrabbable(entityID, grabbable) { var properties = Entities.getEntityProperties(entityID, ['avatarEntity', 'grab.grabbable']); - if (properties.avatarEntity && properties.grab.grabable != grabbable) { + if (properties.avatarEntity && properties.grab.grabbable != grabbable) { var editProps = { grab: { grabbable: grabbable }}; Entities.editEntity(entityID, editProps); - sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); } } @@ -422,18 +420,7 @@ function ensureWearableSelected(entityID) { Script.clearInterval(grabbedAvatarEntityChangeNotifier); grabbedAvatarEntityChangeNotifier = null; } - - if(selectedAvatarEntityID !== null) { - setGrabbable(selectedAvatarEntityID, selectedAvatarEntityGrabbable); - } - selectedAvatarEntityID = entityID; - selectedAvatarEntityGrabbable = isGrabbable(entityID); - - if(selectedAvatarEntityID !== null) { - setGrabbable(selectedAvatarEntityID, true); - } - return true; } @@ -457,14 +444,14 @@ function onAddingWearable(entityID) { updateAvatarWearables(currentAvatar, function() { sendToQml({'method' : 'updateAvatarInBookmarks'}); }); - sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); } function onDeletingWearable(entityID) { updateAvatarWearables(currentAvatar, function() { sendToQml({'method' : 'updateAvatarInBookmarks'}); }); - sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); } function handleWearableMessages(channel, message, sender) { @@ -647,8 +634,8 @@ function onTabletScreenChanged(type, url) { onAvatarAppScreen = onAvatarAppScreenNow; if(onAvatarAppScreenNow) { - sendToQml({ method : 'initialize', data : { jointNames : MyAvatar.getJointNames() }}); - sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); + sendToQml({ 'method' : 'initialize', 'data' : { jointNames : MyAvatar.getJointNames() }}); + sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); } } From 5695c1580941e94eae37cb28b9afbed3e1bd8e70 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 22 Mar 2019 16:55:21 -0700 Subject: [PATCH 379/446] avoid a deadlock when code invoked by onAddingEntity or onDeletingEntity runs more code that locks the entity tree --- libraries/entities/src/EntityScriptingInterface.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index aa4b3902c2..ca914731b5 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -1101,13 +1101,13 @@ void EntityScriptingInterface::handleEntityScriptCallMethodPacket(QSharedPointer void EntityScriptingInterface::onAddingEntity(EntityItem* entity) { if (entity->isWearable()) { - emit addingWearable(entity->getEntityItemID()); + QMetaObject::invokeMethod(this, "addingWearable", Q_ARG(QUuid, entity->getEntityItemID())); } } void EntityScriptingInterface::onDeletingEntity(EntityItem* entity) { if (entity->isWearable()) { - emit deletingWearable(entity->getEntityItemID()); + QMetaObject::invokeMethod(this, "deletingWearable", Q_ARG(QUuid, entity->getEntityItemID())); } } From e6c279ee5bd2efe83c931a5621fb8aa9904da3c6 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 25 Mar 2019 16:58:29 -0700 Subject: [PATCH 380/446] unlock, rather than lock entities when adjust-attachments window is open, because the windows says you can adjust with hand-controllers --- scripts/system/avatarapp.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index ee337694a2..65b422f1ab 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -262,7 +262,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See case 'adjustWearablesOpened': currentAvatarWearablesBackup = getMyAvatarWearables(); adjustWearables.setOpened(true); - lockWearables(); + unlockWearables(); Entities.mousePressOnEntity.connect(onSelectedEntity); Messages.subscribe('Hifi-Object-Manipulation'); From d25d290394c34a4262a8e58d15e88e2dea462a2c Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 25 Mar 2019 17:06:24 -0700 Subject: [PATCH 381/446] refer to ungrabbable wearables as 'frozen' rather than locked, because locked is such an overloaded term --- interface/resources/qml/hifi/AvatarApp.qml | 10 +++--- scripts/system/avatarapp.js | 40 +++++++++++----------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index a57b5713ed..997407885b 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -50,7 +50,7 @@ Rectangle { property var jointNames: [] property var currentAvatarSettings; - property bool wearablesLocked; + property bool wearablesFrozen; function fetchAvatarModelName(marketId, avatar) { var xmlhttp = new XMLHttpRequest(); @@ -190,8 +190,8 @@ Rectangle { updateCurrentAvatarInBookmarks(currentAvatar); } else if (message.method === 'selectAvatarEntity') { adjustWearables.selectWearableByID(message.entityID); - } else if (message.method === 'wearablesLockedChanged') { - wearablesLocked = message.wearablesLocked; + } else if (message.method === 'wearablesFrozenChanged') { + wearablesFrozen = message.wearablesFrozen; } } @@ -526,10 +526,10 @@ Rectangle { anchors.right: adjustLabel.left anchors.verticalCenter: wearablesLabel.verticalCenter anchors.rightMargin: 15 - glyphText: wearablesLocked ? hifi.glyphs.lock : hifi.glyphs.unlock; + glyphText: wearablesFrozen ? hifi.glyphs.lock : hifi.glyphs.unlock; onClicked: { - emitSendToScript({'method' : 'toggleWearablesLock'}); + emitSendToScript({'method' : 'toggleWearablesFrozen'}); } } } diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index 65b422f1ab..509497669b 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -77,7 +77,7 @@ function updateAvatarWearables(avatar, callback, wearablesOverride) { avatar[ENTRY_AVATAR_ENTITIES] = wearables; sendToQml({'method' : 'wearablesUpdated', 'wearables' : wearables}); - sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()}); if(callback) callback(); @@ -179,26 +179,26 @@ var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/wallet/Wallet.qml"; var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace"; var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("html/js/marketplacesInject.js"); -function getWearablesLocked() { - var wearablesLocked = true; +function getWearablesFrozen() { + var wearablesFrozen = true; var wearablesArray = getMyAvatarWearables(); wearablesArray.forEach(function(wearable) { if (isGrabbable(wearable.id)) { - wearablesLocked = false; + wearablesFrozen = false; } }); - return wearablesLocked; + return wearablesFrozen; } -function lockWearables() { +function freezeWearables() { var wearablesArray = getMyAvatarWearables(); wearablesArray.forEach(function(wearable) { setGrabbable(wearable.id, false); }); } -function unlockWearables() { +function unfreezeWearables() { var wearablesArray = getMyAvatarWearables(); wearablesArray.forEach(function(wearable) { setGrabbable(wearable.id, true); @@ -237,7 +237,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See AvatarBookmarks.loadBookmark(message.name); Entities.addingWearable.connect(onAddingWearable); Entities.deletingWearable.connect(onDeletingWearable); - sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()}); break; case 'deleteAvatar': AvatarBookmarks.removeBookmark(message.name); @@ -262,7 +262,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See case 'adjustWearablesOpened': currentAvatarWearablesBackup = getMyAvatarWearables(); adjustWearables.setOpened(true); - unlockWearables(); + unfreezeWearables(); Entities.mousePressOnEntity.connect(onSelectedEntity); Messages.subscribe('Hifi-Object-Manipulation'); @@ -377,15 +377,15 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See currentAvatarSettings = getMyAvatarSettings(); break; - case 'toggleWearablesLock': - var wearablesLocked = getWearablesLocked(); - wearablesLocked = !wearablesLocked; - if (wearablesLocked) { - lockWearables(); + case 'toggleWearablesFrozen': + var wearablesFrozen = getWearablesFrozen(); + wearablesFrozen = !wearablesFrozen; + if (wearablesFrozen) { + freezeWearables(); } else { - unlockWearables(); + unfreezeWearables(); } - sendToQml({'method' : 'wearablesLockedChanged', 'wearablesLocked' : wearablesLocked}); + sendToQml({'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : wearablesFrozen}); break; default: print('Unrecognized message from AvatarApp.qml'); @@ -410,7 +410,7 @@ function setGrabbable(entityID, grabbable) { if (properties.avatarEntity && properties.grab.grabbable != grabbable) { var editProps = { grab: { grabbable: grabbable }}; Entities.editEntity(entityID, editProps); - sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()}); } } @@ -444,14 +444,14 @@ function onAddingWearable(entityID) { updateAvatarWearables(currentAvatar, function() { sendToQml({'method' : 'updateAvatarInBookmarks'}); }); - sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()}); } function onDeletingWearable(entityID) { updateAvatarWearables(currentAvatar, function() { sendToQml({'method' : 'updateAvatarInBookmarks'}); }); - sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()}); } function handleWearableMessages(channel, message, sender) { @@ -635,7 +635,7 @@ function onTabletScreenChanged(type, url) { if(onAvatarAppScreenNow) { sendToQml({ 'method' : 'initialize', 'data' : { jointNames : MyAvatar.getJointNames() }}); - sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()}); } } From e085a00256d8bb1c83e13d481aa3c247d2bd5343 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 26 Mar 2019 09:32:12 -0700 Subject: [PATCH 382/446] cause 'save' button to unghost if an attachment is adjusted via grab while the adjust-wearables page is open --- .../qml/hifi/avatarapp/AdjustWearables.qml | 1 + scripts/system/avatarapp.js | 33 +++++++++++-------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml index 136d535b3f..391e4fab37 100644 --- a/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml +++ b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml @@ -113,6 +113,7 @@ Rectangle { } else if (prop === 'dimensions') { scalespinner.set(wearable[prop].x / wearable.naturalDimensions.x); } + modified = true; } } diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index 509497669b..6439d30023 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -468,30 +468,35 @@ function handleWearableMessages(channel, message, sender) { } var entityID = parsedMessage.grabbedEntity; + + var updateWearable = function() { + // for some reasons Entities.getEntityProperties returns more than was asked.. + var propertyNames = ['localPosition', 'localRotation', 'dimensions', 'naturalDimensions']; + var entityProperties = Entities.getEntityProperties(selectedAvatarEntityID, propertyNames); + var properties = {}; + + propertyNames.forEach(function(propertyName) { + properties[propertyName] = entityProperties[propertyName]; + }); + + properties.localRotationAngles = Quat.safeEulerAngles(properties.localRotation); + sendToQml({'method' : 'wearableUpdated', 'entityID' : selectedAvatarEntityID, + 'wearableIndex' : -1, 'properties' : properties, updateUI : true}); + + }; + if(parsedMessage.action === 'grab') { if(selectedAvatarEntityID !== entityID) { ensureWearableSelected(entityID); sendToQml({'method' : 'selectAvatarEntity', 'entityID' : selectedAvatarEntityID}); } - grabbedAvatarEntityChangeNotifier = Script.setInterval(function() { - // for some reasons Entities.getEntityProperties returns more than was asked.. - var propertyNames = ['localPosition', 'localRotation', 'dimensions', 'naturalDimensions']; - var entityProperties = Entities.getEntityProperties(selectedAvatarEntityID, propertyNames); - var properties = {}; - - propertyNames.forEach(function(propertyName) { - properties[propertyName] = entityProperties[propertyName]; - }); - - properties.localRotationAngles = Quat.safeEulerAngles(properties.localRotation); - sendToQml({'method' : 'wearableUpdated', 'entityID' : selectedAvatarEntityID, 'wearableIndex' : -1, 'properties' : properties, updateUI : true}); - - }, 1000); + grabbedAvatarEntityChangeNotifier = Script.setInterval(updateWearable, 1000); } else if(parsedMessage.action === 'release') { if(grabbedAvatarEntityChangeNotifier !== null) { Script.clearInterval(grabbedAvatarEntityChangeNotifier); grabbedAvatarEntityChangeNotifier = null; + updateWearable(); } } } From fb7daa185d63aa7445bf20959ff9ac1df63dee51 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 27 Mar 2019 09:53:55 -0700 Subject: [PATCH 383/446] improved physics for grabbed AvatarEntities --- interface/src/avatar/OtherAvatar.cpp | 12 +++++ .../src/avatars-renderer/Avatar.cpp | 26 ----------- .../src/avatars-renderer/Avatar.h | 2 - libraries/entities/src/EntityItem.cpp | 45 ++++++++++--------- libraries/entities/src/EntityItem.h | 8 ++-- 5 files changed, 41 insertions(+), 52 deletions(-) diff --git a/interface/src/avatar/OtherAvatar.cpp b/interface/src/avatar/OtherAvatar.cpp index 2058408596..b100b33dc8 100755 --- a/interface/src/avatar/OtherAvatar.cpp +++ b/interface/src/avatar/OtherAvatar.cpp @@ -495,6 +495,18 @@ void OtherAvatar::handleChangedAvatarEntityData() { const QUuid NULL_ID = QUuid("{00000000-0000-0000-0000-000000000005}"); entity->setParentID(NULL_ID); entity->setParentID(oldParentID); + + if (entity->stillHasMyGrabAction()) { + // For this case: we want to ignore transform+velocities coming from authoritative OtherAvatar + // because the MyAvatar is grabbing and we expect the local grab state + // to have enough information to prevent simulation drift. + // + // Clever readers might realize this could cause problems. For example, + // if an ignored OtherAvagtar were to simultanously grab the object then there would be + // a noticeable discrepancy between participants in the distributed physics simulation, + // however the difference would be stable and would not drift. + properties.clearTransformOrVelocityChanges(); + } if (entityTree->updateEntity(entityID, properties)) { entity->updateLastEditedFromRemote(); } else { diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index b0a8875cbc..839c4ed1d9 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -372,23 +372,6 @@ bool Avatar::applyGrabChanges() { target->removeGrab(grab); _avatarGrabs.erase(itr); grabAddedOrRemoved = true; - const EntityItemPointer& entity = std::dynamic_pointer_cast(target); - if (entity && entity->getEntityHostType() == entity::HostType::AVATAR) { - // grabs are able to move avatar-entities which belong ot other avatars (assuming - // the entities are grabbable, unlocked, etc). Regardless of who released the grab - // on this entity, the entity's owner needs to send off an update. - QUuid entityOwnerID = entity->getOwningAvatarID(); - if (entityOwnerID == getMyAvatarID() || entityOwnerID == AVATAR_SELF_ID) { - bool success; - SpatiallyNestablePointer myAvatarSN = SpatiallyNestable::findByID(entityOwnerID, success); - if (success) { - std::shared_ptr myAvatar = std::dynamic_pointer_cast(myAvatarSN); - if (myAvatar) { - myAvatar->sendPacket(entity->getID()); - } - } - } - } } else { undeleted.push_back(id); } @@ -2113,12 +2096,3 @@ void Avatar::updateDescendantRenderIDs() { } }); } - -QUuid Avatar::getMyAvatarID() const { - auto nodeList = DependencyManager::get(); - if (nodeList) { - return nodeList->getSessionUUID(); - } else { - return QUuid(); - } -} diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 2bed13cb12..974fae2034 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -180,8 +180,6 @@ public: /// Returns the distance to use as a LOD parameter. float getLODDistance() const; - QUuid getMyAvatarID() const; - virtual void createOrb() { } enum class LoadingStatus { diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index f0bf13891b..bd4c6e5c71 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -789,8 +789,10 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef auto lastEdited = lastEditedFromBufferAdjusted; bool otherOverwrites = overwriteLocalData && !weOwnSimulation; - auto shouldUpdate = [this, lastEdited, otherOverwrites, filterRejection](quint64 updatedTimestamp, bool valueChanged) { - if (stillHasGrabActions()) { + // calculate hasGrab once outside the lambda rather than calling it every time inside + bool hasGrab = stillHasGrabAction(); + auto shouldUpdate = [this, lastEdited, otherOverwrites, filterRejection, hasGrab](quint64 updatedTimestamp, bool valueChanged) { + if (hasGrab) { return false; } bool simulationChanged = lastEdited > updatedTimestamp; @@ -957,12 +959,18 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef // by doing this parsing here... but it's not likely going to fully recover the content. // - if (overwriteLocalData && (getDirtyFlags() & (Simulation::DIRTY_TRANSFORM | Simulation::DIRTY_VELOCITIES))) { + if (overwriteLocalData && + !hasGrab && + (getDirtyFlags() & (Simulation::DIRTY_TRANSFORM | Simulation::DIRTY_VELOCITIES))) { // NOTE: This code is attempting to "repair" the old data we just got from the server to make it more // closely match where the entities should be if they'd stepped forward in time to "now". The server // is sending us data with a known "last simulated" time. That time is likely in the past, and therefore // this "new" data is actually slightly out of date. We calculate the time we need to skip forward and // use our simulation helper routine to get a best estimate of where the entity should be. + // + // NOTE: We don't want to do this in the hasGrab case because grabs "know best" + // (e.g. grabs will prevent drift between distributed physics simulations). + // float skipTimeForward = (float)(now - lastSimulatedFromBufferAdjusted) / (float)(USECS_PER_SECOND); // we want to extrapolate the motion forward to compensate for packet travel time, but @@ -1426,7 +1434,7 @@ void EntityItem::getTransformAndVelocityProperties(EntityItemProperties& propert void EntityItem::upgradeScriptSimulationPriority(uint8_t priority) { uint8_t newPriority = glm::max(priority, _scriptSimulationPriority); - if (newPriority < SCRIPT_GRAB_SIMULATION_PRIORITY && stillHasGrabActions()) { + if (newPriority < SCRIPT_GRAB_SIMULATION_PRIORITY && stillHasMyGrabAction()) { newPriority = SCRIPT_GRAB_SIMULATION_PRIORITY; } if (newPriority != _scriptSimulationPriority) { @@ -1439,7 +1447,7 @@ void EntityItem::upgradeScriptSimulationPriority(uint8_t priority) { void EntityItem::clearScriptSimulationPriority() { // DO NOT markDirtyFlags(Simulation::DIRTY_SIMULATION_OWNERSHIP_PRIORITY) here, because this // is only ever called from the code that actually handles the dirty flags, and it knows best. - _scriptSimulationPriority = stillHasGrabActions() ? SCRIPT_GRAB_SIMULATION_PRIORITY : 0; + _scriptSimulationPriority = stillHasMyGrabAction() ? SCRIPT_GRAB_SIMULATION_PRIORITY : 0; } void EntityItem::setPendingOwnershipPriority(uint8_t priority) { @@ -2186,7 +2194,7 @@ void EntityItem::enableNoBootstrap() { } void EntityItem::disableNoBootstrap() { - if (!stillHasGrabActions()) { + if (!stillHasMyGrabAction()) { _flags &= ~Simulation::SPECIAL_FLAGS_NO_BOOTSTRAPPING; _flags |= Simulation::DIRTY_COLLISION_GROUP; // may need to not collide with own avatar @@ -2272,7 +2280,13 @@ bool EntityItem::removeAction(EntitySimulationPointer simulation, const QUuid& a return success; } -bool EntityItem::stillHasGrabActions() const { +bool EntityItem::stillHasGrabAction() const { + return !_grabActions.empty(); +} + +// retutrns 'true' if there exists an action that returns 'true' for EntityActionInterface::isMine() +// (e.g. the action belongs to the MyAvatar instance) +bool EntityItem::stillHasMyGrabAction() const { QList holdActions = getActionsOfType(DYNAMIC_TYPE_HOLD); QList::const_iterator i = holdActions.begin(); while (i != holdActions.end()) { @@ -2700,20 +2714,6 @@ void EntityItem::setLastEdited(quint64 lastEdited) { }); } -quint64 EntityItem::getLastBroadcast() const { - quint64 result; - withReadLock([&] { - result = _lastBroadcast; - }); - return result; -} - -void EntityItem::setLastBroadcast(quint64 lastBroadcast) { - withWriteLock([&] { - _lastBroadcast = lastBroadcast; - }); -} - void EntityItem::markAsChangedOnServer() { withWriteLock([&] { _changedOnServer = usecTimestampNow(); @@ -3479,6 +3479,9 @@ void EntityItem::addGrab(GrabPointer grab) { simulation->addDynamic(action); markDirtyFlags(Simulation::DIRTY_MOTION_TYPE); simulation->changeEntity(getThisPointer()); + + // don't forget to set isMine() for locally-created grabs + action->setIsMine(grab->getOwnerID() == Physics::getSessionUUID()); } } diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index fae871a124..01ed949a0c 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -124,8 +124,8 @@ public: { return (float)(usecTimestampNow() - getLastEdited()) / (float)USECS_PER_SECOND; } /// Last time we sent out an edit packet for this entity - quint64 getLastBroadcast() const; - void setLastBroadcast(quint64 lastBroadcast); + quint64 getLastBroadcast() const { return _lastBroadcast; } + void setLastBroadcast(quint64 lastBroadcast) { _lastBroadcast = lastBroadcast; } void markAsChangedOnServer(); quint64 getLastChangedOnServer() const; @@ -562,6 +562,8 @@ public: static void setPrimaryViewFrustumPositionOperator(std::function getPrimaryViewFrustumPositionOperator) { _getPrimaryViewFrustumPositionOperator = getPrimaryViewFrustumPositionOperator; } static glm::vec3 getPrimaryViewFrustumPosition() { return _getPrimaryViewFrustumPositionOperator(); } + bool stillHasMyGrabAction() const; + signals: void requestRenderUpdate(); void spaceUpdate(std::pair data); @@ -574,7 +576,7 @@ protected: void setSimulated(bool simulated) { _simulated = simulated; } const QByteArray getDynamicDataInternal() const; - bool stillHasGrabActions() const; + bool stillHasGrabAction() const; void setDynamicDataInternal(QByteArray dynamicData); virtual void dimensionsChanged() override; From dae69ea4cd75d249562fc3773eaff47cc5dca216 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Wed, 27 Mar 2019 10:58:52 -0700 Subject: [PATCH 384/446] Ensure server echo always has unity gain --- .../src/audio/AudioMixerSlave.cpp | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/assignment-client/src/audio/AudioMixerSlave.cpp b/assignment-client/src/audio/AudioMixerSlave.cpp index f7f8e8a9c1..cb90df58e5 100644 --- a/assignment-client/src/audio/AudioMixerSlave.cpp +++ b/assignment-client/src/audio/AudioMixerSlave.cpp @@ -51,7 +51,7 @@ void sendEnvironmentPacket(const SharedNodePointer& node, AudioMixerClientData& // mix helpers inline float approximateGain(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd); inline float computeGain(float masterAvatarGain, float masterInjectorGain, const AvatarAudioStream& listeningNodeStream, - const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, float distance, bool isEcho); + const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, float distance); inline float computeAzimuth(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition); @@ -504,14 +504,12 @@ void AudioMixerSlave::addStream(AudioMixerClientData::MixableStream& mixableStre glm::vec3 relativePosition = streamToAdd->getPosition() - listeningNodeStream.getPosition(); float distance = glm::max(glm::length(relativePosition), EPSILON); + float gain = isEcho ? 1.0f + : (isSoloing ? masterAvatarGain + : computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd, + relativePosition, distance)); float azimuth = isEcho ? 0.0f : computeAzimuth(listeningNodeStream, listeningNodeStream, relativePosition); - float gain = masterAvatarGain; - if (!isSoloing) { - gain = computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd, relativePosition, - distance, isEcho); - } - const int HRTF_DATASET_INDEX = 1; if (!streamToAdd->lastPopSucceeded()) { @@ -599,8 +597,8 @@ void AudioMixerSlave::updateHRTFParameters(AudioMixerClientData::MixableStream& glm::vec3 relativePosition = streamToAdd->getPosition() - listeningNodeStream.getPosition(); float distance = glm::max(glm::length(relativePosition), EPSILON); - float gain = computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd, relativePosition, - distance, isEcho); + float gain = isEcho ? 1.0f : computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd, + relativePosition, distance); float azimuth = isEcho ? 0.0f : computeAzimuth(listeningNodeStream, listeningNodeStream, relativePosition); mixableStream.hrtf->setParameterHistory(azimuth, distance, gain); @@ -743,8 +741,7 @@ float computeGain(float masterAvatarGain, const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, - float distance, - bool isEcho) { + float distance) { float gain = 1.0f; // injector: apply attenuation @@ -754,7 +751,7 @@ float computeGain(float masterAvatarGain, gain *= masterInjectorGain; // avatar: apply fixed off-axis attenuation to make them quieter as they turn away - } else if (!isEcho && (streamToAdd.getType() == PositionalAudioStream::Microphone)) { + } else if (streamToAdd.getType() == PositionalAudioStream::Microphone) { glm::vec3 rotatedListenerPosition = glm::inverse(streamToAdd.getOrientation()) * relativePosition; // source directivity is based on angle of emission, in local coordinates From fcb45802bde88aefe26154acbeead8ae63a38465 Mon Sep 17 00:00:00 2001 From: Angus Antley Date: Wed, 27 Mar 2019 11:19:28 -0700 Subject: [PATCH 385/446] removed debug print --- libraries/fbx/src/FBXSerializer.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 8ff3005ddc..0090eea2e7 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -466,7 +466,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } else if (object.name == "FBXVersion") { fbxVersionNumber = object.properties.at(0).toInt(); - qCDebug(modelformat) << "the fbx version number " << fbxVersionNumber; } } } else if (child.name == "GlobalSettings") { From 71111936a2d1eb38d1d1c2c6453ae9fe2403ddfb Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 27 Mar 2019 11:42:16 -0700 Subject: [PATCH 386/446] prep for entity lists --- libraries/entities/src/EntityTree.cpp | 69 +++++++++++++++------------ 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 4fc234122b..f4b82ad4b1 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1398,32 +1398,32 @@ bool EntityTree::isScriptInWhitelist(const QString& scriptProperty) { void EntityTree::addCertifiedEntityOnServer(EntityItemPointer entity) { QString certID(entity->getCertificateID()); - EntityItemID entityItemID = entity->getEntityItemID(); EntityItemID existingEntityItemID; - - { + if (!certID.isEmpty()) { + EntityItemID entityItemID = entity->getEntityItemID(); QWriteLocker locker(&_entityCertificateIDMapLock); existingEntityItemID = _entityCertificateIDMap.value(certID); - if (!certID.isEmpty()) { - _entityCertificateIDMap.insert(certID, entityItemID); - qCDebug(entities) << "Certificate ID" << certID << "belongs to" << entityItemID; - } + _entityCertificateIDMap.insert(certID, entityItemID); + qCDebug(entities) << "Certificate ID" << certID << "belongs to" << entityItemID; } - // Delete an already-existing entity from the tree if it has the same // CertificateID as the entity we're trying to add. if (!existingEntityItemID.isNull() && !entity->getCertificateType().contains(DOMAIN_UNLIMITED)) { qCDebug(entities) << "Certificate ID" << certID << "already exists on entity with ID" << existingEntityItemID << ". Deleting existing entity."; - deleteEntity(existingEntityItemID, true); + withWriteLock([&] { + deleteEntity(existingEntityItemID, true); + }); } } void EntityTree::removeCertifiedEntityOnServer(EntityItemPointer entity) { - QWriteLocker entityCertificateIDMapLocker(&_entityCertificateIDMapLock); QString certID = entity->getCertificateID(); - if (entity->getEntityItemID() == _entityCertificateIDMap.value(certID)) { - _entityCertificateIDMap.remove(certID); + if (!certID.isEmpty()) { + QWriteLocker entityCertificateIDMapLocker(&_entityCertificateIDMapLock); + if (entity->getEntityItemID() == _entityCertificateIDMap.value(certID)) { + _entityCertificateIDMap.remove(certID); + } } } @@ -1449,15 +1449,16 @@ void EntityTree::startDynamicDomainVerificationOnServer(float minimumAgeToRemove QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson()); - connect(networkReply, &QNetworkReply::finished, this, [this, entityID, networkReply, minimumAgeToRemove] { + connect(networkReply, &QNetworkReply::finished, this, [this, entityID, networkReply, minimumAgeToRemove, &certificateID] { QJsonObject jsonObject = QJsonDocument::fromJson(networkReply->readAll()).object(); jsonObject = jsonObject["data"].toObject(); + bool failure = networkReply->error() != QNetworkReply::NoError; + auto failureReason = networkReply->error(); networkReply->deleteLater(); - - if (networkReply->error() != QNetworkReply::NoError) { - qCDebug(entities) << "Call to" << networkReply->url() << "failed with error" << networkReply->error() << "; NOT deleting entity" << entityID - << "More info:" << jsonObject; + if (failure) { + qCDebug(entities) << "Call to" << networkReply->url() << "failed with error" << failureReason + << "; NOT deleting entity" << entityID << "More info:" << jsonObject; return; } QString thisDomainID = DependencyManager::get()->getDomainID().remove(QRegExp("\\{|\\}")); @@ -1466,20 +1467,26 @@ void EntityTree::startDynamicDomainVerificationOnServer(float minimumAgeToRemove return; } // Entity does not belong here: - EntityItemPointer entity = findEntityByEntityItemID(entityID); - if (!entity) { - qCDebug(entities) << "Entity undergoing dynamic domain verification is no longer available:" << entityID; - return; + { + EntityItemPointer entity = findEntityByEntityItemID(entityID); + if (!entity) { + qCDebug(entities) << "Entity undergoing dynamic domain verification is no longer available:" << entityID; + return; + } + if (entity->getAge() <= minimumAgeToRemove) { + qCDebug(entities) << "Entity failed dynamic domain verification, but was created too recently to necessitate deletion:" << entityID; + return; + } + qCDebug(entities) << "Entity's cert's domain ID" << jsonObject["domain_id"].toString() + << "doesn't match the current Domain ID" << thisDomainID << "; deleting entity" << entityID; + withWriteLock([&] { + deleteEntity(entityID, true); + }); } - if (entity->getAge() <= minimumAgeToRemove) { - qCDebug(entities) << "Entity failed dynamic domain verification, but was created too recently to necessitate deletion:" << entityID; - return; + { + QWriteLocker entityCertificateIDMapLocker(&_entityCertificateIDMapLock); + _entityCertificateIDMap.remove(certificateID); } - qCDebug(entities) << "Entity's cert's domain ID" << jsonObject["domain_id"].toString() - << "doesn't match the current Domain ID" << thisDomainID << "; deleting entity" << entityID; - withWriteLock([&] { - deleteEntity(entityID, true); - }); }); } } @@ -1704,7 +1711,9 @@ void EntityTree::processChallengeOwnershipPacket(ReceivedMessage& message, const emit killChallengeOwnershipTimeoutTimer(id); if (!verifyNonce(id, text)) { - deleteEntity(id, true); + withWriteLock([&] { + deleteEntity(id, true); + }); } } From 36921276c6dedb30178ead92f4fbc6f7b6e0f2c2 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 27 Mar 2019 11:52:37 -0700 Subject: [PATCH 387/446] fix transparent shape flickering --- .../src/RenderableShapeEntityItem.cpp | 4 ++-- libraries/render-utils/src/GeometryCache.cpp | 4 ++++ libraries/render-utils/src/GeometryCache.h | 17 ++++++++++++----- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 28b0bb8f23..33f4f2d751 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -281,9 +281,9 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) { outColor.a *= _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; render::ShapePipelinePointer pipeline; if (renderLayer == RenderLayer::WORLD) { - pipeline = GeometryCache::getShapePipeline(false, outColor.a < 1.0f, true, false); + pipeline = outColor.a < 1.0f ? geometryCache->getTransparentShapePipeline() : geometryCache->getOpaqueShapePipeline(); } else { - pipeline = GeometryCache::getShapePipeline(false, outColor.a < 1.0f, true, false, false, true); + pipeline = outColor.a < 1.0f ? geometryCache->getForwardTransparentShapePipeline() : geometryCache->getForwardOpaqueShapePipeline(); } if (render::ShapeKey(args->_globalShapeKey).isWireframe() || primitiveMode == PrimitiveMode::LINES) { geometryCache->renderWireShapeInstance(args, batch, geometryShape, outColor, pipeline); diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index c189798a42..2e762a0107 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -722,6 +722,8 @@ gpu::ShaderPointer GeometryCache::_unlitFadeShader; render::ShapePipelinePointer GeometryCache::_simpleOpaquePipeline; render::ShapePipelinePointer GeometryCache::_simpleTransparentPipeline; +render::ShapePipelinePointer GeometryCache::_forwardSimpleOpaquePipeline; +render::ShapePipelinePointer GeometryCache::_forwardSimpleTransparentPipeline; render::ShapePipelinePointer GeometryCache::_simpleOpaqueFadePipeline; render::ShapePipelinePointer GeometryCache::_simpleTransparentFadePipeline; render::ShapePipelinePointer GeometryCache::_simpleWirePipeline; @@ -801,6 +803,8 @@ void GeometryCache::initializeShapePipelines() { if (!_simpleOpaquePipeline) { _simpleOpaquePipeline = getShapePipeline(false, false, true, false); _simpleTransparentPipeline = getShapePipeline(false, true, true, false); + _forwardSimpleOpaquePipeline = getShapePipeline(false, false, true, false, false, true); + _forwardSimpleTransparentPipeline = getShapePipeline(false, true, true, false, false, true); _simpleOpaqueFadePipeline = getFadingShapePipeline(false, false, false, false, false); _simpleTransparentFadePipeline = getFadingShapePipeline(false, true, false, false, false); _simpleWirePipeline = getShapePipeline(false, false, true, true); diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index e84f2e25a4..4ff061786a 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -181,6 +181,11 @@ public: static void initializeShapePipelines(); + render::ShapePipelinePointer getOpaqueShapePipeline() { assert(_simpleOpaquePipeline != nullptr); return _simpleOpaquePipeline; } + render::ShapePipelinePointer getTransparentShapePipeline() { assert(_simpleTransparentPipeline != nullptr); return _simpleTransparentPipeline; } + render::ShapePipelinePointer getForwardOpaqueShapePipeline() { assert(_forwardSimpleOpaquePipeline != nullptr); return _forwardSimpleOpaquePipeline; } + render::ShapePipelinePointer getForwardTransparentShapePipeline() { assert(_forwardSimpleTransparentPipeline != nullptr); return _forwardSimpleTransparentPipeline; } + // Static (instanced) geometry void renderShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& colorBuffer); void renderWireShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& colorBuffer); @@ -369,11 +374,6 @@ public: graphics::MeshPointer meshFromShape(Shape geometryShape, glm::vec3 color); - static render::ShapePipelinePointer getShapePipeline(bool textured = false, bool transparent = false, bool culled = true, - bool unlit = false, bool depthBias = false, bool forward = false); - static render::ShapePipelinePointer getFadingShapePipeline(bool textured = false, bool transparent = false, bool culled = true, - bool unlit = false, bool depthBias = false); - private: GeometryCache(); @@ -467,6 +467,8 @@ private: static gpu::ShaderPointer _unlitFadeShader; static render::ShapePipelinePointer _simpleOpaquePipeline; static render::ShapePipelinePointer _simpleTransparentPipeline; + static render::ShapePipelinePointer _forwardSimpleOpaquePipeline; + static render::ShapePipelinePointer _forwardSimpleTransparentPipeline; static render::ShapePipelinePointer _simpleOpaqueFadePipeline; static render::ShapePipelinePointer _simpleTransparentFadePipeline; static render::ShapePipelinePointer _simpleWirePipeline; @@ -477,6 +479,11 @@ private: gpu::PipelinePointer _simpleOpaqueWebBrowserPipeline; gpu::ShaderPointer _simpleTransparentWebBrowserShader; gpu::PipelinePointer _simpleTransparentWebBrowserPipeline; + + static render::ShapePipelinePointer getShapePipeline(bool textured = false, bool transparent = false, bool culled = true, + bool unlit = false, bool depthBias = false, bool forward = false); + static render::ShapePipelinePointer getFadingShapePipeline(bool textured = false, bool transparent = false, bool culled = true, + bool unlit = false, bool depthBias = false); }; #endif // hifi_GeometryCache_h From e62270fccf4156a238e4e58cf1b8cd814eb7e1aa Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 27 Mar 2019 12:00:30 -0700 Subject: [PATCH 388/446] Fixes for inline jsdoc --- assignment-client/src/avatars/ScriptableAvatar.h | 1 + libraries/avatars/src/AvatarData.h | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h index fbe5675bd8..e5df411099 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.h +++ b/assignment-client/src/avatars/ScriptableAvatar.h @@ -74,6 +74,7 @@ * avatar. Read-only. * @property {number} sensorToWorldScale - The scale that transforms dimensions in the user's real world to the avatar's * size in the virtual world. Read-only. + * @property {boolean} hasPriority - is the avatar in a Hero zone? Read-only. * * @example * (function () { diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index c55d5270e1..43ddbda996 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -479,7 +479,7 @@ class AvatarData : public QObject, public SpatiallyNestable { * avatar. Read-only. * @property {number} sensorToWorldScale - The scale that transforms dimensions in the user's real world to the avatar's * size in the virtual world. Read-only. - * @property {boolean} hasPriority - is the avatar in a Hero zone? Read-only + * @property {boolean} hasPriority - is the avatar in a Hero zone? Read-only. */ Q_PROPERTY(glm::vec3 position READ getWorldPosition WRITE setPositionViaScript) Q_PROPERTY(float scale READ getDomainLimitedScale WRITE setTargetScale) From f6ee77a6ae5b31881efcbe56d67bbaff1e36fe70 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 27 Mar 2019 12:23:02 -0700 Subject: [PATCH 389/446] an entity list of one --- libraries/entities/src/EntityTree.cpp | 27 ++++++++++++++++++--------- libraries/entities/src/EntityTree.h | 2 +- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index f4b82ad4b1..01c00b82e5 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1398,17 +1398,20 @@ bool EntityTree::isScriptInWhitelist(const QString& scriptProperty) { void EntityTree::addCertifiedEntityOnServer(EntityItemPointer entity) { QString certID(entity->getCertificateID()); - EntityItemID existingEntityItemID; + QList entityList; if (!certID.isEmpty()) { EntityItemID entityItemID = entity->getEntityItemID(); QWriteLocker locker(&_entityCertificateIDMapLock); - existingEntityItemID = _entityCertificateIDMap.value(certID); - _entityCertificateIDMap.insert(certID, entityItemID); + entityList = _entityCertificateIDMap.value(certID); + QList newList; + newList << entityItemID; + _entityCertificateIDMap.insert(certID, newList); qCDebug(entities) << "Certificate ID" << certID << "belongs to" << entityItemID; } // Delete an already-existing entity from the tree if it has the same // CertificateID as the entity we're trying to add. - if (!existingEntityItemID.isNull() && !entity->getCertificateType().contains(DOMAIN_UNLIMITED)) { + if (!entityList.isEmpty() && !entity->getCertificateType().contains(DOMAIN_UNLIMITED)) { + EntityItemID existingEntityItemID = entityList.first(); qCDebug(entities) << "Certificate ID" << certID << "already exists on entity with ID" << existingEntityItemID << ". Deleting existing entity."; withWriteLock([&] { @@ -1421,7 +1424,8 @@ void EntityTree::removeCertifiedEntityOnServer(EntityItemPointer entity) { QString certID = entity->getCertificateID(); if (!certID.isEmpty()) { QWriteLocker entityCertificateIDMapLocker(&_entityCertificateIDMapLock); - if (entity->getEntityItemID() == _entityCertificateIDMap.value(certID)) { + QList entityList = _entityCertificateIDMap.value(certID); + if (!entityList.isEmpty() && (entity->getEntityItemID() == entityList.first())) { _entityCertificateIDMap.remove(certID); } } @@ -1429,12 +1433,16 @@ void EntityTree::removeCertifiedEntityOnServer(EntityItemPointer entity) { void EntityTree::startDynamicDomainVerificationOnServer(float minimumAgeToRemove) { QReadLocker locker(&_entityCertificateIDMapLock); - QHashIterator i(_entityCertificateIDMap); + QHashIterator> i(_entityCertificateIDMap); qCDebug(entities) << _entityCertificateIDMap.size() << "certificates present."; while (i.hasNext()) { i.next(); const auto& certificateID = i.key(); - const auto& entityID = i.value(); + const auto& entityIDs = i.value(); + + if (entityIDs.isEmpty()) { + continue; + } // Examine each cert: QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); @@ -1449,7 +1457,7 @@ void EntityTree::startDynamicDomainVerificationOnServer(float minimumAgeToRemove QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson()); - connect(networkReply, &QNetworkReply::finished, this, [this, entityID, networkReply, minimumAgeToRemove, &certificateID] { + connect(networkReply, &QNetworkReply::finished, this, [this, entityIDs, networkReply, minimumAgeToRemove, &certificateID] { QJsonObject jsonObject = QJsonDocument::fromJson(networkReply->readAll()).object(); jsonObject = jsonObject["data"].toObject(); @@ -1458,7 +1466,7 @@ void EntityTree::startDynamicDomainVerificationOnServer(float minimumAgeToRemove networkReply->deleteLater(); if (failure) { qCDebug(entities) << "Call to" << networkReply->url() << "failed with error" << failureReason - << "; NOT deleting entity" << entityID << "More info:" << jsonObject; + << "; NOT deleting cert" << certificateID << "More info:" << jsonObject; return; } QString thisDomainID = DependencyManager::get()->getDomainID().remove(QRegExp("\\{|\\}")); @@ -1468,6 +1476,7 @@ void EntityTree::startDynamicDomainVerificationOnServer(float minimumAgeToRemove } // Entity does not belong here: { + EntityItemID entityID = entityIDs.first(); EntityItemPointer entity = findEntityByEntityItemID(entityID); if (!entity) { qCDebug(entities) << "Entity undergoing dynamic domain verification is no longer available:" << entityID; diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 2b3ee93ab7..c80517c82b 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -323,7 +323,7 @@ protected: QHash _entityMap; mutable QReadWriteLock _entityCertificateIDMapLock; - QHash _entityCertificateIDMap; + QHash> _entityCertificateIDMap; mutable QReadWriteLock _entityNonceMapLock; QHash> _entityNonceMap; From 489fadda6ea3e406fbfd145125ef01119fe88bbc Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 27 Mar 2019 14:58:58 -0700 Subject: [PATCH 390/446] and now it's a list that cleans up after itself --- libraries/entities/src/EntityTree.cpp | 42 +++++++++++++++++---------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 01c00b82e5..1ccf3fcfa2 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1398,20 +1398,21 @@ bool EntityTree::isScriptInWhitelist(const QString& scriptProperty) { void EntityTree::addCertifiedEntityOnServer(EntityItemPointer entity) { QString certID(entity->getCertificateID()); - QList entityList; + EntityItemID existingEntityItemID; if (!certID.isEmpty()) { EntityItemID entityItemID = entity->getEntityItemID(); QWriteLocker locker(&_entityCertificateIDMapLock); - entityList = _entityCertificateIDMap.value(certID); - QList newList; - newList << entityItemID; - _entityCertificateIDMap.insert(certID, newList); - qCDebug(entities) << "Certificate ID" << certID << "belongs to" << entityItemID; + QList& entityList = _entityCertificateIDMap[certID]; // inserts it if needed. + if (!entityList.isEmpty() && !entity->getCertificateType().contains(DOMAIN_UNLIMITED)) { + existingEntityItemID = entityList.first(); // we will only care about the first, if any, below. + entityList.removeOne(existingEntityItemID); + } + entityList << entityItemID; // adds to list within hash because entityList is a reference. + qCDebug(entities) << "Certificate ID" << certID << "belongs to" << entityItemID << "total" << entityList.size() << "entities."; } // Delete an already-existing entity from the tree if it has the same // CertificateID as the entity we're trying to add. - if (!entityList.isEmpty() && !entity->getCertificateType().contains(DOMAIN_UNLIMITED)) { - EntityItemID existingEntityItemID = entityList.first(); + if (!existingEntityItemID.isNull()) { qCDebug(entities) << "Certificate ID" << certID << "already exists on entity with ID" << existingEntityItemID << ". Deleting existing entity."; withWriteLock([&] { @@ -1424,8 +1425,10 @@ void EntityTree::removeCertifiedEntityOnServer(EntityItemPointer entity) { QString certID = entity->getCertificateID(); if (!certID.isEmpty()) { QWriteLocker entityCertificateIDMapLocker(&_entityCertificateIDMapLock); - QList entityList = _entityCertificateIDMap.value(certID); - if (!entityList.isEmpty() && (entity->getEntityItemID() == entityList.first())) { + QList& entityList = _entityCertificateIDMap[certID]; + entityList.removeOne(entity->getEntityItemID()); + if (entityList.isEmpty()) { + // hmmm, do we to make it be a hash instead of a list, so that this is faster if you stamp out 1000 of a domainUnlimited? _entityCertificateIDMap.remove(certID); } } @@ -1439,7 +1442,6 @@ void EntityTree::startDynamicDomainVerificationOnServer(float minimumAgeToRemove i.next(); const auto& certificateID = i.key(); const auto& entityIDs = i.value(); - if (entityIDs.isEmpty()) { continue; } @@ -1475,16 +1477,18 @@ void EntityTree::startDynamicDomainVerificationOnServer(float minimumAgeToRemove return; } // Entity does not belong here: - { - EntityItemID entityID = entityIDs.first(); + QList retained; + for (int i = 0; i < entityIDs.size(); i++) { + EntityItemID entityID = entityIDs.at(i); EntityItemPointer entity = findEntityByEntityItemID(entityID); if (!entity) { qCDebug(entities) << "Entity undergoing dynamic domain verification is no longer available:" << entityID; - return; + continue; } if (entity->getAge() <= minimumAgeToRemove) { qCDebug(entities) << "Entity failed dynamic domain verification, but was created too recently to necessitate deletion:" << entityID; - return; + retained << entityID; + continue; } qCDebug(entities) << "Entity's cert's domain ID" << jsonObject["domain_id"].toString() << "doesn't match the current Domain ID" << thisDomainID << "; deleting entity" << entityID; @@ -1494,7 +1498,13 @@ void EntityTree::startDynamicDomainVerificationOnServer(float minimumAgeToRemove } { QWriteLocker entityCertificateIDMapLocker(&_entityCertificateIDMapLock); - _entityCertificateIDMap.remove(certificateID); + if (retained.isEmpty()) { + qCDebug(entities) << "Removed" << certificateID; + _entityCertificateIDMap.remove(certificateID); + } else { + qCDebug(entities) << "Retained" << retained.size() << "young entities for" << certificateID; + _entityCertificateIDMap[certificateID] = retained; + } } }); } From 483b7a67b9322974075b64590907db32f749f226 Mon Sep 17 00:00:00 2001 From: Clement Date: Tue, 12 Feb 2019 15:10:32 -0800 Subject: [PATCH 391/446] Fix simple traits vector bad init --- libraries/avatars/src/AssociatedTraitValues.h | 13 +++++++------ libraries/avatars/src/AvatarTraits.h | 14 +++++++++++++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/libraries/avatars/src/AssociatedTraitValues.h b/libraries/avatars/src/AssociatedTraitValues.h index e3060a8097..0df8cd9bb5 100644 --- a/libraries/avatars/src/AssociatedTraitValues.h +++ b/libraries/avatars/src/AssociatedTraitValues.h @@ -28,9 +28,10 @@ namespace AvatarTraits { template class AssociatedTraitValues { + using SimpleTypesArray = std::array; public: // constructor that pre-fills _simpleTypes with the default value specified by the template - AssociatedTraitValues() : _simpleTypes(FirstInstancedTrait, defaultValue) {} + AssociatedTraitValues() { std::fill(_simpleTypes.begin(), _simpleTypes.end(), defaultValue); } /// inserts the given value for the given simple trait type void insert(TraitType type, T value) { _simpleTypes[type] = value; } @@ -71,12 +72,12 @@ namespace AvatarTraits { } /// const iterators for the vector of simple type values - typename std::vector::const_iterator simpleCBegin() const { return _simpleTypes.cbegin(); } - typename std::vector::const_iterator simpleCEnd() const { return _simpleTypes.cend(); } + typename SimpleTypesArray::const_iterator simpleCBegin() const { return _simpleTypes.cbegin(); } + typename SimpleTypesArray::const_iterator simpleCEnd() const { return _simpleTypes.cend(); } /// non-const iterators for the vector of simple type values - typename std::vector::iterator simpleBegin() { return _simpleTypes.begin(); } - typename std::vector::iterator simpleEnd() { return _simpleTypes.end(); } + typename SimpleTypesArray::iterator simpleBegin() { return _simpleTypes.begin(); } + typename SimpleTypesArray::iterator simpleEnd() { return _simpleTypes.end(); } struct TraitWithInstances { TraitType traitType; @@ -96,7 +97,7 @@ namespace AvatarTraits { typename std::vector::iterator instancedEnd() { return _instancedTypes.end(); } private: - std::vector _simpleTypes; + SimpleTypesArray _simpleTypes; /// return the iterator to the matching TraitWithInstances object for a given instanced trait type typename std::vector::iterator instancesForTrait(TraitType traitType) { diff --git a/libraries/avatars/src/AvatarTraits.h b/libraries/avatars/src/AvatarTraits.h index 4516572e42..5542acd37f 100644 --- a/libraries/avatars/src/AvatarTraits.h +++ b/libraries/avatars/src/AvatarTraits.h @@ -14,19 +14,31 @@ #include #include +#include #include #include namespace AvatarTraits { enum TraitType : int8_t { + // Null trait NullTrait = -1, - SkeletonModelURL, + + // Simple traits + SkeletonModelURL = 0, + + + // Instanced traits FirstInstancedTrait, AvatarEntity = FirstInstancedTrait, Grab, + + // Traits count TotalTraitTypes }; + const int NUM_SIMPLE_TRAITS = (int)FirstInstancedTrait; + const int NUM_INSTANCED_TRAITS = (int)TotalTraitTypes - (int)FirstInstancedTrait; + const int NUM_TRAITS = (int)TotalTraitTypes; using TraitInstanceID = QUuid; From d7d5938c20ae6718428e1085fe22b13d6cc93ca5 Mon Sep 17 00:00:00 2001 From: Clement Date: Mon, 18 Mar 2019 17:47:52 -0700 Subject: [PATCH 392/446] Pack all simple traits --- libraries/avatars/src/ClientTraitsHandler.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libraries/avatars/src/ClientTraitsHandler.cpp b/libraries/avatars/src/ClientTraitsHandler.cpp index bcbe5308c7..c9e3b67f16 100644 --- a/libraries/avatars/src/ClientTraitsHandler.cpp +++ b/libraries/avatars/src/ClientTraitsHandler.cpp @@ -106,9 +106,10 @@ int ClientTraitsHandler::sendChangedTraitsToMixer() { auto traitType = static_cast(std::distance(traitStatusesCopy.simpleCBegin(), simpleIt)); if (initialSend || *simpleIt == Updated) { - if (traitType == AvatarTraits::SkeletonModelURL) { - bytesWritten += _owningAvatar->packTrait(traitType, *traitsPacketList); + bytesWritten += _owningAvatar->packTrait(traitType, *traitsPacketList); + + if (traitType == AvatarTraits::SkeletonModelURL) { // keep track of our skeleton version in case we get an override back _currentSkeletonVersion = _currentTraitVersion; } From 3221e1dbd59c4b333e4285427a3b0a595acf732e Mon Sep 17 00:00:00 2001 From: Clement Date: Tue, 19 Mar 2019 19:39:17 -0700 Subject: [PATCH 393/446] Simplify packing/unpacking for easier extension --- .../src/avatars/AvatarMixerClientData.cpp | 2 +- .../src/avatars/AvatarMixerSlave.cpp | 6 +- libraries/avatars/src/AvatarData.cpp | 151 +++++------------- libraries/avatars/src/AvatarData.h | 22 ++- libraries/avatars/src/AvatarTraits.cpp | 135 ++++++++++++++++ libraries/avatars/src/AvatarTraits.h | 27 ++-- libraries/avatars/src/ClientTraitsHandler.cpp | 6 +- 7 files changed, 209 insertions(+), 140 deletions(-) create mode 100644 libraries/avatars/src/AvatarTraits.cpp diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index 557c5c9fe3..dfbeca96ce 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -341,7 +341,7 @@ void AvatarMixerClientData::checkSkeletonURLAgainstWhitelist(const SlaveSharedDa // the returned set traits packet uses the trait version from the incoming packet // so the client knows they should not overwrite if they have since changed the trait - _avatar->packTrait(AvatarTraits::SkeletonModelURL, *packet, traitVersion); + AvatarTraits::packVersionedTrait(AvatarTraits::SkeletonModelURL, *packet, traitVersion, *_avatar); auto nodeList = DependencyManager::get(); nodeList->sendPacket(std::move(packet), sendingNode); diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index e59c81f4b7..fdbdf21607 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -139,7 +139,8 @@ qint64 AvatarMixerSlave::addChangedTraitsToBulkPacket(AvatarMixerClientData* lis if (lastReceivedVersion > lastSentVersionRef) { bytesWritten += addTraitsNodeHeader(listeningNodeData, sendingNodeData, traitsPacketList, bytesWritten); // there is an update to this trait, add it to the traits packet - bytesWritten += sendingAvatar->packTrait(traitType, traitsPacketList, lastReceivedVersion); + bytesWritten += AvatarTraits::packVersionedTrait(traitType, traitsPacketList, + lastReceivedVersion, *sendingAvatar); // update the last sent version lastSentVersionRef = lastReceivedVersion; // Remember which versions we sent in this particular packet @@ -194,7 +195,8 @@ qint64 AvatarMixerSlave::addChangedTraitsToBulkPacket(AvatarMixerClientData* lis bytesWritten += addTraitsNodeHeader(listeningNodeData, sendingNodeData, traitsPacketList, bytesWritten); // this instance version exists and has never been sent or is newer so we need to send it - bytesWritten += sendingAvatar->packTraitInstance(traitType, instanceID, traitsPacketList, receivedVersion); + bytesWritten += AvatarTraits::packVersionedTraitInstance(traitType, instanceID, traitsPacketList, + receivedVersion, *sendingAvatar); if (sentInstanceIt != sentIDValuePairs.end()) { sentInstanceIt->value = receivedVersion; diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 39dfaa8a1a..9c923580f9 100755 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1990,42 +1990,16 @@ QUrl AvatarData::getWireSafeSkeletonModelURL() const { } } -qint64 AvatarData::packTrait(AvatarTraits::TraitType traitType, ExtendedIODevice& destination, - AvatarTraits::TraitVersion traitVersion) { - - qint64 bytesWritten = 0; - - if (traitType == AvatarTraits::SkeletonModelURL) { - - QByteArray encodedSkeletonURL = getWireSafeSkeletonModelURL().toEncoded(); - - if (encodedSkeletonURL.size() > AvatarTraits::MAXIMUM_TRAIT_SIZE) { - qWarning() << "Refusing to pack simple trait" << traitType << "of size" << encodedSkeletonURL.size() - << "bytes since it exceeds the maximum size" << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes"; - return 0; - } - - bytesWritten += destination.writePrimitive(traitType); - - if (traitVersion > AvatarTraits::DEFAULT_TRAIT_VERSION) { - bytesWritten += destination.writePrimitive(traitVersion); - } - - AvatarTraits::TraitWireSize encodedURLSize = encodedSkeletonURL.size(); - bytesWritten += destination.writePrimitive(encodedURLSize); - - bytesWritten += destination.write(encodedSkeletonURL); - } - - return bytesWritten; +QByteArray AvatarData::packSkeletonModelURL() const { + return getWireSafeSkeletonModelURL().toEncoded(); } +void AvatarData::unpackSkeletonModelURL(const QByteArray& data) { + auto skeletonModelURL = QUrl::fromEncoded(data); + setSkeletonModelURL(skeletonModelURL); +} -qint64 AvatarData::packAvatarEntityTraitInstance(AvatarTraits::TraitType traitType, - AvatarTraits::TraitInstanceID traitInstanceID, - ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion) { - qint64 bytesWritten = 0; - +QByteArray AvatarData::packAvatarEntityTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID) { // grab a read lock on the avatar entities and check for entity data for the given ID QByteArray entityBinaryData; _avatarEntitiesLock.withReadLock([this, &entityBinaryData, &traitInstanceID] { @@ -2034,104 +2008,48 @@ qint64 AvatarData::packAvatarEntityTraitInstance(AvatarTraits::TraitType traitTy } }); - if (entityBinaryData.size() > AvatarTraits::MAXIMUM_TRAIT_SIZE) { - qWarning() << "Refusing to pack instanced trait" << traitType << "of size" << entityBinaryData.size() - << "bytes since it exceeds the maximum size " << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes"; - return 0; - } - - bytesWritten += destination.writePrimitive(traitType); - - if (traitVersion > AvatarTraits::DEFAULT_TRAIT_VERSION) { - bytesWritten += destination.writePrimitive(traitVersion); - } - - bytesWritten += destination.write(traitInstanceID.toRfc4122()); - - if (!entityBinaryData.isNull()) { - AvatarTraits::TraitWireSize entityBinarySize = entityBinaryData.size(); - - bytesWritten += destination.writePrimitive(entityBinarySize); - bytesWritten += destination.write(entityBinaryData); - } else { - bytesWritten += destination.writePrimitive(AvatarTraits::DELETED_TRAIT_SIZE); - } - - return bytesWritten; + return entityBinaryData; } - -qint64 AvatarData::packGrabTraitInstance(AvatarTraits::TraitType traitType, - AvatarTraits::TraitInstanceID traitInstanceID, - ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion) { - qint64 bytesWritten = 0; - +QByteArray AvatarData::packGrabTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID) { // grab a read lock on the avatar grabs and check for grab data for the given ID QByteArray grabBinaryData; - _avatarGrabsLock.withReadLock([this, &grabBinaryData, &traitInstanceID] { if (_avatarGrabData.contains(traitInstanceID)) { grabBinaryData = _avatarGrabData[traitInstanceID]; } }); - if (grabBinaryData.size() > AvatarTraits::MAXIMUM_TRAIT_SIZE) { - qWarning() << "Refusing to pack instanced trait" << traitType << "of size" << grabBinaryData.size() - << "bytes since it exceeds the maximum size " << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes"; - return 0; - } - - bytesWritten += destination.writePrimitive(traitType); - - if (traitVersion > AvatarTraits::DEFAULT_TRAIT_VERSION) { - bytesWritten += destination.writePrimitive(traitVersion); - } - - bytesWritten += destination.write(traitInstanceID.toRfc4122()); - - if (!grabBinaryData.isNull()) { - AvatarTraits::TraitWireSize grabBinarySize = grabBinaryData.size(); - - bytesWritten += destination.writePrimitive(grabBinarySize); - bytesWritten += destination.write(grabBinaryData); - } else { - bytesWritten += destination.writePrimitive(AvatarTraits::DELETED_TRAIT_SIZE); - } - - return bytesWritten; + return grabBinaryData; } -qint64 AvatarData::packTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID traitInstanceID, - ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion) { - qint64 bytesWritten = 0; +QByteArray AvatarData::packTrait(AvatarTraits::TraitType traitType) const { + QByteArray traitBinaryData; + // Call packer function + if (traitType == AvatarTraits::SkeletonModelURL) { + traitBinaryData = packSkeletonModelURL(); + } + + return traitBinaryData; +} + +QByteArray AvatarData::packTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID traitInstanceID) { + QByteArray traitBinaryData; + + // Call packer function if (traitType == AvatarTraits::AvatarEntity) { - bytesWritten += packAvatarEntityTraitInstance(traitType, traitInstanceID, destination, traitVersion); + traitBinaryData = packAvatarEntityTraitInstance(traitInstanceID); } else if (traitType == AvatarTraits::Grab) { - bytesWritten += packGrabTraitInstance(traitType, traitInstanceID, destination, traitVersion); + traitBinaryData = packGrabTraitInstance(traitInstanceID); } - return bytesWritten; -} - -void AvatarData::prepareResetTraitInstances() { - if (_clientTraitsHandler) { - _avatarEntitiesLock.withReadLock([this]{ - foreach (auto entityID, _packedAvatarEntityData.keys()) { - _clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::AvatarEntity, entityID); - } - foreach (auto grabID, _avatarGrabData.keys()) { - _clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::Grab, grabID); - } - }); - } + return traitBinaryData; } void AvatarData::processTrait(AvatarTraits::TraitType traitType, QByteArray traitBinaryData) { if (traitType == AvatarTraits::SkeletonModelURL) { - // get the URL from the binary data - auto skeletonModelURL = QUrl::fromEncoded(traitBinaryData); - setSkeletonModelURL(skeletonModelURL); + unpackSkeletonModelURL(traitBinaryData); } } @@ -2152,6 +2070,19 @@ void AvatarData::processDeletedTraitInstance(AvatarTraits::TraitType traitType, } } +void AvatarData::prepareResetTraitInstances() { + if (_clientTraitsHandler) { + _avatarEntitiesLock.withReadLock([this]{ + foreach (auto entityID, _packedAvatarEntityData.keys()) { + _clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::AvatarEntity, entityID); + } + foreach (auto grabID, _avatarGrabData.keys()) { + _clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::Grab, grabID); + } + }); + } +} + QByteArray AvatarData::identityByteArray(bool setIsReplicated) const { QByteArray identityData; QDataStream identityStream(&identityData, QIODevice::Append); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 00e7e67923..dd6f0a9efd 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -1134,18 +1134,16 @@ public: // identityChanged returns true if identity has changed, false otherwise. Similarly for displayNameChanged and skeletonModelUrlChange. void processAvatarIdentity(QDataStream& packetStream, bool& identityChanged, bool& displayNameChanged); - qint64 packTrait(AvatarTraits::TraitType traitType, ExtendedIODevice& destination, - AvatarTraits::TraitVersion traitVersion = AvatarTraits::NULL_TRAIT_VERSION); - qint64 packTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID, - ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion = AvatarTraits::NULL_TRAIT_VERSION); - - void prepareResetTraitInstances(); + QByteArray packTrait(AvatarTraits::TraitType traitType) const; + QByteArray packTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID); void processTrait(AvatarTraits::TraitType traitType, QByteArray traitBinaryData); void processTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID, QByteArray traitBinaryData); void processDeletedTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID); + void prepareResetTraitInstances(); + QByteArray identityByteArray(bool setIsReplicated = false) const; QUrl getWireSafeSkeletonModelURL() const; @@ -1596,13 +1594,13 @@ protected: bool hasParent() const { return !getParentID().isNull(); } bool hasFaceTracker() const { return _headData ? _headData->_isFaceTrackerConnected : false; } - qint64 packAvatarEntityTraitInstance(AvatarTraits::TraitType traitType, - AvatarTraits::TraitInstanceID traitInstanceID, - ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion); - qint64 packGrabTraitInstance(AvatarTraits::TraitType traitType, - AvatarTraits::TraitInstanceID traitInstanceID, - ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion); + QByteArray packSkeletonModelURL() const; + QByteArray packAvatarEntityTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID); + QByteArray packGrabTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID); + void unpackSkeletonModelURL(const QByteArray& data); + + // isReplicated will be true on downstream Avatar Mixers and their clients, but false on the upstream "master" // Audio Mixer that the replicated avatar is connected to. bool _isReplicated{ false }; diff --git a/libraries/avatars/src/AvatarTraits.cpp b/libraries/avatars/src/AvatarTraits.cpp new file mode 100644 index 0000000000..724f30e2f3 --- /dev/null +++ b/libraries/avatars/src/AvatarTraits.cpp @@ -0,0 +1,135 @@ +// +// AvatarTraits.cpp +// libraries/avatars/src +// +// Created by Clement Brisset on 3/19/19. +// Copyright 2019 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 +// + +#include "AvatarTraits.h" + +#include + +#include "AvatarData.h" + +namespace AvatarTraits { + + qint64 packTrait(TraitType traitType, ExtendedIODevice& destination, const AvatarData& avatar) { + // Call packer function + auto traitBinaryData = avatar.packTrait(traitType); + auto traitBinaryDataSize = traitBinaryData.size(); + + // Verify packed data + if (traitBinaryDataSize > MAXIMUM_TRAIT_SIZE) { + qWarning() << "Refusing to pack simple trait" << traitType << "of size" << traitBinaryDataSize + << "bytes since it exceeds the maximum size" << MAXIMUM_TRAIT_SIZE << "bytes"; + return 0; + } + + // Write packed data to stream + qint64 bytesWritten = 0; + bytesWritten += destination.writePrimitive((TraitType)traitType); + bytesWritten += destination.writePrimitive((TraitWireSize)traitBinaryDataSize); + bytesWritten += destination.write(traitBinaryData); + return bytesWritten; + } + + qint64 packVersionedTrait(TraitType traitType, ExtendedIODevice& destination, + TraitVersion traitVersion, const AvatarData& avatar) { + // Call packer function + auto traitBinaryData = avatar.packTrait(traitType); + auto traitBinaryDataSize = traitBinaryData.size(); + + // Verify packed data + if (traitBinaryDataSize > MAXIMUM_TRAIT_SIZE) { + qWarning() << "Refusing to pack simple trait" << traitType << "of size" << traitBinaryDataSize + << "bytes since it exceeds the maximum size" << MAXIMUM_TRAIT_SIZE << "bytes"; + return 0; + } + + // Write packed data to stream + qint64 bytesWritten = 0; + bytesWritten += destination.writePrimitive((TraitType)traitType); + bytesWritten += destination.writePrimitive((TraitVersion)traitVersion); + bytesWritten += destination.writePrimitive((TraitWireSize)traitBinaryDataSize); + bytesWritten += destination.write(traitBinaryData); + return bytesWritten; + } + + + qint64 packTraitInstance(TraitType traitType, TraitInstanceID traitInstanceID, + ExtendedIODevice& destination, AvatarData& avatar) { + // Call packer function + auto traitBinaryData = avatar.packTraitInstance(traitType, traitInstanceID); + auto traitBinaryDataSize = traitBinaryData.size(); + + + // Verify packed data + if (traitBinaryDataSize > AvatarTraits::MAXIMUM_TRAIT_SIZE) { + qWarning() << "Refusing to pack instanced trait" << traitType << "of size" << traitBinaryDataSize + << "bytes since it exceeds the maximum size " << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes"; + return 0; + } + + // Write packed data to stream + qint64 bytesWritten = 0; + bytesWritten += destination.writePrimitive((TraitType)traitType); + bytesWritten += destination.write(traitInstanceID.toRfc4122()); + + if (!traitBinaryData.isNull()) { + bytesWritten += destination.writePrimitive((TraitWireSize)traitBinaryDataSize); + bytesWritten += destination.write(traitBinaryData); + } else { + bytesWritten += destination.writePrimitive(AvatarTraits::DELETED_TRAIT_SIZE); + } + + return bytesWritten; + } + + qint64 packVersionedTraitInstance(TraitType traitType, TraitInstanceID traitInstanceID, + ExtendedIODevice& destination, TraitVersion traitVersion, + AvatarData& avatar) { + // Call packer function + auto traitBinaryData = avatar.packTraitInstance(traitType, traitInstanceID); + auto traitBinaryDataSize = traitBinaryData.size(); + + + // Verify packed data + if (traitBinaryDataSize > AvatarTraits::MAXIMUM_TRAIT_SIZE) { + qWarning() << "Refusing to pack instanced trait" << traitType << "of size" << traitBinaryDataSize + << "bytes since it exceeds the maximum size " << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes"; + return 0; + } + + // Write packed data to stream + qint64 bytesWritten = 0; + bytesWritten += destination.writePrimitive((TraitType)traitType); + bytesWritten += destination.writePrimitive((TraitVersion)traitVersion); + bytesWritten += destination.write(traitInstanceID.toRfc4122()); + + if (!traitBinaryData.isNull()) { + bytesWritten += destination.writePrimitive((TraitWireSize)traitBinaryDataSize); + bytesWritten += destination.write(traitBinaryData); + } else { + bytesWritten += destination.writePrimitive(AvatarTraits::DELETED_TRAIT_SIZE); + } + + return bytesWritten; + } + + + qint64 packInstancedTraitDelete(TraitType traitType, TraitInstanceID instanceID, ExtendedIODevice& destination, + TraitVersion traitVersion) { + qint64 bytesWritten = 0; + bytesWritten += destination.writePrimitive(traitType); + if (traitVersion > DEFAULT_TRAIT_VERSION) { + bytesWritten += destination.writePrimitive(traitVersion); + } + bytesWritten += destination.write(instanceID.toRfc4122()); + bytesWritten += destination.writePrimitive(DELETED_TRAIT_SIZE); + return bytesWritten; + } +}; diff --git a/libraries/avatars/src/AvatarTraits.h b/libraries/avatars/src/AvatarTraits.h index 5542acd37f..adff34f351 100644 --- a/libraries/avatars/src/AvatarTraits.h +++ b/libraries/avatars/src/AvatarTraits.h @@ -19,6 +19,9 @@ #include +class ExtendedIODevice; +class AvatarData; + namespace AvatarTraits { enum TraitType : int8_t { // Null trait @@ -36,6 +39,7 @@ namespace AvatarTraits { // Traits count TotalTraitTypes }; + const int NUM_SIMPLE_TRAITS = (int)FirstInstancedTrait; const int NUM_INSTANCED_TRAITS = (int)TotalTraitTypes - (int)FirstInstancedTrait; const int NUM_TRAITS = (int)TotalTraitTypes; @@ -58,22 +62,19 @@ namespace AvatarTraits { const TraitMessageSequence FIRST_TRAIT_SEQUENCE = 0; const TraitMessageSequence MAX_TRAIT_SEQUENCE = INT64_MAX; - inline qint64 packInstancedTraitDelete(TraitType traitType, TraitInstanceID instanceID, ExtendedIODevice& destination, - TraitVersion traitVersion = NULL_TRAIT_VERSION) { - qint64 bytesWritten = 0; + qint64 packTrait(TraitType traitType, ExtendedIODevice& destination, const AvatarData& avatar); + qint64 packVersionedTrait(TraitType traitType, ExtendedIODevice& destination, + TraitVersion traitVersion, const AvatarData& avatar); - bytesWritten += destination.writePrimitive(traitType); + qint64 packTraitInstance(TraitType traitType, TraitInstanceID traitInstanceID, + ExtendedIODevice& destination, AvatarData& avatar); + qint64 packVersionedTraitInstance(TraitType traitType, TraitInstanceID traitInstanceID, + ExtendedIODevice& destination, TraitVersion traitVersion, + AvatarData& avatar); - if (traitVersion > DEFAULT_TRAIT_VERSION) { - bytesWritten += destination.writePrimitive(traitVersion); - } + qint64 packInstancedTraitDelete(TraitType traitType, TraitInstanceID instanceID, ExtendedIODevice& destination, + TraitVersion traitVersion = NULL_TRAIT_VERSION); - bytesWritten += destination.write(instanceID.toRfc4122()); - - bytesWritten += destination.writePrimitive(DELETED_TRAIT_SIZE); - - return bytesWritten; - } }; #endif // hifi_AvatarTraits_h diff --git a/libraries/avatars/src/ClientTraitsHandler.cpp b/libraries/avatars/src/ClientTraitsHandler.cpp index c9e3b67f16..a2d21fed54 100644 --- a/libraries/avatars/src/ClientTraitsHandler.cpp +++ b/libraries/avatars/src/ClientTraitsHandler.cpp @@ -106,7 +106,7 @@ int ClientTraitsHandler::sendChangedTraitsToMixer() { auto traitType = static_cast(std::distance(traitStatusesCopy.simpleCBegin(), simpleIt)); if (initialSend || *simpleIt == Updated) { - bytesWritten += _owningAvatar->packTrait(traitType, *traitsPacketList); + bytesWritten += AvatarTraits::packTrait(traitType, *traitsPacketList, *_owningAvatar); if (traitType == AvatarTraits::SkeletonModelURL) { @@ -125,7 +125,9 @@ int ClientTraitsHandler::sendChangedTraitsToMixer() { || instanceIDValuePair.value == Updated) { // this is a changed trait we need to send or we haven't send out trait information yet // ask the owning avatar to pack it - bytesWritten += _owningAvatar->packTraitInstance(instancedIt->traitType, instanceIDValuePair.id, *traitsPacketList); + bytesWritten += AvatarTraits::packTraitInstance(instancedIt->traitType, instanceIDValuePair.id, + *traitsPacketList, *_owningAvatar); + } else if (!initialSend && instanceIDValuePair.value == Deleted) { // pack delete for this trait instance bytesWritten += AvatarTraits::packInstancedTraitDelete(instancedIt->traitType, instanceIDValuePair.id, From 88a19f26e20e7d5e88e1d2b2c00cadf96e5c5540 Mon Sep 17 00:00:00 2001 From: Clement Date: Wed, 20 Mar 2019 17:04:22 -0700 Subject: [PATCH 394/446] Use process function for overrides --- libraries/avatars/src/AvatarTraits.h | 1 - libraries/avatars/src/ClientTraitsHandler.cpp | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/libraries/avatars/src/AvatarTraits.h b/libraries/avatars/src/AvatarTraits.h index adff34f351..13d64ec225 100644 --- a/libraries/avatars/src/AvatarTraits.h +++ b/libraries/avatars/src/AvatarTraits.h @@ -30,7 +30,6 @@ namespace AvatarTraits { // Simple traits SkeletonModelURL = 0, - // Instanced traits FirstInstancedTrait, AvatarEntity = FirstInstancedTrait, diff --git a/libraries/avatars/src/ClientTraitsHandler.cpp b/libraries/avatars/src/ClientTraitsHandler.cpp index a2d21fed54..f6bd66e89a 100644 --- a/libraries/avatars/src/ClientTraitsHandler.cpp +++ b/libraries/avatars/src/ClientTraitsHandler.cpp @@ -165,11 +165,11 @@ void ClientTraitsHandler::processTraitOverride(QSharedPointer m // override the skeleton URL but do not mark the trait as having changed // so that we don't unecessarily send a new trait packet to the mixer with the overriden URL - auto encodedSkeletonURL = QUrl::fromEncoded(message->readWithoutCopy(traitBinarySize)); auto hasChangesBefore = _hasChangedTraits; - _owningAvatar->setSkeletonModelURL(encodedSkeletonURL); + auto traitBinaryData = message->readWithoutCopy(traitBinarySize); + _owningAvatar->processTrait(traitType, traitBinaryData); // setSkeletonModelURL will flag us for changes to the SkeletonModelURL so we reset some state here to // avoid unnecessarily sending the overriden skeleton model URL back to the mixer From 6cc95fe8ac4f2e8e804add3686e319066abd8140 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 27 Mar 2019 15:46:25 -0700 Subject: [PATCH 395/446] CR --- libraries/entities/src/ModelEntityItem.h | 2 +- libraries/entities/src/ParticleEffectEntityItem.cpp | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/libraries/entities/src/ModelEntityItem.h b/libraries/entities/src/ModelEntityItem.h index 8f5e24ad76..d532fefe7e 100644 --- a/libraries/entities/src/ModelEntityItem.h +++ b/libraries/entities/src/ModelEntityItem.h @@ -175,7 +175,7 @@ protected: QString _textures; - ShapeType _shapeType { SHAPE_TYPE_NONE } ; + ShapeType _shapeType { SHAPE_TYPE_NONE }; private: uint64_t _lastAnimated{ 0 }; diff --git a/libraries/entities/src/ParticleEffectEntityItem.cpp b/libraries/entities/src/ParticleEffectEntityItem.cpp index 5f285ca91b..12119f1466 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.cpp +++ b/libraries/entities/src/ParticleEffectEntityItem.cpp @@ -740,10 +740,7 @@ void ParticleEffectEntityItem::setShapeType(ShapeType type) { } withWriteLock([&] { - if (type != _shapeType) { - _shapeType = type; - _flags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; - } + _shapeType = type; }); } From 40d424a01d686077a114971a6704942bfe8de1b5 Mon Sep 17 00:00:00 2001 From: danteruiz Date: Wed, 27 Mar 2019 16:42:34 -0700 Subject: [PATCH 396/446] avatar fading --- interface/src/avatar/.#AvatarManager.cpp | 1 + interface/src/avatar/AvatarManager.cpp | 26 ++++++++---- interface/src/avatar/AvatarManager.h | 7 ++-- interface/src/avatar/OtherAvatar.cpp | 1 + .../src/avatars-renderer/Avatar.h | 1 + libraries/avatars/src/AvatarData.cpp | 1 + libraries/render/src/render/Scene.cpp | 42 +++++++++++++++++-- libraries/render/src/render/Scene.h | 12 +++++- libraries/render/src/render/Transition.h | 2 +- 9 files changed, 76 insertions(+), 17 deletions(-) create mode 100644 interface/src/avatar/.#AvatarManager.cpp diff --git a/interface/src/avatar/.#AvatarManager.cpp b/interface/src/avatar/.#AvatarManager.cpp new file mode 100644 index 0000000000..b55ef44c07 --- /dev/null +++ b/interface/src/avatar/.#AvatarManager.cpp @@ -0,0 +1 @@ +Dante@DESKTOP-TUOA3HH.30568:1553633650 \ No newline at end of file diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 69f7054953..ea8cdc8105 100755 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -210,7 +210,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { { // lock the hash for read to check the size QReadLocker lock(&_hashLock); - if (_avatarHash.size() < 2 && _avatarsToFadeOut.isEmpty()) { + if (_avatarHash.size() < 2 && _avatarsToFadeOut.empty()) { return; } } @@ -386,8 +386,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { _numAvatarsNotUpdated = numAvatarsNotUpdated; _numHeroAvatarsUpdated = numHerosUpdated; - simulateAvatarFades(deltaTime); - + removeFadedAvatars(); _avatarSimulationTime = (float)(usecTimestampNow() - startTime) / (float)USECS_PER_MSEC; } @@ -400,18 +399,17 @@ void AvatarManager::postUpdate(float deltaTime, const render::ScenePointer& scen } } -void AvatarManager::simulateAvatarFades(float deltaTime) { +void AvatarManager::removeFadedAvatars() { if (_avatarsToFadeOut.empty()) { return; } QReadLocker locker(&_hashLock); - QVector::iterator avatarItr = _avatarsToFadeOut.begin(); + auto avatarItr = _avatarsToFadeOut.begin(); const render::ScenePointer& scene = qApp->getMain3DScene(); render::Transaction transaction; while (avatarItr != _avatarsToFadeOut.end()) { auto avatar = std::static_pointer_cast(*avatarItr); - avatar->updateFadingStatus(); if (!avatar->isFading()) { // fading to zero is such a rare event we push a unique transaction for each if (avatar->isInScene()) { @@ -452,7 +450,6 @@ void AvatarManager::buildPhysicsTransaction(PhysicsEngine::Transaction& transact transaction.objectsToRemove.push_back(mState); } avatar->resetDetailedMotionStates(); - } else { if (avatar->getDetailedMotionStates().size() == 0) { avatar->createDetailedMotionStates(avatar); @@ -541,8 +538,21 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar // remove from node sets, if present DependencyManager::get()->removeFromIgnoreMuteSets(avatar->getSessionUUID()); DependencyManager::get()->avatarDisconnected(avatar->getSessionUUID()); - avatar->fadeOut(qApp->getMain3DScene(), removalReason); + render::Transaction transaction; + auto scene = qApp->getMain3DScene(); + avatar->fadeOut(scene, removalReason); + + AvatarData* avatarData = removedAvatar.get(); + transaction.transitionFinishedOperator(avatar->getRenderItemID(), [avatarDataWeakPtr]() { + auto avatarDataPtr = avatarDataWeakPtr.lock(); + + if (avatarDataPtr) { + auto avatar = std::static_pointer_cast(avatarDataPtr); + avatar->setIsFading(false); + } + }); } + _avatarsToFadeOut.push_back(removedAvatar); } diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 0468fbd809..9dde3a11fb 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -220,10 +220,10 @@ private: explicit AvatarManager(QObject* parent = 0); explicit AvatarManager(const AvatarManager& other); - void simulateAvatarFades(float deltaTime); - AvatarSharedPointer newSharedAvatar(const QUuid& sessionUUID) override; + void removeFadedAvatars(); + // called only from the AvatarHashMap thread - cannot be called while this thread holds the // hash lock, since handleRemovedAvatar needs a write lock on the entity tree and the entity tree // frequently grabs a read lock on the hash to get a given avatar by ID @@ -231,8 +231,7 @@ private: KillAvatarReason removalReason = KillAvatarReason::NoReason) override; void handleTransitAnimations(AvatarTransit::Status status); - QVector _avatarsToFadeOut; - + std::vector _avatarsToFadeOut; using SetOfOtherAvatars = std::set; SetOfOtherAvatars _avatarsToChangeInPhysics; diff --git a/interface/src/avatar/OtherAvatar.cpp b/interface/src/avatar/OtherAvatar.cpp index 11eb6542c4..22ddea14c6 100755 --- a/interface/src/avatar/OtherAvatar.cpp +++ b/interface/src/avatar/OtherAvatar.cpp @@ -50,6 +50,7 @@ OtherAvatar::OtherAvatar(QThread* thread) : Avatar(thread) { } OtherAvatar::~OtherAvatar() { + qDebug() << "-------->"; removeOrb(); } diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 6026367440..1eb760b857 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -463,6 +463,7 @@ public: void fadeIn(render::ScenePointer scene); void fadeOut(render::ScenePointer scene, KillAvatarReason reason); bool isFading() const { return _isFading; } + void setIsFading(bool isFading) { _isFading = isFading; } void updateFadingStatus(); // JSDoc is in AvatarData.h. diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 26407c3564..ee701020b5 100755 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -132,6 +132,7 @@ AvatarData::AvatarData() : } AvatarData::~AvatarData() { + qDebug() << "AvatarData::~AvatarData()"; delete _headData; } diff --git a/libraries/render/src/render/Scene.cpp b/libraries/render/src/render/Scene.cpp index 1850261c99..d3bcfb1f95 100644 --- a/libraries/render/src/render/Scene.cpp +++ b/libraries/render/src/render/Scene.cpp @@ -47,6 +47,10 @@ void Transaction::queryTransitionOnItem(ItemID id, TransitionQueryFunc func) { _queriedTransitions.emplace_back(id, func); } +void Transaction::transitionFinishedOperator(ItemID id, TransitionFinishedFunc func) { + _transitionFinishedOperators.emplace_back(id, func); +} + void Transaction::updateItem(ItemID id, const UpdateFunctorPointer& functor) { _updatedItems.emplace_back(id, functor); } @@ -75,6 +79,7 @@ void Transaction::reserve(const std::vector& transactionContainer) size_t addedTransitionsCount = 0; size_t queriedTransitionsCount = 0; size_t reAppliedTransitionsCount = 0; + size_t transitionFinishedOperatorsCount = 0; size_t highlightResetsCount = 0; size_t highlightRemovesCount = 0; size_t highlightQueriesCount = 0; @@ -85,6 +90,7 @@ void Transaction::reserve(const std::vector& transactionContainer) updatedItemsCount += transaction._updatedItems.size(); resetSelectionsCount += transaction._resetSelections.size(); addedTransitionsCount += transaction._addedTransitions.size(); + transitionFinishedOperatorsCount += transaction._transitionFinishedOperators.size(); queriedTransitionsCount += transaction._queriedTransitions.size(); reAppliedTransitionsCount += transaction._reAppliedTransitions.size(); highlightResetsCount += transaction._highlightResets.size(); @@ -99,6 +105,7 @@ void Transaction::reserve(const std::vector& transactionContainer) _addedTransitions.reserve(addedTransitionsCount); _queriedTransitions.reserve(queriedTransitionsCount); _reAppliedTransitions.reserve(reAppliedTransitionsCount); + _transitionFinishedOperators.reserve(transitionFinishedOperatorsCount); _highlightResets.reserve(highlightResetsCount); _highlightRemoves.reserve(highlightRemovesCount); _highlightQueries.reserve(highlightQueriesCount); @@ -142,6 +149,7 @@ void Transaction::merge(Transaction&& transaction) { moveElements(_resetSelections, transaction._resetSelections); moveElements(_addedTransitions, transaction._addedTransitions); moveElements(_queriedTransitions, transaction._queriedTransitions); + moveElements(_transitionFinishedOperators, transaction._transitionFinishedOperators); moveElements(_reAppliedTransitions, transaction._reAppliedTransitions); moveElements(_highlightResets, transaction._highlightResets); moveElements(_highlightRemoves, transaction._highlightRemoves); @@ -156,6 +164,7 @@ void Transaction::merge(const Transaction& transaction) { copyElements(_addedTransitions, transaction._addedTransitions); copyElements(_queriedTransitions, transaction._queriedTransitions); copyElements(_reAppliedTransitions, transaction._reAppliedTransitions); + copyElements(_transitionFinishedOperators, transaction._transitionFinishedOperators); copyElements(_highlightResets, transaction._highlightResets); copyElements(_highlightRemoves, transaction._highlightRemoves); copyElements(_highlightQueries, transaction._highlightQueries); @@ -168,6 +177,7 @@ void Transaction::clear() { _resetSelections.clear(); _addedTransitions.clear(); _queriedTransitions.clear(); + _transitionFinishedOperators.clear(); _reAppliedTransitions.clear(); _highlightResets.clear(); _highlightRemoves.clear(); @@ -261,6 +271,10 @@ void Scene::processTransactionFrame(const Transaction& transaction) { // Update the numItemsAtomic counter AFTER the reset changes went through _numAllocatedItems.exchange(maxID); + // reset transition finished operator + + resetTransitionFinishedOperator(transaction._transitionFinishedOperators); + // updates updateItems(transaction._updatedItems); @@ -440,6 +454,18 @@ void Scene::queryTransitionItems(const Transaction::TransitionQueries& transacti } } +void Scene::resetTransitionFinishedOperator(const Transaction::TransitionFinishedOperators& transactions) { + for (auto& finishedOperator : transactions) { + auto itemId = std::get<0>(finishedOperator); + const auto& item = _items[itemId]; + auto func = std::get<1>(finishedOperator); + + if (item.exist() && func != nullptr) { + _transitionFinishedOperatorMap[itemId] = func; + } + } +} + void Scene::resetHighlights(const Transaction::HighlightResets& transactions) { auto outlineStage = getStage(HighlightStage::getName()); if (outlineStage) { @@ -528,8 +554,18 @@ void Scene::resetItemTransition(ItemID itemId) { auto& item = _items[itemId]; if (!render::TransitionStage::isIndexInvalid(item.getTransitionId())) { auto transitionStage = getStage(TransitionStage::getName()); - transitionStage->removeTransition(item.getTransitionId()); - setItemTransition(itemId, render::TransitionStage::INVALID_INDEX); + auto transitionItemId = transitionStage->getTransition(item.getTransitionId()).itemId; + + if (transitionItemId == itemId) { + auto transitionFinishedOperator = _transitionFinishedOperatorMap[transitionItemId]; + + if (transitionFinishedOperator) { + transitionFinishedOperator(); + _transitionFinishedOperatorMap[transitionItemId] = nullptr; + } + transitionStage->removeTransition(item.getTransitionId()); + setItemTransition(itemId, render::TransitionStage::INVALID_INDEX); + } } } @@ -587,4 +623,4 @@ void Scene::resetStage(const Stage::Name& name, const StagePointer& stage) { } else { (*found).second = stage; } -} \ No newline at end of file +} diff --git a/libraries/render/src/render/Scene.h b/libraries/render/src/render/Scene.h index f00c74775d..c8eafcb696 100644 --- a/libraries/render/src/render/Scene.h +++ b/libraries/render/src/render/Scene.h @@ -32,12 +32,14 @@ class Scene; // These changes must be expressed through the corresponding command from the Transaction // THe Transaction is then queued on the Scene so all the pending transactions can be consolidated and processed at the time // of updating the scene before it s rendered. -// +// + class Transaction { friend class Scene; public: typedef std::function TransitionQueryFunc; + typedef std::function TransitionFinishedFunc; typedef std::function SelectionHighlightQueryFunc; Transaction() {} @@ -52,6 +54,7 @@ public: void removeTransitionFromItem(ItemID id); void reApplyTransitionToItem(ItemID id); void queryTransitionOnItem(ItemID id, TransitionQueryFunc func); + void transitionFinishedOperator(ItemID id, TransitionFinishedFunc func); template void updateItem(ItemID id, std::function func) { updateItem(id, std::make_shared>(func)); @@ -84,6 +87,7 @@ protected: using Update = std::tuple; using TransitionAdd = std::tuple; using TransitionQuery = std::tuple; + using TransitionFinishedOperator = std::tuple; using TransitionReApply = ItemID; using SelectionReset = Selection; using HighlightReset = std::tuple; @@ -95,6 +99,7 @@ protected: using Updates = std::vector; using TransitionAdds = std::vector; using TransitionQueries = std::vector; + using TransitionFinishedOperators = std::vector; using TransitionReApplies = std::vector; using SelectionResets = std::vector; using HighlightResets = std::vector; @@ -107,6 +112,7 @@ protected: TransitionAdds _addedTransitions; TransitionQueries _queriedTransitions; TransitionReApplies _reAppliedTransitions; + TransitionFinishedOperators _transitionFinishedOperators; SelectionResets _resetSelections; HighlightResets _highlightResets; HighlightRemoves _highlightRemoves; @@ -208,6 +214,7 @@ protected: ItemIDSet _masterNonspatialSet; void resetItems(const Transaction::Resets& transactions); + void resetTransitionFinishedOperator(const Transaction::TransitionFinishedOperators& transactions); void removeItems(const Transaction::Removes& transactions); void updateItems(const Transaction::Updates& transactions); void transitionItems(const Transaction::TransitionAdds& transactions); @@ -223,6 +230,9 @@ protected: mutable std::mutex _selectionsMutex; // mutable so it can be used in the thread safe getSelection const method SelectionMap _selections; + mutable std::mutex _transitionFinishedOperatorMapMutex; + std::unordered_map _transitionFinishedOperatorMap; + void resetSelections(const Transaction::SelectionResets& transactions); // More actions coming to selections soon: // void removeFromSelection(const Selection& selection); diff --git a/libraries/render/src/render/Transition.h b/libraries/render/src/render/Transition.h index 30bda8aa2a..eca41e9d6c 100644 --- a/libraries/render/src/render/Transition.h +++ b/libraries/render/src/render/Transition.h @@ -50,4 +50,4 @@ namespace render { typedef std::vector TransitionTypes; } -#endif // hifi_render_Transition_h \ No newline at end of file +#endif // hifi_render_Transition_h From d9b522d10cb55025e6f8927f72d31f94690b8e96 Mon Sep 17 00:00:00 2001 From: danteruiz Date: Wed, 27 Mar 2019 16:43:03 -0700 Subject: [PATCH 397/446] remove error file --- interface/src/avatar/.#AvatarManager.cpp | 1 - interface/src/avatar/AvatarManager.cpp | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 interface/src/avatar/.#AvatarManager.cpp diff --git a/interface/src/avatar/.#AvatarManager.cpp b/interface/src/avatar/.#AvatarManager.cpp deleted file mode 100644 index b55ef44c07..0000000000 --- a/interface/src/avatar/.#AvatarManager.cpp +++ /dev/null @@ -1 +0,0 @@ -Dante@DESKTOP-TUOA3HH.30568:1553633650 \ No newline at end of file diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index ea8cdc8105..33cd48a047 100755 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -542,7 +542,7 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar auto scene = qApp->getMain3DScene(); avatar->fadeOut(scene, removalReason); - AvatarData* avatarData = removedAvatar.get(); + std::weak_ptr avatarDataWeakPtr = removedAvatar; transaction.transitionFinishedOperator(avatar->getRenderItemID(), [avatarDataWeakPtr]() { auto avatarDataPtr = avatarDataWeakPtr.lock(); From e95efc29e4c955e8f3573e6a6f794a2bdefbdeae Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Wed, 27 Mar 2019 19:00:28 -0700 Subject: [PATCH 398/446] Bug fix after code review. --- tools/nitpick/src/TestRunnerMobile.cpp | 35 +++++++++++++++----------- tools/nitpick/src/TestRunnerMobile.h | 2 ++ 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index a848c94755..969da02c9e 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -114,19 +114,17 @@ void TestRunnerMobile::connectDevice() { QString deviceID = tokens[0]; // Find the model entry - int i; - for (i = 0; i < tokens.size(); ++i) { - if (tokens[i].contains(MODEL)) { - break; - } - } - _modelName = "UNKNOWN"; - if (i < tokens.size()) { - QString modelID = tokens[i].split(':')[1]; + for (int i = 0; i < tokens.size(); ++i) { + if (tokens[i].contains(MODEL)) { + if (i < tokens.size()) { + QString modelID = tokens[i].split(':')[1]; - if (modelNames.count(modelID) == 1) { - _modelName = modelNames[modelID]; + if (modelNames.count(modelID) == 1) { + _modelName = modelNames[modelID]; + } + } + break; } } @@ -225,10 +223,16 @@ void TestRunnerMobile::runInterface() { startCommand = "io.highfidelity.hifiinterface/.PermissionChecker"; } + QString serverIP { getServerIP() }; + if (serverIP == NETWORK_NOT_FOUND) { + _runInterfacePushbutton->setEnabled(false); + return; + } + QString command = _adbInterface->getAdbCommand() + " shell am start -n " + startCommand + " --es args \\\"" + - " --url hifi://" + getServerIP() + "/0,0,0" + " --url hifi://" + serverIP + "/0,0,0" " --no-updater" + " --no-login-suggestion" + " --testScript " + testScript + " quitWhenFinished" + @@ -268,10 +272,10 @@ QString TestRunnerMobile::getServerIP() { QString line = ifconfigFile.readLine(); while (!line.isNull()) { // The device IP is in the line following the "wlan0" line - line = ifconfigFile.readLine(); if (line.left(6) == "wlan0 ") { break; } + line = ifconfigFile.readLine(); } // The following line looks like this "inet addr:192.168.0.15 Bcast:192.168.0.255 Mask:255.255.255.0" @@ -280,8 +284,9 @@ QString TestRunnerMobile::getServerIP() { QStringList lineParts = line.split(':'); if (lineParts.size() < 4) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), - "IP address line not in expected format: " + line); - exit(-1); + "IP address line not in expected format: " + line + "(check that device WIFI is on)"); + + return NETWORK_NOT_FOUND; } qint64 deviceIP = convertToBinary(lineParts[1].split(' ')[0]); diff --git a/tools/nitpick/src/TestRunnerMobile.h b/tools/nitpick/src/TestRunnerMobile.h index b94cd47647..09f847785b 100644 --- a/tools/nitpick/src/TestRunnerMobile.h +++ b/tools/nitpick/src/TestRunnerMobile.h @@ -80,5 +80,7 @@ private: AdbInterface* _adbInterface; QString _modelName; + + QString NETWORK_NOT_FOUND{ "NETWORK NOT FOUND"}; }; #endif From 3d5035886c8533ccce974e59e3db6ff054f65c10 Mon Sep 17 00:00:00 2001 From: Oren Hurvitz Date: Thu, 27 Dec 2018 12:55:52 +0200 Subject: [PATCH 399/446] Allow logging-in with an email that contains a '+' sign. Previously, attempts to login using an email such as "my+name@example.com" didn't work because the username wasn't URL-encoded when it was sent to the server, so on the server the '+' was changed to a space. --- libraries/networking/src/AccountManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 4647c50496..226433e388 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -536,7 +536,7 @@ void AccountManager::requestAccessToken(const QString& login, const QString& pas QByteArray postData; postData.append("grant_type=password&"); - postData.append("username=" + login + "&"); + postData.append("username=" + QUrl::toPercentEncoding(login) + "&"); postData.append("password=" + QUrl::toPercentEncoding(password) + "&"); postData.append("scope=" + ACCOUNT_MANAGER_REQUESTED_SCOPE); From 9349514ff723d0696b36e89f1fae28279e2719bd Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Thu, 28 Mar 2019 10:35:28 -0700 Subject: [PATCH 400/446] make audio screen inputs/outputs unflickable --- interface/resources/qml/hifi/audio/Audio.qml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index 27c7048053..8bec821f34 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -365,6 +365,7 @@ Rectangle { anchors.top: inputDeviceHeader.bottom; anchors.topMargin: 10; x: margins.paddings + interactive: false; height: contentHeight; spacing: 4; clip: true; @@ -456,7 +457,8 @@ Rectangle { ListView { id: outputView width: parent.width - margins.paddings*2 - x: margins.paddings + x: margins.paddings; + interactive: false; height: contentHeight; anchors.top: outputDeviceHeader.bottom; anchors.topMargin: 10; From c3e5c49f693d33c09b7f9d9889f9683e71242373 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Thu, 28 Mar 2019 10:40:39 -0700 Subject: [PATCH 401/446] update muted in AudioScriptingInterface --- interface/resources/qml/hifi/audio/MicBar.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index b6254b168c..9f970faaa9 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -80,7 +80,7 @@ Rectangle { if (pushToTalk) { return; } - muted = !muted; + AudioScriptingInterface.muted = !muted; Tablet.playSound(TabletEnums.ButtonClick); } drag.target: dragTarget; From de1c40e994092e6c56f16434ba5df051c63743eb Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Thu, 28 Mar 2019 11:27:35 -0700 Subject: [PATCH 402/446] Changed version. --- tools/nitpick/src/common.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/nitpick/src/common.h b/tools/nitpick/src/common.h index 09bf23fdfc..51f1f85113 100644 --- a/tools/nitpick/src/common.h +++ b/tools/nitpick/src/common.h @@ -60,5 +60,5 @@ const double R_Y = 0.212655f; const double G_Y = 0.715158f; const double B_Y = 0.072187f; -const QString nitpickVersion{ "21660" }; +const QString nitpickVersion{ "v3.1.5" }; #endif // hifi_common_h \ No newline at end of file From 4ddbdbbb6c05e0a1e523a920ece767a7596b84f5 Mon Sep 17 00:00:00 2001 From: David Back Date: Tue, 26 Mar 2019 18:36:52 -0700 Subject: [PATCH 403/446] fix bone rule warnings --- .../Editor/AvatarExporter/AvatarExporter.cs | 228 ++++++++++++------ tools/unity-avatar-exporter/Assets/README.txt | 2 +- .../avatarExporter.unitypackage | Bin 74729 -> 75752 bytes 3 files changed, 158 insertions(+), 72 deletions(-) diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs index 4e06772f4b..1070449080 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs @@ -17,9 +17,10 @@ using System.Text.RegularExpressions; class AvatarExporter : MonoBehaviour { // update version number for every PR that changes this file, also set updated version in README file - static readonly string AVATAR_EXPORTER_VERSION = "0.4.0"; + static readonly string AVATAR_EXPORTER_VERSION = "0.4.1"; - static readonly float HIPS_GROUND_MIN_Y = 0.01f; + static readonly float HIPS_MIN_Y_PERCENT_OF_HEIGHT = 0.03f; + static readonly float BELOW_GROUND_THRESHOLD_PERCENT_OF_HEIGHT = -0.15f; static readonly float HIPS_SPINE_CHEST_MIN_SEPARATION = 0.001f; static readonly int MAXIMUM_USER_BONE_COUNT = 256; static readonly string EMPTY_WARNING_TEXT = "None"; @@ -231,7 +232,8 @@ class AvatarExporter : MonoBehaviour { HeadMapped, HeadDescendantOfChest, EyesMapped, - HipsNotOnGround, + HipsNotAtBottom, + ExtentsNotBelowGround, HipsSpineChestNotCoincident, TotalBoneCountUnderLimit, AvatarRuleEnd, @@ -247,18 +249,26 @@ class AvatarExporter : MonoBehaviour { class UserBoneInformation { public string humanName; // bone name in Humanoid if it is mapped, otherwise "" public string parentName; // parent user bone name + public BoneTreeNode boneTreeNode; // node within the user bone tree public int mappingCount; // number of times this bone is mapped in Humanoid public Vector3 position; // absolute position public Quaternion rotation; // absolute rotation - public BoneTreeNode boneTreeNode; public UserBoneInformation() { humanName = ""; parentName = ""; + boneTreeNode = new BoneTreeNode(); mappingCount = 0; position = new Vector3(); rotation = new Quaternion(); - boneTreeNode = new BoneTreeNode(); + } + public UserBoneInformation(string parent, BoneTreeNode treeNode, Vector3 pos) { + humanName = ""; + parentName = parent; + boneTreeNode = treeNode; + mappingCount = 0; + position = pos; + rotation = new Quaternion(); } public bool HasHumanMapping() { return !string.IsNullOrEmpty(humanName); } @@ -266,11 +276,13 @@ class AvatarExporter : MonoBehaviour { class BoneTreeNode { public string boneName; + public string parentName; public List children = new List(); public BoneTreeNode() {} - public BoneTreeNode(string name) { + public BoneTreeNode(string name, string parent) { boneName = name; + parentName = parent; } } @@ -732,9 +744,11 @@ class AvatarExporter : MonoBehaviour { // instantiate a game object of the user avatar to traverse the bone tree to gather // bone parents and positions as well as build a bone tree, then destroy it - GameObject assetGameObject = (GameObject)Instantiate(avatarResource); - TraverseUserBoneTree(assetGameObject.transform); - DestroyImmediate(assetGameObject); + GameObject avatarGameObject = (GameObject)Instantiate(avatarResource, Vector3.zero, Quaternion.identity); + TraverseUserBoneTree(avatarGameObject.transform, userBoneTree); + Bounds bounds = AvatarUtilities.GetAvatarBounds(avatarGameObject); + float height = AvatarUtilities.GetAvatarHeight(avatarGameObject); + DestroyImmediate(avatarGameObject); // iterate over Humanoid bones and update user bone info to increase human mapping counts for each bone // as well as set their Humanoid name and build a Humanoid to user bone mapping @@ -753,10 +767,10 @@ class AvatarExporter : MonoBehaviour { } // generate the list of avatar rule failure strings for any avatar rules that are not satisfied by this avatar - SetFailedAvatarRules(); + SetFailedAvatarRules(bounds, height); } - static void TraverseUserBoneTree(Transform modelBone) { + static void TraverseUserBoneTree(Transform modelBone, BoneTreeNode boneTreeNode) { GameObject gameObject = modelBone.gameObject; // check if this transform is a node containing mesh, light, or camera instead of a bone @@ -770,33 +784,52 @@ class AvatarExporter : MonoBehaviour { if (mesh) { Material[] materials = skinnedMeshRenderer != null ? skinnedMeshRenderer.sharedMaterials : meshRenderer.sharedMaterials; StoreMaterialData(materials); + + // ensure branches within the transform hierarchy that contain meshes are removed from the user bone tree + Transform ancestorBone = modelBone; + string previousBoneName = ""; + // find the name of the root child bone that this mesh is underneath + while (ancestorBone != null) { + if (ancestorBone.parent == null) { + break; + } + previousBoneName = ancestorBone.name; + ancestorBone = ancestorBone.parent; + } + // remove the bone tree node from root's children for the root child bone that has mesh children + if (!string.IsNullOrEmpty(previousBoneName)) { + foreach (BoneTreeNode rootChild in userBoneTree.children) { + if (rootChild.boneName == previousBoneName) { + userBoneTree.children.Remove(rootChild); + break; + } + } + } } else if (!light && !camera) { // if it is in fact a bone, add it to the bone tree as well as user bone infos list with position and parent name - UserBoneInformation userBoneInfo = new UserBoneInformation(); - userBoneInfo.position = modelBone.position; // bone's absolute position - string boneName = modelBone.name; if (modelBone.parent == null) { // if no parent then this is actual root bone node of the user avatar, so consider it's parent as "root" boneName = GetRootBoneName(); // ensure we use the root bone name from the skeleton list for consistency - userBoneTree = new BoneTreeNode(boneName); // initialize root of tree - userBoneInfo.parentName = "root"; - userBoneInfo.boneTreeNode = userBoneTree; + boneTreeNode.boneName = boneName; + boneTreeNode.parentName = "root"; } else { // otherwise add this bone node as a child to it's parent's children list // if its a child of the root bone, use the root bone name from the skeleton list as the parent for consistency string parentName = modelBone.parent.parent == null ? GetRootBoneName() : modelBone.parent.name; - BoneTreeNode boneTreeNode = new BoneTreeNode(boneName); - userBoneInfos[parentName].boneTreeNode.children.Add(boneTreeNode); - userBoneInfo.parentName = parentName; + BoneTreeNode node = new BoneTreeNode(boneName, parentName); + boneTreeNode.children.Add(node); + boneTreeNode = node; } + Vector3 bonePosition = modelBone.position; // bone's absolute position in avatar space + UserBoneInformation userBoneInfo = new UserBoneInformation(boneTreeNode.parentName, boneTreeNode, bonePosition); userBoneInfos.Add(boneName, userBoneInfo); } // recurse over transform node's children for (int i = 0; i < modelBone.childCount; ++i) { - TraverseUserBoneTree(modelBone.GetChild(i)); + TraverseUserBoneTree(modelBone.GetChild(i), boneTreeNode); } } @@ -840,7 +873,7 @@ class AvatarExporter : MonoBehaviour { return ""; } - static void SetFailedAvatarRules() { + static void SetFailedAvatarRules(Bounds avatarBounds, float avatarHeight) { failedAvatarRules.Clear(); string hipsUserBone = ""; @@ -905,18 +938,29 @@ class AvatarExporter : MonoBehaviour { break; case AvatarRule.ChestMapped: if (!humanoidToUserBoneMappings.TryGetValue("Chest", out chestUserBone)) { - // check to see if there is a child of Spine that we can suggest to be mapped to Chest - string spineChild = ""; + // check to see if there is an unmapped child of Spine that we can suggest to be mapped to Chest + string chestMappingCandidate = ""; if (!string.IsNullOrEmpty(spineUserBone)) { BoneTreeNode spineTreeNode = userBoneInfos[spineUserBone].boneTreeNode; - if (spineTreeNode.children.Count == 1) { - spineChild = spineTreeNode.children[0].boneName; + foreach (BoneTreeNode spineChildTreeNode in spineTreeNode.children) { + string spineChildBone = spineChildTreeNode.boneName; + if (userBoneInfos[spineChildBone].HasHumanMapping()) { + continue; + } + // a suitable candidate for Chest should have Neck/Head or Shoulder mappings in its descendants + if (IsHumanBoneInHierarchy(spineChildTreeNode, "Neck") || + IsHumanBoneInHierarchy(spineChildTreeNode, "Head") || + IsHumanBoneInHierarchy(spineChildTreeNode, "LeftShoulder") || + IsHumanBoneInHierarchy(spineChildTreeNode, "RightShoulder")) { + chestMappingCandidate = spineChildBone; + break; + } } } failedAvatarRules.Add(avatarRule, "There is no Chest bone mapped in Humanoid for the selected avatar."); // if the only found child of Spine is not yet mapped then add it as a suggestion for Chest mapping - if (!string.IsNullOrEmpty(spineChild) && !userBoneInfos[spineChild].HasHumanMapping()) { - failedAvatarRules[avatarRule] += " It is suggested that you map bone " + spineChild + + if (!string.IsNullOrEmpty(chestMappingCandidate)) { + failedAvatarRules[avatarRule] += " It is suggested that you map bone " + chestMappingCandidate + " to Chest in Humanoid."; } } @@ -949,15 +993,34 @@ class AvatarExporter : MonoBehaviour { } } break; - case AvatarRule.HipsNotOnGround: - // ensure the absolute Y position for the bone mapped to Hips (if its mapped) is at least HIPS_GROUND_MIN_Y + case AvatarRule.HipsNotAtBottom: + // ensure that Hips is not below a proportional percentage of the avatar's height in avatar space if (!string.IsNullOrEmpty(hipsUserBone)) { UserBoneInformation hipsBoneInfo = userBoneInfos[hipsUserBone]; hipsPosition = hipsBoneInfo.position; - if (hipsPosition.y < HIPS_GROUND_MIN_Y) { - failedAvatarRules.Add(avatarRule, "The bone mapped to Hips in Humanoid (" + hipsUserBone + - ") should not be at ground level."); + + // find the lowest y position of the bones + float minBoneYPosition = float.MaxValue; + foreach (var userBoneInfo in userBoneInfos) { + Vector3 position = userBoneInfo.Value.position; + if (position.y < minBoneYPosition) { + minBoneYPosition = position.y; + } } + + // check that Hips is within a percentage of avatar's height from the lowest Y point of the avatar + float bottomYRange = HIPS_MIN_Y_PERCENT_OF_HEIGHT * avatarHeight; + if (Mathf.Abs(hipsPosition.y - minBoneYPosition) < bottomYRange) { + failedAvatarRules.Add(avatarRule, "The bone mapped to Hips in Humanoid (" + hipsUserBone + + ") should not be at the bottom of the selected avatar."); + } + } + break; + case AvatarRule.ExtentsNotBelowGround: + // ensure the minimum Y extent of the model's bounds is not below a proportional threshold of avatar's height + float belowGroundThreshold = BELOW_GROUND_THRESHOLD_PERCENT_OF_HEIGHT * avatarHeight; + if (avatarBounds.min.y < belowGroundThreshold) { + failedAvatarRules.Add(avatarRule, "The bottom extents of the selected avatar go below ground level."); } break; case AvatarRule.HipsSpineChestNotCoincident: @@ -989,6 +1052,23 @@ class AvatarExporter : MonoBehaviour { } } + static bool IsHumanBoneInHierarchy(BoneTreeNode boneTreeNode, string humanBoneName) { + UserBoneInformation userBoneInfo; + if (userBoneInfos.TryGetValue(boneTreeNode.boneName, out userBoneInfo) && userBoneInfo.humanName == humanBoneName) { + // this bone matches the human bone name being searched for + return true; + } + + // recursively check downward through children bones for target human bone + foreach (BoneTreeNode childNode in boneTreeNode.children) { + if (IsHumanBoneInHierarchy(childNode, humanBoneName)) { + return true; + } + } + + return false; + } + static string CheckHumanBoneMappingRule(AvatarRule avatarRule, string humanBoneName) { string userBoneName = ""; // avatar rule fails if bone is not mapped in Humanoid @@ -999,8 +1079,8 @@ class AvatarExporter : MonoBehaviour { return userBoneName; } - static void CheckUserBoneDescendantOfHumanRule(AvatarRule avatarRule, string userBoneName, string descendantOfHumanName) { - if (string.IsNullOrEmpty(userBoneName)) { + static void CheckUserBoneDescendantOfHumanRule(AvatarRule avatarRule, string descendantUserBoneName, string descendantOfHumanName) { + if (string.IsNullOrEmpty(descendantUserBoneName)) { return; } @@ -1009,27 +1089,26 @@ class AvatarExporter : MonoBehaviour { return; } - string userBone = userBoneName; - string ancestorUserBone = ""; - UserBoneInformation userBoneInfo = new UserBoneInformation(); + string userBoneName = descendantUserBoneName; + UserBoneInformation userBoneInfo = userBoneInfos[userBoneName]; + string descendantHumanName = userBoneInfo.humanName; // iterate upward from user bone through user bone info parent names until root // is reached or the ancestor bone name matches the target descendant of name - while (ancestorUserBone != "root") { - if (userBoneInfos.TryGetValue(userBone, out userBoneInfo)) { - ancestorUserBone = userBoneInfo.parentName; - if (ancestorUserBone == descendantOfUserBoneName) { - return; - } - userBone = ancestorUserBone; + while (userBoneName != "root") { + if (userBoneName == descendantOfUserBoneName) { + return; + } + if (userBoneInfos.TryGetValue(userBoneName, out userBoneInfo)) { + userBoneName = userBoneInfo.parentName; } else { break; } } // avatar rule fails if no ancestor of given user bone matched the descendant of name (no early return) - failedAvatarRules.Add(avatarRule, "The bone mapped to " + userBoneInfo.humanName + " in Humanoid (" + userBoneName + - ") is not a child of the bone mapped to " + descendantOfHumanName + " in Humanoid (" + - descendantOfUserBoneName + ")."); + failedAvatarRules.Add(avatarRule, "The bone mapped to " + descendantHumanName + " in Humanoid (" + + descendantUserBoneName + ") is not a descendant of the bone mapped to " + + descendantOfHumanName + " in Humanoid (" + descendantOfUserBoneName + ")."); } static void CheckAsymmetricalMappingRule(AvatarRule avatarRule, string[] mappingSuffixes, string appendage) { @@ -1296,9 +1375,8 @@ class ExportProjectWindow : EditorWindow { const float MAX_SCALE_SLIDER = 2.0f; const int SLIDER_SCALE_EXPONENT = 10; const float ACTUAL_SCALE_OFFSET = 1.0f; - const float DEFAULT_AVATAR_HEIGHT = 1.755f; - const float MAXIMUM_RECOMMENDED_HEIGHT = DEFAULT_AVATAR_HEIGHT * 1.5f; - const float MINIMUM_RECOMMENDED_HEIGHT = DEFAULT_AVATAR_HEIGHT * 0.25f; + const float MAXIMUM_RECOMMENDED_HEIGHT = AvatarUtilities.DEFAULT_AVATAR_HEIGHT * 1.5f; + const float MINIMUM_RECOMMENDED_HEIGHT = AvatarUtilities.DEFAULT_AVATAR_HEIGHT * 0.25f; const float SLIDER_DIFFERENCE_REMOVE_TEXT = 0.01f; readonly Color COLOR_YELLOW = Color.yellow; //new Color(0.9176f, 0.8274f, 0.0f); readonly Color COLOR_BACKGROUND = new Color(0.5f, 0.5f, 0.5f); @@ -1339,9 +1417,9 @@ class ExportProjectWindow : EditorWindow { ShowUtility(); // if the avatar's starting height is outside of the recommended ranges, auto-adjust the scale to default height - float height = GetAvatarHeight(); + float height = AvatarUtilities.GetAvatarHeight(avatarPreviewObject); if (height < MINIMUM_RECOMMENDED_HEIGHT || height > MAXIMUM_RECOMMENDED_HEIGHT) { - float newScale = DEFAULT_AVATAR_HEIGHT / height; + float newScale = AvatarUtilities.DEFAULT_AVATAR_HEIGHT / height; SetAvatarScale(newScale); scaleWarningText = "Avatar's scale automatically adjusted to be within the recommended range."; } @@ -1524,7 +1602,7 @@ class ExportProjectWindow : EditorWindow { void UpdateScaleWarning() { // called on any scale changes - float height = GetAvatarHeight(); + float height = AvatarUtilities.GetAvatarHeight(avatarPreviewObject); if (height < MINIMUM_RECOMMENDED_HEIGHT) { scaleWarningText = "The height of the avatar is below the recommended minimum."; } else if (height > MAXIMUM_RECOMMENDED_HEIGHT) { @@ -1535,23 +1613,6 @@ class ExportProjectWindow : EditorWindow { } } - float GetAvatarHeight() { - // height of an avatar model can be determined to be the max Y extents of the combined bounds for all its mesh renderers - if (avatarPreviewObject != null) { - Bounds bounds = new Bounds(); - var meshRenderers = avatarPreviewObject.GetComponentsInChildren(); - var skinnedMeshRenderers = avatarPreviewObject.GetComponentsInChildren(); - foreach (var renderer in meshRenderers) { - bounds.Encapsulate(renderer.bounds); - } - foreach (var renderer in skinnedMeshRenderers) { - bounds.Encapsulate(renderer.bounds); - } - return bounds.max.y; - } - return 0.0f; - } - void SetAvatarScale(float actualScale) { // set the new scale uniformly on the preview avatar's transform to show the resulting avatar size avatarPreviewObject.transform.localScale = new Vector3(actualScale, actualScale, actualScale); @@ -1571,3 +1632,28 @@ class ExportProjectWindow : EditorWindow { onCloseCallback(); } } + +class AvatarUtilities { + public const float DEFAULT_AVATAR_HEIGHT = 1.755f; + + public static Bounds GetAvatarBounds(GameObject avatarObject) { + Bounds bounds = new Bounds(); + if (avatarObject != null) { + var meshRenderers = avatarObject.GetComponentsInChildren(); + var skinnedMeshRenderers = avatarObject.GetComponentsInChildren(); + foreach (var renderer in meshRenderers) { + bounds.Encapsulate(renderer.bounds); + } + foreach (var renderer in skinnedMeshRenderers) { + bounds.Encapsulate(renderer.bounds); + } + } + return bounds; + } + + public static float GetAvatarHeight(GameObject avatarObject) { + // height of an avatar model can be determined to be the max Y extents of the combined bounds for all its mesh renderers + Bounds avatarBounds = GetAvatarBounds(avatarObject); + return avatarBounds.max.y - avatarBounds.min.y; + } +} diff --git a/tools/unity-avatar-exporter/Assets/README.txt b/tools/unity-avatar-exporter/Assets/README.txt index 410314d8b4..0da0fc0d9d 100644 --- a/tools/unity-avatar-exporter/Assets/README.txt +++ b/tools/unity-avatar-exporter/Assets/README.txt @@ -1,6 +1,6 @@ High Fidelity, Inc. Avatar Exporter -Version 0.4.0 +Version 0.4.1 Note: It is recommended to use Unity versions between 2017.4.15f1 and 2018.2.12f1 for this Avatar Exporter. diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage index 328972736bea1c62716589eb2c0cad5171001f2d..8e7aa0e7aa6032274a682af74d203194f4337538 100644 GIT binary patch literal 75752 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%0AaZF?BT+>&x;;zo5WZdp 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 Date: Thu, 28 Mar 2019 11:28:14 -0700 Subject: [PATCH 404/446] update package --- .../avatarExporter.unitypackage | Bin 75752 -> 75756 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage index 8e7aa0e7aa6032274a682af74d203194f4337538..2ce40a8a8f1f47dbbe8af31472a91ed7ef4a2c22 100644 GIT binary patch delta 61406 zcmV(kK=r@q&jjqx1O*?92naQi1%?WWf}|t?AlwC4oRO6$e?-Lr96!Y-|L69HczeUq z-hf~5e+m$8&xtxpg5c7kFqo8-1Pm$zg-JR}L8ai1P-!XY|BU|^laP@3#sB{d{7w5q zzdHzF4>%eM_($=#{J)r_#INlyDk&i*EiHyCk2Am6FaG~$zzN|F*Fd5?plCBV${T_7 zl;aR1bM`^Nf8;p+*3uwzLb}7?s9&ETavZnd-ehWUC#a7*TFb)=i9*9sa%3DF@SC_B zPpG?rgDcz-?JdU<5Qvldc*9X@P&8DI1DE*q3RCg%gt^1@aYW-!WoA$m0_uszi2=Xh zUyi?Jk|4WEUfqvQlKLbDbpNKjPfkvVLf4|`W4WiY5)HW{}Y$~#sB{l{1yK9x0c2){O^AN zf6K@}h5w05{2BiPN&m9{e+K@D|A|0+p}5&lzxnI^`wJcXKY*wsOjHU65(mL0WMn~5 z2}h`df0!6dMi#gCog8IgGXEj|Cn^3*{O_mWZ`%LYCo$lk!r$=!;?hz->@SH6Mo5CB zaplFOK)=NQehRoPAbPqSqQatN+$IoBj!Qn5IM7gMIUi31`j$9M*b(U=D296y^x+Tgr~DH(g)=T|4|ah5WjO$Ej3)pz+V?NMB!+qgO=wHBKha}KS-mo z3+~J^K$$?@-G41Lc7fu^=^vDqkP_thOBBapU?8b^YC-)sQ~8d}G{Pz%;dfHxsxO zi1aXmd*YN_^MU)|6bp%dpHVal*MtewSr6*<8`=y*cz;&_`_UuA?^_esww29~ZbE(G zFhdm53ywk~aB`gRFPcRhm+`&43H+wIf2Z$n$_a7UsrYz!;Z*%d|E>ty2L=D*DgS-8 zu_M&|d*)5tvw|SUE!_Vf<{CIT;XKEWY!EK{d*$EiQo}t?BHSVF4saOq=jzmhdb|BZ zb^6~Qt=xas5Y;^p-rtSVrl)IpOLEOaA4h{bx1QywNJ|ICqAt^IOpm zqy4_5AZ>7c_NF>_jdpCYFf6=NgP)|>|`yVuF>;-r9afhO=;{3il?uY)vJ{hAN ze~kF|B2zPC_#gJs@;VBE{>{*ToPX{})bD2gM-pcu)c20bib_k$;G$x<^Xk`0DKRlA z2{Azqhwmvd8A(}5Nf}%w6xZ6HZT&xN=tun@^?qCYALD=j=J=n~kMDnQe?&u4O5)eg z|B~Xr#Q%N@{!0AsZ!HZneJC2?`=j>X5%crF9Tq<%8o%{nzeEE6-f{weW4$v=z# ziAnzQ{ohZ)UyJ{V{J}Goum|*?i~{_E|3>`n{f~nCBH(_)UY^c?e-M9t|BFdVii`f~ z{+AXP{bm3E4BR!;*Q6w4fBt@=QfggQGs0a%zW+$@asRs3U$OxJ4uF;##N;O4W)30? z-r-Z|T^h{mR1z58~DJxJ!j!{47oe^8+*zlDtn*vvi4 zf816Fysgt}tTD?^-Ljw3H8ZdyV%<>NAY1IHpFxD5$PUfzsBm%;FtES=Wg z%4yn1y=Bz>6;m@)V^d>*xp+#)dlGl}m`x;;E3?n=lj9pIw-vq5ruk{TmzO$zCU?0$W|K7^2)}!8}Kbs z56yxFuz%EC(_1K<_q)KbEzEYdF?ZrTI@<5~M7dV+6Pl#nYjv=!awc(9pkF3}paT1* zgwxYS{>UcXe+n?)Kq>wVByvrURNty{lUOSWP z#B?Ov@e;F(>sIcMi0fzSqtRcIoJx=E5+T+*?X+M%t?oK%H$b z*p1Wr9fGxJ?-x3*qemPkvyZQTI)a8(W8>~iy`=TrH2 zSP<3)*&i_Eq`ERQ)_G@W@6-3+miG3%HEUSzg@x!0RN$R75WmPF?Z$B06XJn<>LGLs z5yhXue|G20=XrT!*6Xim)K}AC9kuU9b3vh=5Yf`vqKAYJsT4mEW9bvm4P%(9`x@lk zSXy%OtZc1i$e43w@UG7sX|Eg$+nVDwoSdJP8@WdGWvI~;)M`zNO%)M*n-xyF@=hd> z*e#l3i|wey!SV8!g$~VhbGw$$D}$Sbk;@4}e@baj^9)acr1dwy4!R+y6ocoWt@w-f z(ev+6SjOu+Rmb!mu+?&-Mo6cu<<2$wf$+J1NY0PcJ>8a58MEDIn?sy6mHn zf3{wgbuH5RPFMohFeZizc$Cb4g&F83@D{J>kWmz>BOND3WPaoT!d4ZPSNR@~ys3M@ zkVisn5E0UHEFwLSlQvJ_I9plFMX~aUN7hF&`I$31Ue?sG9^?UCs(-^Mz=LS{RD%b2?qQ_M2)tz~H z2Y$%iPOWz0(7L43l}~P8@a_s8oxt@s*perThW89|gsbNDI52wn;q~->9IE1Y{k4}e zA*-U*=ZooF#Ls<@I$ElNJxB8Rr`)s2=?qvaC3gZ?VTP6e{)HP z0slzg`w6<}#@CXq5}3Obl36Tl2k-TpdPYDQf}G^UOGBv_As?tLgVh)?`}(JG~Ayi%q!pKaCHfG(!OMBdHJb~ zybQ^%93;caN`;4PE+44#3(Ts_cxEJ@dhK!TGb)DpwX2Cb+=?qK{@Y>C36?&;1rOXr zpTMaPFN5ogi#f<5imaY;9mQx>$CIHJ)kbE1wKnoDV z#i;jVEHdpQAIYz%Y6v;Tx!YeMR@Fhd(%(g5rG$EA$n9-&A6~Sc`g%A$TGxEMG5PKk zQ{2T3re-uVW3SP~&x}m`f3lw{x|x`5xojcrBWMyE#xoVmePtA)vDIyibrjOEqn=V$ zm%fyx@KSFC9H?73CK*62NIo#qSz4TJM?cot4A;)4PBk2qMX}JHVD48J>4cN z!-4lH(DPvPBscGweqNT>aIc)=@{P4 z=G-N3Hvu*S<0fiYA#%ni*5$wtsiB}s_kjC`{K74tgEV}mgbZ!H`8BiA6yfWWBZTPq zEGorYFA6)pO@-f)J|F$1k@`y$!SwO*dRQUPcz!R8e-fx0LCHBXnCBb(?Zc^^JYeSF z;?*skr$Q6@SfU+UX2u9}YB(V5PJ=ruP}-H(F#bi!L_?gWy5I0IBe~>*yF?UJt3hX# z$J4-L&e^7x#^!4Y{zvFINSk4Z#5aP}lbMgLZGh%E>3z1KbT2_|g)qWaJDBpIfijSW zD_NY)f9WHFTXX*52W}Ql_+pf7cfNO`H~phk&!{%{p8JJeW@JrIKZ%?z2MS@#&o@gBg3(^%^H>7KxtZIRkxBd*25o_FbH;q%pn zVxIap!9+;998a+y*k2jE*An&O!=om; zf1Rxv+vWB0!E>(fF1CsTQsbo88W+}KJx+pjJi+pH<3c{JNTFSQvg zQk#(3UjXCzqKhJT;*4CNTHK@T13rvdf8`OevOG|Fd4Q=aUtpbDt(Iy*lw}3B>|@lI z+~D!!3C)HGW)!910hWR}*L2mPjD_}rQs#}~$CeFvIv07PE?q&0#;DYJelWKfSESiQ z5Wn0#k)NWa3a{Qc(j6LarEkaMbrK~~zSbi9to{DU?Mp!eJUa;~I-3(LrQK}JfBt@% z)$#`=m#|C2MdQ2z=1D5y+I*;Iq7Q-W<{J0(q@Ty@z@_(9S&}48%k*}T>?boTe*Jzk zc>a##TLY-nZi$q|HD+1(GTUPaKm{B(I@$rgb~R>Jegp6&qoNgCa-Ue3EHH@uTvMlK z?NT}u%!SKzIbt>}laoQPo02!re>gOeI;%n5?c(v3K?D9$(vG_kxnEt5>O5^{dU?hI ziC>=_N6&w)eM*j>m!ijdyV808E{lvem^jdmw4Td*{~Y1#F`&4e`c&*)=os^}{vfHM z4AQo@Z&c>qnOs2u`_2ib<(8VDO5B{#n#AAaEco=Uo25=5qHChU6O>L>e`_aBYsm3X zXN&KSx00sC6W-idZ<`I#ViAO1r6GK=LqjD;>67e8_V&&yXypwiUiD4lo&HPdfR&Kg zhx6yh)l*n=YZsr@3|)nSILLi{>NU?lj?*I*^%H%z_$(h zEakU&(P&`FN4JhxudF4~9x(TDP zyMU?S$UB~Ef=vM=)X8oBTg{3^g=2RJzBo{&HoC)ZxFLyvFfv)zfBKkfk0UJK=Pi&X zTG)?Y6qQxiD)G4}zNkW+#W?ojLP1T%k+x|kCcmfRjTb-migy3|aj{O+hb;H7oMxf1 zyCDKIdwFE=%NO|zL`|Y%hdP*NlE<#}F4-F_>`Y&Uh$vje=5R|zcUb$hf2btd;CwDw zaMzu&dv}s!HjYxZe>^ZocPnD3OndMsrCgtrxoa9f60I?4BY(iR+F-R3bd+qM^z6g? zaed%Q)}(|U**`5Z5TSMefBDf%d^4TvvywJyfHw=_ z9dW8F`t6Nv}k#-E}CI zBr252v4{>qqj7ZC?74gOTav9%pSlW{NiTbj;_R|V#uf$G?arglyt8k`VmY!NI)JV1 z!s4tRHzxE3fA=*rQ&%1B3_^)&#^ljw3aWM=+OBKKo00}2x-d7meM_H}PhNcxRdf`Y z%lf>^(@Ey~c^O?RO{Wy8LVf}#WifmHK1?lPuhENAp)_jYE$b-r$Mx+^*yrr8`-Nix zoraGLHOctC@#*hjl0t>N1-cWGlsMoq^tXiWB^MP&e;pB!o!;xnD_zl=X}ntECSvp2 zvhu;&{rLr~^Z>M={!thgHF4^MUT51G5~#bQJg+U1E3W9@>@g&HeVudkO3xKzM1My0Wh*>a z4CR4;fA8q%doE*Zx7vrH6Az*EW^Y3Q4_ZZvxtbN%pGBVQ@{XNCg?4duX=XL1s(M%$ zO{Tst^f&@XiB0en1eYIF4=9;-xKH>K-=rU;C#Am5JM{<~uvqxEzX};(c2m@eLL_8F zD)@?t@OF#kBY}$6ryB_jrSF!C-q5V9FX3OZf67PskVmpouF zbDTUY^}{&EhwqGdFrX?kq3GoGu#^D3;pp?!iaBrZq&l7d?1 zRZYtB&sV0ZREdKuFBE6g5aKzf?Nt=J6H-*)C|j%0jM11FTCb)mVyyiJnE2kTvSLcn$MvYvuI_ROi=pP~8X0rSirj?| z+Z2psD?lWX9NKa<>!2I*DynBB?qpUru}s4to$UmY7a(AvwOvaCdmqSs^&4>zSJK_p zMtd;3SnG?$EwcD1&bt&;^A#^+f6T=4cwUd)dT6#Z^{$-`T<|sRb#qOAIZwMCt?M|% zeygnViw0QXswm`)E*v|^EC`C4ryvkuQX_ZAFV8OaIMozqq=^rY;pOLw;vX{HuK#p@ zeC0TdLQxDp6yid~8VzQhb*5r;EveO>Y%RRkYNN@^d49NovXFJSRW~~de-S|X?!mY| zo7er3NST&Eo#@#{4)N6IV|ZyT-fUfM#cW}WLQkWgv-!b9H8-hPKVfxGJ0s$lN`X7yfguiS}?dhr%rY35Yte`eKM52s%ezH6TSr=NgBaB!+gRq({>LpiZwk*3<(B z=U~i^5)}x4NhV9lLf-dJoKxSQQ+@hsyPi@a1iS=tgUU^Y0m@(Nf0q!F*ht!Q-(zl@ zcyf3-bTo^4ZVY>1NLaCiiB-8l!(w-{LCW;oo2$9!UklJ;STN^dS0=cYXksb+qHjQ3 zKSUw_Pxn*<+wJvm>pkcM< zJYI9;h5P>UkS+54f6Xfxya;Qr>juJM^%aljFMm?cf}B~$xj+TJzTJx=am~-$J=qAU z4|bxqCCoeVCuc@o7-?!Ir|beS1Kx!9s#T=oITqe9HL)4iEs|e7(h88HtZatBa%@X3 zC{vzIO-bPK=6z*$xDn_xe@H;Q&&M-$u?`=d`jUVr{~)Bbf9x*F4Ip*7?z7(I=q;d0xnlj8&ZTm{J@Xl97GKhZK9$9{T_!;1>Mi4Fw_`3nB@X~kAwMB-+B zLL^aB;($ggf13iQ!R5FYjd&#TP9Q=?4MeRzvPJev}}3Rs*3xIEzVG8e_4IaIuT=yd_#RnFI5^4$IlV##TDvc4V+ z!6m=?tsD;V8+;$f4wy`(^xHlRLzOrNMBWStjv-ksf3AdF2}M!m6f)7yI(xfh>0O|> ztx6*}F=&&T8wiRbB6RE7>iI$toMHM^Z9Jg#roUdgK8I-_R!iKoci+E3eUzLyUB76R zZPpf`pXa_<-Ar+)UFvk1;8A>|UzD(bAx24vc1K`!w>%GQ;88&+aUgPqPF8$KJ3ZJc zX?DjUe~I`bj~}KHgay44HNSrV?A91}n$3+LqL^4QEpoAic_t||t${wgxRYK&%X%(t z=UT5gg&I5WC6x}oJ{k@Q`!|=Y0f09voD#sI*Y3*2HNpqbUf7cJDp<7Yc;=GDuWkkw+$tcUmM01|R zA5#y~YxQ z&J}4Y3HPjIHP*Z@**hoS(g{Q;NWdQ1%#Ij&mr4?*26c~hot`Mi4Aqyq6gxL$OjFQtI3&;p+^#A~1<&ET2`@!0UH zFh0Sd;W+c9tL5PHRgf7#f!l%%EHr@DzRRzT6--!Pr9yJwFcu!zjpsrG=$E3+9#8A%OC;YYl&%}dQP9UNms;QC50hVUglRLfWQe=SYxc+m3gf@D zti+ZPU*L#!5;QNmLJUH~AZq@Zvh(hvos4Yr`a^(wTv@dh80<=;>4bmEenG! zYi=%qwKamybyTZ8SW-xT;d1y;L@lJ1k56QuJPj8qO?7+sU?FG-gw^zj(uS>+uwkv{ zSl`AM2#FX*lG=ox;6M3x^YQZV2t7*=iif6)+T#H$0A zeKUWtOk*O?RsjX|kGla28!=A3ugSqzC=DtYXH|u&t6GxOswMS zf7B|Za}Y=2J2w0pt7$e#w^HmBR=V*H6Yp}VqX#h=(q~a7P4Jb5gF+Mg+95MUlUg5{ zy>#iGQ>0|RbsA9lf!DQcx&5-FauD7L5U(f_!n_~u&aY6Dol(8;e|)JkbdW71Ssl;A z3~U(7bHMZXBRIsy)RF?+a!EalQJUF)Gzat$9^y%<*lqY|QtsiSV8#5>TDoBWkFmU- z;WhO2R3gs~uVwPg&V?JV3Dk#+*nM`OIxGn1OCF>(-3vDk0S$&&|G*6pT)w62b-NJIt<-xPbp9LhYZ;+A`;4bkB=;*(>X7Oae)y zgz-5F4e%Yax~*t1XAbb7xqC_^>v#EP&|e!?x=;EdfB08{k42~vJ1ir~;U)4q6Ah~%$%|wscEBy!O0-)h7AnW}e-T zof(1buj9^c_BjglT5X094+smMsD}UqXLO%YPjMgQoB#sl18A-z?87uFSmLh_@81=4 zzkjSb9wBJ*WRMA(LSQi*N0XLeXhw#oIylLle+}gTQo4ldhr6bSK3+_6EF_l#(ci{P zE7-d26rNzla41izayeG%*2fImi`<@&5LbIs%z8g5Gx}g8|9TxaflNU>>Vj2~cVU=q zA?;BLV??s|1!5-tXx#;fcUVZwu^7EIek=ul)pHPhDpahNNb!rhl>{*3m8F00yo0Y% ze=}=&U6N~J!Eky={Uw=u6rCU61D_}!^?!WYveK6GAdz?M!zUqdcz)NqYPehujY9Y9 z$HG82Qw;!bLQ=A}8Q|gKLcz!9Kzs4am`vbQ)Nyp&wT1jSz!8b3u?sWvmZ8y|Q%;6A zwu@*d{AbjxAQNR}KmVn2ti)tyNMC&QfBJgx;M=g~T)jh6eLvoG!bY|h6`Iz%a{D&x zIG)kha>pj!qdYXWuH$oCs8Nzfug<;X@dG1>DvML|sXuZ~#n(>w%_LaZ!%0lfz2@Y> zK29e%IB;W%6U)+A8P%^U?q^dDa(Hi^Sx$<3$C~UGiy4)92nla!xfok|wfO!ef4aw| z=b%nOL`=yf7LW(|FE4OoHu$LVk4C~U3l8cSrp9{#ArDB+ht+m09>8E8%8@}XRfX}y zJj_f-uN(V0@0dS-r=Ly!*%zPjQZ=Z>0Dm80zxj4(imh20#6ZV-n+Nr|=`&Ayy3zU8 z8tz_>$7;edG&1U7K~jimLAHZOf7o-OkAvotukkV+#HljZdcyq0qbt;n_s9CkXBh7j z-&j`3Ti;>uQ>v9J0De;-_(+wD-hKV1G*{N6ngCXJ!^z|1%R{FrWJC;s&vZq5JRl?O zI6&lW+(RF#7d+&-R86k@QhINc@>4)P{Q9&ZJTTw^AjeFlD&|}8<* zOk|}`?PXB*@$)<7920d0AxhfT`1N#x)-oVGW;evAubOU4U@YKZpXa_Fb_T(IbE18U z+_PP&8#A81()|#Gf932G+!uQ`#37CUakuFTQBPJiXRC!HAr%(5GWTi{n-&zXQ-YWM z;BdENa&A1Mr8|8MR?HT%TBJlHLBhb)p1>`}IR9?`P6UT9uo{hEbaD6?vJr8geLKVq zMD8Aem@1Y`CThF>LTl<;&8%Ysz2SugU|x(0=RUn}&6gmGfAw*VD?9N4*b55I16#Xx zeH$vRcjpBT*aa&qXQ+f@RECei)wCNK#u5C7$7mMn>`?iyKw=`>*

F9_?)@(;ZE8 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 From 8627a92f299778de6aa389595ef59e05ae427f0f Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Thu, 28 Mar 2019 11:29:34 -0700 Subject: [PATCH 405/446] adding functions to update variables --- interface/resources/qml/hifi/audio/MicBar.qml | 6 ++++++ interface/resources/qml/hifi/audio/MicBarApplication.qml | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index 9f970faaa9..89a30b4b91 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -34,6 +34,12 @@ Rectangle { muted = AudioScriptingInterface.muted; pushToTalk = AudioScriptingInterface.pushToTalk; }); + AudioScriptingInterface.mutedChanged.connect(function() { + muted = AudioScriptingInterface.muted; + }); + AudioScriptingInterface.pushToTalkChanged.connect(function() { + pushToTalk = AudioScriptingInterface.pushToTalk; + }); } property bool standalone: false; diff --git a/interface/resources/qml/hifi/audio/MicBarApplication.qml b/interface/resources/qml/hifi/audio/MicBarApplication.qml index 509517063d..a39707e052 100644 --- a/interface/resources/qml/hifi/audio/MicBarApplication.qml +++ b/interface/resources/qml/hifi/audio/MicBarApplication.qml @@ -31,6 +31,12 @@ Rectangle { muted = AudioScriptingInterface.muted; pushToTalk = AudioScriptingInterface.pushToTalk; }); + AudioScriptingInterface.mutedChanged.connect(function() { + muted = AudioScriptingInterface.muted; + }); + AudioScriptingInterface.pushToTalkChanged.connect(function() { + pushToTalk = AudioScriptingInterface.pushToTalk; + }); } readonly property string unmutedIcon: "../../../icons/tablet-icons/mic-unmute-i.svg"; From 04d9858f028fd642891fdb2ab43036df35cafb68 Mon Sep 17 00:00:00 2001 From: danteruiz Date: Thu, 28 Mar 2019 11:43:53 -0700 Subject: [PATCH 406/446] fixing remaining issues --- interface/src/avatar/AvatarManager.cpp | 12 ++++-------- interface/src/avatar/OtherAvatar.cpp | 1 - libraries/avatars/src/AvatarData.cpp | 1 - libraries/render/src/render/Scene.cpp | 2 +- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 33cd48a047..aa1847f64b 100755 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -542,15 +542,11 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar auto scene = qApp->getMain3DScene(); avatar->fadeOut(scene, removalReason); - std::weak_ptr avatarDataWeakPtr = removedAvatar; - transaction.transitionFinishedOperator(avatar->getRenderItemID(), [avatarDataWeakPtr]() { - auto avatarDataPtr = avatarDataWeakPtr.lock(); - - if (avatarDataPtr) { - auto avatar = std::static_pointer_cast(avatarDataPtr); - avatar->setIsFading(false); - } + transaction.transitionFinishedOperator(avatar->getRenderItemID(), [avatar]() { + avatar->setIsFading(false); }); + + scene->enqueueTransaction(transaction); } _avatarsToFadeOut.push_back(removedAvatar); diff --git a/interface/src/avatar/OtherAvatar.cpp b/interface/src/avatar/OtherAvatar.cpp index 22ddea14c6..11eb6542c4 100755 --- a/interface/src/avatar/OtherAvatar.cpp +++ b/interface/src/avatar/OtherAvatar.cpp @@ -50,7 +50,6 @@ OtherAvatar::OtherAvatar(QThread* thread) : Avatar(thread) { } OtherAvatar::~OtherAvatar() { - qDebug() << "-------->"; removeOrb(); } diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index ee701020b5..26407c3564 100755 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -132,7 +132,6 @@ AvatarData::AvatarData() : } AvatarData::~AvatarData() { - qDebug() << "AvatarData::~AvatarData()"; delete _headData; } diff --git a/libraries/render/src/render/Scene.cpp b/libraries/render/src/render/Scene.cpp index d3bcfb1f95..0cbb7e1214 100644 --- a/libraries/render/src/render/Scene.cpp +++ b/libraries/render/src/render/Scene.cpp @@ -408,7 +408,7 @@ void Scene::transitionItems(const Transaction::TransitionAdds& transactions) { // Only remove if: // transitioning to something other than none or we're transitioning to none from ELEMENT_LEAVE_DOMAIN or USER_LEAVE_DOMAIN const auto& oldTransitionType = transitionStage->getTransition(transitionId).eventType; - if (transitionType != Transition::NONE || !(oldTransitionType == Transition::ELEMENT_LEAVE_DOMAIN || oldTransitionType == Transition::USER_LEAVE_DOMAIN)) { + if (transitionType == Transition::NONE && oldTransitionType != Transition::NONE) { resetItemTransition(itemId); } } From 7e805562ef3319bb1b796b47e194547e6d86a324 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 28 Mar 2019 11:59:27 -0700 Subject: [PATCH 407/446] Fix some URLs with query params not working with model baker After talking with Sabrina, we decided that we could safely leave the query param and fragment intact for a URL in the model baker. --- libraries/baking/src/baking/BakerLibrary.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/libraries/baking/src/baking/BakerLibrary.cpp b/libraries/baking/src/baking/BakerLibrary.cpp index 2afeef4800..f9445bd432 100644 --- a/libraries/baking/src/baking/BakerLibrary.cpp +++ b/libraries/baking/src/baking/BakerLibrary.cpp @@ -26,11 +26,10 @@ QUrl getBakeableModelURL(const QUrl& url) { GLTF_EXTENSION }; - QUrl cleanURL = url.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); - QString cleanURLString = cleanURL.fileName(); + QString filename = url.fileName(); for (auto& extension : extensionsToBake) { - if (cleanURLString.endsWith(extension, Qt::CaseInsensitive)) { - return cleanURL; + if (filename.endsWith(extension, Qt::CaseInsensitive)) { + return url; } } From d230fc86db509ef1d55243a983983106b5822272 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Thu, 28 Mar 2019 12:23:25 -0700 Subject: [PATCH 408/446] CR comments. --- tools/nitpick/src/TestRunnerMobile.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 969da02c9e..53a74da82f 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -117,12 +117,10 @@ void TestRunnerMobile::connectDevice() { _modelName = "UNKNOWN"; for (int i = 0; i < tokens.size(); ++i) { if (tokens[i].contains(MODEL)) { - if (i < tokens.size()) { - QString modelID = tokens[i].split(':')[1]; + QString modelID = tokens[i].split(':')[1]; - if (modelNames.count(modelID) == 1) { - _modelName = modelNames[modelID]; - } + if (modelNames.count(modelID) == 1) { + _modelName = modelNames[modelID]; } break; } @@ -223,7 +221,7 @@ void TestRunnerMobile::runInterface() { startCommand = "io.highfidelity.hifiinterface/.PermissionChecker"; } - QString serverIP { getServerIP() }; + QString serverIP { getServerIP() }; if (serverIP == NETWORK_NOT_FOUND) { _runInterfacePushbutton->setEnabled(false); return; From 171aebd6aa77b4e5bc818fe79bb2aa44cfd81121 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Thu, 28 Mar 2019 13:04:16 -0700 Subject: [PATCH 409/446] Remove disabling of --url for Android. --- interface/src/Application.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 47f3b774b2..c1489c3265 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3722,14 +3722,11 @@ void Application::handleSandboxStatus(QNetworkReply* reply) { // If this is a first run we short-circuit the address passed in if (_firstRun.get()) { -#if !defined(Q_OS_ANDROID) DependencyManager::get()->goToEntry(); sentTo = SENT_TO_ENTRY; -#endif _firstRun.set(false); } else { -#if !defined(Q_OS_ANDROID) QString goingTo = ""; if (addressLookupString.isEmpty()) { if (Menu::getInstance()->isOptionChecked(MenuOption::HomeLocation)) { @@ -3743,7 +3740,6 @@ void Application::handleSandboxStatus(QNetworkReply* reply) { qCDebug(interfaceapp) << "Not first run... going to" << qPrintable(!goingTo.isEmpty() ? goingTo : addressLookupString); DependencyManager::get()->loadSettings(addressLookupString); sentTo = SENT_TO_PREVIOUS_LOCATION; -#endif } UserActivityLogger::getInstance().logAction("startup_sent_to", { From 88b7687183b1533716bb0cf44d8f53498c0f1608 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Thu, 28 Mar 2019 13:19:16 -0700 Subject: [PATCH 410/446] Case 21726 - Domain lost recent entity edits upon restart When an entity server starts up, grabs the version of the models.json file locally, and then queries the domain server for its copy of the models.json file...iff the domain server version is newer. There was a bug in this process in that the comparison was made between the wrong version, specifically the 'file format version' which doesn't change unless there was a protocol change...and not the data version, which increments every time a change is made to a domain. Therefore, the version of the models.json on the domain server was never downloaded to the entity server, even when it was newer. It would be newer if the entity server assignment was moved to a machine with an old version of the models.json file, which was in fact the case on distributed3 during this period of time. --- domain-server/src/DomainServer.cpp | 10 +++++----- libraries/octree/src/OctreePersistThread.cpp | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 8d5cb165cb..5f82700e9c 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1766,14 +1766,14 @@ void DomainServer::processOctreeDataRequestMessage(QSharedPointerreadPrimitive(&remoteHasExistingData); if (remoteHasExistingData) { constexpr size_t UUID_SIZE_BYTES = 16; auto idData = message->read(UUID_SIZE_BYTES); id = QUuid::fromRfc4122(idData); - message->readPrimitive(&version); - qCDebug(domain_server) << "Entity server does have existing data: ID(" << id << ") DataVersion(" << version << ")"; + message->readPrimitive(&dataVersion); + qCDebug(domain_server) << "Entity server does have existing data: ID(" << id << ") DataVersion(" << dataVersion << ")"; } else { qCDebug(domain_server) << "Entity server does not have existing data"; } @@ -1782,11 +1782,11 @@ void DomainServer::processOctreeDataRequestMessage(QSharedPointerwritePrimitive(false); } else { - qCDebug(domain_server) << "Sending newer octree data to ES: ID(" << data.id << ") DataVersion(" << data.version << ")"; + qCDebug(domain_server) << "Sending newer octree data to ES: ID(" << data.id << ") DataVersion(" << data.dataVersion << ")"; QFile file(entityFilePath); if (file.open(QIODevice::ReadOnly)) { reply->writePrimitive(true); diff --git a/libraries/octree/src/OctreePersistThread.cpp b/libraries/octree/src/OctreePersistThread.cpp index 32ee72ea1c..20ba3cde60 100644 --- a/libraries/octree/src/OctreePersistThread.cpp +++ b/libraries/octree/src/OctreePersistThread.cpp @@ -82,11 +82,11 @@ void OctreePersistThread::start() { } if (data.readOctreeDataInfoFromData(_cachedJSONData)) { - qCDebug(octree) << "Current octree data: ID(" << data.id << ") DataVersion(" << data.version << ")"; + qCDebug(octree) << "Current octree data: ID(" << data.id << ") DataVersion(" << data.dataVersion << ")"; packet->writePrimitive(true); auto id = data.id.toRfc4122(); packet->write(id); - packet->writePrimitive(data.version); + packet->writePrimitive(data.dataVersion); } else { _cachedJSONData.clear(); qCWarning(octree) << "No octree data found"; @@ -144,8 +144,8 @@ void OctreePersistThread::handleOctreeDataFileReply(QSharedPointersetOctreeVersionInfo(data.id, data.version); + qDebug() << "Setting entity version info to: " << data.id << data.dataVersion; + _tree->setOctreeVersionInfo(data.id, data.dataVersion); } bool persistentFileRead; From 5480c9f5ca0f85349d7a2631209df143034298ed Mon Sep 17 00:00:00 2001 From: raveenajain Date: Thu, 28 Mar 2019 15:27:49 -0700 Subject: [PATCH 411/446] skinning, skeleton for models, avatar --- libraries/fbx/src/GLTFSerializer.cpp | 270 +++++++++++++++++++++++---- libraries/fbx/src/GLTFSerializer.h | 2 + 2 files changed, 232 insertions(+), 40 deletions(-) diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp index b8d4e53b65..1b6f5767f4 100755 --- a/libraries/fbx/src/GLTFSerializer.cpp +++ b/libraries/fbx/src/GLTFSerializer.cpp @@ -711,19 +711,19 @@ glm::mat4 GLTFSerializer::getModelTransform(const GLTFNode& node) { node.matrix[12], node.matrix[13], node.matrix[14], node.matrix[15]); } else { - if (node.defined["rotation"] && node.rotation.size() == 4) { - //quat(x,y,z,w) to quat(w,x,y,z) - glm::quat rotquat = glm::quat(node.rotation[3], node.rotation[0], node.rotation[1], node.rotation[2]); - tmat = glm::mat4_cast(rotquat) * tmat; - } - if (node.defined["scale"] && node.scale.size() == 3) { glm::vec3 scale = glm::vec3(node.scale[0], node.scale[1], node.scale[2]); glm::mat4 s = glm::mat4(1.0); s = glm::scale(s, scale); tmat = s * tmat; } - + + if (node.defined["rotation"] && node.rotation.size() == 4) { + //quat(x,y,z,w) to quat(w,x,y,z) + glm::quat rotquat = glm::quat(node.rotation[3], node.rotation[0], node.rotation[1], node.rotation[2]); + tmat = glm::mat4_cast(rotquat) * tmat; + } + if (node.defined["translation"] && node.translation.size() == 3) { glm::vec3 trans = glm::vec3(node.translation[0], node.translation[1], node.translation[2]); glm::mat4 t = glm::mat4(1.0); @@ -734,15 +734,58 @@ glm::mat4 GLTFSerializer::getModelTransform(const GLTFNode& node) { return tmat; } +std::vector> GLTFSerializer::getSkinInverseBindMatrices() { + std::vector> inverseBindMatrixValues; + for (auto &skin : _file.skins) { + GLTFAccessor& indicesAccessor = _file.accessors[skin.inverseBindMatrices]; + GLTFBufferView& indicesBufferview = _file.bufferviews[indicesAccessor.bufferView]; + GLTFBuffer& indicesBuffer = _file.buffers[indicesBufferview.buffer]; + int accBoffset = indicesAccessor.defined["byteOffset"] ? indicesAccessor.byteOffset : 0; + QVector matrices; + addArrayOfType(indicesBuffer.blob, + indicesBufferview.byteOffset + accBoffset, + indicesAccessor.count, + matrices, + indicesAccessor.type, + indicesAccessor.componentType); + inverseBindMatrixValues.push_back(matrices); + } + return inverseBindMatrixValues; +} + +QVector GLTFSerializer::nodeDFS(int n, std::vector& children, bool order) { + QVector result; + result.append(n); + int begin = 0; + int finish = children.size(); + if (order) { + begin = children.size() - 1; + finish = -1; + } + int index = begin; + while (index != finish) { + int c = children[index]; + std::vector nested = _file.nodes[c].children.toStdVector(); + if (nested.size() != 0) { + std::sort(nested.begin(), nested.end()); + result.append(nodeDFS(c, nested, order)); + } else { + result.append(c); + } + begin < finish ? index++ : index--; + } + return result; +} + + bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { + int nodesSize = _file.nodes.size(); //Build dependencies - QVector> nodeDependencies(_file.nodes.size()); + QVector> nodeDependencies(nodesSize); int nodecount = 0; - bool hasChildren = false; foreach(auto &node, _file.nodes) { //nodes_transforms.push_back(getModelTransform(node)); - hasChildren |= !node.children.isEmpty(); foreach(int child, node.children) nodeDependencies[child].push_back(nodecount); nodecount++; } @@ -764,26 +807,99 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { nodecount++; } - - HFMJoint joint; - joint.isSkeletonJoint = true; - joint.bindTransformFoundInCluster = false; - joint.distanceToParent = 0; - joint.parentIndex = -1; - hfmModel.joints.resize(_file.nodes.size()); - hfmModel.jointIndices["x"] = _file.nodes.size(); - int jointInd = 0; - for (auto& node : _file.nodes) { - int size = node.transforms.size(); - if (hasChildren) { size--; } - joint.preTransform = glm::mat4(1); - for (int i = 0; i < size; i++) { - joint.preTransform = node.transforms[i] * joint.preTransform; - } - joint.name = node.name; - hfmModel.joints[jointInd] = joint; - jointInd++; + + + // initialize order in which nodes will be parsed + QVector nodeQueue; + int start = 0; + int end = nodesSize; + if (!_file.scenes[_file.scene].nodes.contains(0)) { + end = -1; + start = nodesSize - 1; } + QVector init = _file.scenes[_file.scene].nodes; + std::sort(init.begin(), init.end()); + int begin = 0; + int finish = init.size(); + if (start > end) { + begin = init.size() - 1; + finish = -1; + } + int index = begin; + while (index != finish) { + int i = init[index]; + std::vector children = _file.nodes[i].children.toStdVector(); + std::sort(children.begin(), children.end()); + nodeQueue.append(nodeDFS(i, children, start > end)); + begin < finish ? index++ : index--; + } + + + // Build joints + HFMJoint joint; + joint.distanceToParent = 0; + hfmModel.jointIndices["x"] = nodesSize; + hfmModel.hasSkeletonJoints = false; + + for (int nodeIndex : nodeQueue) { + auto& node = _file.nodes[nodeIndex]; + + joint.parentIndex = -1; + if (!_file.scenes[_file.scene].nodes.contains(nodeIndex)) { + joint.parentIndex = nodeQueue.indexOf(nodeDependencies[nodeIndex][0]); + } + joint.transform = node.transforms.first(); + joint.postTransform = glm::mat4(); + glm:vec3 scale = extractScale(joint.transform); + joint.postTransform[0][0] = scale.x; + joint.postTransform[1][1] = scale.y; + joint.postTransform[2][2] = scale.z; + joint.rotation = glmExtractRotation(joint.transform); + joint.translation = extractTranslation(joint.transform); + + joint.name = node.name; + hfmModel.joints.push_back(joint); + } + + + // Build skeleton + int matrixIndex = 0; + std::vector> inverseBindValues = getSkinInverseBindMatrices(); + std::vector jointInverseBindTransforms; + jointInverseBindTransforms.resize(nodesSize); + + int jointIndex = end; + while (jointIndex != start) { + start < end ? jointIndex-- : jointIndex++; + int jOffset = nodeQueue[jointIndex]; + auto joint = hfmModel.joints[jointIndex]; + + joint.isSkeletonJoint = false; + if (!_file.skins.isEmpty()) { + hfmModel.hasSkeletonJoints = true; + for (int s = 0; s < _file.skins.size(); s++) { + auto skin = _file.skins[s]; + joint.isSkeletonJoint = skin.joints.contains(jOffset); + + if (joint.isSkeletonJoint) { + QVector value = inverseBindValues[s]; + int matrixCount = 16 * skin.joints.indexOf(jOffset); + jointInverseBindTransforms[jointIndex] = + glm::mat4(value[matrixCount], value[matrixCount + 1], value[matrixCount + 2], value[matrixCount + 3], + value[matrixCount + 4], value[matrixCount + 5], value[matrixCount + 6], value[matrixCount + 7], + value[matrixCount + 8], value[matrixCount + 9], value[matrixCount + 10], value[matrixCount + 11], + value[matrixCount + 12], value[matrixCount + 13], value[matrixCount + 14], value[matrixCount + 15]); + matrixIndex++; + } else { + jointInverseBindTransforms[jointIndex] = glm::mat4(); + } + } + glm::vec3 bindTranslation = extractTranslation(hfmModel.offset * glm::inverse(jointInverseBindTransforms[jointIndex])); + hfmModel.bindExtents.addPoint(bindTranslation); + } + hfmModel.joints[jointIndex] = joint; + } + //Build materials QVector materialIDs; @@ -803,23 +919,34 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { } - - nodecount = 0; // Build meshes - foreach(auto &node, _file.nodes) { + nodecount = 0; + int nodeIndex = start; + while (nodeIndex != end) { + auto& node = _file.nodes[nodeIndex]; if (node.defined["mesh"]) { qCDebug(modelformat) << "node_transforms" << node.transforms; foreach(auto &primitive, _file.meshes[node.mesh].primitives) { hfmModel.meshes.append(HFMMesh()); HFMMesh& mesh = hfmModel.meshes[hfmModel.meshes.size() - 1]; - HFMCluster cluster; - cluster.jointIndex = nodecount; - cluster.inverseBindMatrix = glm::mat4(1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1); - mesh.clusters.append(cluster); + if (!hfmModel.hasSkeletonJoints) { + HFMCluster cluster; + cluster.jointIndex = nodecount; + cluster.inverseBindMatrix = glm::mat4(); + cluster.inverseBindTransform = Transform(cluster.inverseBindMatrix); + mesh.clusters.append(cluster); + } else { + int j = start; + while (j != end) { + HFMCluster cluster; + cluster.jointIndex = j; + cluster.inverseBindMatrix = jointInverseBindTransforms[j]; + cluster.inverseBindTransform = Transform(cluster.inverseBindMatrix); + mesh.clusters.append(cluster); + start < end ? j++ : j--; + } + } HFMMeshPart part = HFMMeshPart(); @@ -848,6 +975,8 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { } QList keys = primitive.attributes.values.keys(); + QVector clusterJoints; + QVector clusterWeights; foreach(auto &key, keys) { int accessorIdx = primitive.attributes.values[key]; @@ -949,8 +1078,68 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { for (int n = 0; n < texcoords.size(); n = n + 2) { mesh.texCoords1.push_back(glm::vec2(texcoords[n], texcoords[n + 1])); } + } else if (key == "JOINTS_0") { + QVector joints; + success = addArrayOfType(buffer.blob, + bufferview.byteOffset + accBoffset, + accessor.count, + joints, + accessor.type, + accessor.componentType); + if (!success) { + qWarning(modelformat) << "There was a problem reading glTF JOINTS_0 data for model " << _url; + continue; + } + for (int n = 0; n < joints.size(); n++) { + clusterJoints.push_back(joints[n]); + } + } else if (key == "WEIGHTS_0") { + QVector weights; + success = addArrayOfType(buffer.blob, + bufferview.byteOffset + accBoffset, + accessor.count, + weights, + accessor.type, + accessor.componentType); + if (!success) { + qWarning(modelformat) << "There was a problem reading glTF WEIGHTS_0 data for model " << _url; + continue; + } + for (int n = 0; n < weights.size(); n++) { + clusterWeights.push_back(weights[n]); + } + } + } + + // adapted from FBXSerializer.cpp + if (hfmModel.hasSkeletonJoints) { + int numClusterIndices = clusterJoints.size(); + const int WEIGHTS_PER_VERTEX = 4; + const float ALMOST_HALF = 0.499f; + int numVertices = mesh.vertices.size(); + mesh.clusterIndices.fill(0, numClusterIndices); + mesh.clusterWeights.fill(0, numClusterIndices); + + for (int c = 0; c < clusterJoints.size(); c++) { + mesh.clusterIndices[c] = _file.skins[node.skin].joints[clusterJoints[c]]; } + // normalize and compress to 16-bits + for (int i = 0; i < numVertices; ++i) { + int j = i * WEIGHTS_PER_VERTEX; + + float totalWeight = 0.0f; + for (int k = j; k < j + WEIGHTS_PER_VERTEX; ++k) { + totalWeight += clusterWeights[k]; + } + const float ALMOST_HALF = 0.499f; + if (totalWeight > 0.0f) { + float weightScalingFactor = (float)(UINT16_MAX) / totalWeight; + for (int k = j; k < j + WEIGHTS_PER_VERTEX; ++k) { + mesh.clusterWeights[k] = (uint16_t)(weightScalingFactor * clusterWeights[k] + ALMOST_HALF); + } + } + } } if (primitive.defined["material"]) { @@ -959,7 +1148,7 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { mesh.parts.push_back(part); // populate the texture coordinates if they don't exist - if (mesh.texCoords.size() == 0) { + if (mesh.texCoords.size() == 0 && !hfmModel.hasSkeletonJoints) { for (int i = 0; i < part.triangleIndices.size(); i++) mesh.texCoords.push_back(glm::vec2(0.0, 1.0)); } mesh.meshExtents.reset(); @@ -973,6 +1162,7 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { } nodecount++; + start < end ? nodeIndex++ : nodeIndex--; } diff --git a/libraries/fbx/src/GLTFSerializer.h b/libraries/fbx/src/GLTFSerializer.h index 05dc526f79..331f5937ed 100755 --- a/libraries/fbx/src/GLTFSerializer.h +++ b/libraries/fbx/src/GLTFSerializer.h @@ -712,6 +712,8 @@ private: hifi::ByteArray _glbBinary; glm::mat4 getModelTransform(const GLTFNode& node); + std::vector> getSkinInverseBindMatrices(); + QVector nodeDFS(int n, std::vector& children, bool order); bool buildGeometry(HFMModel& hfmModel, const hifi::URL& url); bool parseGLTF(const hifi::ByteArray& data); From 44b92c542b2706bd30f77ccd98529fe14b69c099 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Thu, 28 Mar 2019 16:05:24 -0700 Subject: [PATCH 412/446] Case 20499 - Scripts that use AppUI don't call `that.onClosed()` if the script is restarted while the app is open --- scripts/modules/appUi.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/modules/appUi.js b/scripts/modules/appUi.js index 3e8e0b1008..9771348377 100644 --- a/scripts/modules/appUi.js +++ b/scripts/modules/appUi.js @@ -353,10 +353,11 @@ function AppUi(properties) { // Close if necessary, clean up any remaining handlers, and remove the button. GlobalServices.myUsernameChanged.disconnect(restartNotificationPoll); GlobalServices.findableByChanged.disconnect(restartNotificationPoll); + that.tablet.screenChanged.disconnect(that.onScreenChanged); if (that.isOpen) { that.close(); + that.onScreenChanged("", ""); } - that.tablet.screenChanged.disconnect(that.onScreenChanged); if (that.button) { if (that.onClicked) { that.button.clicked.disconnect(that.onClicked); From f2b0e47c39aa56d7277dc237a60a6b497939b50e Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Thu, 28 Mar 2019 16:19:35 -0700 Subject: [PATCH 413/446] changing opacity --- interface/resources/qml/BubbleIcon.qml | 6 +++--- .../resources/qml/hifi/audio/MicBarApplication.qml | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/interface/resources/qml/BubbleIcon.qml b/interface/resources/qml/BubbleIcon.qml index 430eb19860..f4e99f136c 100644 --- a/interface/resources/qml/BubbleIcon.qml +++ b/interface/resources/qml/BubbleIcon.qml @@ -24,9 +24,9 @@ Rectangle { function updateOpacity() { if (ignoreRadiusEnabled) { - bubbleRect.opacity = 0.7; + bubbleRect.opacity = 1.0; } else { - bubbleRect.opacity = 0.3; + bubbleRect.opacity = 0.7; } } @@ -74,7 +74,7 @@ Rectangle { } drag.target: dragTarget; onContainsMouseChanged: { - var rectOpacity = ignoreRadiusEnabled ? (containsMouse ? 0.9 : 0.7) : (containsMouse ? 0.5 : 0.3); + var rectOpacity = (ignoreRadiusEnabled && containsMouse) ? 1.0 : (containsMouse ? 1.0 : 0.7); if (containsMouse) { Tablet.playSound(TabletEnums.ButtonHover); } diff --git a/interface/resources/qml/hifi/audio/MicBarApplication.qml b/interface/resources/qml/hifi/audio/MicBarApplication.qml index a39707e052..6bb418688e 100644 --- a/interface/resources/qml/hifi/audio/MicBarApplication.qml +++ b/interface/resources/qml/hifi/audio/MicBarApplication.qml @@ -51,14 +51,14 @@ Rectangle { height: 44; radius: 5; - opacity: 0.7 + opacity: 0.7; onLevelChanged: { - var rectOpacity = muted && (level >= userSpeakingLevel) ? 0.9 : 0.3; + var rectOpacity = muted && (level >= userSpeakingLevel) ? 1.0 : 0.7; if (pushToTalk && !pushingToTalk) { - rectOpacity = (level >= userSpeakingLevel) ? 0.9 : 0.7; - } else if (mouseArea.containsMouse && rectOpacity != 0.9) { - rectOpacity = 0.5; + rectOpacity = (level >= userSpeakingLevel) ? 1.0 : 0.7; + } else if (mouseArea.containsMouse && rectOpacity != 1.0) { + rectOpacity = 1.0; } micBar.opacity = rectOpacity; } From 3241d2f9602c9ef0d86643323b99cc311dbd897e Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Thu, 28 Mar 2019 16:24:37 -0700 Subject: [PATCH 414/446] don't allow clicking on push to talk in MicBarApplication --- interface/resources/qml/hifi/audio/MicBarApplication.qml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/interface/resources/qml/hifi/audio/MicBarApplication.qml b/interface/resources/qml/hifi/audio/MicBarApplication.qml index 6bb418688e..c19cc54f4a 100644 --- a/interface/resources/qml/hifi/audio/MicBarApplication.qml +++ b/interface/resources/qml/hifi/audio/MicBarApplication.qml @@ -96,6 +96,9 @@ Rectangle { hoverEnabled: true; scrollGestureEnabled: false; onClicked: { + if (pushToTalk) { + return; + } AudioScriptingInterface.muted = !muted; Tablet.playSound(TabletEnums.ButtonClick); muted = Qt.binding(function() { return AudioScriptingInterface.muted; }); // restore binding From b9d04c6ebb48181166da49fa8787faa5b75cd5fc Mon Sep 17 00:00:00 2001 From: danteruiz Date: Thu, 28 Mar 2019 16:42:29 -0700 Subject: [PATCH 415/446] adding entity to a faded list --- interface/src/avatar/MyAvatar.cpp | 2 -- interface/src/avatar/OtherAvatar.cpp | 1 - .../src/avatars-renderer/Avatar.cpp | 12 ------- .../src/avatars-renderer/Avatar.h | 1 - .../src/EntityTreeRenderer.cpp | 11 ++++++ .../src/EntityTreeRenderer.h | 4 +++ .../src/RenderableEntityItem.h | 3 ++ libraries/render/src/render/Scene.cpp | 36 ++++++++++--------- libraries/render/src/render/Scene.h | 3 +- 9 files changed, 38 insertions(+), 35 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 298e661f24..c5175aff73 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -939,8 +939,6 @@ void MyAvatar::simulate(float deltaTime, bool inView) { } handleChangedAvatarEntityData(); - - updateFadingStatus(); } // As far as I know no HMD system supports a play area of a kilometer in radius. diff --git a/interface/src/avatar/OtherAvatar.cpp b/interface/src/avatar/OtherAvatar.cpp index 11eb6542c4..81d88302fe 100755 --- a/interface/src/avatar/OtherAvatar.cpp +++ b/interface/src/avatar/OtherAvatar.cpp @@ -356,7 +356,6 @@ void OtherAvatar::simulate(float deltaTime, bool inView) { PROFILE_RANGE(simulation, "grabs"); applyGrabChanges(); } - updateFadingStatus(); } void OtherAvatar::handleChangedAvatarEntityData() { diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 992ee5db96..96a545fa97 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -682,18 +682,6 @@ void Avatar::fade(render::Transaction& transaction, render::Transition::Type typ _isFading = true; } -void Avatar::updateFadingStatus() { - if (_isFading) { - render::Transaction transaction; - transaction.queryTransitionOnItem(_renderItemID, [this](render::ItemID id, const render::Transition* transition) { - if (!transition || transition->isFinished) { - _isFading = false; - } - }); - AbstractViewStateInterface::instance()->getMain3DScene()->enqueueTransaction(transaction); - } -} - void Avatar::removeFromScene(AvatarSharedPointer self, const render::ScenePointer& scene, render::Transaction& transaction) { transaction.removeItem(_renderItemID); render::Item::clearID(_renderItemID); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 1eb760b857..da04c4adf7 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -464,7 +464,6 @@ public: void fadeOut(render::ScenePointer scene, KillAvatarReason reason); bool isFading() const { return _isFading; } void setIsFading(bool isFading) { _isFading = isFading; } - void updateFadingStatus(); // JSDoc is in AvatarData.h. Q_INVOKABLE virtual float getEyeHeight() const override; diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index c235460404..3eed625916 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -1057,6 +1057,17 @@ void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, bool } } +void EntityTreeRenderable::fadeOutRenderable(const EntityRendererPointer& renderable) { + render::Transaction transaction; + auto scene = qApp->getMain3DScene(); + + transaction.transitionFinishedOperator(renderable->getRenderItemID(), [renderable]() { + renderable->setIsFading(false); + }); + + scene->enqueueTransaction(transaction); +} + void EntityTreeRenderer::playEntityCollisionSound(const EntityItemPointer& entity, const Collision& collision) { assert((bool)entity); auto renderable = renderableForEntity(entity); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index a511d73210..4d6c0e3ba2 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -93,6 +93,9 @@ public: /// reloads the entity scripts, calling unload and preload void reloadEntityScripts(); + void fadeOutRenderable(const EntityRenderablePointer& renderable); + void removeFadedRenderables(); + // event handles which may generate entity related events QUuid mousePressEvent(QMouseEvent* event); void mouseReleaseEvent(QMouseEvent* event); @@ -255,6 +258,7 @@ private: std::unordered_map _renderablesToUpdate; std::unordered_map _entitiesInScene; std::unordered_map _entitiesToAdd; + std::vector _entityRendersToFadeOut; // For Scene.shouldRenderEntities QList _entityIDsLastInScene; diff --git a/libraries/entities-renderer/src/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h index 39f9ad091e..b37e46d02e 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.h +++ b/libraries/entities-renderer/src/RenderableEntityItem.h @@ -44,6 +44,9 @@ public: const EntityItemPointer& getEntity() const { return _entity; } const ItemID& getRenderItemID() const { return _renderItemID; } + bool getIsFading() { return _isFading; } + void setIsFading(bool isFading) { _isFading = isFading; } + const SharedSoundPointer& getCollisionSound() { return _collisionSound; } void setCollisionSound(const SharedSoundPointer& sound) { _collisionSound = sound; } diff --git a/libraries/render/src/render/Scene.cpp b/libraries/render/src/render/Scene.cpp index 0cbb7e1214..ad6523ce11 100644 --- a/libraries/render/src/render/Scene.cpp +++ b/libraries/render/src/render/Scene.cpp @@ -271,10 +271,6 @@ void Scene::processTransactionFrame(const Transaction& transaction) { // Update the numItemsAtomic counter AFTER the reset changes went through _numAllocatedItems.exchange(maxID); - // reset transition finished operator - - resetTransitionFinishedOperator(transaction._transitionFinishedOperators); - // updates updateItems(transaction._updatedItems); @@ -285,6 +281,7 @@ void Scene::processTransactionFrame(const Transaction& transaction) { transitionItems(transaction._addedTransitions); reApplyTransitions(transaction._reAppliedTransitions); queryTransitionItems(transaction._queriedTransitions); + resetTransitionFinishedOperator(transaction._transitionFinishedOperators); // Update the numItemsAtomic counter AFTER the pending changes went through _numAllocatedItems.exchange(maxID); @@ -408,7 +405,7 @@ void Scene::transitionItems(const Transaction::TransitionAdds& transactions) { // Only remove if: // transitioning to something other than none or we're transitioning to none from ELEMENT_LEAVE_DOMAIN or USER_LEAVE_DOMAIN const auto& oldTransitionType = transitionStage->getTransition(transitionId).eventType; - if (transitionType == Transition::NONE && oldTransitionType != Transition::NONE) { + if (transitionType != oldTransitionType) { resetItemTransition(itemId); } } @@ -454,14 +451,19 @@ void Scene::queryTransitionItems(const Transaction::TransitionQueries& transacti } } -void Scene::resetTransitionFinishedOperator(const Transaction::TransitionFinishedOperators& transactions) { - for (auto& finishedOperator : transactions) { +void Scene::resetTransitionFinishedOperator(const Transaction::TransitionFinishedOperators& operators) { + for (auto& finishedOperator : operators) { auto itemId = std::get<0>(finishedOperator); const auto& item = _items[itemId]; auto func = std::get<1>(finishedOperator); if (item.exist() && func != nullptr) { - _transitionFinishedOperatorMap[itemId] = func; + TransitionStage::Index transitionId = item.getTransitionId(); + if (!TransitionStage::isIndexInvalid(transitionId)) { + _transitionFinishedOperatorMap[transitionId].emplace_back(func); + } else { + fucn(); + } } } } @@ -552,20 +554,20 @@ void Scene::setItemTransition(ItemID itemId, Index transitionId) { void Scene::resetItemTransition(ItemID itemId) { auto& item = _items[itemId]; - if (!render::TransitionStage::isIndexInvalid(item.getTransitionId())) { + TransitionStage::Index transitionId = item.getTransitionId(); + if (!render::TransitionStage::isIndexInvalid(transitionId)) { auto transitionStage = getStage(TransitionStage::getName()); - auto transitionItemId = transitionStage->getTransition(item.getTransitionId()).itemId; - if (transitionItemId == itemId) { - auto transitionFinishedOperator = _transitionFinishedOperatorMap[transitionItemId]; + auto finishedOperators = _transitionFinishedOperatorMap[transitionId]; - if (transitionFinishedOperator) { - transitionFinishedOperator(); - _transitionFinishedOperatorMap[transitionItemId] = nullptr; + for (auto finishedOperator : finishedOperators) { + if (finishedOperator) { + finishedOperator(); } - transitionStage->removeTransition(item.getTransitionId()); - setItemTransition(itemId, render::TransitionStage::INVALID_INDEX); } + _transitionFinishedOperatorMap.erase(transitionId); + transitionStage->removeTransition(transitionId); + setItemTransition(itemId, render::TransitionStage::INVALID_INDEX); } } diff --git a/libraries/render/src/render/Scene.h b/libraries/render/src/render/Scene.h index c8eafcb696..e2195cf8c1 100644 --- a/libraries/render/src/render/Scene.h +++ b/libraries/render/src/render/Scene.h @@ -230,8 +230,7 @@ protected: mutable std::mutex _selectionsMutex; // mutable so it can be used in the thread safe getSelection const method SelectionMap _selections; - mutable std::mutex _transitionFinishedOperatorMapMutex; - std::unordered_map _transitionFinishedOperatorMap; + std::unordered_map> _transitionFinishedOperatorMap; void resetSelections(const Transaction::SelectionResets& transactions); // More actions coming to selections soon: From 687409b756081de9997fc4173ee945c7a6827f83 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Fri, 29 Mar 2019 01:46:57 +0100 Subject: [PATCH 416/446] ignore case for .fbx file extension in AvatarDoctor --- interface/src/avatar/AvatarDoctor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/avatar/AvatarDoctor.cpp b/interface/src/avatar/AvatarDoctor.cpp index 43e50ea049..01a40e89fd 100644 --- a/interface/src/avatar/AvatarDoctor.cpp +++ b/interface/src/avatar/AvatarDoctor.cpp @@ -92,7 +92,7 @@ void AvatarDoctor::startDiagnosing() { _model = resource; const auto model = resource.data(); const auto avatarModel = resource.data()->getHFMModel(); - if (!avatarModel.originalURL.endsWith(".fbx")) { + if (!avatarModel.originalURL.toLower().endsWith(".fbx")) { addError("Unsupported avatar model format.", "unsupported-format"); emit complete(getErrors()); return; From 7e21a3f372f6c6ce04dffa621bd36c48f0727ef6 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Fri, 29 Mar 2019 01:48:57 +0100 Subject: [PATCH 417/446] disable case sensitivity in file browsers (allow .fbx .fBx .FBX instead of only .fbx) --- interface/resources/qml/dialogs/FileDialog.qml | 3 ++- interface/resources/qml/dialogs/TabletFileDialog.qml | 3 ++- .../qml/hifi/tablet/tabletWindows/TabletFileDialog.qml | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index ba5e162391..4eea3566b8 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -9,7 +9,7 @@ // import QtQuick 2.7 -import Qt.labs.folderlistmodel 2.1 +import Qt.labs.folderlistmodel 2.2 import Qt.labs.settings 1.0 import QtQuick.Dialogs 1.2 as OriginalDialogs import QtQuick.Controls 1.4 as QQC1 @@ -320,6 +320,7 @@ ModalWindow { FolderListModel { id: folderListModel nameFilters: selectionType.currentFilter + caseSensitive: false showDirsFirst: true showDotAndDotDot: false showFiles: !root.selectDirectory diff --git a/interface/resources/qml/dialogs/TabletFileDialog.qml b/interface/resources/qml/dialogs/TabletFileDialog.qml index 6c4e32dc5a..5bcc42f101 100644 --- a/interface/resources/qml/dialogs/TabletFileDialog.qml +++ b/interface/resources/qml/dialogs/TabletFileDialog.qml @@ -9,7 +9,7 @@ // import QtQuick 2.7 -import Qt.labs.folderlistmodel 2.1 +import Qt.labs.folderlistmodel 2.2 import Qt.labs.settings 1.0 import QtQuick.Dialogs 1.2 as OriginalDialogs import QtQuick.Controls 1.4 as QQC1 @@ -285,6 +285,7 @@ TabletModalWindow { FolderListModel { id: folderListModel nameFilters: selectionType.currentFilter + caseSensitive: false showDirsFirst: true showDotAndDotDot: false showFiles: !root.selectDirectory diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml index a27c7b59dc..36a37134bf 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml @@ -9,7 +9,7 @@ // import QtQuick 2.7 -import Qt.labs.folderlistmodel 2.1 +import Qt.labs.folderlistmodel 2.2 import Qt.labs.settings 1.0 import QtQuick.Dialogs 1.2 as OriginalDialogs import QtQuick.Controls 1.4 as QQC1 @@ -279,6 +279,7 @@ Rectangle { FolderListModel { id: folderListModel nameFilters: selectionType.currentFilter + caseSensitive: false showDirsFirst: true showDotAndDotDot: false showFiles: !root.selectDirectory From bc7fb10ab95c3f648a5bb1bd056568410fd4a593 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Thu, 28 Mar 2019 17:54:35 -0700 Subject: [PATCH 418/446] Fixes from review --- assignment-client/src/avatars/AvatarMixer.cpp | 4 ++-- assignment-client/src/avatars/AvatarMixer.h | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 5f7e197c8f..9816cebf43 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -262,7 +262,7 @@ void AvatarMixer::start() { { if (_dirtyHeroStatus) { _dirtyHeroStatus = false; - nodeList->nestedEach([&](NodeList::const_iterator cbegin, NodeList::const_iterator cend) { + nodeList->nestedEach([](NodeList::const_iterator cbegin, NodeList::const_iterator cend) { std::for_each(cbegin, cend, [](const SharedNodePointer& node) { if (node->getType() == NodeType::Agent) { NodeData* nodeData = node->getLinkedData(); @@ -1108,7 +1108,7 @@ void AvatarMixer::entityAdded(EntityItem* entity) { if (entity->getType() == EntityTypes::Zone) { _dirtyHeroStatus = true; entity->registerChangeHandler([this](const EntityItemID& entityItemID) { - this->entityChange(); + entityChange(); }); } } diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h index f65f04f279..10dff5e8a4 100644 --- a/assignment-client/src/avatars/AvatarMixer.h +++ b/assignment-client/src/avatars/AvatarMixer.h @@ -46,6 +46,11 @@ public slots: void sendStatsPacket() override; + // Avatar zone possibly changed + void entityAdded(EntityItem* entity); + void entityRemoved(EntityItem* entity); + void entityChange(); + private slots: void queueIncomingPacket(QSharedPointer message, SharedNodePointer node); void handleAdjustAvatarSorting(QSharedPointer message, SharedNodePointer senderNode); @@ -147,12 +152,6 @@ private: AvatarMixerSlavePool _slavePool; SlaveSharedData _slaveSharedData; - -public slots: - // Avatar zone possibly changed - void entityAdded(EntityItem* entity); - void entityRemoved(EntityItem* entity); - void entityChange(); }; #endif // hifi_AvatarMixer_h From b283bb303d83fb9bf460216ffc99aedfa46ee1ee Mon Sep 17 00:00:00 2001 From: raveenajain Date: Thu, 28 Mar 2019 17:55:51 -0700 Subject: [PATCH 419/446] jenkins warning fixes --- libraries/fbx/src/GLTFSerializer.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp index 1b6f5767f4..cf8d40cfc1 100755 --- a/libraries/fbx/src/GLTFSerializer.cpp +++ b/libraries/fbx/src/GLTFSerializer.cpp @@ -757,9 +757,9 @@ QVector GLTFSerializer::nodeDFS(int n, std::vector& children, bool ord QVector result; result.append(n); int begin = 0; - int finish = children.size(); + int finish = (int)children.size(); if (order) { - begin = children.size() - 1; + begin = (int)children.size() - 1; finish = -1; } int index = begin; @@ -850,7 +850,7 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { } joint.transform = node.transforms.first(); joint.postTransform = glm::mat4(); - glm:vec3 scale = extractScale(joint.transform); + glm::vec3 scale = extractScale(joint.transform); joint.postTransform[0][0] = scale.x; joint.postTransform[1][1] = scale.y; joint.postTransform[2][2] = scale.z; @@ -1132,7 +1132,6 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { for (int k = j; k < j + WEIGHTS_PER_VERTEX; ++k) { totalWeight += clusterWeights[k]; } - const float ALMOST_HALF = 0.499f; if (totalWeight > 0.0f) { float weightScalingFactor = (float)(UINT16_MAX) / totalWeight; for (int k = j; k < j + WEIGHTS_PER_VERTEX; ++k) { From bcb38b6626f093d12ca0c189a9257871512276ea Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 29 Mar 2019 09:00:25 -0700 Subject: [PATCH 420/446] Add damping to new dynamic entities in Create --- scripts/system/edit.js | 54 +++++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 894ea2b696..7c5af27b82 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -282,6 +282,28 @@ function checkEditPermissionsAndUpdate() { } } +// Copies the properties in `b` into `a`. `a` will be modified. +function copyProperties(a, b) { + for (var key in b) { + a[key] = b[key]; + } + return a; +} + +const DEFAULT_DYNAMIC_PROPERTIES = { + dynamic: true, + damping: 0.39347, + angularDamping: 0.39347, + gravity: { x: 0, y: -9.8, z: 0 }, +}; + +const DEFAULT_NON_DYNAMIC_PROPERTIES = { + dynamic: false, + damping: 0, + angularDamping: 0, + gravity: { x: 0, y: 0, z: 0 }, +}; + const DEFAULT_ENTITY_PROPERTIES = { All: { description: "", @@ -299,26 +321,14 @@ const DEFAULT_ENTITY_PROPERTIES = { y: 0, z: 0 }, - damping: 0, angularVelocity: { x: 0, y: 0, z: 0 }, - angularDamping: 0, restitution: 0.5, friction: 0.5, density: 1000, - gravity: { - x: 0, - y: 0, - z: 0 - }, - acceleration: { - x: 0, - y: 0, - z: 0 - }, dynamic: false, }, Shape: { @@ -484,11 +494,6 @@ var toolBar = (function () { dialogWindow = null, tablet = null; - function applyProperties(originalProperties, newProperties) { - for (var key in newProperties) { - originalProperties[key] = newProperties[key]; - } - } function createNewEntity(requestedProperties) { var dimensions = requestedProperties.dimensions ? requestedProperties.dimensions : DEFAULT_DIMENSIONS; var position = getPositionToCreateEntity(); @@ -496,17 +501,23 @@ var toolBar = (function () { var properties = {}; - applyProperties(properties, DEFAULT_ENTITY_PROPERTIES.All); + copyProperties(properties, DEFAULT_ENTITY_PROPERTIES.All); var type = requestedProperties.type; if (type === "Box" || type === "Sphere") { - applyProperties(properties, DEFAULT_ENTITY_PROPERTIES.Shape); + copyProperties(properties, DEFAULT_ENTITY_PROPERTIES.Shape); } else { - applyProperties(properties, DEFAULT_ENTITY_PROPERTIES[type]); + copyProperties(properties, DEFAULT_ENTITY_PROPERTIES[type]); } // We apply the requested properties first so that they take priority over any default properties. - applyProperties(properties, requestedProperties); + copyProperties(properties, requestedProperties); + + if (properties.dynamic) { + copyProperties(properties, DEFAULT_DYNAMIC_PROPERTIES); + } else { + copyProperties(properties, DEFAULT_NON_DYNAMIC_PROPERTIES); + } if (position !== null && position !== undefined) { @@ -675,7 +686,6 @@ var toolBar = (function () { grabbable: result.grabbable }, dynamic: dynamic, - gravity: dynamic ? { x: 0, y: -10, z: 0 } : { x: 0, y: 0, z: 0 } }); } } From 248f9ba375e07faac10a09dfd9cdbdd675a2b393 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Fri, 29 Mar 2019 10:32:13 -0700 Subject: [PATCH 421/446] adding mute overlay back into HMD and have warn when muted disable in desktop --- .../qml/hifi/audio/MicBarApplication.qml | 7 ++++--- scripts/defaultScripts.js | 3 ++- scripts/system/audioMuteOverlay.js | 16 +--------------- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/interface/resources/qml/hifi/audio/MicBarApplication.qml b/interface/resources/qml/hifi/audio/MicBarApplication.qml index c19cc54f4a..4e0adfd95c 100644 --- a/interface/resources/qml/hifi/audio/MicBarApplication.qml +++ b/interface/resources/qml/hifi/audio/MicBarApplication.qml @@ -18,6 +18,7 @@ import TabletScriptingInterface 1.0 Rectangle { id: micBar; readonly property var level: AudioScriptingInterface.inputLevel; + readonly property var warnWhenMuted: AudioScriptingInterface.warnWhenMuted; readonly property var clipping: AudioScriptingInterface.clipping; property var muted: AudioScriptingInterface.muted; property var pushToTalk: AudioScriptingInterface.pushToTalk; @@ -54,7 +55,7 @@ Rectangle { opacity: 0.7; onLevelChanged: { - var rectOpacity = muted && (level >= userSpeakingLevel) ? 1.0 : 0.7; + var rectOpacity = (muted && (level >= userSpeakingLevel)) && warnWhenMuted ? 1.0 : 0.7; if (pushToTalk && !pushingToTalk) { rectOpacity = (level >= userSpeakingLevel) ? 1.0 : 0.7; } else if (mouseArea.containsMouse && rectOpacity != 1.0) { @@ -163,7 +164,7 @@ Rectangle { Item { id: status; - visible: pushToTalk || (muted && (level >= userSpeakingLevel)); + visible: pushToTalk || (muted && (level >= userSpeakingLevel) && warnWhenMuted); anchors { left: parent.left; @@ -187,7 +188,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - color: pushToTalk ? (pushingToTalk ? colors.unmutedColor : colors.mutedColor) : (level >= userSpeakingLevel && muted) ? colors.mutedColor : colors.unmutedColor; + color: pushToTalk ? (pushingToTalk ? colors.unmutedColor : colors.mutedColor) : (level >= userSpeakingLevel && muted && warnWhenMuted) ? colors.mutedColor : colors.unmutedColor; font.bold: true text: pushToTalk ? (HMD.active ? "PTT" : "PTT-(T)") : (muted ? "MUTED" : "MUTE"); diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index bd7e79dffc..e392680df9 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -32,7 +32,8 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/firstPersonHMD.js", "system/tablet-ui/tabletUI.js", "system/emote.js", - "system/miniTablet.js" + "system/miniTablet.js", + "system/audioMuteOverlay.js" ]; var DEFAULT_SCRIPTS_SEPARATE = [ "system/controllers/controllerScripts.js", diff --git a/scripts/system/audioMuteOverlay.js b/scripts/system/audioMuteOverlay.js index e715e97575..feea604a92 100644 --- a/scripts/system/audioMuteOverlay.js +++ b/scripts/system/audioMuteOverlay.js @@ -58,20 +58,6 @@ parentID: MyAvatar.SELF_ID, parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX") }); - } else { - var textDimensions = { x: 100, y: 50 }; - warningOverlayID = Overlays.addOverlay("text", { - name: "Muted-Warning", - font: { size: 36 }, - text: warningText, - x: (Window.innerWidth - textDimensions.x) / 2, - y: (Window.innerHeight - textDimensions.y), - width: textDimensions.x, - height: textDimensions.y, - textColor: { red: 226, green: 51, blue: 77 }, - backgroundAlpha: 0, - visible: true - }); } } @@ -141,4 +127,4 @@ Audio.mutedChanged.connect(startOrStopPoll); Audio.warnWhenMutedChanged.connect(startOrStopPoll); -}()); // END LOCAL_SCOPE \ No newline at end of file +}()); // END LOCAL_SCOPE From 3c65b92ff50127672c271e8e05a84b17c8e1aaa2 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Fri, 29 Mar 2019 10:41:16 -0700 Subject: [PATCH 422/446] moving text position higher --- scripts/system/audioMuteOverlay.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/audioMuteOverlay.js b/scripts/system/audioMuteOverlay.js index feea604a92..9acc5ab123 100644 --- a/scripts/system/audioMuteOverlay.js +++ b/scripts/system/audioMuteOverlay.js @@ -43,7 +43,7 @@ if (HMD.active) { warningOverlayID = Overlays.addOverlay("text3d", { name: "Muted-Warning", - localPosition: { x: 0.0, y: -0.5, z: -1.0 }, + localPosition: { x: 0.0, y: -0.45, z: -1.0 }, localOrientation: Quat.fromVec3Degrees({ x: 0.0, y: 0.0, z: 0.0, w: 1.0 }), text: warningText, textAlpha: 1, From 26224087924d765da046035b8f4ed4df6de5595f Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Fri, 29 Mar 2019 10:45:40 -0700 Subject: [PATCH 423/446] warn when muted only affects hmd --- interface/resources/qml/hifi/audio/Audio.qml | 4 ++-- interface/resources/qml/hifi/audio/MicBarApplication.qml | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index 8bec821f34..f7e2494813 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -228,7 +228,7 @@ Rectangle { anchors.top: noiseReductionSwitch.bottom anchors.topMargin: 24 anchors.left: parent.left - labelTextOn: qsTr("Push To Talk (T)"); + labelTextOn: (bar.currentIndex === 0) ? qsTr("Push To Talk (T)") : qsTr("Push To Talk"); labelTextSize: 16; backgroundOnColor: "#E3E3E3"; checked: (bar.currentIndex === 0) ? AudioScriptingInterface.pushToTalkDesktop : AudioScriptingInterface.pushToTalkHMD; @@ -254,7 +254,7 @@ Rectangle { switchWidth: root.switchWidth; anchors.top: parent.top anchors.left: parent.left - labelTextOn: qsTr("Warn when muted"); + labelTextOn: qsTr("Warn when muted in HMD"); labelTextSize: 16; backgroundOnColor: "#E3E3E3"; checked: AudioScriptingInterface.warnWhenMuted; diff --git a/interface/resources/qml/hifi/audio/MicBarApplication.qml b/interface/resources/qml/hifi/audio/MicBarApplication.qml index 4e0adfd95c..70bded0fc6 100644 --- a/interface/resources/qml/hifi/audio/MicBarApplication.qml +++ b/interface/resources/qml/hifi/audio/MicBarApplication.qml @@ -18,7 +18,6 @@ import TabletScriptingInterface 1.0 Rectangle { id: micBar; readonly property var level: AudioScriptingInterface.inputLevel; - readonly property var warnWhenMuted: AudioScriptingInterface.warnWhenMuted; readonly property var clipping: AudioScriptingInterface.clipping; property var muted: AudioScriptingInterface.muted; property var pushToTalk: AudioScriptingInterface.pushToTalk; @@ -55,7 +54,7 @@ Rectangle { opacity: 0.7; onLevelChanged: { - var rectOpacity = (muted && (level >= userSpeakingLevel)) && warnWhenMuted ? 1.0 : 0.7; + var rectOpacity = (muted && (level >= userSpeakingLevel)) ? 1.0 : 0.7; if (pushToTalk && !pushingToTalk) { rectOpacity = (level >= userSpeakingLevel) ? 1.0 : 0.7; } else if (mouseArea.containsMouse && rectOpacity != 1.0) { @@ -164,7 +163,7 @@ Rectangle { Item { id: status; - visible: pushToTalk || (muted && (level >= userSpeakingLevel) && warnWhenMuted); + visible: pushToTalk || (muted && (level >= userSpeakingLevel); anchors { left: parent.left; @@ -188,7 +187,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - color: pushToTalk ? (pushingToTalk ? colors.unmutedColor : colors.mutedColor) : (level >= userSpeakingLevel && muted && warnWhenMuted) ? colors.mutedColor : colors.unmutedColor; + color: pushToTalk ? (pushingToTalk ? colors.unmutedColor : colors.mutedColor) : (level >= userSpeakingLevel && muted) ? colors.mutedColor : colors.unmutedColor; font.bold: true text: pushToTalk ? (HMD.active ? "PTT" : "PTT-(T)") : (muted ? "MUTED" : "MUTE"); From 709515dd7408d3283dedc00098bdfb85e2b94dd2 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 29 Mar 2019 11:48:15 -0700 Subject: [PATCH 424/446] Use place holder instead of RCC file. --- tools/nitpick/compiledResources/.placeholder | 0 tools/nitpick/compiledResources/resources.rcc | Bin 42 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tools/nitpick/compiledResources/.placeholder delete mode 100644 tools/nitpick/compiledResources/resources.rcc diff --git a/tools/nitpick/compiledResources/.placeholder b/tools/nitpick/compiledResources/.placeholder new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/nitpick/compiledResources/resources.rcc b/tools/nitpick/compiledResources/resources.rcc deleted file mode 100644 index 15f51ed7f4d9aa2328eca21473fd352cf3605021..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42 dcmXRcN-bt!U|?ckU=TsV5D^ey1d|L53;<0C0sQ~~ From db17f094dab32fdd822eefbd77c32e26d582a187 Mon Sep 17 00:00:00 2001 From: danteruiz Date: Fri, 29 Mar 2019 15:23:16 -0700 Subject: [PATCH 425/446] adding entities to fade out list; --- interface/src/avatar/AvatarManager.cpp | 5 +- .../src/avatars-renderer/Avatar.cpp | 4 +- .../src/avatars-renderer/Avatar.h | 2 +- .../src/EntityTreeRenderer.cpp | 50 +++++++++++++------ .../src/EntityTreeRenderer.h | 6 ++- .../src/RenderableEntityItem.cpp | 8 ++- libraries/render/src/render/Scene.cpp | 6 +-- libraries/render/src/render/Scene.h | 3 +- 8 files changed, 51 insertions(+), 33 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index aa1847f64b..fea0652964 100755 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -531,7 +531,7 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar // it might not fire until after we create a new instance for the same remote avatar, which creates a race // on the creation of entities for that avatar instance and the deletion of entities for this instance avatar->removeAvatarEntitiesFromTree(); - + avatar->setIsFading(false); if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble) { emit DependencyManager::get()->enteredIgnoreRadius(); } else if (removalReason == KillAvatarReason::AvatarDisconnected) { @@ -540,12 +540,11 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar DependencyManager::get()->avatarDisconnected(avatar->getSessionUUID()); render::Transaction transaction; auto scene = qApp->getMain3DScene(); - avatar->fadeOut(scene, removalReason); + avatar->fadeOut(transaction, removalReason); transaction.transitionFinishedOperator(avatar->getRenderItemID(), [avatar]() { avatar->setIsFading(false); }); - scene->enqueueTransaction(transaction); } diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 96a545fa97..dccf37c5b8 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -659,9 +659,8 @@ void Avatar::fadeIn(render::ScenePointer scene) { scene->enqueueTransaction(transaction); } -void Avatar::fadeOut(render::ScenePointer scene, KillAvatarReason reason) { +void Avatar::fadeOut(render::Transaction& transaction, KillAvatarReason reason) { render::Transition::Type transitionType = render::Transition::USER_LEAVE_DOMAIN; - render::Transaction transaction; if (reason == KillAvatarReason::YourAvatarEnteredTheirBubble) { transitionType = render::Transition::BUBBLE_ISECT_TRESPASSER; @@ -669,7 +668,6 @@ void Avatar::fadeOut(render::ScenePointer scene, KillAvatarReason reason) { transitionType = render::Transition::BUBBLE_ISECT_OWNER; } fade(transaction, transitionType); - scene->enqueueTransaction(transaction); } void Avatar::fade(render::Transaction& transaction, render::Transition::Type type) { diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index da04c4adf7..b4cad4f967 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -461,7 +461,7 @@ public: bool isMoving() const { return _moving; } void fadeIn(render::ScenePointer scene); - void fadeOut(render::ScenePointer scene, KillAvatarReason reason); + void fadeOut(render::Transaction& transaction, KillAvatarReason reason); bool isFading() const { return _isFading; } void setIsFading(bool isFading) { _isFading = isFading; } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 3eed625916..2701467a2d 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -214,6 +214,30 @@ void EntityTreeRenderer::stopDomainAndNonOwnedEntities() { } } +void EntityTreeRenderer::removeFadedRenderables() { + if (_entityRenderablesToFadeOut.empty()) { + return; + } + + std::unique_lock lock(_entitiesToFadeLock); + auto entityIter = _entityRenderablesToFadeOut.begin(); + auto scene = _viewState->getMain3DScene(); + render::Transaction transaction; + + while (entityIter != _entityRenderablesToFadeOut.end()) { + auto entityRenderable = *entityIter; + + if (!entityRenderable->getIsFading()) { + entityRenderable->removeFromScene(scene, transaction); + entityIter = _entityRenderablesToFadeOut.erase(entityIter); + } else { + ++entityIter; + } + } + + scene->enqueueTransaction(transaction); +} + void EntityTreeRenderer::clearDomainAndNonOwnedEntities() { stopDomainAndNonOwnedEntities(); @@ -221,17 +245,15 @@ void EntityTreeRenderer::clearDomainAndNonOwnedEntities() { // remove all entities from the scene auto scene = _viewState->getMain3DScene(); if (scene) { - render::Transaction transaction; for (const auto& entry : _entitiesInScene) { const auto& renderer = entry.second; const EntityItemPointer& entityItem = renderer->getEntity(); if (!(entityItem->isLocalEntity() || (entityItem->isAvatarEntity() && entityItem->getOwningAvatarID() == getTree()->getMyAvatarSessionUUID()))) { - renderer->removeFromScene(scene, transaction); + fadeOutRenderable(renderer); } else { savedEntities[entry.first] = entry.second; } } - scene->enqueueTransaction(transaction); } _renderablesToUpdate = savedEntities; @@ -258,12 +280,10 @@ void EntityTreeRenderer::clear() { // remove all entities from the scene auto scene = _viewState->getMain3DScene(); if (scene) { - render::Transaction transaction; for (const auto& entry : _entitiesInScene) { const auto& renderer = entry.second; - renderer->removeFromScene(scene, transaction); + fadeOutRenderable(renderer); } - scene->enqueueTransaction(transaction); } else { qCWarning(entitiesrenderer) << "EntitityTreeRenderer::clear(), Unexpected null scene, possibly during application shutdown"; } @@ -531,6 +551,7 @@ void EntityTreeRenderer::update(bool simulate) { } } + removeFadedRenderables(); } void EntityTreeRenderer::handleSpaceUpdate(std::pair proxyUpdate) { @@ -1016,10 +1037,7 @@ void EntityTreeRenderer::deletingEntity(const EntityItemID& entityID) { forceRecheckEntities(); // reset our state to force checking our inside/outsideness of entities - // here's where we remove the entity payload from the scene - render::Transaction transaction; - renderable->removeFromScene(scene, transaction); - scene->enqueueTransaction(transaction); + fadeOutRenderable(renderable); } void EntityTreeRenderer::addingEntity(const EntityItemID& entityID) { @@ -1057,24 +1075,26 @@ void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, bool } } -void EntityTreeRenderable::fadeOutRenderable(const EntityRendererPointer& renderable) { +void EntityTreeRenderer::fadeOutRenderable(const EntityRendererPointer& renderable) { render::Transaction transaction; - auto scene = qApp->getMain3DScene(); + auto scene = _viewState->getMain3DScene(); + renderable->setIsFading(true); transaction.transitionFinishedOperator(renderable->getRenderItemID(), [renderable]() { renderable->setIsFading(false); }); scene->enqueueTransaction(transaction); + _entityRenderablesToFadeOut.push_back(renderable); } void EntityTreeRenderer::playEntityCollisionSound(const EntityItemPointer& entity, const Collision& collision) { assert((bool)entity); auto renderable = renderableForEntity(entity); - if (!renderable) { - return; + if (!renderable) { + return; } - + SharedSoundPointer collisionSound = renderable->getCollisionSound(); if (!collisionSound) { return; diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 4d6c0e3ba2..32504abd56 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -93,7 +93,7 @@ public: /// reloads the entity scripts, calling unload and preload void reloadEntityScripts(); - void fadeOutRenderable(const EntityRenderablePointer& renderable); + void fadeOutRenderable(const EntityRendererPointer& renderable); void removeFadedRenderables(); // event handles which may generate entity related events @@ -258,7 +258,9 @@ private: std::unordered_map _renderablesToUpdate; std::unordered_map _entitiesInScene; std::unordered_map _entitiesToAdd; - std::vector _entityRendersToFadeOut; + + std::mutex _entitiesToFadeLock; + std::vector _entityRenderablesToFadeOut; // For Scene.shouldRenderEntities QList _entityIDsLastInScene; diff --git a/libraries/entities-renderer/src/RenderableEntityItem.cpp b/libraries/entities-renderer/src/RenderableEntityItem.cpp index a6826da91b..e4e135cd7f 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableEntityItem.cpp @@ -148,7 +148,7 @@ EntityRenderer::EntityRenderer(const EntityItemPointer& entity) : _created(entit }); } -EntityRenderer::~EntityRenderer() { } +EntityRenderer::~EntityRenderer() {} // // Smart payload proxy members, implementing the payload interface @@ -418,9 +418,7 @@ void EntityRenderer::doRenderUpdateSynchronous(const ScenePointer& scene, Transa if (fading || _prevIsTransparent != transparent) { emit requestRenderUpdate(); } - if (fading) { - _isFading = Interpolate::calculateFadeRatio(_fadeStartTime) < 1.0f; - } + _prevIsTransparent = transparent; updateModelTransformAndBound(); @@ -493,4 +491,4 @@ glm::vec4 EntityRenderer::calculatePulseColor(const glm::vec4& color, const Puls } return result; -} \ No newline at end of file +} diff --git a/libraries/render/src/render/Scene.cpp b/libraries/render/src/render/Scene.cpp index ad6523ce11..c93054d5fe 100644 --- a/libraries/render/src/render/Scene.cpp +++ b/libraries/render/src/render/Scene.cpp @@ -461,8 +461,8 @@ void Scene::resetTransitionFinishedOperator(const Transaction::TransitionFinishe TransitionStage::Index transitionId = item.getTransitionId(); if (!TransitionStage::isIndexInvalid(transitionId)) { _transitionFinishedOperatorMap[transitionId].emplace_back(func); - } else { - fucn(); + } else if (func) { + func(); } } } @@ -559,7 +559,7 @@ void Scene::resetItemTransition(ItemID itemId) { auto transitionStage = getStage(TransitionStage::getName()); auto finishedOperators = _transitionFinishedOperatorMap[transitionId]; - + qDebug() << "removing transition: " << transitionId; for (auto finishedOperator : finishedOperators) { if (finishedOperator) { finishedOperator(); diff --git a/libraries/render/src/render/Scene.h b/libraries/render/src/render/Scene.h index e2195cf8c1..08fbf33bcd 100644 --- a/libraries/render/src/render/Scene.h +++ b/libraries/render/src/render/Scene.h @@ -34,6 +34,7 @@ class Scene; // of updating the scene before it s rendered. // + class Transaction { friend class Scene; public: @@ -230,7 +231,7 @@ protected: mutable std::mutex _selectionsMutex; // mutable so it can be used in the thread safe getSelection const method SelectionMap _selections; - std::unordered_map> _transitionFinishedOperatorMap; + std::unordered_map> _transitionFinishedOperatorMap; void resetSelections(const Transaction::SelectionResets& transactions); // More actions coming to selections soon: From 426ffe5a142ffeae8d4e28954b12119e70fcb30f Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Fri, 29 Mar 2019 15:38:42 -0700 Subject: [PATCH 426/446] Fixing typo --- interface/resources/qml/hifi/audio/MicBarApplication.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/audio/MicBarApplication.qml b/interface/resources/qml/hifi/audio/MicBarApplication.qml index 70bded0fc6..5a98d03094 100644 --- a/interface/resources/qml/hifi/audio/MicBarApplication.qml +++ b/interface/resources/qml/hifi/audio/MicBarApplication.qml @@ -163,7 +163,7 @@ Rectangle { Item { id: status; - visible: pushToTalk || (muted && (level >= userSpeakingLevel); + visible: pushToTalk || (muted && (level >= userSpeakingLevel)); anchors { left: parent.left; From c4cccc6ef75763ffa1b42df2e382de847de0f8a2 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Fri, 29 Mar 2019 17:04:44 -0700 Subject: [PATCH 427/446] fix opacity for containing mouse only --- interface/resources/qml/hifi/audio/MicBarApplication.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/audio/MicBarApplication.qml b/interface/resources/qml/hifi/audio/MicBarApplication.qml index 5a98d03094..bc3f4dff89 100644 --- a/interface/resources/qml/hifi/audio/MicBarApplication.qml +++ b/interface/resources/qml/hifi/audio/MicBarApplication.qml @@ -56,7 +56,7 @@ Rectangle { onLevelChanged: { var rectOpacity = (muted && (level >= userSpeakingLevel)) ? 1.0 : 0.7; if (pushToTalk && !pushingToTalk) { - rectOpacity = (level >= userSpeakingLevel) ? 1.0 : 0.7; + rectOpacity = (mouseArea.containsMouse) ? 1.0 : 0.7; } else if (mouseArea.containsMouse && rectOpacity != 1.0) { rectOpacity = 1.0; } From 269b910d24a8423ec2e73048bf40829c0115bc31 Mon Sep 17 00:00:00 2001 From: raveenajain Date: Fri, 29 Mar 2019 18:25:17 -0700 Subject: [PATCH 428/446] update variables, loops --- libraries/fbx/src/GLTFSerializer.cpp | 105 ++++++++++++++------------- libraries/fbx/src/GLTFSerializer.h | 4 +- 2 files changed, 56 insertions(+), 53 deletions(-) diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp index cf8d40cfc1..d29c09992f 100755 --- a/libraries/fbx/src/GLTFSerializer.cpp +++ b/libraries/fbx/src/GLTFSerializer.cpp @@ -734,8 +734,8 @@ glm::mat4 GLTFSerializer::getModelTransform(const GLTFNode& node) { return tmat; } -std::vector> GLTFSerializer::getSkinInverseBindMatrices() { - std::vector> inverseBindMatrixValues; +std::vector> GLTFSerializer::getSkinInverseBindMatrices() { + std::vector> inverseBindMatrixValues; for (auto &skin : _file.skins) { GLTFAccessor& indicesAccessor = _file.accessors[skin.inverseBindMatrices]; GLTFBufferView& indicesBufferview = _file.bufferviews[indicesAccessor.bufferView]; @@ -748,14 +748,14 @@ std::vector> GLTFSerializer::getSkinInverseBindMatrices() { matrices, indicesAccessor.type, indicesAccessor.componentType); - inverseBindMatrixValues.push_back(matrices); + inverseBindMatrixValues.push_back(matrices.toStdVector()); } return inverseBindMatrixValues; } -QVector GLTFSerializer::nodeDFS(int n, std::vector& children, bool order) { - QVector result; - result.append(n); +std::vector GLTFSerializer::nodeDFS(int n, std::vector& children, bool order) { + std::vector result; + result.push_back(n); int begin = 0; int finish = (int)children.size(); if (order) { @@ -768,9 +768,11 @@ QVector GLTFSerializer::nodeDFS(int n, std::vector& children, bool ord std::vector nested = _file.nodes[c].children.toStdVector(); if (nested.size() != 0) { std::sort(nested.begin(), nested.end()); - result.append(nodeDFS(c, nested, order)); + for (int n : nodeDFS(c, nested, order)) { + result.push_back(n); + } } else { - result.append(c); + result.push_back(c); } begin < finish ? index++ : index--; } @@ -779,10 +781,10 @@ QVector GLTFSerializer::nodeDFS(int n, std::vector& children, bool ord bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { - int nodesSize = _file.nodes.size(); + int numNodes = _file.nodes.size(); //Build dependencies - QVector> nodeDependencies(nodesSize); + QVector> nodeDependencies(numNodes); int nodecount = 0; foreach(auto &node, _file.nodes) { //nodes_transforms.push_back(getModelTransform(node)); @@ -810,35 +812,40 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { // initialize order in which nodes will be parsed - QVector nodeQueue; - int start = 0; - int end = nodesSize; + QVector nodeQueue; + nodeQueue.reserve(numNodes); + int rootNode = 0; + int finalNode = numNodes; if (!_file.scenes[_file.scene].nodes.contains(0)) { - end = -1; - start = nodesSize - 1; + rootNode = numNodes - 1; + finalNode = -1; } - QVector init = _file.scenes[_file.scene].nodes; - std::sort(init.begin(), init.end()); - int begin = 0; - int finish = init.size(); - if (start > end) { - begin = init.size() - 1; - finish = -1; + bool rootAtStartOfList = rootNode < finalNode; + int nodeListStride = 1; + if (!rootAtStartOfList) { nodeListStride = -1; } + + QVector initialSceneNodes = _file.scenes[_file.scene].nodes; + std::sort(initialSceneNodes.begin(), initialSceneNodes.end()); + int sceneRootNode = 0; + int sceneFinalNode = initialSceneNodes.size(); + if (!rootAtStartOfList) { + sceneRootNode = initialSceneNodes.size() - 1; + sceneFinalNode = -1; } - int index = begin; - while (index != finish) { - int i = init[index]; + for (int index = sceneRootNode; index != sceneFinalNode; index += nodeListStride) { + int i = initialSceneNodes[index]; std::vector children = _file.nodes[i].children.toStdVector(); std::sort(children.begin(), children.end()); - nodeQueue.append(nodeDFS(i, children, start > end)); - begin < finish ? index++ : index--; + for (int n : nodeDFS(i, children, !rootAtStartOfList)) { + nodeQueue.append(n); + } } // Build joints HFMJoint joint; joint.distanceToParent = 0; - hfmModel.jointIndices["x"] = nodesSize; + hfmModel.jointIndices["x"] = numNodes; hfmModel.hasSkeletonJoints = false; for (int nodeIndex : nodeQueue) { @@ -858,46 +865,46 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { joint.translation = extractTranslation(joint.transform); joint.name = node.name; + joint.isSkeletonJoint = false; hfmModel.joints.push_back(joint); } // Build skeleton - int matrixIndex = 0; - std::vector> inverseBindValues = getSkinInverseBindMatrices(); std::vector jointInverseBindTransforms; - jointInverseBindTransforms.resize(nodesSize); + if (!_file.skins.isEmpty()) { + int matrixIndex = 0; + std::vector> inverseBindValues = getSkinInverseBindMatrices(); + jointInverseBindTransforms.resize(numNodes); - int jointIndex = end; - while (jointIndex != start) { - start < end ? jointIndex-- : jointIndex++; - int jOffset = nodeQueue[jointIndex]; - auto joint = hfmModel.joints[jointIndex]; + int jointIndex = finalNode; + while (jointIndex != rootNode) { + rootAtStartOfList ? jointIndex-- : jointIndex++; + int jOffset = nodeQueue[jointIndex]; + auto joint = hfmModel.joints[jointIndex]; - joint.isSkeletonJoint = false; - if (!_file.skins.isEmpty()) { hfmModel.hasSkeletonJoints = true; for (int s = 0; s < _file.skins.size(); s++) { auto skin = _file.skins[s]; joint.isSkeletonJoint = skin.joints.contains(jOffset); if (joint.isSkeletonJoint) { - QVector value = inverseBindValues[s]; + std::vector value = inverseBindValues[s]; int matrixCount = 16 * skin.joints.indexOf(jOffset); jointInverseBindTransforms[jointIndex] = - glm::mat4(value[matrixCount], value[matrixCount + 1], value[matrixCount + 2], value[matrixCount + 3], + glm::mat4(value[matrixCount], value[matrixCount + 1], value[matrixCount + 2], value[matrixCount + 3], value[matrixCount + 4], value[matrixCount + 5], value[matrixCount + 6], value[matrixCount + 7], - value[matrixCount + 8], value[matrixCount + 9], value[matrixCount + 10], value[matrixCount + 11], + value[matrixCount + 8], value[matrixCount + 9], value[matrixCount + 10], value[matrixCount + 11], value[matrixCount + 12], value[matrixCount + 13], value[matrixCount + 14], value[matrixCount + 15]); matrixIndex++; } else { jointInverseBindTransforms[jointIndex] = glm::mat4(); } + glm::vec3 bindTranslation = extractTranslation(hfmModel.offset * glm::inverse(jointInverseBindTransforms[jointIndex])); + hfmModel.bindExtents.addPoint(bindTranslation); } - glm::vec3 bindTranslation = extractTranslation(hfmModel.offset * glm::inverse(jointInverseBindTransforms[jointIndex])); - hfmModel.bindExtents.addPoint(bindTranslation); + hfmModel.joints[jointIndex] = joint; } - hfmModel.joints[jointIndex] = joint; } @@ -921,8 +928,7 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { // Build meshes nodecount = 0; - int nodeIndex = start; - while (nodeIndex != end) { + for (int nodeIndex = rootNode; nodeIndex != finalNode; nodeIndex += nodeListStride) { auto& node = _file.nodes[nodeIndex]; if (node.defined["mesh"]) { @@ -937,14 +943,12 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { cluster.inverseBindTransform = Transform(cluster.inverseBindMatrix); mesh.clusters.append(cluster); } else { - int j = start; - while (j != end) { + for (int j = rootNode; j != finalNode; j += nodeListStride) { HFMCluster cluster; cluster.jointIndex = j; cluster.inverseBindMatrix = jointInverseBindTransforms[j]; cluster.inverseBindTransform = Transform(cluster.inverseBindMatrix); mesh.clusters.append(cluster); - start < end ? j++ : j--; } } @@ -1148,7 +1152,7 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { // populate the texture coordinates if they don't exist if (mesh.texCoords.size() == 0 && !hfmModel.hasSkeletonJoints) { - for (int i = 0; i < part.triangleIndices.size(); i++) mesh.texCoords.push_back(glm::vec2(0.0, 1.0)); + for (int i = 0; i < part.triangleIndices.size(); i++) { mesh.texCoords.push_back(glm::vec2(0.0, 1.0)); } } mesh.meshExtents.reset(); foreach(const glm::vec3& vertex, mesh.vertices) { @@ -1161,7 +1165,6 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { } nodecount++; - start < end ? nodeIndex++ : nodeIndex--; } diff --git a/libraries/fbx/src/GLTFSerializer.h b/libraries/fbx/src/GLTFSerializer.h index 331f5937ed..e383e6581c 100755 --- a/libraries/fbx/src/GLTFSerializer.h +++ b/libraries/fbx/src/GLTFSerializer.h @@ -712,8 +712,8 @@ private: hifi::ByteArray _glbBinary; glm::mat4 getModelTransform(const GLTFNode& node); - std::vector> getSkinInverseBindMatrices(); - QVector nodeDFS(int n, std::vector& children, bool order); + std::vector> getSkinInverseBindMatrices(); + std::vector nodeDFS(int n, std::vector& children, bool order); bool buildGeometry(HFMModel& hfmModel, const hifi::URL& url); bool parseGLTF(const hifi::ByteArray& data); From 58abfb44c4936664a18299a9d34eb65a76409f55 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sun, 31 Mar 2019 10:35:03 -0700 Subject: [PATCH 429/446] Globally disallow use of the camera or microphone by hosted web content --- interface/src/Application.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 635932ea1c..676534f6db 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2882,11 +2882,19 @@ void Application::initializeGL() { } #if !defined(DISABLE_QML) + QStringList chromiumFlags; + // Bug 21993: disable microphone and camera input + chromiumFlags << "--use-fake-device-for-media-stream"; // Disable signed distance field font rendering on ATI/AMD GPUs, due to // https://highfidelity.manuscript.com/f/cases/13677/Text-showing-up-white-on-Marketplace-app std::string vendor{ (const char*)glGetString(GL_VENDOR) }; if ((vendor.find("AMD") != std::string::npos) || (vendor.find("ATI") != std::string::npos)) { - qputenv("QTWEBENGINE_CHROMIUM_FLAGS", QByteArray("--disable-distance-field-text")); + chromiumFlags << "--disable-distance-field-text"; + } + + // Ensure all Qt webengine processes launched from us have the appropriate command line flags + if (!chromiumFlags.empty()) { + qputenv("QTWEBENGINE_CHROMIUM_FLAGS", chromiumFlags.join(' ').toLocal8Bit()); } #endif From 7d4e59bfe3a6227ce6405698e4ab5c8fbbe7621d Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sun, 31 Mar 2019 12:53:24 -0700 Subject: [PATCH 430/446] Force packet version change --- libraries/networking/src/udt/PacketHeaders.h | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 413ff14b17..8c76a3ebd0 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -266,6 +266,7 @@ enum class EntityVersion : PacketVersion { ModelScale, ReOrderParentIDProperties, CertificateTypeProperty, + DisableWebMedia, // Add new versions above here NUM_PACKET_TYPE, From 0b822b2cbc2c3cd638027bae6c6099fc688ca22b Mon Sep 17 00:00:00 2001 From: Oren Hurvitz Date: Mon, 1 Apr 2019 18:26:00 +0300 Subject: [PATCH 431/446] Fixed calls to addingWearable() and deletingWearable(). The calls used a parameter type of QUuid instead of EntityItemID. This resulted in the following warnings in the log: [04/01 18:16:11.012] [WARNING] [default] [53296] QMetaObject::invokeMethod: No such method EntityScriptingInterface::addingWearable(QUuid) [04/01 18:16:11.012] [WARNING] [default] [53296] Candidates are: [04/01 18:16:11.012] [WARNING] [default] [53296] addingWearable(EntityItemID) --- libraries/entities/src/EntityScriptingInterface.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index ca914731b5..f8c45b792a 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -1101,13 +1101,13 @@ void EntityScriptingInterface::handleEntityScriptCallMethodPacket(QSharedPointer void EntityScriptingInterface::onAddingEntity(EntityItem* entity) { if (entity->isWearable()) { - QMetaObject::invokeMethod(this, "addingWearable", Q_ARG(QUuid, entity->getEntityItemID())); + QMetaObject::invokeMethod(this, "addingWearable", Q_ARG(EntityItemID, entity->getEntityItemID())); } } void EntityScriptingInterface::onDeletingEntity(EntityItem* entity) { if (entity->isWearable()) { - QMetaObject::invokeMethod(this, "deletingWearable", Q_ARG(QUuid, entity->getEntityItemID())); + QMetaObject::invokeMethod(this, "deletingWearable", Q_ARG(EntityItemID, entity->getEntityItemID())); } } From 0a8d195f6f09cdccd157ab0e92bec5f807d616a6 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 1 Apr 2019 11:32:52 -0700 Subject: [PATCH 432/446] adding top padding for my name card --- interface/resources/qml/hifi/NameCard.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 141ddf0077..7e8218b7df 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -129,6 +129,7 @@ Item { height: 40 // Anchors anchors.top: avatarImage.top + anchors.topMargin: avatarImage.visible ? 18 : 0; anchors.left: avatarImage.right anchors.leftMargin: avatarImage.visible ? 5 : 0; anchors.rightMargin: 5; From afdf95c894de5302d2213288dff9c134b35042ce Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 1 Apr 2019 12:20:55 -0700 Subject: [PATCH 433/446] remove unused slotted function --- interface/src/Application.cpp | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 734eb7221b..ecafbfdb2c 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1295,21 +1295,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo setCrashAnnotation("display_plugin", displayPlugin->getName().toStdString()); setCrashAnnotation("hmd", displayPlugin->isHmd() ? "1" : "0"); }); - connect(this, &Application::activeDisplayPluginChanged, this, [&](){ -#if !defined(Q_OS_ANDROID) - if (!getLoginDialogPoppedUp() && _desktopRootItemCreated) { -/* if (isHMDMode()) {*/ - //createAvatarInputsBar(); - //auto offscreenUi = getOffscreenUI(); - //offscreenUi->hide(AVATAR_INPUTS_BAR_QML.toString()); - //} else { - //destroyAvatarInputsBar(); - //auto offscreenUi = getOffscreenUI(); - //offscreenUi->show(AVATAR_INPUTS_BAR_QML.toString(), "AvatarInputsBar"); - /*}*/ - } -#endif - }); connect(this, &Application::activeDisplayPluginChanged, this, &Application::updateSystemTabletMode); connect(this, &Application::activeDisplayPluginChanged, this, [&](){ if (getLoginDialogPoppedUp()) { From 2920fdc966b93e0e50f498168a50a25454d99883 Mon Sep 17 00:00:00 2001 From: raveenajain Date: Mon, 1 Apr 2019 21:09:14 +0100 Subject: [PATCH 434/446] variables, cluster size --- libraries/fbx/src/GLTFSerializer.cpp | 33 ++++++++++++---------------- libraries/fbx/src/GLTFSerializer.h | 2 +- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp index d29c09992f..1192952b9e 100755 --- a/libraries/fbx/src/GLTFSerializer.cpp +++ b/libraries/fbx/src/GLTFSerializer.cpp @@ -753,28 +753,26 @@ std::vector> GLTFSerializer::getSkinInverseBindMatrices() { return inverseBindMatrixValues; } -std::vector GLTFSerializer::nodeDFS(int n, std::vector& children, bool order) { +std::vector GLTFSerializer::nodeDFS(int n, std::vector& children, int stride) { std::vector result; result.push_back(n); - int begin = 0; - int finish = (int)children.size(); - if (order) { - begin = (int)children.size() - 1; - finish = -1; + int rootDFS = 0; + int finalDFS = (int)children.size(); + if (stride == -1) { + rootDFS = (int)children.size() - 1; + finalDFS = -1; } - int index = begin; - while (index != finish) { + for (int index = rootDFS; index != finalDFS; index += stride) { int c = children[index]; std::vector nested = _file.nodes[c].children.toStdVector(); if (nested.size() != 0) { std::sort(nested.begin(), nested.end()); - for (int n : nodeDFS(c, nested, order)) { + for (int n : nodeDFS(c, nested, stride)) { result.push_back(n); } } else { result.push_back(c); } - begin < finish ? index++ : index--; } return result; } @@ -836,7 +834,7 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { int i = initialSceneNodes[index]; std::vector children = _file.nodes[i].children.toStdVector(); std::sort(children.begin(), children.end()); - for (int n : nodeDFS(i, children, !rootAtStartOfList)) { + for (int n : nodeDFS(i, children, nodeListStride)) { nodeQueue.append(n); } } @@ -856,14 +854,11 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { joint.parentIndex = nodeQueue.indexOf(nodeDependencies[nodeIndex][0]); } joint.transform = node.transforms.first(); - joint.postTransform = glm::mat4(); - glm::vec3 scale = extractScale(joint.transform); - joint.postTransform[0][0] = scale.x; - joint.postTransform[1][1] = scale.y; - joint.postTransform[2][2] = scale.z; - joint.rotation = glmExtractRotation(joint.transform); joint.translation = extractTranslation(joint.transform); - + joint.rotation = glmExtractRotation(joint.transform); + glm::vec3 scale = extractScale(joint.transform); + joint.postTransform = glm::scale(glm::mat4(), scale); + joint.name = node.name; joint.isSkeletonJoint = false; hfmModel.joints.push_back(joint); @@ -1121,7 +1116,7 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { const int WEIGHTS_PER_VERTEX = 4; const float ALMOST_HALF = 0.499f; int numVertices = mesh.vertices.size(); - mesh.clusterIndices.fill(0, numClusterIndices); + mesh.clusterIndices.fill(mesh.clusters.size() - 1, numClusterIndices); mesh.clusterWeights.fill(0, numClusterIndices); for (int c = 0; c < clusterJoints.size(); c++) { diff --git a/libraries/fbx/src/GLTFSerializer.h b/libraries/fbx/src/GLTFSerializer.h index e383e6581c..af91a1753d 100755 --- a/libraries/fbx/src/GLTFSerializer.h +++ b/libraries/fbx/src/GLTFSerializer.h @@ -713,7 +713,7 @@ private: glm::mat4 getModelTransform(const GLTFNode& node); std::vector> getSkinInverseBindMatrices(); - std::vector nodeDFS(int n, std::vector& children, bool order); + std::vector nodeDFS(int n, std::vector& children, int stride); bool buildGeometry(HFMModel& hfmModel, const hifi::URL& url); bool parseGLTF(const hifi::ByteArray& data); From 8df34e5d5973713cd3a067a4d871dd725aadbd3f Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 1 Apr 2019 14:50:16 -0700 Subject: [PATCH 435/446] fixing on clicked for audio mic bar --- interface/resources/qml/hifi/audio/MicBar.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index d161f12d70..100f07fe1a 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -87,7 +87,7 @@ Rectangle { if (pushToTalk) { return; } - muted = !muted; + AudioScriptingInterface.muted = !muted; Tablet.playSound(TabletEnums.ButtonClick); } drag.target: dragTarget; From fcb838a71f6d6e2c3f1b94051b131dbcd528f37d Mon Sep 17 00:00:00 2001 From: danteruiz Date: Mon, 1 Apr 2019 15:43:04 -0700 Subject: [PATCH 436/446] addressing review requests --- interface/src/avatar/AvatarManager.cpp | 59 +++++++------------ interface/src/avatar/AvatarManager.h | 4 -- libraries/avatars/src/AvatarHashMap.cpp | 1 - .../src/EntityTreeRenderer.cpp | 31 ++-------- .../src/EntityTreeRenderer.h | 2 - libraries/render/src/render/Scene.cpp | 1 - 6 files changed, 24 insertions(+), 74 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index fea0652964..956ea12aee 100755 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -84,7 +84,6 @@ AvatarManager::AvatarManager(QObject* parent) : AvatarSharedPointer AvatarManager::addAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) { AvatarSharedPointer avatar = AvatarHashMap::addAvatar(sessionUUID, mixerWeakPointer); - const auto otherAvatar = std::static_pointer_cast(avatar); if (otherAvatar && _space) { std::unique_lock lock(_spaceLock); @@ -210,7 +209,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { { // lock the hash for read to check the size QReadLocker lock(&_hashLock); - if (_avatarHash.size() < 2 && _avatarsToFadeOut.empty()) { + if (_avatarHash.size() < 2) { return; } } @@ -375,18 +374,12 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { qApp->getMain3DScene()->enqueueTransaction(renderTransaction); } - if (!_spaceProxiesToDelete.empty() && _space) { - std::unique_lock lock(_spaceLock); - workloadTransaction.remove(_spaceProxiesToDelete); - _spaceProxiesToDelete.clear(); - } _space->enqueueTransaction(workloadTransaction); _numAvatarsUpdated = numAvatarsUpdated; _numAvatarsNotUpdated = numAvatarsNotUpdated; _numHeroAvatarsUpdated = numHerosUpdated; - removeFadedAvatars(); _avatarSimulationTime = (float)(usecTimestampNow() - startTime) / (float)USECS_PER_MSEC; } @@ -399,30 +392,6 @@ void AvatarManager::postUpdate(float deltaTime, const render::ScenePointer& scen } } -void AvatarManager::removeFadedAvatars() { - if (_avatarsToFadeOut.empty()) { - return; - } - - QReadLocker locker(&_hashLock); - auto avatarItr = _avatarsToFadeOut.begin(); - const render::ScenePointer& scene = qApp->getMain3DScene(); - render::Transaction transaction; - while (avatarItr != _avatarsToFadeOut.end()) { - auto avatar = std::static_pointer_cast(*avatarItr); - if (!avatar->isFading()) { - // fading to zero is such a rare event we push a unique transaction for each - if (avatar->isInScene()) { - avatar->removeFromScene(*avatarItr, scene, transaction); - } - avatarItr = _avatarsToFadeOut.erase(avatarItr); - } else { - ++avatarItr; - } - } - scene->enqueueTransaction(transaction); -} - AvatarSharedPointer AvatarManager::newSharedAvatar(const QUuid& sessionUUID) { auto otherAvatar = new OtherAvatar(qApp->thread()); otherAvatar->setSessionUUID(sessionUUID); @@ -517,10 +486,6 @@ void AvatarManager::removeDeadAvatarEntities(const SetOfEntities& deadEntities) void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason) { auto avatar = std::static_pointer_cast(removedAvatar); - { - std::unique_lock lock(_spaceLock); - _spaceProxiesToDelete.push_back(avatar->getSpaceIndex()); - } AvatarHashMap::handleRemovedAvatar(avatar, removalReason); avatar->tearDownGrabs(); @@ -534,6 +499,15 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar avatar->setIsFading(false); if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble) { emit DependencyManager::get()->enteredIgnoreRadius(); + + workload::Transaction workloadTransaction; + workloadTransaction.remove(avatar->getSpaceIndex()); + _space->enqueueTransaction(workloadTransaction); + + const render::ScenePointer& scene = qApp->getMain3DScene(); + render::Transaction transaction; + avatar->removeFromScene(avatar, scene, transaction); + scene->enqueueTransaction(transaction); } else if (removalReason == KillAvatarReason::AvatarDisconnected) { // remove from node sets, if present DependencyManager::get()->removeFromIgnoreMuteSets(avatar->getSessionUUID()); @@ -542,13 +516,20 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar auto scene = qApp->getMain3DScene(); avatar->fadeOut(transaction, removalReason); - transaction.transitionFinishedOperator(avatar->getRenderItemID(), [avatar]() { + workload::SpacePointer space = _space; + transaction.transitionFinishedOperator(avatar->getRenderItemID(), [space, avatar]() { avatar->setIsFading(false); + const render::ScenePointer& scene = qApp->getMain3DScene(); + render::Transaction transaction; + avatar->removeFromScene(avatar, scene, transaction); + scene->enqueueTransaction(transaction); + + workload::Transaction workloadTransaction; + workloadTransaction.remove(avatar->getSpaceIndex()); + space->enqueueTransaction(workloadTransaction); }); scene->enqueueTransaction(transaction); } - - _avatarsToFadeOut.push_back(removedAvatar); } void AvatarManager::clearOtherAvatars() { diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 9dde3a11fb..f9b82da0c1 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -222,8 +222,6 @@ private: AvatarSharedPointer newSharedAvatar(const QUuid& sessionUUID) override; - void removeFadedAvatars(); - // called only from the AvatarHashMap thread - cannot be called while this thread holds the // hash lock, since handleRemovedAvatar needs a write lock on the entity tree and the entity tree // frequently grabs a read lock on the hash to get a given avatar by ID @@ -231,7 +229,6 @@ private: KillAvatarReason removalReason = KillAvatarReason::NoReason) override; void handleTransitAnimations(AvatarTransit::Status status); - std::vector _avatarsToFadeOut; using SetOfOtherAvatars = std::set; SetOfOtherAvatars _avatarsToChangeInPhysics; @@ -251,7 +248,6 @@ private: mutable std::mutex _spaceLock; workload::SpacePointer _space; - std::vector _spaceProxiesToDelete; AvatarTransit::TransitConfig _transitConfig; }; diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index c16d65506a..3abd352778 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -439,7 +439,6 @@ void AvatarHashMap::removeAvatar(const QUuid& sessionUUID, KillAvatarReason remo } auto removedAvatar = _avatarHash.take(sessionUUID); - if (removedAvatar) { removedAvatars.push_back(removedAvatar); } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 2701467a2d..7c9c724212 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -214,30 +214,6 @@ void EntityTreeRenderer::stopDomainAndNonOwnedEntities() { } } -void EntityTreeRenderer::removeFadedRenderables() { - if (_entityRenderablesToFadeOut.empty()) { - return; - } - - std::unique_lock lock(_entitiesToFadeLock); - auto entityIter = _entityRenderablesToFadeOut.begin(); - auto scene = _viewState->getMain3DScene(); - render::Transaction transaction; - - while (entityIter != _entityRenderablesToFadeOut.end()) { - auto entityRenderable = *entityIter; - - if (!entityRenderable->getIsFading()) { - entityRenderable->removeFromScene(scene, transaction); - entityIter = _entityRenderablesToFadeOut.erase(entityIter); - } else { - ++entityIter; - } - } - - scene->enqueueTransaction(transaction); -} - void EntityTreeRenderer::clearDomainAndNonOwnedEntities() { stopDomainAndNonOwnedEntities(); @@ -551,7 +527,6 @@ void EntityTreeRenderer::update(bool simulate) { } } - removeFadedRenderables(); } void EntityTreeRenderer::handleSpaceUpdate(std::pair proxyUpdate) { @@ -1080,12 +1055,14 @@ void EntityTreeRenderer::fadeOutRenderable(const EntityRendererPointer& renderab auto scene = _viewState->getMain3DScene(); renderable->setIsFading(true); - transaction.transitionFinishedOperator(renderable->getRenderItemID(), [renderable]() { + transaction.transitionFinishedOperator(renderable->getRenderItemID(), [scene, renderable]() { renderable->setIsFading(false); + render::Transaction transaction; + renderable->removeFromScene(scene, transaction); + scene->enqueueTransaction(transaction); }); scene->enqueueTransaction(transaction); - _entityRenderablesToFadeOut.push_back(renderable); } void EntityTreeRenderer::playEntityCollisionSound(const EntityItemPointer& entity, const Collision& collision) { diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 32504abd56..08dd06f5c1 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -259,8 +259,6 @@ private: std::unordered_map _entitiesInScene; std::unordered_map _entitiesToAdd; - std::mutex _entitiesToFadeLock; - std::vector _entityRenderablesToFadeOut; // For Scene.shouldRenderEntities QList _entityIDsLastInScene; diff --git a/libraries/render/src/render/Scene.cpp b/libraries/render/src/render/Scene.cpp index c93054d5fe..16d1d49d9f 100644 --- a/libraries/render/src/render/Scene.cpp +++ b/libraries/render/src/render/Scene.cpp @@ -559,7 +559,6 @@ void Scene::resetItemTransition(ItemID itemId) { auto transitionStage = getStage(TransitionStage::getName()); auto finishedOperators = _transitionFinishedOperatorMap[transitionId]; - qDebug() << "removing transition: " << transitionId; for (auto finishedOperator : finishedOperators) { if (finishedOperator) { finishedOperator(); From 8115ef6d3613dfe228d201d145759f96e77c45dd Mon Sep 17 00:00:00 2001 From: danteruiz Date: Mon, 1 Apr 2019 16:46:15 -0700 Subject: [PATCH 437/446] addressing more requested changes --- interface/src/avatar/AvatarManager.cpp | 2 -- libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp | 1 - libraries/avatars-renderer/src/avatars-renderer/Avatar.h | 3 --- libraries/entities-renderer/src/EntityTreeRenderer.cpp | 2 -- libraries/entities-renderer/src/EntityTreeRenderer.h | 1 - libraries/entities-renderer/src/RenderableEntityItem.cpp | 3 +++ libraries/entities-renderer/src/RenderableEntityItem.h | 3 --- 7 files changed, 3 insertions(+), 12 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 956ea12aee..3fbff292d7 100755 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -496,7 +496,6 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar // it might not fire until after we create a new instance for the same remote avatar, which creates a race // on the creation of entities for that avatar instance and the deletion of entities for this instance avatar->removeAvatarEntitiesFromTree(); - avatar->setIsFading(false); if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble) { emit DependencyManager::get()->enteredIgnoreRadius(); @@ -518,7 +517,6 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar workload::SpacePointer space = _space; transaction.transitionFinishedOperator(avatar->getRenderItemID(), [space, avatar]() { - avatar->setIsFading(false); const render::ScenePointer& scene = qApp->getMain3DScene(); render::Transaction transaction; avatar->removeFromScene(avatar, scene, transaction); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index dccf37c5b8..77e5933d3d 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -677,7 +677,6 @@ void Avatar::fade(render::Transaction& transaction, render::Transition::Type typ transaction.addTransitionToItem(itemId, type, _renderItemID); } } - _isFading = true; } void Avatar::removeFromScene(AvatarSharedPointer self, const render::ScenePointer& scene, render::Transaction& transaction) { diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index b4cad4f967..56684e34b3 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -462,8 +462,6 @@ public: void fadeIn(render::ScenePointer scene); void fadeOut(render::Transaction& transaction, KillAvatarReason reason); - bool isFading() const { return _isFading; } - void setIsFading(bool isFading) { _isFading = isFading; } // JSDoc is in AvatarData.h. Q_INVOKABLE virtual float getEyeHeight() const override; @@ -655,7 +653,6 @@ protected: bool _initialized { false }; bool _isAnimatingScale { false }; bool _mustFadeIn { false }; - bool _isFading { false }; bool _reconstructSoftEntitiesJointMap { false }; float _modelScale { 1.0f }; diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 7c9c724212..6cfff7bc41 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -1054,9 +1054,7 @@ void EntityTreeRenderer::fadeOutRenderable(const EntityRendererPointer& renderab render::Transaction transaction; auto scene = _viewState->getMain3DScene(); - renderable->setIsFading(true); transaction.transitionFinishedOperator(renderable->getRenderItemID(), [scene, renderable]() { - renderable->setIsFading(false); render::Transaction transaction; renderable->removeFromScene(scene, transaction); scene->enqueueTransaction(transaction); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 08dd06f5c1..cee91ad1c7 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -94,7 +94,6 @@ public: void reloadEntityScripts(); void fadeOutRenderable(const EntityRendererPointer& renderable); - void removeFadedRenderables(); // event handles which may generate entity related events QUuid mousePressEvent(QMouseEvent* event); diff --git a/libraries/entities-renderer/src/RenderableEntityItem.cpp b/libraries/entities-renderer/src/RenderableEntityItem.cpp index e4e135cd7f..3a56521702 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableEntityItem.cpp @@ -418,6 +418,9 @@ void EntityRenderer::doRenderUpdateSynchronous(const ScenePointer& scene, Transa if (fading || _prevIsTransparent != transparent) { emit requestRenderUpdate(); } + if (fading) { + _isFading = Interpolate::calculateFadeRatio(_fadeStartTime) < 1.0f; + } _prevIsTransparent = transparent; diff --git a/libraries/entities-renderer/src/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h index b37e46d02e..39f9ad091e 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.h +++ b/libraries/entities-renderer/src/RenderableEntityItem.h @@ -44,9 +44,6 @@ public: const EntityItemPointer& getEntity() const { return _entity; } const ItemID& getRenderItemID() const { return _renderItemID; } - bool getIsFading() { return _isFading; } - void setIsFading(bool isFading) { _isFading = isFading; } - const SharedSoundPointer& getCollisionSound() { return _collisionSound; } void setCollisionSound(const SharedSoundPointer& sound) { _collisionSound = sound; } From 8439019c4eef997a05fe768aad22da3b83b65c6b Mon Sep 17 00:00:00 2001 From: raveenajain Date: Tue, 2 Apr 2019 19:56:17 +0100 Subject: [PATCH 438/446] update use of vectors --- libraries/fbx/src/GLTFSerializer.cpp | 43 ++++++++++++---------------- libraries/fbx/src/GLTFSerializer.h | 4 +-- 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp index 1192952b9e..84609be250 100755 --- a/libraries/fbx/src/GLTFSerializer.cpp +++ b/libraries/fbx/src/GLTFSerializer.cpp @@ -734,8 +734,7 @@ glm::mat4 GLTFSerializer::getModelTransform(const GLTFNode& node) { return tmat; } -std::vector> GLTFSerializer::getSkinInverseBindMatrices() { - std::vector> inverseBindMatrixValues; +void GLTFSerializer::getSkinInverseBindMatrices(std::vector>& inverseBindMatrixValues) { for (auto &skin : _file.skins) { GLTFAccessor& indicesAccessor = _file.accessors[skin.inverseBindMatrices]; GLTFBufferView& indicesBufferview = _file.bufferviews[indicesAccessor.bufferView]; @@ -750,31 +749,28 @@ std::vector> GLTFSerializer::getSkinInverseBindMatrices() { indicesAccessor.componentType); inverseBindMatrixValues.push_back(matrices.toStdVector()); } - return inverseBindMatrixValues; } -std::vector GLTFSerializer::nodeDFS(int n, std::vector& children, int stride) { - std::vector result; - result.push_back(n); - int rootDFS = 0; - int finalDFS = (int)children.size(); +void GLTFSerializer::getNodeQueueByDepthFirstChildren(std::vector& children, int stride, std::vector& result) { + int startingIndex = 0; + int finalIndex = (int)children.size(); if (stride == -1) { - rootDFS = (int)children.size() - 1; - finalDFS = -1; + startingIndex = (int)children.size() - 1; + finalIndex = -1; } - for (int index = rootDFS; index != finalDFS; index += stride) { + for (int index = startingIndex; index != finalIndex; index += stride) { int c = children[index]; + result.push_back(c); std::vector nested = _file.nodes[c].children.toStdVector(); if (nested.size() != 0) { std::sort(nested.begin(), nested.end()); - for (int n : nodeDFS(c, nested, stride)) { - result.push_back(n); + for (int r : nested) { + if (result.end() == std::find(result.begin(), result.end(), r)) { + getNodeQueueByDepthFirstChildren(nested, stride, result); + } } - } else { - result.push_back(c); - } + } } - return result; } @@ -810,7 +806,7 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { // initialize order in which nodes will be parsed - QVector nodeQueue; + std::vector nodeQueue; nodeQueue.reserve(numNodes); int rootNode = 0; int finalNode = numNodes; @@ -832,14 +828,12 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { } for (int index = sceneRootNode; index != sceneFinalNode; index += nodeListStride) { int i = initialSceneNodes[index]; + nodeQueue.push_back(i); std::vector children = _file.nodes[i].children.toStdVector(); std::sort(children.begin(), children.end()); - for (int n : nodeDFS(i, children, nodeListStride)) { - nodeQueue.append(n); - } + getNodeQueueByDepthFirstChildren(children, nodeListStride, nodeQueue); } - // Build joints HFMJoint joint; joint.distanceToParent = 0; @@ -851,7 +845,7 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { joint.parentIndex = -1; if (!_file.scenes[_file.scene].nodes.contains(nodeIndex)) { - joint.parentIndex = nodeQueue.indexOf(nodeDependencies[nodeIndex][0]); + joint.parentIndex = std::distance(nodeQueue.begin(), std::find(nodeQueue.begin(), nodeQueue.end(), nodeDependencies[nodeIndex][0])); } joint.transform = node.transforms.first(); joint.translation = extractTranslation(joint.transform); @@ -869,7 +863,8 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { std::vector jointInverseBindTransforms; if (!_file.skins.isEmpty()) { int matrixIndex = 0; - std::vector> inverseBindValues = getSkinInverseBindMatrices(); + std::vector> inverseBindValues; + getSkinInverseBindMatrices(inverseBindValues); jointInverseBindTransforms.resize(numNodes); int jointIndex = finalNode; diff --git a/libraries/fbx/src/GLTFSerializer.h b/libraries/fbx/src/GLTFSerializer.h index af91a1753d..d9c477bd99 100755 --- a/libraries/fbx/src/GLTFSerializer.h +++ b/libraries/fbx/src/GLTFSerializer.h @@ -712,8 +712,8 @@ private: hifi::ByteArray _glbBinary; glm::mat4 getModelTransform(const GLTFNode& node); - std::vector> getSkinInverseBindMatrices(); - std::vector nodeDFS(int n, std::vector& children, int stride); + void getSkinInverseBindMatrices(std::vector>& inverseBindMatrixValues); + void getNodeQueueByDepthFirstChildren(std::vector& children, int stride, std::vector& result); bool buildGeometry(HFMModel& hfmModel, const hifi::URL& url); bool parseGLTF(const hifi::ByteArray& data); From 14352452793f4e9d1b477c86176276b9e4fdbbcf Mon Sep 17 00:00:00 2001 From: raveenajain Date: Tue, 2 Apr 2019 21:18:16 +0100 Subject: [PATCH 439/446] root cluster --- libraries/fbx/src/GLTFSerializer.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp index 84609be250..5e4716c56d 100755 --- a/libraries/fbx/src/GLTFSerializer.cpp +++ b/libraries/fbx/src/GLTFSerializer.cpp @@ -861,11 +861,11 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { // Build skeleton std::vector jointInverseBindTransforms; + jointInverseBindTransforms.resize(numNodes); if (!_file.skins.isEmpty()) { int matrixIndex = 0; std::vector> inverseBindValues; getSkinInverseBindMatrices(inverseBindValues); - jointInverseBindTransforms.resize(numNodes); int jointIndex = finalNode; while (jointIndex != rootNode) { @@ -941,6 +941,12 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { mesh.clusters.append(cluster); } } + HFMCluster root; + root.jointIndex = rootNode; + if (root.jointIndex == -1) { root.jointIndex = 0; } + root.inverseBindMatrix = jointInverseBindTransforms[root.jointIndex]; + root.inverseBindTransform = Transform(root.inverseBindMatrix); + mesh.clusters.append(root); HFMMeshPart part = HFMMeshPart(); @@ -1131,6 +1137,8 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { for (int k = j; k < j + WEIGHTS_PER_VERTEX; ++k) { mesh.clusterWeights[k] = (uint16_t)(weightScalingFactor * clusterWeights[k] + ALMOST_HALF); } + } else { + mesh.clusterWeights[j] = (uint16_t)((float)(UINT16_MAX) + ALMOST_HALF); } } } From 2d71ec1c7581fb6ddb44e8f13b34ce5704e9ed31 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Tue, 2 Apr 2019 13:19:06 -0700 Subject: [PATCH 440/446] Case20410 Automatic Content Archives doesn't restore correctly with old Tutorial content set The tutorial content set had a content set Id of Null, which caused a failure to parse the content set. --- domain-server/src/DomainServer.cpp | 2 +- libraries/octree/src/OctreeEntitiesFileParser.cpp | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 5f82700e9c..400dc3642d 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1734,7 +1734,7 @@ void DomainServer::processOctreeDataPersistMessage(QSharedPointer Date: Tue, 2 Apr 2019 13:26:50 -0700 Subject: [PATCH 441/446] Case 22010: Bump protocol number to fix joint order mismatch The joint order change was introduced in PR #15178, to fix an FBX Serializer bug. --- 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 7f20f881da..e527e660b3 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -38,10 +38,10 @@ PacketVersion versionForPacketType(PacketType packetType) { return static_cast(EntityQueryPacketVersion::ConicalFrustums); case PacketType::AvatarIdentity: case PacketType::AvatarData: - return static_cast(AvatarMixerPacketVersion::SendMaxTranslationDimension); + return static_cast(AvatarMixerPacketVersion::FBXJointOrderChange); case PacketType::BulkAvatarData: case PacketType::KillAvatar: - return static_cast(AvatarMixerPacketVersion::SendMaxTranslationDimension); + return static_cast(AvatarMixerPacketVersion::FBXJointOrderChange); case PacketType::MessagesData: return static_cast(MessageDataVersion::TextOrBinaryData); // ICE packets diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 8c76a3ebd0..780e3d8546 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -328,7 +328,8 @@ enum class AvatarMixerPacketVersion : PacketVersion { CollisionFlag, AvatarTraitsAck, FasterAvatarEntities, - SendMaxTranslationDimension + SendMaxTranslationDimension, + FBXJointOrderChange }; enum class DomainConnectRequestVersion : PacketVersion { From fcc7e9af3048b23f551bec322ec1595fab8ff16f Mon Sep 17 00:00:00 2001 From: raveenajain Date: Wed, 3 Apr 2019 00:09:36 +0100 Subject: [PATCH 442/446] minor syntax fix --- libraries/fbx/src/GLTFSerializer.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp index 5e4716c56d..1699722215 100755 --- a/libraries/fbx/src/GLTFSerializer.cpp +++ b/libraries/fbx/src/GLTFSerializer.cpp @@ -863,7 +863,6 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { std::vector jointInverseBindTransforms; jointInverseBindTransforms.resize(numNodes); if (!_file.skins.isEmpty()) { - int matrixIndex = 0; std::vector> inverseBindValues; getSkinInverseBindMatrices(inverseBindValues); @@ -886,7 +885,6 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { value[matrixCount + 4], value[matrixCount + 5], value[matrixCount + 6], value[matrixCount + 7], value[matrixCount + 8], value[matrixCount + 9], value[matrixCount + 10], value[matrixCount + 11], value[matrixCount + 12], value[matrixCount + 13], value[matrixCount + 14], value[matrixCount + 15]); - matrixIndex++; } else { jointInverseBindTransforms[jointIndex] = glm::mat4(); } @@ -943,7 +941,9 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { } HFMCluster root; root.jointIndex = rootNode; - if (root.jointIndex == -1) { root.jointIndex = 0; } + if (root.jointIndex == -1) { + root.jointIndex = 0; + } root.inverseBindMatrix = jointInverseBindTransforms[root.jointIndex]; root.inverseBindTransform = Transform(root.inverseBindMatrix); mesh.clusters.append(root); From ff3e5bbc6129762c1ce0fa18fb3cbad3e759d75a Mon Sep 17 00:00:00 2001 From: danteruiz Date: Tue, 2 Apr 2019 16:20:10 -0700 Subject: [PATCH 443/446] make sure to delete avatar when thier is no reason --- interface/src/avatar/AvatarManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 38bdd061c0..7a64edae11 100755 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -497,7 +497,7 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar // it might not fire until after we create a new instance for the same remote avatar, which creates a race // on the creation of entities for that avatar instance and the deletion of entities for this instance avatar->removeAvatarEntitiesFromTree(); - if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble) { + if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble || removalReason == KillAvatarReason::NoReason) { emit AvatarInputs::getInstance()->avatarEnteredIgnoreRadius(avatar->getSessionUUID()); emit DependencyManager::get()->enteredIgnoreRadius(); From ac97a82badc5ed679c650f4a6399e5863fec41a6 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Tue, 2 Apr 2019 17:25:00 -0700 Subject: [PATCH 444/446] Address a CR comment. --- .../octree/src/OctreeEntitiesFileParser.cpp | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/libraries/octree/src/OctreeEntitiesFileParser.cpp b/libraries/octree/src/OctreeEntitiesFileParser.cpp index 8e3fc76e2c..e82201adfd 100644 --- a/libraries/octree/src/OctreeEntitiesFileParser.cpp +++ b/libraries/octree/src/OctreeEntitiesFileParser.cpp @@ -108,22 +108,20 @@ bool OctreeEntitiesFileParser::parseEntities(QVariantMap& parsedEntities) { return false; } - if (idString == "{00000000-0000-0000-0000-000000000000}") { - // some older archives may have a null string id, so - // return success without setting parsedEntities, - // which will result in a new uuid for the restored - // archive. (not parsing and using isNull as parsing - // results in null if there is a corrupt string) - return true; - } + // some older archives may have a null string id, so + // return success without setting parsedEntities, + // which will result in a new uuid for the restored + // archive. (not parsing and using isNull as parsing + // results in null if there is a corrupt string) - QUuid idValue = QUuid::fromString(QLatin1String(idString.c_str()) ); - if (idValue.isNull()) { - _errorString = "Id value invalid UUID string: " + idString; - return false; + if (idString != "{00000000-0000-0000-0000-000000000000}") { + QUuid idValue = QUuid::fromString(QLatin1String(idString.c_str()) ); + if (idValue.isNull()) { + _errorString = "Id value invalid UUID string: " + idString; + return false; + } + parsedEntities["Id"] = idValue; } - - parsedEntities["Id"] = idValue; } else if (key == "Version") { if (gotVersion) { _errorString = "Duplicate Version entries"; From 1860e61a3014be9cee540b2765e099485abf2dd9 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 3 Apr 2019 07:42:56 -0700 Subject: [PATCH 445/446] more correct collision group during grabs --- libraries/entities/src/EntityItem.cpp | 29 +++++---------------- libraries/entities/src/EntityItem.h | 2 +- libraries/physics/src/EntityMotionState.cpp | 5 ++-- 3 files changed, 10 insertions(+), 26 deletions(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index bd4c6e5c71..8a50c39da9 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -1698,10 +1698,7 @@ AACube EntityItem::getQueryAACube(bool& success) const { } bool EntityItem::shouldPuffQueryAACube() const { - bool hasGrabs = _grabsLock.resultWithReadLock([&] { - return _grabs.count() > 0; - }); - return hasActions() || isChildOfMyAvatar() || isMovingRelativeToParent() || hasGrabs; + return hasActions() || isChildOfMyAvatar() || isMovingRelativeToParent(); } // TODO: get rid of all users of this function... @@ -1896,7 +1893,8 @@ void EntityItem::setScaledDimensions(const glm::vec3& value) { void EntityItem::setUnscaledDimensions(const glm::vec3& value) { glm::vec3 newDimensions = glm::max(value, glm::vec3(ENTITY_ITEM_MIN_DIMENSION)); - if (getUnscaledDimensions() != newDimensions) { + const float MIN_SCALE_CHANGE_SQUARED = 1.0e-6f; + if (glm::length2(getUnscaledDimensions() - newDimensions) > MIN_SCALE_CHANGE_SQUARED) { withWriteLock([&] { _unscaledDimensions = newDimensions; }); @@ -2086,7 +2084,7 @@ void EntityItem::computeCollisionGroupAndFinalMask(int32_t& group, int32_t& mask } else { if (getDynamic()) { group = BULLET_COLLISION_GROUP_DYNAMIC; - } else if (isMovingRelativeToParent() || hasActions()) { + } else if (hasActions() || isMovingRelativeToParent()) { group = BULLET_COLLISION_GROUP_KINEMATIC; } else { group = BULLET_COLLISION_GROUP_STATIC; @@ -3057,30 +3055,18 @@ bool EntityItem::getCollisionless() const { } uint16_t EntityItem::getCollisionMask() const { - uint16_t result; - withReadLock([&] { - result = _collisionMask; - }); - return result; + return _collisionMask; } bool EntityItem::getDynamic() const { if (SHAPE_TYPE_STATIC_MESH == getShapeType()) { return false; } - bool result; - withReadLock([&] { - result = _dynamic; - }); - return result; + return _dynamic; } bool EntityItem::getLocked() const { - bool result; - withReadLock([&] { - result = _locked; - }); - return result; + return _locked; } void EntityItem::setLocked(bool value) { @@ -3152,7 +3138,6 @@ uint32_t EntityItem::getDirtyFlags() const { return result; } - void EntityItem::markDirtyFlags(uint32_t mask) { withWriteLock([&] { mask &= Simulation::DIRTY_FLAGS; diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 01ed949a0c..c57fd16a2e 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -448,7 +448,7 @@ public: bool clearActions(EntitySimulationPointer simulation); void setDynamicData(QByteArray dynamicData); const QByteArray getDynamicData() const; - bool hasActions() const { return !_objectActions.empty(); } + bool hasActions() const { return !_objectActions.empty() || !_grabActions.empty(); } QList getActionIDs() const { return _objectActions.keys(); } QVariantMap getActionArguments(const QUuid& actionID) const; void deserializeActions(); diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 4d210c96c5..6abe5c3899 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -217,9 +217,8 @@ PhysicsMotionType EntityMotionState::computePhysicsMotionType() const { } return MOTION_TYPE_DYNAMIC; } - if (_entity->isMovingRelativeToParent() || - _entity->hasActions() || - _entity->hasGrabs() || + if (_entity->hasActions() || + _entity->isMovingRelativeToParent() || _entity->hasAncestorOfType(NestableType::Avatar)) { return MOTION_TYPE_KINEMATIC; } From 44fc0d21dbc6542fcb56e26c86f02bed059d6e23 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 3 Apr 2019 13:43:39 -0700 Subject: [PATCH 446/446] Revert "Remove _compositeFramebuffer from display plugins" This reverts commit cb311408c68f2d465a80d76e0fdfe979cde96e13. --- .../Basic2DWindowOpenGLDisplayPlugin.cpp | 6 +- .../Basic2DWindowOpenGLDisplayPlugin.h | 2 +- .../display-plugins/OpenGLDisplayPlugin.cpp | 175 ++++++++++-------- .../src/display-plugins/OpenGLDisplayPlugin.h | 28 +-- .../hmd/DebugHmdDisplayPlugin.h | 2 +- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 64 ++++--- .../display-plugins/hmd/HmdDisplayPlugin.h | 11 +- .../stereo/InterleavedStereoDisplayPlugin.cpp | 4 +- .../stereo/InterleavedStereoDisplayPlugin.h | 2 +- .../src/OculusMobileDisplayPlugin.cpp | 10 +- .../src/OculusMobileDisplayPlugin.h | 4 +- .../plugins/src/plugins/DisplayPlugin.cpp | 12 +- libraries/plugins/src/plugins/DisplayPlugin.h | 8 +- .../render-utils/src/RenderCommonTask.cpp | 4 +- libraries/render/src/render/Args.h | 2 +- plugins/oculus/src/OculusDebugDisplayPlugin.h | 2 +- plugins/oculus/src/OculusDisplayPlugin.cpp | 28 +-- plugins/oculus/src/OculusDisplayPlugin.h | 2 +- .../src/OculusLegacyDisplayPlugin.cpp | 4 +- .../src/OculusLegacyDisplayPlugin.h | 2 +- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 16 +- plugins/openvr/src/OpenVrDisplayPlugin.h | 4 +- 22 files changed, 211 insertions(+), 181 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp index f55f5919f5..9828a8beda 100644 --- a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp @@ -109,7 +109,7 @@ bool Basic2DWindowOpenGLDisplayPlugin::internalActivate() { return Parent::internalActivate(); } -void Basic2DWindowOpenGLDisplayPlugin::compositeExtra(const gpu::FramebufferPointer& compositeFramebuffer) { +void Basic2DWindowOpenGLDisplayPlugin::compositeExtra() { #if defined(Q_OS_ANDROID) auto& virtualPadManager = VirtualPad::Manager::instance(); if(virtualPadManager.getLeftVirtualPad()->isShown()) { @@ -121,7 +121,7 @@ void Basic2DWindowOpenGLDisplayPlugin::compositeExtra(const gpu::FramebufferPoin render([&](gpu::Batch& batch) { batch.enableStereo(false); - batch.setFramebuffer(compositeFramebuffer); + batch.setFramebuffer(_compositeFramebuffer); batch.resetViewTransform(); batch.setProjectionTransform(mat4()); batch.setPipeline(_cursorPipeline); @@ -140,7 +140,7 @@ void Basic2DWindowOpenGLDisplayPlugin::compositeExtra(const gpu::FramebufferPoin }); } #endif - Parent::compositeExtra(compositeFramebuffer); + Parent::compositeExtra(); } static const uint32_t MIN_THROTTLE_CHECK_FRAMES = 60; diff --git a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h index d4c321a571..cc304c19c2 100644 --- a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h @@ -33,7 +33,7 @@ public: virtual bool isThrottled() const override; - virtual void compositeExtra(const gpu::FramebufferPointer&) override; + virtual void compositeExtra() override; virtual void pluginUpdate() override {}; diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 5bc84acc6a..c536e6b6e2 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -379,6 +379,14 @@ void OpenGLDisplayPlugin::customizeContext() { scissorState->setDepthTest(gpu::State::DepthTest(false)); scissorState->setScissorEnable(true); + { +#ifdef Q_OS_ANDROID + gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTextureGammaLinearToSRGB); +#else + gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTexture); +#endif + _simplePipeline = gpu::Pipeline::create(program, scissorState); + } { #ifdef Q_OS_ANDROID gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTextureGammaLinearToSRGB); @@ -388,59 +396,29 @@ void OpenGLDisplayPlugin::customizeContext() { _presentPipeline = gpu::Pipeline::create(program, scissorState); } - - // HUD operator { - gpu::PipelinePointer hudPipeline; - { - gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTexture); - hudPipeline = gpu::Pipeline::create(program, blendState); - } - - gpu::PipelinePointer hudMirrorPipeline; - { - gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTextureMirroredX); - hudMirrorPipeline = gpu::Pipeline::create(program, blendState); - } - - - _hudOperator = [=](gpu::Batch& batch, const gpu::TexturePointer& hudTexture, const gpu::FramebufferPointer& compositeFramebuffer, bool mirror) { - auto hudStereo = isStereo(); - auto hudCompositeFramebufferSize = compositeFramebuffer->getSize(); - std::array hudEyeViewports; - for_each_eye([&](Eye eye) { - hudEyeViewports[eye] = eyeViewport(eye); - }); - if (hudPipeline && hudTexture) { - batch.enableStereo(false); - batch.setPipeline(mirror ? hudMirrorPipeline : hudPipeline); - batch.setResourceTexture(0, hudTexture); - if (hudStereo) { - for_each_eye([&](Eye eye) { - batch.setViewportTransform(hudEyeViewports[eye]); - batch.draw(gpu::TRIANGLE_STRIP, 4); - }); - } else { - batch.setViewportTransform(ivec4(uvec2(0), hudCompositeFramebufferSize)); - batch.draw(gpu::TRIANGLE_STRIP, 4); - } - } - }; - + gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTexture); + _hudPipeline = gpu::Pipeline::create(program, blendState); + } + { + gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTextureMirroredX); + _mirrorHUDPipeline = gpu::Pipeline::create(program, blendState); } - { gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTransformedTexture); _cursorPipeline = gpu::Pipeline::create(program, blendState); } } + updateCompositeFramebuffer(); } void OpenGLDisplayPlugin::uncustomizeContext() { _presentPipeline.reset(); _cursorPipeline.reset(); - _hudOperator = DEFAULT_HUD_OPERATOR; + _hudPipeline.reset(); + _mirrorHUDPipeline.reset(); + _compositeFramebuffer.reset(); withPresentThreadLock([&] { _currentFrame.reset(); _lastFrame = nullptr; @@ -532,16 +510,24 @@ void OpenGLDisplayPlugin::captureFrame(const std::string& filename) const { }); } +void OpenGLDisplayPlugin::renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer& texture, const glm::ivec4& viewport, const glm::ivec4& scissor) { + renderFromTexture(batch, texture, viewport, scissor, nullptr); +} -void OpenGLDisplayPlugin::renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer& texture, const glm::ivec4& viewport, const glm::ivec4& scissor, const gpu::FramebufferPointer& destFbo, const gpu::FramebufferPointer& copyFbo /*=gpu::FramebufferPointer()*/) { +void OpenGLDisplayPlugin::renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer& texture, const glm::ivec4& viewport, const glm::ivec4& scissor, const gpu::FramebufferPointer& copyFbo /*=gpu::FramebufferPointer()*/) { + auto fbo = gpu::FramebufferPointer(); batch.enableStereo(false); batch.resetViewTransform(); - batch.setFramebuffer(destFbo); + batch.setFramebuffer(fbo); batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); batch.setStateScissorRect(scissor); batch.setViewportTransform(viewport); batch.setResourceTexture(0, texture); +#ifndef USE_GLES batch.setPipeline(_presentPipeline); +#else + batch.setPipeline(_simplePipeline); +#endif batch.draw(gpu::TRIANGLE_STRIP, 4); if (copyFbo) { gpu::Vec4i copyFboRect(0, 0, copyFbo->getWidth(), copyFbo->getHeight()); @@ -567,7 +553,7 @@ void OpenGLDisplayPlugin::renderFromTexture(gpu::Batch& batch, const gpu::Textur batch.setViewportTransform(copyFboRect); batch.setStateScissorRect(copyFboRect); batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, {0.0f, 0.0f, 0.0f, 1.0f}); - batch.blit(destFbo, sourceRect, copyFbo, copyRect); + batch.blit(fbo, sourceRect, copyFbo, copyRect); } } @@ -595,14 +581,41 @@ void OpenGLDisplayPlugin::updateFrameData() { }); } -void OpenGLDisplayPlugin::compositePointer(const gpu::FramebufferPointer& compositeFramebuffer) { +std::function OpenGLDisplayPlugin::getHUDOperator() { + auto hudPipeline = _hudPipeline; + auto hudMirrorPipeline = _mirrorHUDPipeline; + auto hudStereo = isStereo(); + auto hudCompositeFramebufferSize = _compositeFramebuffer->getSize(); + std::array hudEyeViewports; + for_each_eye([&](Eye eye) { + hudEyeViewports[eye] = eyeViewport(eye); + }); + return [=](gpu::Batch& batch, const gpu::TexturePointer& hudTexture, bool mirror) { + if (hudPipeline && hudTexture) { + batch.enableStereo(false); + batch.setPipeline(mirror ? hudMirrorPipeline : hudPipeline); + batch.setResourceTexture(0, hudTexture); + if (hudStereo) { + for_each_eye([&](Eye eye) { + batch.setViewportTransform(hudEyeViewports[eye]); + batch.draw(gpu::TRIANGLE_STRIP, 4); + }); + } else { + batch.setViewportTransform(ivec4(uvec2(0), hudCompositeFramebufferSize)); + batch.draw(gpu::TRIANGLE_STRIP, 4); + } + } + }; +} + +void OpenGLDisplayPlugin::compositePointer() { auto& cursorManager = Cursor::Manager::instance(); const auto& cursorData = _cursorsData[cursorManager.getCursor()->getIcon()]; auto cursorTransform = DependencyManager::get()->getReticleTransform(glm::mat4()); render([&](gpu::Batch& batch) { batch.enableStereo(false); batch.setProjectionTransform(mat4()); - batch.setFramebuffer(compositeFramebuffer); + batch.setFramebuffer(_compositeFramebuffer); batch.setPipeline(_cursorPipeline); batch.setResourceTexture(0, cursorData.texture); batch.resetViewTransform(); @@ -613,13 +626,34 @@ void OpenGLDisplayPlugin::compositePointer(const gpu::FramebufferPointer& compos batch.draw(gpu::TRIANGLE_STRIP, 4); }); } else { - batch.setViewportTransform(ivec4(uvec2(0), compositeFramebuffer->getSize())); + batch.setViewportTransform(ivec4(uvec2(0), _compositeFramebuffer->getSize())); batch.draw(gpu::TRIANGLE_STRIP, 4); } }); } -void OpenGLDisplayPlugin::compositeLayers(const gpu::FramebufferPointer& compositeFramebuffer) { +void OpenGLDisplayPlugin::compositeScene() { + render([&](gpu::Batch& batch) { + batch.enableStereo(false); + batch.setFramebuffer(_compositeFramebuffer); + batch.setViewportTransform(ivec4(uvec2(), _compositeFramebuffer->getSize())); + batch.setStateScissorRect(ivec4(uvec2(), _compositeFramebuffer->getSize())); + batch.resetViewTransform(); + batch.setProjectionTransform(mat4()); + batch.setPipeline(_simplePipeline); + batch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); + batch.draw(gpu::TRIANGLE_STRIP, 4); + }); +} + +void OpenGLDisplayPlugin::compositeLayers() { + updateCompositeFramebuffer(); + + { + PROFILE_RANGE_EX(render_detail, "compositeScene", 0xff0077ff, (uint64_t)presentCount()) + compositeScene(); + } + #ifdef HIFI_ENABLE_NSIGHT_DEBUG if (false) // do not draw the HUD if running nsight debug #endif @@ -633,35 +667,23 @@ void OpenGLDisplayPlugin::compositeLayers(const gpu::FramebufferPointer& composi { PROFILE_RANGE_EX(render_detail, "compositeExtra", 0xff0077ff, (uint64_t)presentCount()) - compositeExtra(compositeFramebuffer); + compositeExtra(); } // Draw the pointer last so it's on top of everything auto compositorHelper = DependencyManager::get(); if (compositorHelper->getReticleVisible()) { PROFILE_RANGE_EX(render_detail, "compositePointer", 0xff0077ff, (uint64_t)presentCount()) - compositePointer(compositeFramebuffer); + compositePointer(); } } -void OpenGLDisplayPlugin::internalPresent(const gpu::FramebufferPointer& compositeFramebuffer) { +void OpenGLDisplayPlugin::internalPresent() { render([&](gpu::Batch& batch) { // Note: _displayTexture must currently be the same size as the display. uvec2 dims = _displayTexture ? uvec2(_displayTexture->getDimensions()) : getSurfacePixels(); auto viewport = ivec4(uvec2(0), dims); - - gpu::TexturePointer finalTexture; - if (_displayTexture) { - finalTexture = _displayTexture; - } else if (compositeFramebuffer) { - finalTexture = compositeFramebuffer->getRenderBuffer(0); - } else { - qCWarning(displayPlugins) << "No valid texture for output"; - } - - if (finalTexture) { - renderFromTexture(batch, finalTexture, viewport, viewport); - } + renderFromTexture(batch, _displayTexture ? _displayTexture : _compositeFramebuffer->getRenderBuffer(0), viewport, viewport); }); swapBuffers(); _presentRate.increment(); @@ -678,7 +700,7 @@ void OpenGLDisplayPlugin::present() { } incrementPresentCount(); - if (_currentFrame && _currentFrame->framebuffer) { + if (_currentFrame) { auto correction = getViewCorrection(); getGLBackend()->setCameraCorrection(correction, _prevRenderView); _prevRenderView = correction * _currentFrame->view; @@ -698,18 +720,18 @@ void OpenGLDisplayPlugin::present() { // Write all layers to a local framebuffer { PROFILE_RANGE_EX(render, "composite", 0xff00ffff, frameId) - compositeLayers(_currentFrame->framebuffer); + compositeLayers(); } // Take the composite framebuffer and send it to the output device { PROFILE_RANGE_EX(render, "internalPresent", 0xff00ffff, frameId) - internalPresent(_currentFrame->framebuffer); + internalPresent(); } gpu::Backend::freeGPUMemSize.set(gpu::gl::getFreeDedicatedMemory()); } else if (alwaysPresent()) { - internalPresent(nullptr); + internalPresent(); } _movingAveragePresent.addSample((float)(usecTimestampNow() - startPresent)); } @@ -766,12 +788,7 @@ bool OpenGLDisplayPlugin::setDisplayTexture(const QString& name) { } QImage OpenGLDisplayPlugin::getScreenshot(float aspectRatio) const { - if (!_currentFrame || !_currentFrame->framebuffer) { - return QImage(); - } - - auto compositeFramebuffer = _currentFrame->framebuffer; - auto size = compositeFramebuffer->getSize(); + auto size = _compositeFramebuffer->getSize(); if (isHmd()) { size.x /= 2; } @@ -789,7 +806,7 @@ QImage OpenGLDisplayPlugin::getScreenshot(float aspectRatio) const { auto glBackend = const_cast(*this).getGLBackend(); QImage screenshot(bestSize.x, bestSize.y, QImage::Format_ARGB32); withOtherThreadContext([&] { - glBackend->downloadFramebuffer(compositeFramebuffer, ivec4(corner, bestSize), screenshot); + glBackend->downloadFramebuffer(_compositeFramebuffer, ivec4(corner, bestSize), screenshot); }); return screenshot.mirrored(false, true); } @@ -841,7 +858,7 @@ bool OpenGLDisplayPlugin::beginFrameRender(uint32_t frameIndex) { } ivec4 OpenGLDisplayPlugin::eyeViewport(Eye eye) const { - auto vpSize = glm::uvec2(getRecommendedRenderSize()); + uvec2 vpSize = _compositeFramebuffer->getSize(); vpSize.x /= 2; uvec2 vpPos; if (eye == Eye::Right) { @@ -874,6 +891,14 @@ void OpenGLDisplayPlugin::render(std::function f) { OpenGLDisplayPlugin::~OpenGLDisplayPlugin() { } +void OpenGLDisplayPlugin::updateCompositeFramebuffer() { + auto renderSize = glm::uvec2(getRecommendedRenderSize()); + if (!_compositeFramebuffer || _compositeFramebuffer->getSize() != renderSize) { + _compositeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("OpenGLDisplayPlugin::composite", gpu::Element::COLOR_RGBA_32, renderSize.x, renderSize.y)); + // _compositeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("OpenGLDisplayPlugin::composite", gpu::Element::COLOR_SRGBA_32, renderSize.x, renderSize.y)); + } +} + void OpenGLDisplayPlugin::copyTextureToQuickFramebuffer(NetworkTexturePointer networkTexture, QOpenGLFramebufferObject* target, GLsync* fenceSync) { #if !defined(USE_GLES) auto glBackend = const_cast(*this).getGLBackend(); diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index 3c48e8fc48..49a38ecb4c 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -94,10 +94,14 @@ protected: // is not populated virtual bool alwaysPresent() const { return false; } + void updateCompositeFramebuffer(); + virtual QThread::Priority getPresentPriority() { return QThread::HighPriority; } - virtual void compositeLayers(const gpu::FramebufferPointer&); - virtual void compositePointer(const gpu::FramebufferPointer&); - virtual void compositeExtra(const gpu::FramebufferPointer&) {}; + virtual void compositeLayers(); + virtual void compositeScene(); + virtual std::function getHUDOperator(); + virtual void compositePointer(); + virtual void compositeExtra() {}; // These functions must only be called on the presentation thread virtual void customizeContext(); @@ -112,10 +116,10 @@ protected: virtual void deactivateSession() {} // Plugin specific functionality to send the composed scene to the output window or device - virtual void internalPresent(const gpu::FramebufferPointer&); + virtual void internalPresent(); - - void renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer& texture, const glm::ivec4& viewport, const glm::ivec4& scissor, const gpu::FramebufferPointer& destFbo = nullptr, const gpu::FramebufferPointer& copyFbo = nullptr); + void renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer& texture, const glm::ivec4& viewport, const glm::ivec4& scissor, const gpu::FramebufferPointer& fbo); + void renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer& texture, const glm::ivec4& viewport, const glm::ivec4& scissor); virtual void updateFrameData(); virtual glm::mat4 getViewCorrection() { return glm::mat4(); } @@ -138,8 +142,14 @@ protected: gpu::FramePointer _currentFrame; gpu::Frame* _lastFrame { nullptr }; mat4 _prevRenderView; + gpu::FramebufferPointer _compositeFramebuffer; + gpu::PipelinePointer _hudPipeline; + gpu::PipelinePointer _mirrorHUDPipeline; + gpu::ShaderPointer _mirrorHUDPS; + gpu::PipelinePointer _simplePipeline; + gpu::PipelinePointer _presentPipeline; gpu::PipelinePointer _cursorPipeline; - gpu::TexturePointer _displayTexture; + gpu::TexturePointer _displayTexture{}; float _compositeHUDAlpha { 1.0f }; struct CursorData { @@ -175,9 +185,5 @@ protected: // be serialized through this mutex mutable Mutex _presentMutex; float _hudAlpha{ 1.0f }; - -private: - gpu::PipelinePointer _presentPipeline; - }; diff --git a/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.h index 95592cc490..f2b1f36419 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.h @@ -24,7 +24,7 @@ public: protected: void updatePresentPose() override; - void hmdPresent(const gpu::FramebufferPointer&) override {} + void hmdPresent() override {} bool isHmdMounted() const override { return true; } bool internalActivate() override; private: diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index a515232b3f..321bcc3fd2 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -114,23 +114,20 @@ void HmdDisplayPlugin::internalDeactivate() { void HmdDisplayPlugin::customizeContext() { Parent::customizeContext(); - _hudOperator = _hudRenderer.build(); + _hudRenderer.build(); } void HmdDisplayPlugin::uncustomizeContext() { // This stops the weirdness where if the preview was disabled, on switching back to 2D, // the vsync was stuck in the disabled state. No idea why that happens though. _disablePreview = false; - if (_currentFrame && _currentFrame->framebuffer) { - render([&](gpu::Batch& batch) { - batch.enableStereo(false); - batch.resetViewTransform(); - batch.setFramebuffer(_currentFrame->framebuffer); - batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); - }); - - } - _hudRenderer = {}; + render([&](gpu::Batch& batch) { + batch.enableStereo(false); + batch.resetViewTransform(); + batch.setFramebuffer(_compositeFramebuffer); + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); + }); + _hudRenderer = HUDRenderer(); _previewTexture.reset(); Parent::uncustomizeContext(); } @@ -177,11 +174,11 @@ float HmdDisplayPlugin::getLeftCenterPixel() const { return leftCenterPixel; } -void HmdDisplayPlugin::internalPresent(const gpu::FramebufferPointer& compositeFramebuffer) { +void HmdDisplayPlugin::internalPresent() { PROFILE_RANGE_EX(render, __FUNCTION__, 0xff00ff00, (uint64_t)presentCount()) // Composite together the scene, hud and mouse cursor - hmdPresent(compositeFramebuffer); + hmdPresent(); if (_displayTexture) { // Note: _displayTexture must currently be the same size as the display. @@ -263,7 +260,7 @@ void HmdDisplayPlugin::internalPresent(const gpu::FramebufferPointer& compositeF viewport.z *= 2; } - renderFromTexture(batch, compositeFramebuffer->getRenderBuffer(0), viewport, scissor, nullptr, fbo); + renderFromTexture(batch, _compositeFramebuffer->getRenderBuffer(0), viewport, scissor, fbo); }); swapBuffers(); @@ -348,7 +345,7 @@ glm::mat4 HmdDisplayPlugin::getViewCorrection() { } } -DisplayPlugin::HUDOperator HmdDisplayPlugin::HUDRenderer::build() { +void HmdDisplayPlugin::HUDRenderer::build() { vertices = std::make_shared(); indices = std::make_shared(); @@ -383,7 +380,7 @@ DisplayPlugin::HUDOperator HmdDisplayPlugin::HUDRenderer::build() { indexCount = numberOfRectangles * TRIANGLE_PER_RECTANGLE * VERTEX_PER_TRANGLE; // Compute indices order - std::vector indexData; + std::vector indices; for (int i = 0; i < stacks - 1; i++) { for (int j = 0; j < slices - 1; j++) { GLushort bottomLeftIndex = i * slices + j; @@ -391,21 +388,24 @@ DisplayPlugin::HUDOperator HmdDisplayPlugin::HUDRenderer::build() { GLushort topLeftIndex = bottomLeftIndex + slices; GLushort topRightIndex = topLeftIndex + 1; // FIXME make a z-order curve for better vertex cache locality - indexData.push_back(topLeftIndex); - indexData.push_back(bottomLeftIndex); - indexData.push_back(topRightIndex); + indices.push_back(topLeftIndex); + indices.push_back(bottomLeftIndex); + indices.push_back(topRightIndex); - indexData.push_back(topRightIndex); - indexData.push_back(bottomLeftIndex); - indexData.push_back(bottomRightIndex); + indices.push_back(topRightIndex); + indices.push_back(bottomLeftIndex); + indices.push_back(bottomRightIndex); } } - indices->append(indexData); + this->indices->append(indices); format = std::make_shared(); // 1 for everyone format->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); format->setAttribute(gpu::Stream::TEXCOORD, gpu::Stream::TEXCOORD, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV)); uniformsBuffer = std::make_shared(sizeof(Uniforms), nullptr); + updatePipeline(); +} +void HmdDisplayPlugin::HUDRenderer::updatePipeline() { if (!pipeline) { auto program = gpu::Shader::createProgram(shader::render_utils::program::hmd_ui); gpu::StatePointer state = gpu::StatePointer(new gpu::State()); @@ -416,6 +416,10 @@ DisplayPlugin::HUDOperator HmdDisplayPlugin::HUDRenderer::build() { pipeline = gpu::Pipeline::create(program, state); } +} + +std::function HmdDisplayPlugin::HUDRenderer::render(HmdDisplayPlugin& plugin) { + updatePipeline(); auto hudPipeline = pipeline; auto hudFormat = format; @@ -424,9 +428,9 @@ DisplayPlugin::HUDOperator HmdDisplayPlugin::HUDRenderer::build() { auto hudUniformBuffer = uniformsBuffer; auto hudUniforms = uniforms; auto hudIndexCount = indexCount; - return [=](gpu::Batch& batch, const gpu::TexturePointer& hudTexture, const gpu::FramebufferPointer&, const bool mirror) { - if (pipeline && hudTexture) { - batch.setPipeline(pipeline); + return [=](gpu::Batch& batch, const gpu::TexturePointer& hudTexture, bool mirror) { + if (hudPipeline && hudTexture) { + batch.setPipeline(hudPipeline); batch.setInputFormat(hudFormat); gpu::BufferView posView(hudVertices, VERTEX_OFFSET, hudVertices->getSize(), VERTEX_STRIDE, hudFormat->getAttributes().at(gpu::Stream::POSITION)._element); @@ -450,7 +454,7 @@ DisplayPlugin::HUDOperator HmdDisplayPlugin::HUDRenderer::build() { }; } -void HmdDisplayPlugin::compositePointer(const gpu::FramebufferPointer& compositeFramebuffer) { +void HmdDisplayPlugin::compositePointer() { auto& cursorManager = Cursor::Manager::instance(); const auto& cursorData = _cursorsData[cursorManager.getCursor()->getIcon()]; auto compositorHelper = DependencyManager::get(); @@ -459,7 +463,7 @@ void HmdDisplayPlugin::compositePointer(const gpu::FramebufferPointer& composite render([&](gpu::Batch& batch) { // FIXME use standard gpu stereo rendering for this. batch.enableStereo(false); - batch.setFramebuffer(compositeFramebuffer); + batch.setFramebuffer(_compositeFramebuffer); batch.setPipeline(_cursorPipeline); batch.setResourceTexture(0, cursorData.texture); batch.resetViewTransform(); @@ -474,6 +478,10 @@ void HmdDisplayPlugin::compositePointer(const gpu::FramebufferPointer& composite }); } +std::function HmdDisplayPlugin::getHUDOperator() { + return _hudRenderer.render(*this); +} + HmdDisplayPlugin::~HmdDisplayPlugin() { } diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index 6755c5b7e0..d8c0ce8e1d 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -53,15 +53,16 @@ signals: void hmdVisibleChanged(bool visible); protected: - virtual void hmdPresent(const gpu::FramebufferPointer&) = 0; + virtual void hmdPresent() = 0; virtual bool isHmdMounted() const = 0; virtual void postPreview() {}; virtual void updatePresentPose(); bool internalActivate() override; void internalDeactivate() override; - void compositePointer(const gpu::FramebufferPointer&) override; - void internalPresent(const gpu::FramebufferPointer&) override; + std::function getHUDOperator() override; + void compositePointer() override; + void internalPresent() override; void customizeContext() override; void uncustomizeContext() override; void updateFrameData() override; @@ -119,6 +120,8 @@ private: static const size_t TEXTURE_OFFSET { offsetof(Vertex, uv) }; static const int VERTEX_STRIDE { sizeof(Vertex) }; - HUDOperator build(); + void build(); + void updatePipeline(); + std::function render(HmdDisplayPlugin& plugin); } _hudRenderer; }; diff --git a/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp index 69aa7fc344..0ae0f9b1b6 100644 --- a/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp @@ -37,13 +37,13 @@ glm::uvec2 InterleavedStereoDisplayPlugin::getRecommendedRenderSize() const { return result; } -void InterleavedStereoDisplayPlugin::internalPresent(const gpu::FramebufferPointer& compositeFramebuffer) { +void InterleavedStereoDisplayPlugin::internalPresent() { render([&](gpu::Batch& batch) { batch.enableStereo(false); batch.resetViewTransform(); batch.setFramebuffer(gpu::FramebufferPointer()); batch.setViewportTransform(ivec4(uvec2(0), getSurfacePixels())); - batch.setResourceTexture(0, compositeFramebuffer->getRenderBuffer(0)); + batch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); batch.setPipeline(_interleavedPresentPipeline); batch.draw(gpu::TRIANGLE_STRIP, 4); }); diff --git a/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.h index 52dfa8f402..debd340f24 100644 --- a/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.h @@ -21,7 +21,7 @@ protected: // initialize OpenGL context settings needed by the plugin void customizeContext() override; void uncustomizeContext() override; - void internalPresent(const gpu::FramebufferPointer&) override; + void internalPresent() override; private: static const QString NAME; diff --git a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp index 12a9b12adc..9809d02866 100644 --- a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp +++ b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp @@ -245,7 +245,7 @@ void OculusMobileDisplayPlugin::updatePresentPose() { }); } -void OculusMobileDisplayPlugin::internalPresent(const gpu::FramebufferPointer& compsiteFramebuffer) { +void OculusMobileDisplayPlugin::internalPresent() { VrHandler::pollTask(); if (!vrActive()) { @@ -253,12 +253,8 @@ void OculusMobileDisplayPlugin::internalPresent(const gpu::FramebufferPointer& c return; } - GLuint sourceTexture = 0; - glm::uvec2 sourceSize; - if (compsiteFramebuffer) { - sourceTexture = getGLBackend()->getTextureID(compsiteFramebuffer->getRenderBuffer(0)); - sourceSize = { compsiteFramebuffer->getWidth(), compsiteFramebuffer->getHeight() }; - } + auto sourceTexture = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0)); + glm::uvec2 sourceSize{ _compositeFramebuffer->getWidth(), _compositeFramebuffer->getHeight() }; VrHandler::presentFrame(sourceTexture, sourceSize, presentTracking); _presentRate.increment(); } diff --git a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.h b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.h index b5f7aa57b0..a98989655e 100644 --- a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.h +++ b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.h @@ -54,8 +54,8 @@ protected: void uncustomizeContext() override; void updatePresentPose() override; - void internalPresent(const gpu::FramebufferPointer&) override; - void hmdPresent(const gpu::FramebufferPointer&) override { throw std::runtime_error("Unused"); } + void internalPresent() override; + void hmdPresent() override { throw std::runtime_error("Unused"); } bool isHmdMounted() const override; bool alwaysPresent() const override { return true; } diff --git a/libraries/plugins/src/plugins/DisplayPlugin.cpp b/libraries/plugins/src/plugins/DisplayPlugin.cpp index 71db87557c..47503e8f85 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.cpp +++ b/libraries/plugins/src/plugins/DisplayPlugin.cpp @@ -2,12 +2,6 @@ #include - -const DisplayPlugin::HUDOperator DisplayPlugin::DEFAULT_HUD_OPERATOR{ std::function() }; - -DisplayPlugin::DisplayPlugin() : _hudOperator{ DEFAULT_HUD_OPERATOR } { -} - int64_t DisplayPlugin::getPaintDelayUsecs() const { std::lock_guard lock(_paintDelayMutex); return _paintDelayTimer.isValid() ? _paintDelayTimer.nsecsElapsed() / NSECS_PER_USEC : 0; @@ -41,8 +35,8 @@ void DisplayPlugin::waitForPresent() { } } -std::function DisplayPlugin::getHUDOperator() { - HUDOperator hudOperator; +std::function DisplayPlugin::getHUDOperator() { + std::function hudOperator; { QMutexLocker locker(&_presentMutex); hudOperator = _hudOperator; @@ -54,5 +48,3 @@ glm::mat4 HmdDisplay::getEyeToHeadTransform(Eye eye) const { static const glm::mat4 xform; return xform; } - - diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index 9194fde3ac..aa52e57c3f 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -121,8 +121,6 @@ class DisplayPlugin : public Plugin, public HmdDisplay { Q_OBJECT using Parent = Plugin; public: - DisplayPlugin(); - virtual int getRequiredThreadCount() const { return 0; } virtual bool isHmd() const { return false; } virtual int getHmdScreen() const { return -1; } @@ -216,8 +214,7 @@ public: void waitForPresent(); float getAveragePresentTime() { return _movingAveragePresent.average / (float)USECS_PER_MSEC; } // in msec - using HUDOperator = std::function; - virtual HUDOperator getHUDOperator() final; + std::function getHUDOperator(); static const QString& MENU_PATH(); @@ -234,8 +231,7 @@ protected: gpu::ContextPointer _gpuContext; - static const HUDOperator DEFAULT_HUD_OPERATOR; - HUDOperator _hudOperator; + std::function _hudOperator { std::function() }; MovingAverage _movingAveragePresent; diff --git a/libraries/render-utils/src/RenderCommonTask.cpp b/libraries/render-utils/src/RenderCommonTask.cpp index e021465ff3..b1a62625b2 100644 --- a/libraries/render-utils/src/RenderCommonTask.cpp +++ b/libraries/render-utils/src/RenderCommonTask.cpp @@ -126,8 +126,8 @@ void CompositeHUD::run(const RenderContextPointer& renderContext, const gpu::Fra if (inputs) { batch.setFramebuffer(inputs); } - if (renderContext->args->_hudOperator && renderContext->args->_blitFramebuffer) { - renderContext->args->_hudOperator(batch, renderContext->args->_hudTexture, renderContext->args->_blitFramebuffer, renderContext->args->_renderMode == RenderArgs::RenderMode::MIRROR_RENDER_MODE); + if (renderContext->args->_hudOperator) { + renderContext->args->_hudOperator(batch, renderContext->args->_hudTexture, renderContext->args->_renderMode == RenderArgs::RenderMode::MIRROR_RENDER_MODE); } }); #endif diff --git a/libraries/render/src/render/Args.h b/libraries/render/src/render/Args.h index 8b2fff68c6..b5c98e3428 100644 --- a/libraries/render/src/render/Args.h +++ b/libraries/render/src/render/Args.h @@ -131,7 +131,7 @@ namespace render { render::ScenePointer _scene; int8_t _cameraMode { -1 }; - std::function _hudOperator; + std::function _hudOperator; gpu::TexturePointer _hudTexture; }; diff --git a/plugins/oculus/src/OculusDebugDisplayPlugin.h b/plugins/oculus/src/OculusDebugDisplayPlugin.h index 690a488b34..ec05cd92e2 100644 --- a/plugins/oculus/src/OculusDebugDisplayPlugin.h +++ b/plugins/oculus/src/OculusDebugDisplayPlugin.h @@ -16,7 +16,7 @@ public: bool isSupported() const override; protected: - void hmdPresent(const gpu::FramebufferPointer&) override {} + void hmdPresent() override {} bool isHmdMounted() const override { return true; } private: diff --git a/plugins/oculus/src/OculusDisplayPlugin.cpp b/plugins/oculus/src/OculusDisplayPlugin.cpp index 48440ac80f..df01591639 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusDisplayPlugin.cpp @@ -108,16 +108,13 @@ void OculusDisplayPlugin::customizeContext() { } void OculusDisplayPlugin::uncustomizeContext() { - #if 0 - if (_currentFrame && _currentFrame->framebuffer) { - // Present a final black frame to the HMD - _currentFrame->framebuffer->Bound(FramebufferTarget::Draw, [] { - Context::ClearColor(0, 0, 0, 1); - Context::Clear().ColorBuffer(); - }); - hmdPresent(); - } + // Present a final black frame to the HMD + _compositeFramebuffer->Bound(FramebufferTarget::Draw, [] { + Context::ClearColor(0, 0, 0, 1); + Context::Clear().ColorBuffer(); + }); + hmdPresent(); #endif ovr_DestroyTextureSwapChain(_session, _textureSwapChain); @@ -130,7 +127,7 @@ void OculusDisplayPlugin::uncustomizeContext() { static const uint64_t FRAME_BUDGET = (11 * USECS_PER_MSEC); static const uint64_t FRAME_OVER_BUDGET = (15 * USECS_PER_MSEC); -void OculusDisplayPlugin::hmdPresent(const gpu::FramebufferPointer& compositeFramebuffer) { +void OculusDisplayPlugin::hmdPresent() { static uint64_t lastSubmitEnd = 0; if (!_customized) { @@ -160,8 +157,15 @@ void OculusDisplayPlugin::hmdPresent(const gpu::FramebufferPointer& compositeFra auto fbo = getGLBackend()->getFramebufferID(_outputFramebuffer); glNamedFramebufferTexture(fbo, GL_COLOR_ATTACHMENT0, curTexId, 0); render([&](gpu::Batch& batch) { - auto viewport = ivec4(uvec2(), _outputFramebuffer->getSize()); - renderFromTexture(batch, compositeFramebuffer->getRenderBuffer(0), viewport, viewport, _outputFramebuffer); + batch.enableStereo(false); + batch.setFramebuffer(_outputFramebuffer); + batch.setViewportTransform(ivec4(uvec2(), _outputFramebuffer->getSize())); + batch.setStateScissorRect(ivec4(uvec2(), _outputFramebuffer->getSize())); + batch.resetViewTransform(); + batch.setProjectionTransform(mat4()); + batch.setPipeline(_presentPipeline); + batch.setResourceTexture(0, _compositeFramebuffer->getRenderBuffer(0)); + batch.draw(gpu::TRIANGLE_STRIP, 4); }); glNamedFramebufferTexture(fbo, GL_COLOR_ATTACHMENT0, 0, 0); } diff --git a/plugins/oculus/src/OculusDisplayPlugin.h b/plugins/oculus/src/OculusDisplayPlugin.h index a0126d2e58..9209fd373e 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.h +++ b/plugins/oculus/src/OculusDisplayPlugin.h @@ -28,7 +28,7 @@ protected: QThread::Priority getPresentPriority() override { return QThread::TimeCriticalPriority; } bool internalActivate() override; - void hmdPresent(const gpu::FramebufferPointer&) override; + void hmdPresent() override; bool isHmdMounted() const override; void customizeContext() override; void uncustomizeContext() override; diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp index a928887866..e6b555443f 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp @@ -237,7 +237,7 @@ void OculusLegacyDisplayPlugin::uncustomizeContext() { Parent::uncustomizeContext(); } -void OculusLegacyDisplayPlugin::hmdPresent(const gpu::FramebufferPointer& compositeFramebuffer) { +void OculusLegacyDisplayPlugin::hmdPresent() { if (!_hswDismissed) { ovrHSWDisplayState hswState; ovrHmd_GetHSWDisplayState(_hmd, &hswState); @@ -252,7 +252,7 @@ void OculusLegacyDisplayPlugin::hmdPresent(const gpu::FramebufferPointer& compos memset(eyePoses, 0, sizeof(ovrPosef) * 2); eyePoses[0].Orientation = eyePoses[1].Orientation = ovrRotation; - GLint texture = getGLBackend()->getTextureID(compositeFramebuffer->getRenderBuffer(0)); + GLint texture = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0)); auto sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); glFlush(); if (_hmdWindow->makeCurrent()) { diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h index 241d626f0c..36bdd1c792 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h @@ -39,7 +39,7 @@ protected: void customizeContext() override; void uncustomizeContext() override; - void hmdPresent(const gpu::FramebufferPointer&) override; + void hmdPresent() override; bool isHmdMounted() const override { return true; } private: diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 3d22268472..11d941dcd0 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -511,13 +511,13 @@ void OpenVrDisplayPlugin::customizeContext() { Parent::customizeContext(); if (_threadedSubmit) { -// _compositeInfos[0].texture = _compositeFramebuffer->getRenderBuffer(0); + _compositeInfos[0].texture = _compositeFramebuffer->getRenderBuffer(0); for (size_t i = 0; i < COMPOSITING_BUFFER_SIZE; ++i) { -// if (0 != i) { + if (0 != i) { _compositeInfos[i].texture = gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, _renderTargetSize.x, _renderTargetSize.y, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT)); -// } + } _compositeInfos[i].textureID = getGLBackend()->getTextureID(_compositeInfos[i].texture); } _submitThread->_canvas = _submitCanvas; @@ -613,17 +613,17 @@ bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { return Parent::beginFrameRender(frameIndex); } -void OpenVrDisplayPlugin::compositeLayers(const gpu::FramebufferPointer& compositeFramebuffer) { +void OpenVrDisplayPlugin::compositeLayers() { if (_threadedSubmit) { ++_renderingIndex; _renderingIndex %= COMPOSITING_BUFFER_SIZE; auto& newComposite = _compositeInfos[_renderingIndex]; newComposite.pose = _currentPresentFrameInfo.presentPose; - compositeFramebuffer->setRenderBuffer(0, newComposite.texture); + _compositeFramebuffer->setRenderBuffer(0, newComposite.texture); } - Parent::compositeLayers(compositeFramebuffer); + Parent::compositeLayers(); if (_threadedSubmit) { auto& newComposite = _compositeInfos[_renderingIndex]; @@ -645,13 +645,13 @@ void OpenVrDisplayPlugin::compositeLayers(const gpu::FramebufferPointer& composi } } -void OpenVrDisplayPlugin::hmdPresent(const gpu::FramebufferPointer& compositeFramebuffer) { +void OpenVrDisplayPlugin::hmdPresent() { PROFILE_RANGE_EX(render, __FUNCTION__, 0xff00ff00, (uint64_t)_currentFrame->frameIndex) if (_threadedSubmit) { _submitThread->waitForPresent(); } else { - GLuint glTexId = getGLBackend()->getTextureID(compositeFramebuffer->getRenderBuffer(0)); + GLuint glTexId = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0)); vr::Texture_t vrTexture{ (void*)(uintptr_t)glTexId, vr::TextureType_OpenGL, vr::ColorSpace_Auto }; vr::VRCompositor()->Submit(vr::Eye_Left, &vrTexture, &OPENVR_TEXTURE_BOUNDS_LEFT); vr::VRCompositor()->Submit(vr::Eye_Right, &vrTexture, &OPENVR_TEXTURE_BOUNDS_RIGHT); diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.h b/plugins/openvr/src/OpenVrDisplayPlugin.h index 923a0f7a8f..265f328920 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.h +++ b/plugins/openvr/src/OpenVrDisplayPlugin.h @@ -72,8 +72,8 @@ protected: void internalDeactivate() override; void updatePresentPose() override; - void compositeLayers(const gpu::FramebufferPointer&) override; - void hmdPresent(const gpu::FramebufferPointer&) override; + void compositeLayers() override; + void hmdPresent() override; bool isHmdMounted() const override; void postPreview() override;

NameType

Description
leftHandPoleVector{@link Vec3}The direction the elbow should point in rig * coordinates.
leftHandIKEnabledbooleantrue if IK is enabled for the left - * hand.
rightHandIKEnabledbooleantrue if IK is enabled for the right - * hand.
rightHandPosition{@link Vec3}The desired position of the RightHand * joint in rig coordinates.
rightHandRotation{@link Quat}The desired orientation of the @@ -214,8 +210,6 @@ static const QString MAIN_STATE_MACHINE_RIGHT_HAND_POSITION("mainStateMachineRig *
rightFootPoleVector{@link Vec3}The direction the knee should face in rig * coordinates.
splineIKEnabledbooleantrue if IK is enabled for the spline.
isTalkingbooleantrue if the avatar is talking.
notIsTalkingbooleantrue if the avatar is not talking.
Create a ball of green smoke.
- - + + From e6c720f793f79217fe11f99381b6caf878efdf3b Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 22 Mar 2019 10:12:31 -0700 Subject: [PATCH 268/446] Add AudioClient mixing gains for local injectors and system sounds --- libraries/audio-client/src/AudioClient.cpp | 4 +++- libraries/audio-client/src/AudioClient.h | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 1c10d24f23..79811ac98f 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1366,7 +1366,9 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { memset(_localScratchBuffer, 0, bytesToRead); if (0 < injectorBuffer->readData((char*)_localScratchBuffer, bytesToRead)) { - float gain = injector->getVolume(); + bool isSystemSound = !injector->isPositionSet() && !injector->isAmbisonic(); + + float gain = injector->getVolume() * (isSystemSound ? _systemInjectorGain : _localInjectorGain); if (injector->isAmbisonic()) { diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index b9648219a5..6e1f48d5f4 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -239,6 +239,8 @@ public slots: void setInputVolume(float volume, bool emitSignal = true); void setReverb(bool reverb); void setReverbOptions(const AudioEffectOptions* options); + void setLocalInjectorGain(float gain) { _localInjectorGain = gain; }; + void setSystemInjectorGain(float gain) { _systemInjectorGain = gain; }; void outputNotify(); @@ -393,6 +395,8 @@ private: int16_t* _outputScratchBuffer { NULL }; // for local audio (used by audio injectors thread) + std::atomic _localInjectorGain { 1.0f }; + std::atomic _systemInjectorGain { 1.0f }; float _localMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; float* _localOutputMixBuffer { NULL }; From 23a6a66528ac68792594cd0cf7547737c7ddba94 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 22 Mar 2019 10:21:54 -0700 Subject: [PATCH 269/446] Add local injector gains to the Audio scripting interface --- interface/src/scripting/Audio.cpp | 34 ++++++++++++++++++++++++++ interface/src/scripting/Audio.h | 40 +++++++++++++++++++++++++++---- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index e0474b7bba..7e1a35762a 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -423,3 +423,37 @@ float Audio::getInjectorGain() { return DependencyManager::get()->getInjectorGain(); }); } + +void Audio::setLocalInjectorGain(float gain) { + withWriteLock([&] { + if (_localInjectorGain != gain) { + _localInjectorGain = gain; + // convert dB to amplitude + gain = fastExp2f(gain / 6.02059991f); + DependencyManager::get()->setLocalInjectorGain(gain); + } + }); +} + +float Audio::getLocalInjectorGain() { + return resultWithReadLock([&] { + return _localInjectorGain; + }); +} + +void Audio::setSystemInjectorGain(float gain) { + withWriteLock([&] { + if (_systemInjectorGain != gain) { + _systemInjectorGain = gain; + // convert dB to amplitude + gain = fastExp2f(gain / 6.02059991f); + DependencyManager::get()->setSystemInjectorGain(gain); + } + }); +} + +float Audio::getSystemInjectorGain() { + return resultWithReadLock([&] { + return _systemInjectorGain; + }); +} diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index 14a75d5ffe..d6823ea452 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -171,7 +171,7 @@ public: Q_INVOKABLE void setReverbOptions(const AudioEffectOptions* options); /**jsdoc - * Sets the master avatar gain at the server. + * Sets the avatar gain at the server. * Units are Decibels (dB) * @function Audio.setAvatarGain * @param {number} gain (in dB) @@ -179,14 +179,14 @@ public: Q_INVOKABLE void setAvatarGain(float gain); /**jsdoc - * Gets the master avatar gain at the server. + * Gets the avatar gain at the server. * @function Audio.getAvatarGain * @returns {number} gain (in dB) */ Q_INVOKABLE float getAvatarGain(); /**jsdoc - * Sets the audio injector gain at the server. + * Sets the injector gain at the server. * Units are Decibels (dB) * @function Audio.setInjectorGain * @param {number} gain (in dB) @@ -194,12 +194,42 @@ public: Q_INVOKABLE void setInjectorGain(float gain); /**jsdoc - * Gets the audio injector gain at the server. + * Gets the injector gain at the server. * @function Audio.getInjectorGain * @returns {number} gain (in dB) */ Q_INVOKABLE float getInjectorGain(); + /**jsdoc + * Sets the local injector gain in the client. + * Units are Decibels (dB) + * @function Audio.setLocalInjectorGain + * @param {number} gain (in dB) + */ + Q_INVOKABLE void setLocalInjectorGain(float gain); + + /**jsdoc + * Gets the local injector gain in the client. + * @function Audio.getLocalInjectorGain + * @returns {number} gain (in dB) + */ + Q_INVOKABLE float getLocalInjectorGain(); + + /**jsdoc + * Sets the injector gain for system sounds. + * Units are Decibels (dB) + * @function Audio.setSystemInjectorGain + * @param {number} gain (in dB) + */ + Q_INVOKABLE void setSystemInjectorGain(float gain); + + /**jsdoc + * Gets the injector gain for system sounds. + * @function Audio.getSystemInjectorGain + * @returns {number} gain (in dB) + */ + Q_INVOKABLE float getSystemInjectorGain(); + /**jsdoc * Starts making an audio recording of the audio being played in-world (i.e., not local-only audio) to a file in WAV format. * @function Audio.startRecording @@ -380,6 +410,8 @@ private: float _inputVolume { 1.0f }; float _inputLevel { 0.0f }; + float _localInjectorGain { 0.0f }; // in dB + float _systemInjectorGain { 0.0f }; // in dB bool _isClipping { false }; bool _enableNoiseReduction { true }; // Match default value of AudioClient::_isNoiseGateEnabled. bool _enableWarnWhenMuted { true }; From e8ddee280d6e8c11f897791c7d7041a1edd1a7c7 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 22 Mar 2019 10:24:30 -0700 Subject: [PATCH 270/446] Quantize and limit the local injector gains to match the network protocol --- interface/src/scripting/Audio.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 7e1a35762a..6dd1c40ef5 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -430,6 +430,8 @@ void Audio::setLocalInjectorGain(float gain) { _localInjectorGain = gain; // convert dB to amplitude gain = fastExp2f(gain / 6.02059991f); + // quantize and limit to match NodeList::setInjectorGain() + gain = unpackFloatGainFromByte(packFloatGainToByte(gain)); DependencyManager::get()->setLocalInjectorGain(gain); } }); @@ -447,6 +449,8 @@ void Audio::setSystemInjectorGain(float gain) { _systemInjectorGain = gain; // convert dB to amplitude gain = fastExp2f(gain / 6.02059991f); + // quantize and limit to match NodeList::setInjectorGain() + gain = unpackFloatGainFromByte(packFloatGainToByte(gain)); DependencyManager::get()->setSystemInjectorGain(gain); } }); From 77ea47a9dbbb49c626e3ccae79fbcd34645fffd1 Mon Sep 17 00:00:00 2001 From: David Back Date: Fri, 22 Mar 2019 11:21:39 -0700 Subject: [PATCH 271/446] fix no name material override, hide auto scale text after sliding --- .../Editor/AvatarExporter/AvatarExporter.cs | 21 +++++++++++------- tools/unity-avatar-exporter/Assets/README.txt | 2 +- .../avatarExporter.unitypackage | Bin 74615 -> 74729 bytes 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs index 11d83a52e8..4e06772f4b 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs @@ -17,7 +17,7 @@ using System.Text.RegularExpressions; class AvatarExporter : MonoBehaviour { // update version number for every PR that changes this file, also set updated version in README file - static readonly string AVATAR_EXPORTER_VERSION = "0.3.7"; + static readonly string AVATAR_EXPORTER_VERSION = "0.4.0"; static readonly float HIPS_GROUND_MIN_Y = 0.01f; static readonly float HIPS_SPINE_CHEST_MIN_SEPARATION = 0.001f; @@ -697,11 +697,10 @@ class AvatarExporter : MonoBehaviour { if (materialDatas.Count > 0) { string materialJson = "{ "; foreach (var materialData in materialDatas) { - // if this is the only material in the mapping and it is the default name No Name mapped to No Name, + // if this is the only material in the mapping and it is mapped to default material name No Name, // then the avatar has no embedded materials and this material should be applied to all meshes string materialName = materialData.Key; - if (materialMappings.Count == 1 && materialName == DEFAULT_MATERIAL_NAME && - materialMappings[materialName] == DEFAULT_MATERIAL_NAME) { + if (materialMappings.Count == 1 && materialName == DEFAULT_MATERIAL_NAME) { materialJson += "\"all\": "; } else { materialJson += "\"mat::" + materialName + "\": "; @@ -1300,6 +1299,7 @@ class ExportProjectWindow : EditorWindow { const float DEFAULT_AVATAR_HEIGHT = 1.755f; const float MAXIMUM_RECOMMENDED_HEIGHT = DEFAULT_AVATAR_HEIGHT * 1.5f; const float MINIMUM_RECOMMENDED_HEIGHT = DEFAULT_AVATAR_HEIGHT * 0.25f; + const float SLIDER_DIFFERENCE_REMOVE_TEXT = 0.01f; readonly Color COLOR_YELLOW = Color.yellow; //new Color(0.9176f, 0.8274f, 0.0f); readonly Color COLOR_BACKGROUND = new Color(0.5f, 0.5f, 0.5f); @@ -1314,6 +1314,7 @@ class ExportProjectWindow : EditorWindow { Vector2 warningScrollPosition = new Vector2(0, 0); string scaleWarningText = ""; float sliderScale = 0.30103f; + float originalSliderScale; public delegate void OnExportDelegate(string projectDirectory, string projectName, float scale); OnExportDelegate onExportCallback; @@ -1344,6 +1345,8 @@ class ExportProjectWindow : EditorWindow { SetAvatarScale(newScale); scaleWarningText = "Avatar's scale automatically adjusted to be within the recommended range."; } + + originalSliderScale = sliderScale; } void OnGUI() { @@ -1460,10 +1463,9 @@ class ExportProjectWindow : EditorWindow { Close(); } - // when any value changes check for any errors and update scale warning if we are not exporting + // When a text field changes check for any errors if we didn't just check errors from clicking Export above if (GUI.changed && !export) { CheckForErrors(false); - UpdateScaleWarning(); } } @@ -1521,13 +1523,14 @@ class ExportProjectWindow : EditorWindow { } void UpdateScaleWarning() { - // called on any input changes + // called on any scale changes float height = GetAvatarHeight(); if (height < MINIMUM_RECOMMENDED_HEIGHT) { scaleWarningText = "The height of the avatar is below the recommended minimum."; } else if (height > MAXIMUM_RECOMMENDED_HEIGHT) { scaleWarningText = "The height of the avatar is above the recommended maximum."; - } else { + } else if (Mathf.Abs(originalSliderScale - sliderScale) > SLIDER_DIFFERENCE_REMOVE_TEXT) { + // once moving slider beyond a small threshold, remove the automatically scaled text scaleWarningText = ""; } } @@ -1555,6 +1558,8 @@ class ExportProjectWindow : EditorWindow { // adjust slider scale value to match the new actual scale value sliderScale = GetSliderScaleFromActualScale(actualScale); + + UpdateScaleWarning(); } float GetSliderScaleFromActualScale(float actualScale) { diff --git a/tools/unity-avatar-exporter/Assets/README.txt b/tools/unity-avatar-exporter/Assets/README.txt index 0b5cb49117..410314d8b4 100644 --- a/tools/unity-avatar-exporter/Assets/README.txt +++ b/tools/unity-avatar-exporter/Assets/README.txt @@ -1,6 +1,6 @@ High Fidelity, Inc. Avatar Exporter -Version 0.3.7 +Version 0.4.0 Note: It is recommended to use Unity versions between 2017.4.15f1 and 2018.2.12f1 for this Avatar Exporter. diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage index d906cfc0a45055b79a0ed8667441799fea682af3..328972736bea1c62716589eb2c0cad5171001f2d 100644 GIT binary patch delta 74262 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%0DP)&)c5gO^Ga*>!vx;Z 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

    jV;9GJz8I}aM(Zh=9i<)~Fn3Taj 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ફ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 From faf11480b639e1b56e3a740b9223651376c51459 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 22 Mar 2019 11:22:14 -0700 Subject: [PATCH 272/446] Working changes before merge. --- tools/nitpick/src/Nitpick.cpp | 2 +- tools/nitpick/src/TestRunnerMobile.cpp | 12 ++++++------ tools/nitpick/src/TestRunnerMobile.h | 2 +- tools/nitpick/src/common.h | 1 + 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/tools/nitpick/src/Nitpick.cpp b/tools/nitpick/src/Nitpick.cpp index e72de9d1ad..e6894ee883 100644 --- a/tools/nitpick/src/Nitpick.cpp +++ b/tools/nitpick/src/Nitpick.cpp @@ -38,7 +38,7 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { _ui.plainTextEdit->setReadOnly(true); - setWindowTitle("Nitpick - v3.1.3"); + setWindowTitle("Nitpick - " + nitpickVersion); clientProfiles << "VR-High" << "Desktop-High" << "Desktop-Low" << "Mobile-Touch" << "VR-Standalone"; _ui.clientProfileComboBox->insertItems(0, clientProfiles); diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 105d30fca4..a848c94755 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -211,8 +211,6 @@ void TestRunnerMobile::runInterface() { _adbInterface = new AdbInterface(); } - sendServerIPToDevice(); - _statusLabel->setText("Starting Interface"); QString testScript = (_runFullSuite->isChecked()) @@ -230,7 +228,7 @@ void TestRunnerMobile::runInterface() { QString command = _adbInterface->getAdbCommand() + " shell am start -n " + startCommand + " --es args \\\"" + - " --url file:///~/serverless/tutorial.json" + + " --url hifi://" + getServerIP() + "/0,0,0" " --no-updater" + " --no-login-suggestion" + " --testScript " + testScript + " quitWhenFinished" + @@ -257,8 +255,8 @@ void TestRunnerMobile::pullFolder() { #endif } -void TestRunnerMobile::sendServerIPToDevice() { - // Get device IP +QString TestRunnerMobile::getServerIP() { + // Get device IP (ifconfig.txt was created when connecting) QFile ifconfigFile{ _workingFolder + "/ifconfig.txt" }; if (!ifconfigFile.open(QIODevice::ReadOnly | QIODevice::Text)) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), @@ -309,7 +307,7 @@ void TestRunnerMobile::sendServerIPToDevice() { if (!serverIP.isNull()) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Cannot identify server IP (multiple interfaces on device submask)"); - return; + return QString("CANNOT IDENTIFY SERVER IP"); } else { union { uint32_t ip; @@ -325,6 +323,8 @@ void TestRunnerMobile::sendServerIPToDevice() { } ifconfigFile.close(); + + return serverIP; } qint64 TestRunnerMobile::convertToBinary(const QString& str) { diff --git a/tools/nitpick/src/TestRunnerMobile.h b/tools/nitpick/src/TestRunnerMobile.h index 7554a075c8..b94cd47647 100644 --- a/tools/nitpick/src/TestRunnerMobile.h +++ b/tools/nitpick/src/TestRunnerMobile.h @@ -52,7 +52,7 @@ public: void pullFolder(); - void sendServerIPToDevice(); + QString getServerIP(); qint64 convertToBinary (const QString& str); private: diff --git a/tools/nitpick/src/common.h b/tools/nitpick/src/common.h index 17090c46db..09bf23fdfc 100644 --- a/tools/nitpick/src/common.h +++ b/tools/nitpick/src/common.h @@ -60,4 +60,5 @@ const double R_Y = 0.212655f; const double G_Y = 0.715158f; const double B_Y = 0.072187f; +const QString nitpickVersion{ "21660" }; #endif // hifi_common_h \ No newline at end of file From c1c45d8a014627c2da1fcda7ada05267a5534e96 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 22 Mar 2019 11:41:02 -0700 Subject: [PATCH 273/446] revert some now unnecessary changes that were impacting performance (cherry picked from commit b6984de16c2fd17f04ea72de7b339c31db6467ab) --- .../render-utils/src/CauterizedModel.cpp | 1 - libraries/render-utils/src/Model.cpp | 20 +++++++------------ 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/libraries/render-utils/src/CauterizedModel.cpp b/libraries/render-utils/src/CauterizedModel.cpp index 6ec69b5e20..7cad337dd5 100644 --- a/libraries/render-utils/src/CauterizedModel.cpp +++ b/libraries/render-utils/src/CauterizedModel.cpp @@ -178,7 +178,6 @@ void CauterizedModel::updateClusterMatrices() { } } } - computeMeshPartLocalBounds(); // post the blender if we're not currently waiting for one to finish auto modelBlender = DependencyManager::get(); diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 30c4000bc7..c0dbf16f9d 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1346,19 +1346,14 @@ void Model::updateRig(float deltaTime, glm::mat4 parentTransform) { } void Model::computeMeshPartLocalBounds() { - render::Transaction transaction; - auto meshStates = _meshStates; - for (auto renderItem : _modelMeshRenderItemIDs) { - transaction.updateItem(renderItem, [this, meshStates](ModelMeshPartPayload& data) { - const Model::MeshState& state = meshStates.at(data._meshIndex); - if (_useDualQuaternionSkinning) { - data.computeAdjustedLocalBound(state.clusterDualQuaternions); - } else { - data.computeAdjustedLocalBound(state.clusterMatrices); - } - }); + for (auto& part : _modelMeshRenderItems) { + const Model::MeshState& state = _meshStates.at(part->_meshIndex); + if (_useDualQuaternionSkinning) { + part->computeAdjustedLocalBound(state.clusterDualQuaternions); + } else { + part->computeAdjustedLocalBound(state.clusterMatrices); + } } - AbstractViewStateInterface::instance()->getMain3DScene()->enqueueTransaction(transaction); } // virtual @@ -1391,7 +1386,6 @@ void Model::updateClusterMatrices() { } } } - computeMeshPartLocalBounds(); // post the blender if we're not currently waiting for one to finish auto modelBlender = DependencyManager::get(); From a1660dad9558758a7c0ad1ef42a5f62739262f7b Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Fri, 22 Mar 2019 12:30:49 -0700 Subject: [PATCH 274/446] Follow dynamic updates to hero zones; make reserved fraction a domain setting --- assignment-client/src/avatars/AvatarMixer.cpp | 69 ++++++++++++++++--- assignment-client/src/avatars/AvatarMixer.h | 11 ++- .../src/avatars/AvatarMixerClientData.cpp | 17 ++--- .../src/avatars/AvatarMixerSlave.cpp | 7 +- .../src/avatars/AvatarMixerSlave.h | 4 +- .../src/avatars/AvatarMixerSlavePool.cpp | 3 +- .../src/avatars/AvatarMixerSlavePool.h | 9 ++- assignment-client/src/avatars/MixerAvatar.h | 4 ++ .../resources/describe-settings.json | 9 +++ 9 files changed, 104 insertions(+), 29 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 33e1034128..ffe084bc33 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -253,10 +253,26 @@ void AvatarMixer::start() { int lockWait, nodeTransform, functor; + // Set our query each frame { _entityViewer.queryOctree(); } + // Dirty the hero status if there's been an entity change. + { + if (_dirtyHeroStatus) { + _dirtyHeroStatus = false; + nodeList->nestedEach([&](NodeList::const_iterator cbegin, NodeList::const_iterator cend) { + std::for_each(cbegin, cend, [](const SharedNodePointer& node) { + if (node->getType() == NodeType::Agent) { + auto& avatar = static_cast(node->getLinkedData())->getAvatar(); + avatar.setNeedsHeroCheck(); + } + }); + }); + } + } + // Allow nodes to process any pending/queued packets across our worker threads { auto start = usecTimestampNow(); @@ -973,19 +989,31 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { { const QString CONNECTION_RATE = "connection_rate"; auto nodeList = DependencyManager::get(); - auto defaultConnectionRate = nodeList->getMaxConnectionRate(); - int connectionRate = avatarMixerGroupObject[CONNECTION_RATE].toInt((int)defaultConnectionRate); - nodeList->setMaxConnectionRate(connectionRate); + bool success; + int connectionRate = avatarMixerGroupObject[CONNECTION_RATE].toString().toInt(&success); + if (success) { + nodeList->setMaxConnectionRate(connectionRate); + } + } + + { // Fraction of downstream bandwidth reserved for 'hero' avatars: + static const QString PRIORITY_FRACTION_KEY = "priority_fraction"; + if (avatarMixerGroupObject.contains(PRIORITY_FRACTION_KEY)) { + bool isDouble = avatarMixerGroupObject[PRIORITY_FRACTION_KEY].isDouble(); + float priorityFraction = float(avatarMixerGroupObject[PRIORITY_FRACTION_KEY].toDouble()); + _slavePool.setPriorityReservedFraction(std::min(std::max(0.0f, priorityFraction), 1.0f)); + qCDebug(avatars) << "Avatar mixer reserving" << priorityFraction << "of bandwidth for priority avatars"; + } } const QString AVATARS_SETTINGS_KEY = "avatars"; static const QString MIN_HEIGHT_OPTION = "min_avatar_height"; - float settingMinHeight = domainSettings[AVATARS_SETTINGS_KEY].toObject()[MIN_HEIGHT_OPTION].toDouble(MIN_AVATAR_HEIGHT); + float settingMinHeight = avatarMixerGroupObject[MIN_HEIGHT_OPTION].toDouble(MIN_AVATAR_HEIGHT); _domainMinimumHeight = glm::clamp(settingMinHeight, MIN_AVATAR_HEIGHT, MAX_AVATAR_HEIGHT); static const QString MAX_HEIGHT_OPTION = "max_avatar_height"; - float settingMaxHeight = domainSettings[AVATARS_SETTINGS_KEY].toObject()[MAX_HEIGHT_OPTION].toDouble(MAX_AVATAR_HEIGHT); + float settingMaxHeight = avatarMixerGroupObject[MAX_HEIGHT_OPTION].toDouble(MAX_AVATAR_HEIGHT); _domainMaximumHeight = glm::clamp(settingMaxHeight, MIN_AVATAR_HEIGHT, MAX_AVATAR_HEIGHT); // make sure that the domain owner didn't flip min and max @@ -997,11 +1025,11 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { << "and a maximum avatar height of" << _domainMaximumHeight; static const QString AVATAR_WHITELIST_OPTION = "avatar_whitelist"; - _slaveSharedData.skeletonURLWhitelist = domainSettings[AVATARS_SETTINGS_KEY].toObject()[AVATAR_WHITELIST_OPTION] + _slaveSharedData.skeletonURLWhitelist = avatarMixerGroupObject[AVATAR_WHITELIST_OPTION] .toString().split(',', QString::KeepEmptyParts); static const QString REPLACEMENT_AVATAR_OPTION = "replacement_avatar"; - _slaveSharedData.skeletonReplacementURL = domainSettings[AVATARS_SETTINGS_KEY].toObject()[REPLACEMENT_AVATAR_OPTION] + _slaveSharedData.skeletonReplacementURL = avatarMixerGroupObject[REPLACEMENT_AVATAR_OPTION] .toString(); if (_slaveSharedData.skeletonURLWhitelist.count() == 1 && _slaveSharedData.skeletonURLWhitelist[0].isEmpty()) { @@ -1018,9 +1046,12 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { void AvatarMixer::setupEntityQuery() { _entityViewer.init(); + EntityTreePointer entityTree = _entityViewer.getTree(); DependencyManager::registerInheritance(); - DependencyManager::set(_entityViewer.getTree()); - _slaveSharedData.entityTree = _entityViewer.getTree(); + DependencyManager::set(entityTree); + + connect(entityTree.get(), &EntityTree::addingEntityPointer, this, &AvatarMixer::entityAdded); + connect(entityTree.get(), &EntityTree::deletingEntityPointer, this, &AvatarMixer::entityChange); // ES query: {"avatarPriority": true, "type": "Zone"} QJsonObject priorityZoneQuery; @@ -1028,6 +1059,7 @@ void AvatarMixer::setupEntityQuery() { priorityZoneQuery["type"] = "Zone"; _entityViewer.getOctreeQuery().setJSONParameters(priorityZoneQuery); + _slaveSharedData.entityTree = entityTree; } void AvatarMixer::handleOctreePacket(QSharedPointer message, SharedNodePointer senderNode) { @@ -1064,6 +1096,25 @@ void AvatarMixer::handleOctreePacket(QSharedPointer message, Sh } } +void AvatarMixer::entityAdded(EntityItem* entity) { + if (entity->getType() == EntityTypes::Zone) { + _dirtyHeroStatus = true; + entity->registerChangeHandler([this](const EntityItemID& entityItemID) { + this->entityChange(); + }); + } +} + +void AvatarMixer::entityRemoved(EntityItem * entity) { + if (entity->getType() == EntityTypes::Zone) { + _dirtyHeroStatus = true; + } +} + +void AvatarMixer::entityChange() { + _dirtyHeroStatus = true; +} + void AvatarMixer::aboutToFinish() { DependencyManager::destroy(); DependencyManager::destroy(); diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h index 9393ea6c56..f65f04f279 100644 --- a/assignment-client/src/avatars/AvatarMixer.h +++ b/assignment-client/src/avatars/AvatarMixer.h @@ -34,8 +34,8 @@ public: static bool shouldReplicateTo(const Node& from, const Node& to) { return to.getType() == NodeType::DownstreamAvatarMixer && - to.getPublicSocket() != from.getPublicSocket() && - to.getLocalSocket() != from.getLocalSocket(); + to.getPublicSocket() != from.getPublicSocket() && + to.getLocalSocket() != from.getLocalSocket(); } public slots: @@ -80,6 +80,7 @@ private: // Attach to entity tree for avatar-priority zone info. EntityTreeHeadlessViewer _entityViewer; + bool _dirtyHeroStatus { true }; // Dirty the needs-hero-update // FIXME - new throttling - use these values somehow float _trailingMixRatio { 0.0f }; @@ -146,6 +147,12 @@ private: AvatarMixerSlavePool _slavePool; SlaveSharedData _slaveSharedData; + +public slots: + // Avatar zone possibly changed + void entityAdded(EntityItem* entity); + void entityRemoved(EntityItem* entity); + void entityChange(); }; #endif // hifi_AvatarMixer_h diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index 557c5c9fe3..a6b675efa4 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -141,22 +141,15 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared _avatar->setHasPriorityWithoutTimestampReset(oldHasPriority); auto newPosition = getPosition(); - if (newPosition != oldPosition) { -//#define AVATAR_HERO_TEST_HACK -#ifdef AVATAR_HERO_TEST_HACK - { - const static QString heroKey { "HERO" }; - _avatar->setPriorityAvatar(_avatar->getDisplayName().contains(heroKey)); - } -#else + if (newPosition != oldPosition || _avatar->getNeedsHeroCheck()) { EntityTree& entityTree = *slaveSharedData.entityTree; FindPriorityZone findPriorityZone { newPosition, false } ; entityTree.recurseTreeWithOperation(&FindPriorityZone::operation, &findPriorityZone); _avatar->setHasPriority(findPriorityZone.isInPriorityZone); - //if (findPriorityZone.isInPriorityZone) { - // qCWarning(avatars) << "Avatar" << _avatar->getSessionDisplayName() << "in hero zone"; - //} -#endif + _avatar->setNeedsHeroCheck(false); + if (findPriorityZone.isInPriorityZone) { + qCWarning(avatars) << "Avatar" << _avatar->getSessionDisplayName() << "in hero zone"; + } } return true; diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index e59c81f4b7..e7de764708 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -43,12 +43,14 @@ void AvatarMixerSlave::configure(ConstIter begin, ConstIter end) { void AvatarMixerSlave::configureBroadcast(ConstIter begin, ConstIter end, p_high_resolution_clock::time_point lastFrameTimestamp, - float maxKbpsPerNode, float throttlingRatio) { + float maxKbpsPerNode, float throttlingRatio, + float priorityReservedFraction) { _begin = begin; _end = end; _lastFrameTimestamp = lastFrameTimestamp; _maxKbpsPerNode = maxKbpsPerNode; _throttlingRatio = throttlingRatio; + _avatarHeroFraction = priorityReservedFraction; } void AvatarMixerSlave::harvestStats(AvatarMixerSlaveStats& stats) { @@ -308,7 +310,6 @@ namespace { } // Close anonymous namespace. void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) { - const float AVATAR_HERO_FRACTION { 0.4f }; const Node* destinationNode = node.data(); auto nodeList = DependencyManager::get(); @@ -343,7 +344,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) // max number of avatarBytes per frame (13 900, typical) const int maxAvatarBytesPerFrame = int(_maxKbpsPerNode * BYTES_PER_KILOBIT / AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND); - const int maxHeroBytesPerFrame = int(maxAvatarBytesPerFrame * AVATAR_HERO_FRACTION); // 5555, typical + const int maxHeroBytesPerFrame = int(maxAvatarBytesPerFrame * _avatarHeroFraction); // 5555, typical // keep track of the number of other avatars held back in this frame int numAvatarsHeldBack = 0; diff --git a/assignment-client/src/avatars/AvatarMixerSlave.h b/assignment-client/src/avatars/AvatarMixerSlave.h index 8c5ad6b181..f14e50e11f 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.h +++ b/assignment-client/src/avatars/AvatarMixerSlave.h @@ -110,7 +110,8 @@ public: void configure(ConstIter begin, ConstIter end); void configureBroadcast(ConstIter begin, ConstIter end, p_high_resolution_clock::time_point lastFrameTimestamp, - float maxKbpsPerNode, float throttlingRatio); + float maxKbpsPerNode, float throttlingRatio, + float priorityReservedFraction); void processIncomingPackets(const SharedNodePointer& node); void broadcastAvatarData(const SharedNodePointer& node); @@ -140,6 +141,7 @@ private: p_high_resolution_clock::time_point _lastFrameTimestamp; float _maxKbpsPerNode { 0.0f }; float _throttlingRatio { 0.0f }; + float _avatarHeroFraction { 0.4f }; AvatarMixerSlaveStats _stats; SlaveSharedData* _sharedData; diff --git a/assignment-client/src/avatars/AvatarMixerSlavePool.cpp b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp index 013d914cbe..357b347a94 100644 --- a/assignment-client/src/avatars/AvatarMixerSlavePool.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp @@ -76,7 +76,8 @@ void AvatarMixerSlavePool::broadcastAvatarData(ConstIter begin, ConstIter end, float maxKbpsPerNode, float throttlingRatio) { _function = &AvatarMixerSlave::broadcastAvatarData; _configure = [=](AvatarMixerSlave& slave) { - slave.configureBroadcast(begin, end, lastFrameTimestamp, maxKbpsPerNode, throttlingRatio); + slave.configureBroadcast(begin, end, lastFrameTimestamp, maxKbpsPerNode, throttlingRatio, + _priorityReservedFraction); }; run(begin, end); } diff --git a/assignment-client/src/avatars/AvatarMixerSlavePool.h b/assignment-client/src/avatars/AvatarMixerSlavePool.h index 71a9ace0d3..b05abde2a3 100644 --- a/assignment-client/src/avatars/AvatarMixerSlavePool.h +++ b/assignment-client/src/avatars/AvatarMixerSlavePool.h @@ -73,7 +73,10 @@ public: void each(std::function functor); void setNumThreads(int numThreads); - int numThreads() { return _numThreads; } + int numThreads() const { return _numThreads; } + + void setPriorityReservedFraction(float fraction) { _priorityReservedFraction = fraction; } + float getPriorityReservedFraction() const { return _priorityReservedFraction; } private: void run(ConstIter begin, ConstIter end); @@ -91,7 +94,11 @@ private: ConditionVariable _poolCondition; void (AvatarMixerSlave::*_function)(const SharedNodePointer& node); std::function _configure; + + // Set from Domain Settings: + float _priorityReservedFraction { 0.4f }; int _numThreads { 0 }; + int _numStarted { 0 }; // guarded by _mutex int _numFinished { 0 }; // guarded by _mutex int _numStopped { 0 }; // guarded by _mutex diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h index 3e80704495..01e5e91b44 100644 --- a/assignment-client/src/avatars/MixerAvatar.h +++ b/assignment-client/src/avatars/MixerAvatar.h @@ -19,8 +19,12 @@ class MixerAvatar : public AvatarData { public: + bool getNeedsHeroCheck() const { return _needsHeroCheck; } + void setNeedsHeroCheck(bool needsHeroCheck = true) + { _needsHeroCheck = needsHeroCheck; } private: + bool _needsHeroCheck { false }; }; using MixerAvatarSharedPointer = std::shared_ptr; diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 140c7d6c17..352106dcf7 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1310,6 +1310,15 @@ "placeholder": "50", "default": "50", "advanced": true + }, + { + "name": "priority_fraction", + "type": "double", + "label": "Hero Bandwidth", + "help": "Fraction of downstream bandwidth reserved for avatars in 'Hero' zones", + "placeholder": "0.40", + "default": "0.40", + "advanced": true } ] }, From 0def39dbaac3debd75877a65f3ff4a6ba7c8f324 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Tue, 26 Feb 2019 18:14:56 -0800 Subject: [PATCH 275/446] adding user speaking level rotated vertically and muted symbol --- interface/resources/qml/hifi/audio/MicBar.qml | 55 ++++++++++--------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index f51da9c381..51ddfb7f3e 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -13,13 +13,14 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 import stylesUit 1.0 +import stylesUit 1.0 import TabletScriptingInterface 1.0 Rectangle { HifiConstants { id: hifi; } readonly property var level: AudioScriptingInterface.inputLevel; - + readonly property var userSpeakingLevel: 0.4; property bool gated: false; Component.onCompleted: { AudioScriptingInterface.noiseGateOpened.connect(function() { gated = false; }); @@ -29,8 +30,8 @@ Rectangle { property bool standalone: false; property var dragTarget: null; - width: 240; - height: 50; + width: 44; + height: 44; radius: 5; @@ -43,8 +44,8 @@ Rectangle { // borders are painted over fill, so reduce the fill to fit inside the border Rectangle { color: standalone ? colors.fill : "#00000000"; - width: 236; - height: 46; + width: 40; + height: 40; radius: 5; @@ -101,7 +102,6 @@ Rectangle { anchors { left: parent.left; - leftMargin: 5; verticalCenter: parent.verticalCenter; } @@ -117,11 +117,11 @@ Rectangle { id: image; source: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) ? pushToTalkIcon : AudioScriptingInterface.muted ? mutedIcon : unmutedIcon; - width: 30; - height: 30; + width: 21; + height: 24; anchors { left: parent.left; - leftMargin: 5; + leftMargin: 7; top: parent.top; topMargin: 5; } @@ -138,20 +138,20 @@ Rectangle { Item { id: status; - readonly property string color: AudioScriptingInterface.muted ? colors.muted : colors.unmuted; + readonly property string color: colors.muted; - visible: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) || AudioScriptingInterface.muted; + visible: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) || (AudioScriptingInterface.muted && (level >= userSpeakingLevel)); anchors { left: parent.left; - leftMargin: 50; - verticalCenter: parent.verticalCenter; + top: parent.bottom + topMargin: 5 } - width: 170; + width: icon.width; height: 8 - Text { + RalewaySemiBold { anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter; @@ -189,11 +189,14 @@ Rectangle { Item { id: bar; - visible: !status.visible; + anchors { + right: parent.right; + rightMargin: 7; + verticalCenter: parent.verticalCenter; + } - anchors.fill: status; - - width: status.width; + width: 8; + height: 32; Rectangle { // base radius: 4; @@ -203,13 +206,12 @@ Rectangle { Rectangle { // mask id: mask; - width: gated ? 0 : parent.width * level; + height: parent.height * level; + width: parent.width; radius: 5; anchors { bottom: parent.bottom; bottomMargin: 0; - top: parent.top; - topMargin: 0; left: parent.left; leftMargin: 0; } @@ -219,10 +221,11 @@ Rectangle { anchors { fill: mask } source: mask start: Qt.point(0, 0); - end: Qt.point(170, 0); + end: Qt.point(0, bar.height); + rotation: 180 gradient: Gradient { GradientStop { - position: 0; + position: 0.0; color: colors.greenStart; } GradientStop { @@ -230,8 +233,8 @@ Rectangle { color: colors.greenEnd; } GradientStop { - position: 1; - color: colors.yellow; + position: 1.0; + color: colors.red; } } } From 1ba366c0d7074bec21df042f743b941f0dfbcd55 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Tue, 26 Feb 2019 18:19:23 -0800 Subject: [PATCH 276/446] moving file as MicBarApplication --- interface/resources/qml/hifi/audio/MicBar.qml | 67 +++-- .../qml/hifi/audio/MicBarApplication.qml | 241 ++++++++++++++++++ 2 files changed, 273 insertions(+), 35 deletions(-) create mode 100644 interface/resources/qml/hifi/audio/MicBarApplication.qml diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index 51ddfb7f3e..7ed9451a04 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -13,25 +13,24 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 import stylesUit 1.0 -import stylesUit 1.0 import TabletScriptingInterface 1.0 Rectangle { HifiConstants { id: hifi; } readonly property var level: AudioScriptingInterface.inputLevel; - readonly property var userSpeakingLevel: 0.4; + property bool gated: false; Component.onCompleted: { AudioScriptingInterface.noiseGateOpened.connect(function() { gated = false; }); AudioScriptingInterface.noiseGateClosed.connect(function() { gated = true; }); } - + property bool standalone: false; property var dragTarget: null; - width: 44; - height: 44; + width: 240; + height: 50; radius: 5; @@ -44,8 +43,8 @@ Rectangle { // borders are painted over fill, so reduce the fill to fit inside the border Rectangle { color: standalone ? colors.fill : "#00000000"; - width: 40; - height: 40; + width: 236; + height: 46; radius: 5; @@ -102,6 +101,7 @@ Rectangle { anchors { left: parent.left; + leftMargin: 5; verticalCenter: parent.verticalCenter; } @@ -117,11 +117,11 @@ Rectangle { id: image; source: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) ? pushToTalkIcon : AudioScriptingInterface.muted ? mutedIcon : unmutedIcon; - width: 21; - height: 24; + width: 30; + height: 30; anchors { left: parent.left; - leftMargin: 7; + leftMargin: 5; top: parent.top; topMargin: 5; } @@ -138,20 +138,20 @@ Rectangle { Item { id: status; - readonly property string color: colors.muted; + readonly property string color: AudioScriptingInterface.muted ? colors.muted : colors.unmuted; visible: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) || (AudioScriptingInterface.muted && (level >= userSpeakingLevel)); anchors { left: parent.left; - top: parent.bottom - topMargin: 5 + leftMargin: 50; + verticalCenter: parent.verticalCenter; } - width: icon.width; + width: 170; height: 8 - RalewaySemiBold { + Text { anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter; @@ -189,14 +189,11 @@ Rectangle { Item { id: bar; - anchors { - right: parent.right; - rightMargin: 7; - verticalCenter: parent.verticalCenter; - } + visible: !status.visible; - width: 8; - height: 32; + anchors.fill: status; + + width: status.width; Rectangle { // base radius: 4; @@ -206,12 +203,13 @@ Rectangle { Rectangle { // mask id: mask; - height: parent.height * level; - width: parent.width; + width: gated ? 0 : parent.width * level; radius: 5; anchors { bottom: parent.bottom; bottomMargin: 0; + top: parent.top; + topMargin: 0; left: parent.left; leftMargin: 0; } @@ -221,11 +219,10 @@ Rectangle { anchors { fill: mask } source: mask start: Qt.point(0, 0); - end: Qt.point(0, bar.height); - rotation: 180 + end: Qt.point(170, 0); gradient: Gradient { GradientStop { - position: 0.0; + position: 0; color: colors.greenStart; } GradientStop { @@ -233,17 +230,17 @@ Rectangle { color: colors.greenEnd; } GradientStop { - position: 1.0; - color: colors.red; + position: 1; + color: colors.yellow; } } } - + Rectangle { id: gatedIndicator; visible: gated && !AudioScriptingInterface.clipping - - radius: 4; + + radius: 4; width: 2 * radius; height: 2 * radius; color: "#0080FF"; @@ -252,12 +249,12 @@ Rectangle { verticalCenter: parent.verticalCenter; } } - + Rectangle { id: clippingIndicator; visible: AudioScriptingInterface.clipping - - radius: 4; + + radius: 4; width: 2 * radius; height: 2 * radius; color: colors.red; diff --git a/interface/resources/qml/hifi/audio/MicBarApplication.qml b/interface/resources/qml/hifi/audio/MicBarApplication.qml new file mode 100644 index 0000000000..33824a3d86 --- /dev/null +++ b/interface/resources/qml/hifi/audio/MicBarApplication.qml @@ -0,0 +1,241 @@ +// +// MicBar.qml +// qml/hifi/audio +// +// Created by Zach Pomerantz on 6/14/2017 +// Copyright 2017 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 stylesUit 1.0 +import TabletScriptingInterface 1.0 + +Rectangle { + readonly property var level: AudioScriptingInterface.inputLevel; + readonly property var userSpeakingLevel: 0.4; + property bool gated: false; + Component.onCompleted: { + AudioScriptingInterface.noiseGateOpened.connect(function() { gated = false; }); + AudioScriptingInterface.noiseGateClosed.connect(function() { gated = true; }); + } + + property bool standalone: false; + property var dragTarget: null; + + width: 44; + height: 44; + + radius: 5; + + color: "#00000000"; + border { + width: mouseArea.containsMouse || mouseArea.containsPress ? 2 : 0; + color: colors.border; + } + + // borders are painted over fill, so reduce the fill to fit inside the border + Rectangle { + color: standalone ? colors.fill : "#00000000"; + width: 40; + height: 40; + + radius: 5; + + anchors { + verticalCenter: parent.verticalCenter; + horizontalCenter: parent.horizontalCenter; + } + } + + MouseArea { + id: mouseArea; + + anchors { + left: icon.left; + right: bar.right; + top: icon.top; + bottom: icon.bottom; + } + + hoverEnabled: true; + scrollGestureEnabled: false; + onClicked: { + AudioScriptingInterface.muted = !AudioScriptingInterface.muted; + Tablet.playSound(TabletEnums.ButtonClick); + } + drag.target: dragTarget; + onContainsMouseChanged: { + if (containsMouse) { + Tablet.playSound(TabletEnums.ButtonHover); + } + } + } + + QtObject { + id: colors; + + readonly property string unmuted: "#FFF"; + readonly property string muted: "#E2334D"; + readonly property string gutter: "#575757"; + readonly property string greenStart: "#39A38F"; + readonly property string greenEnd: "#1FC6A6"; + readonly property string yellow: "#C0C000"; + readonly property string red: colors.muted; + readonly property string fill: "#55000000"; + readonly property string border: standalone ? "#80FFFFFF" : "#55FFFFFF"; + readonly property string icon: AudioScriptingInterface.muted ? muted : unmuted; + } + + Item { + id: icon; + + anchors { + left: parent.left; + verticalCenter: parent.verticalCenter; + } + + width: 40; + height: 40; + + Item { + Image { + readonly property string unmutedIcon: "../../../icons/tablet-icons/mic-unmute-i.svg"; + readonly property string mutedIcon: "../../../icons/tablet-icons/mic-mute-i.svg"; + + id: image; + source: AudioScriptingInterface.muted ? mutedIcon : unmutedIcon; + + width: 21; + height: 24; + anchors { + left: parent.left; + leftMargin: 7; + top: parent.top; + topMargin: 5; + } + } + + ColorOverlay { + anchors { fill: image } + source: image; + color: colors.icon; + } + } + } + + Item { + id: status; + + readonly property string color: colors.muted; + + visible: AudioScriptingInterface.muted && (level >= userSpeakingLevel); + + anchors { + left: parent.left; + top: parent.bottom + topMargin: 5 + } + + width: icon.width; + height: 8 + + RalewaySemiBold { + anchors { + horizontalCenter: parent.horizontalCenter; + verticalCenter: parent.verticalCenter; + } + + color: parent.color; + + text: "MUTED"; + size: 12; + } + } + + Item { + id: bar; + + anchors { + right: parent.right; + rightMargin: 7; + verticalCenter: parent.verticalCenter; + } + + width: 8; + height: 32; + + Rectangle { // base + radius: 4; + anchors { fill: parent } + color: colors.gutter; + } + + Rectangle { // mask + id: mask; + height: parent.height * level; + width: parent.width; + radius: 5; + anchors { + bottom: parent.bottom; + bottomMargin: 0; + left: parent.left; + leftMargin: 0; + } + } + + LinearGradient { + anchors { fill: mask } + source: mask + start: Qt.point(0, 0); + end: Qt.point(0, bar.height); + rotation: 180 + gradient: Gradient { + GradientStop { + position: 0.0; + color: colors.greenStart; + } + GradientStop { + position: 0.5; + color: colors.greenEnd; + } + GradientStop { + position: 1.0; + color: colors.red; + } + } + } + + Rectangle { + id: gatedIndicator; + visible: gated && !AudioScriptingInterface.clipping + + radius: 4; + width: 2 * radius; + height: 2 * radius; + color: "#0080FF"; + anchors { + right: parent.left; + verticalCenter: parent.verticalCenter; + } + } + + Rectangle { + id: clippingIndicator; + visible: AudioScriptingInterface.clipping + + radius: 4; + width: 2 * radius; + height: 2 * radius; + color: colors.red; + anchors { + left: parent.right; + verticalCenter: parent.verticalCenter; + } + } + } +} From 915d22bb15328ae7ddb3563b654c3a4ede6db043 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 27 Feb 2019 10:38:26 -0800 Subject: [PATCH 277/446] adding bubble icon (not working) --- interface/resources/qml/AvatarInputsBar.qml | 24 +++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/AvatarInputsBar.qml b/interface/resources/qml/AvatarInputsBar.qml index 4a071d2d04..b17dfdcd65 100644 --- a/interface/resources/qml/AvatarInputsBar.qml +++ b/interface/resources/qml/AvatarInputsBar.qml @@ -8,6 +8,7 @@ import Hifi 1.0 as Hifi import QtQuick 2.4 +import QtGraphicalEffects 1.0 import "./hifi/audio" as HifiAudio @@ -21,10 +22,29 @@ Item { readonly property bool shouldReposition: true; - HifiAudio.MicBar { + HifiAudio.MicBarApplication { id: audio; visible: AvatarInputs.showAudioTools; standalone: true; - dragTarget: parent; + dragTarget: parent; + } + Image { + id: bubbleIcon + source: "../icons/tablet-icons/bubble-i.svg"; + width: 28; + height: 28; + anchors { + left: root.right + top: root.top + topMargin: (root.height - bubbleIcon.height) / 2 + } + } + ColorOverlay { + anchors.fill: bubbleIcon + source: bubbleIcon + color: Users.getIgnoreRadiusEnabled() ? Qt.rgba(31, 198, 166, 0.3) : Qt.rgba(255, 255, 255, 0.3); + onColorChanged: { + console.log("colorChanged") + } } } From 3ceb1598f614b18eb412c6962b459b79e8fcf376 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 27 Feb 2019 14:54:13 -0800 Subject: [PATCH 278/446] adding working bubble icon --- interface/resources/qml/AvatarInputsBar.qml | 9 ++++----- interface/src/ui/AvatarInputs.cpp | 6 ++++++ interface/src/ui/AvatarInputs.h | 11 ++++++++++- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/interface/resources/qml/AvatarInputsBar.qml b/interface/resources/qml/AvatarInputsBar.qml index b17dfdcd65..6375211a93 100644 --- a/interface/resources/qml/AvatarInputsBar.qml +++ b/interface/resources/qml/AvatarInputsBar.qml @@ -16,10 +16,10 @@ Item { id: root; objectName: "AvatarInputsBar" property int modality: Qt.NonModal + readonly property bool ignoreRadiusEnabled: AvatarInputs.ignoreRadiusEnabled width: audio.width; height: audio.height; x: 10; y: 5; - readonly property bool shouldReposition: true; HifiAudio.MicBarApplication { @@ -40,11 +40,10 @@ Item { } } ColorOverlay { + id: bubbleIconOverlay anchors.fill: bubbleIcon source: bubbleIcon - color: Users.getIgnoreRadiusEnabled() ? Qt.rgba(31, 198, 166, 0.3) : Qt.rgba(255, 255, 255, 0.3); - onColorChanged: { - console.log("colorChanged") - } + color: AvatarInputs.ignoreRadiusEnabled ? "#1FC6A6" : "#FFFFFF"; + opacity: 0.7 } } diff --git a/interface/src/ui/AvatarInputs.cpp b/interface/src/ui/AvatarInputs.cpp index 0aa352de23..6d43507a3e 100644 --- a/interface/src/ui/AvatarInputs.cpp +++ b/interface/src/ui/AvatarInputs.cpp @@ -30,6 +30,8 @@ AvatarInputs* AvatarInputs::getInstance() { AvatarInputs::AvatarInputs(QObject* parent) : QObject(parent) { _showAudioTools = showAudioToolsSetting.get(); + auto nodeList = DependencyManager::get(); + connect(nodeList.data(), &NodeList::ignoreRadiusEnabledChanged, this, &AvatarInputs::ignoreRadiusEnabledChanged); } #define AI_UPDATE(name, src) \ @@ -83,6 +85,10 @@ void AvatarInputs::setShowAudioTools(bool showAudioTools) { emit showAudioToolsChanged(_showAudioTools); } +bool AvatarInputs::getIgnoreRadiusEnabled() const { + return DependencyManager::get()->getIgnoreRadiusEnabled(); +} + void AvatarInputs::toggleCameraMute() { FaceTracker* faceTracker = qApp->getSelectedFaceTracker(); if (faceTracker) { diff --git a/interface/src/ui/AvatarInputs.h b/interface/src/ui/AvatarInputs.h index 6569792807..a41fd0485f 100644 --- a/interface/src/ui/AvatarInputs.h +++ b/interface/src/ui/AvatarInputs.h @@ -42,6 +42,7 @@ class AvatarInputs : public QObject { AI_PROPERTY(bool, isHMD, false) Q_PROPERTY(bool showAudioTools READ showAudioTools WRITE setShowAudioTools NOTIFY showAudioToolsChanged) + Q_PROPERTY(bool ignoreRadiusEnabled READ getIgnoreRadiusEnabled NOTIFY ignoreRadiusEnabledChanged) public: static AvatarInputs* getInstance(); @@ -55,7 +56,8 @@ public: AvatarInputs(QObject* parent = nullptr); void update(); - bool showAudioTools() const { return _showAudioTools; } + bool showAudioTools() const { return _showAudioTools; } + bool getIgnoreRadiusEnabled() const; public slots: @@ -93,6 +95,13 @@ signals: */ void showAudioToolsChanged(bool show); + /**jsdoc + * @function AvatarInputs.ignoreRadiusEnabledChanged + * @param {boolean} enabled + * @returns {Signal} + */ + void ignoreRadiusEnabledChanged(bool enabled); + protected: /**jsdoc From ddacc0ee60fc7169eeac98e83725b28c2b6dffdd Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 27 Feb 2019 14:56:21 -0800 Subject: [PATCH 279/446] bubble icon opacity to 0.3 --- interface/resources/qml/AvatarInputsBar.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/AvatarInputsBar.qml b/interface/resources/qml/AvatarInputsBar.qml index 6375211a93..d539d7ea9e 100644 --- a/interface/resources/qml/AvatarInputsBar.qml +++ b/interface/resources/qml/AvatarInputsBar.qml @@ -44,6 +44,6 @@ Item { anchors.fill: bubbleIcon source: bubbleIcon color: AvatarInputs.ignoreRadiusEnabled ? "#1FC6A6" : "#FFFFFF"; - opacity: 0.7 + opacity: 0.3 } } From e129201d2a04e2af0782d6f0b7b4e514805cbd0b Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 27 Feb 2019 17:38:11 -0800 Subject: [PATCH 280/446] adding more opacity, cleanup --- interface/resources/qml/AvatarInputsBar.qml | 69 +++++++++++++++---- .../qml/hifi/audio/MicBarApplication.qml | 10 +++ 2 files changed, 64 insertions(+), 15 deletions(-) diff --git a/interface/resources/qml/AvatarInputsBar.qml b/interface/resources/qml/AvatarInputsBar.qml index d539d7ea9e..615e260833 100644 --- a/interface/resources/qml/AvatarInputsBar.qml +++ b/interface/resources/qml/AvatarInputsBar.qml @@ -7,11 +7,13 @@ // import Hifi 1.0 as Hifi -import QtQuick 2.4 +import QtQuick 2.5 import QtGraphicalEffects 1.0 import "./hifi/audio" as HifiAudio +import TabletScriptingInterface 1.0 + Item { id: root; objectName: "AvatarInputsBar" @@ -19,7 +21,8 @@ Item { readonly property bool ignoreRadiusEnabled: AvatarInputs.ignoreRadiusEnabled width: audio.width; height: audio.height; - x: 10; y: 5; + x: 10; + y: 5; readonly property bool shouldReposition: true; HifiAudio.MicBarApplication { @@ -28,22 +31,58 @@ Item { standalone: true; dragTarget: parent; } - Image { - id: bubbleIcon - source: "../icons/tablet-icons/bubble-i.svg"; - width: 28; - height: 28; + Rectangle { + id: bubbleRect + width: bubbleIcon.width + 10 + height: parent.height + radius: 5; + opacity: 0.5; + + color: "#00000000"; + border { + width: mouseArea.containsMouse || mouseArea.containsPress ? 2 : 0; + color: "#80FFFFFF"; + } anchors { left: root.right top: root.top - topMargin: (root.height - bubbleIcon.height) / 2 + } + + MouseArea { + id: mouseArea; + anchors.fill: parent + + hoverEnabled: true; + scrollGestureEnabled: false; + onClicked: { + Tablet.playSound(TabletEnums.ButtonClick); + } + drag.target: root; + onContainsMouseChanged: { + if (containsMouse) { + Tablet.playSound(TabletEnums.ButtonHover); + bubbleRect.opacity = 0.7; + } else { + bubbleRect.opacity = 0.5; + } + } + } + Image { + id: bubbleIcon + source: "../icons/tablet-icons/bubble-i.svg"; + sourceSize: Qt.size(28, 28); + smooth: true; + visible: false + anchors.top: parent.top + anchors.topMargin: (parent.height - bubbleIcon.height) / 2 + anchors.left: parent.left + anchors.leftMargin: (parent.width - bubbleIcon.width) / 2 + } + ColorOverlay { + id: bubbleIconOverlay + anchors.fill: bubbleIcon + source: bubbleIcon + color: AvatarInputs.ignoreRadiusEnabled ? "#1FC6A6" : "#FFFFFF"; } } - ColorOverlay { - id: bubbleIconOverlay - anchors.fill: bubbleIcon - source: bubbleIcon - color: AvatarInputs.ignoreRadiusEnabled ? "#1FC6A6" : "#FFFFFF"; - opacity: 0.3 - } } diff --git a/interface/resources/qml/hifi/audio/MicBarApplication.qml b/interface/resources/qml/hifi/audio/MicBarApplication.qml index 33824a3d86..ba6aeef7d8 100644 --- a/interface/resources/qml/hifi/audio/MicBarApplication.qml +++ b/interface/resources/qml/hifi/audio/MicBarApplication.qml @@ -32,6 +32,14 @@ Rectangle { radius: 5; + onLevelChanged: { + var rectOpacity = AudioScriptingInterface.muted && (level >= userSpeakingLevel)? 0.9 : 0.3; + if (mouseArea.containsMouse) { + rectOpacity = 0.5; + } + opacity = rectOpacity; + } + color: "#00000000"; border { width: mouseArea.containsMouse || mouseArea.containsPress ? 2 : 0; @@ -121,6 +129,7 @@ Rectangle { } ColorOverlay { + id: imageOverlay anchors { fill: image } source: image; color: colors.icon; @@ -170,6 +179,7 @@ Rectangle { height: 32; Rectangle { // base + id: baseBar radius: 4; anchors { fill: parent } color: colors.gutter; From 3be44e0a8ea30fc2500c2d3306e1487a4625e82f Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 27 Feb 2019 18:04:09 -0800 Subject: [PATCH 281/446] adding privacy shield files --- interface/src/ui/PrivacyShield.cpp | 12 ++++++++++++ interface/src/ui/PrivacyShield.h | 12 ++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 interface/src/ui/PrivacyShield.cpp create mode 100644 interface/src/ui/PrivacyShield.h diff --git a/interface/src/ui/PrivacyShield.cpp b/interface/src/ui/PrivacyShield.cpp new file mode 100644 index 0000000000..e1035ee5bb --- /dev/null +++ b/interface/src/ui/PrivacyShield.cpp @@ -0,0 +1,12 @@ +// +// PrivacyShield.h +// interface/src/ui +// +// Created by Wayne Chen on 2/27/19. +// Copyright 2019 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 +// + +#include "PrivacyShield.cpp" diff --git a/interface/src/ui/PrivacyShield.h b/interface/src/ui/PrivacyShield.h new file mode 100644 index 0000000000..a609f2775b --- /dev/null +++ b/interface/src/ui/PrivacyShield.h @@ -0,0 +1,12 @@ +// +// PrivacyShield.h +// interface/src/ui +// +// Created by Wayne Chen on 2/27/19. +// Copyright 2019 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 +// + +#pragma once From e1d03a23395d5f640b20f5544399305b34708bec Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 27 Feb 2019 18:04:58 -0800 Subject: [PATCH 282/446] fixing typos --- interface/src/ui/PrivacyShield.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/ui/PrivacyShield.cpp b/interface/src/ui/PrivacyShield.cpp index e1035ee5bb..12687afbea 100644 --- a/interface/src/ui/PrivacyShield.cpp +++ b/interface/src/ui/PrivacyShield.cpp @@ -1,5 +1,5 @@ // -// PrivacyShield.h +// PrivacyShield.cpp // interface/src/ui // // Created by Wayne Chen on 2/27/19. @@ -9,4 +9,4 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "PrivacyShield.cpp" +#include "PrivacyShield.h" From 835153c0dfb7caef7912715589bafea88313d7d4 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Thu, 28 Feb 2019 15:50:13 -0800 Subject: [PATCH 283/446] don't display gated state --- .../qml/hifi/audio/MicBarApplication.qml | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/interface/resources/qml/hifi/audio/MicBarApplication.qml b/interface/resources/qml/hifi/audio/MicBarApplication.qml index ba6aeef7d8..451f410e1d 100644 --- a/interface/resources/qml/hifi/audio/MicBarApplication.qml +++ b/interface/resources/qml/hifi/audio/MicBarApplication.qml @@ -17,6 +17,8 @@ import TabletScriptingInterface 1.0 Rectangle { readonly property var level: AudioScriptingInterface.inputLevel; + readonly property var clipping: AudioScriptingInterface.clipping; + readonly property var muted: AudioScriptingInterface.muted; readonly property var userSpeakingLevel: 0.4; property bool gated: false; Component.onCompleted: { @@ -24,6 +26,9 @@ Rectangle { AudioScriptingInterface.noiseGateClosed.connect(function() { gated = true; }); } + readonly property string unmutedIcon: "../../../icons/tablet-icons/mic-unmute-i.svg"; + readonly property string mutedIcon: "../../../icons/tablet-icons/mic-mute-i.svg"; + readonly property string clippingIcon: "../../../icons/tablet-icons/mic-clip-i.svg"; property bool standalone: false; property var dragTarget: null; @@ -33,8 +38,8 @@ Rectangle { radius: 5; onLevelChanged: { - var rectOpacity = AudioScriptingInterface.muted && (level >= userSpeakingLevel)? 0.9 : 0.3; - if (mouseArea.containsMouse) { + var rectOpacity = muted && (level >= userSpeakingLevel) ? 0.9 : 0.3; + if (mouseArea.containsMouse && rectOpacity != 0.9) { rectOpacity = 0.5; } opacity = rectOpacity; @@ -87,8 +92,8 @@ Rectangle { QtObject { id: colors; - readonly property string unmuted: "#FFF"; - readonly property string muted: "#E2334D"; + readonly property string unmutedColor: "#FFF"; + readonly property string mutedColor: "#E2334D"; readonly property string gutter: "#575757"; readonly property string greenStart: "#39A38F"; readonly property string greenEnd: "#1FC6A6"; @@ -96,7 +101,7 @@ Rectangle { readonly property string red: colors.muted; readonly property string fill: "#55000000"; readonly property string border: standalone ? "#80FFFFFF" : "#55FFFFFF"; - readonly property string icon: AudioScriptingInterface.muted ? muted : unmuted; + readonly property string icon: muted ? mutedColor : unmutedColor; } Item { @@ -112,14 +117,11 @@ Rectangle { Item { Image { - readonly property string unmutedIcon: "../../../icons/tablet-icons/mic-unmute-i.svg"; - readonly property string mutedIcon: "../../../icons/tablet-icons/mic-mute-i.svg"; - id: image; - source: AudioScriptingInterface.muted ? mutedIcon : unmutedIcon; + source: muted ? mutedIcon : clipping ? clippingIcon : unmutedIcon; - width: 21; - height: 24; + width: 29; + height: 32; anchors { left: parent.left; leftMargin: 7; @@ -142,7 +144,8 @@ Rectangle { readonly property string color: colors.muted; - visible: AudioScriptingInterface.muted && (level >= userSpeakingLevel); + visible: muted && (level >= userSpeakingLevel); + opacity: 0.9 anchors { left: parent.left; @@ -215,14 +218,14 @@ Rectangle { } GradientStop { position: 1.0; - color: colors.red; + color: colors.yellow; } } } - +/* Rectangle { id: gatedIndicator; - visible: gated && !AudioScriptingInterface.clipping + visible: gated && !clipping radius: 4; width: 2 * radius; @@ -236,7 +239,7 @@ Rectangle { Rectangle { id: clippingIndicator; - visible: AudioScriptingInterface.clipping + visible: clipping radius: 4; width: 2 * radius; @@ -247,5 +250,6 @@ Rectangle { verticalCenter: parent.verticalCenter; } } +*/ } } From 26895e6f5d1a7dc37a9fa07b1d6be444c9ebfd80 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Thu, 28 Feb 2019 15:53:24 -0800 Subject: [PATCH 284/446] don't display gated state --- interface/resources/icons/tablet-icons/mic-clip-i.svg | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 interface/resources/icons/tablet-icons/mic-clip-i.svg diff --git a/interface/resources/icons/tablet-icons/mic-clip-i.svg b/interface/resources/icons/tablet-icons/mic-clip-i.svg new file mode 100644 index 0000000000..d6f36427a9 --- /dev/null +++ b/interface/resources/icons/tablet-icons/mic-clip-i.svg @@ -0,0 +1,3 @@ + + + From 087f613d60d242602737a2f1872c04badd70d1a3 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Thu, 28 Feb 2019 16:48:17 -0800 Subject: [PATCH 285/446] fixing muted text --- .../icons/tablet-icons/mic-clip-i.svg | 9 ++- .../resources/icons/tablet-icons/mic-mute.svg | 60 ------------------- .../icons/tablet-icons/mic-unmute-i.svg | 23 +------ .../qml/hifi/audio/MicBarApplication.qml | 6 +- 4 files changed, 11 insertions(+), 87 deletions(-) delete mode 100644 interface/resources/icons/tablet-icons/mic-mute.svg diff --git a/interface/resources/icons/tablet-icons/mic-clip-i.svg b/interface/resources/icons/tablet-icons/mic-clip-i.svg index d6f36427a9..8bc1b2b558 100644 --- a/interface/resources/icons/tablet-icons/mic-clip-i.svg +++ b/interface/resources/icons/tablet-icons/mic-clip-i.svg @@ -1,3 +1,10 @@ - + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/mic-mute.svg b/interface/resources/icons/tablet-icons/mic-mute.svg deleted file mode 100644 index bd42fded05..0000000000 --- a/interface/resources/icons/tablet-icons/mic-mute.svg +++ /dev/null @@ -1,60 +0,0 @@ - - - -image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/mic-unmute-i.svg b/interface/resources/icons/tablet-icons/mic-unmute-i.svg index c4eda55cbc..f577a65bc2 100644 --- a/interface/resources/icons/tablet-icons/mic-unmute-i.svg +++ b/interface/resources/icons/tablet-icons/mic-unmute-i.svg @@ -1,22 +1,3 @@ - - - - - - - - - - - - - + + diff --git a/interface/resources/qml/hifi/audio/MicBarApplication.qml b/interface/resources/qml/hifi/audio/MicBarApplication.qml index 451f410e1d..2bf49aa83b 100644 --- a/interface/resources/qml/hifi/audio/MicBarApplication.qml +++ b/interface/resources/qml/hifi/audio/MicBarApplication.qml @@ -124,7 +124,6 @@ Rectangle { height: 32; anchors { left: parent.left; - leftMargin: 7; top: parent.top; topMargin: 5; } @@ -142,10 +141,7 @@ Rectangle { Item { id: status; - readonly property string color: colors.muted; - visible: muted && (level >= userSpeakingLevel); - opacity: 0.9 anchors { left: parent.left; @@ -162,7 +158,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - color: parent.color; + color: colors.mutedColor; text: "MUTED"; size: 12; From d382893e754cee57b27d1fe32b2d6c69e91b6dc2 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Thu, 28 Feb 2019 16:51:19 -0800 Subject: [PATCH 286/446] staging avatar inputs for ignore radius --- interface/src/Application.cpp | 10 ++ interface/src/avatar/AvatarManager.cpp | 2 + interface/src/ui/AvatarInputs.cpp | 3 + interface/src/ui/AvatarInputs.h | 25 +++ interface/src/ui/PrivacyShield.cpp | 147 ++++++++++++++++++ interface/src/ui/PrivacyShield.h | 35 +++++ libraries/avatars/src/AvatarData.h | 1 + .../UserActivityLoggerScriptingInterface.cpp | 8 +- .../UserActivityLoggerScriptingInterface.h | 4 +- scripts/system/bubble.js | 6 +- 10 files changed, 232 insertions(+), 9 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 635932ea1c..e34ae4bcba 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -211,6 +211,7 @@ #include "ui/UpdateDialog.h" #include "ui/DomainConnectionModel.h" #include "ui/Keyboard.h" +#include "ui/PrivacyShield.h" #include "Util.h" #include "InterfaceParentFinder.h" #include "ui/OctreeStatsProvider.h" @@ -927,6 +928,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); return previousSessionCrashed; } @@ -2650,6 +2652,9 @@ void Application::cleanupBeforeQuit() { nodeList->getPacketReceiver().setShouldDropPackets(true); } + // destroy privacy shield before entity shutdown. + DependencyManager::get()->destroyPrivacyShield(); + getEntities()->shutdown(); // tell the entities system we're shutting down, so it will stop running scripts // Clear any queued processing (I/O, FBX/OBJ/Texture parsing) @@ -2728,6 +2733,7 @@ void Application::cleanupBeforeQuit() { DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); + DependencyManager::destroy(); DependencyManager::destroy(); qCDebug(interfaceapp) << "Application::cleanupBeforeQuit() complete"; @@ -5528,6 +5534,8 @@ void Application::resumeAfterLoginDialogActionTaken() { menu->getMenu("Developer")->setVisible(_developerMenuVisible); _myCamera.setMode(_previousCameraMode); cameraModeChanged(); + + DependencyManager::get()->createPrivacyShield(); } void Application::loadAvatarScripts(const QVector& urls) { @@ -6486,6 +6494,8 @@ void Application::update(float deltaTime) { updateLoginDialogPosition(); } + DependencyManager::get()->update(deltaTime); + { PROFILE_RANGE_EX(app, "Overlays", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount()); PerformanceTimer perfTimer("overlays"); diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 69f7054953..76612039db 100755 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include "Application.h" #include "InterfaceLogging.h" @@ -536,6 +537,7 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar avatar->removeAvatarEntitiesFromTree(); if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble) { + emit AvatarInputs::getInstance()->avatarEnteredIgnoreRadius(avatar->getSessionUUID()); emit DependencyManager::get()->enteredIgnoreRadius(); } else if (removalReason == KillAvatarReason::AvatarDisconnected) { // remove from node sets, if present diff --git a/interface/src/ui/AvatarInputs.cpp b/interface/src/ui/AvatarInputs.cpp index 6d43507a3e..80604f354b 100644 --- a/interface/src/ui/AvatarInputs.cpp +++ b/interface/src/ui/AvatarInputs.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include "Application.h" #include "Menu.h" @@ -31,7 +32,9 @@ AvatarInputs* AvatarInputs::getInstance() { AvatarInputs::AvatarInputs(QObject* parent) : QObject(parent) { _showAudioTools = showAudioToolsSetting.get(); auto nodeList = DependencyManager::get(); + auto usersScriptingInterface = DependencyManager::get(); connect(nodeList.data(), &NodeList::ignoreRadiusEnabledChanged, this, &AvatarInputs::ignoreRadiusEnabledChanged); + connect(usersScriptingInterface.data(), &UsersScriptingInterface::enteredIgnoreRadius, this, &AvatarInputs::enteredIgnoreRadiusChanged); } #define AI_UPDATE(name, src) \ diff --git a/interface/src/ui/AvatarInputs.h b/interface/src/ui/AvatarInputs.h index a41fd0485f..f53adc1749 100644 --- a/interface/src/ui/AvatarInputs.h +++ b/interface/src/ui/AvatarInputs.h @@ -43,6 +43,7 @@ class AvatarInputs : public QObject { Q_PROPERTY(bool showAudioTools READ showAudioTools WRITE setShowAudioTools NOTIFY showAudioToolsChanged) Q_PROPERTY(bool ignoreRadiusEnabled READ getIgnoreRadiusEnabled NOTIFY ignoreRadiusEnabledChanged) + //Q_PROPERTY(bool enteredIgnoreRadius READ getEnteredIgnoreRadius NOTIFY enteredIgnoreRadiusChanged) public: static AvatarInputs* getInstance(); @@ -58,6 +59,7 @@ public: void update(); bool showAudioTools() const { return _showAudioTools; } bool getIgnoreRadiusEnabled() const; + //bool getEnteredIgnoreRadius() const; public slots: @@ -95,6 +97,20 @@ signals: */ void showAudioToolsChanged(bool show); + /**jsdoc + * @function AvatarInputs.avatarEnteredIgnoreRadius + * @param {QUuid} avatarID + * @returns {Signal} + */ + void avatarEnteredIgnoreRadius(QUuid avatarID); + + /**jsdoc + * @function AvatarInputs.avatarLeftIgnoreRadius + * @param {QUuid} avatarID + * @returns {Signal} + */ + void avatarLeftIgnoreRadius(QUuid avatarID); + /**jsdoc * @function AvatarInputs.ignoreRadiusEnabledChanged * @param {boolean} enabled @@ -102,6 +118,13 @@ signals: */ void ignoreRadiusEnabledChanged(bool enabled); + /**jsdoc + * @function AvatarInputs.enteredIgnoreRadiusChanged + * @param {boolean} enabled + * @returns {Signal} + */ + void enteredIgnoreRadiusChanged(); + protected: /**jsdoc @@ -115,6 +138,8 @@ protected: Q_INVOKABLE void toggleCameraMute(); private: + void onAvatarEnteredIgnoreRadius(); + void onAvatarLeftIgnoreRadius(); float _trailingAudioLoudness{ 0 }; bool _showAudioTools { false }; }; diff --git a/interface/src/ui/PrivacyShield.cpp b/interface/src/ui/PrivacyShield.cpp index 12687afbea..e8f61ff5bf 100644 --- a/interface/src/ui/PrivacyShield.cpp +++ b/interface/src/ui/PrivacyShield.cpp @@ -10,3 +10,150 @@ // #include "PrivacyShield.h" + +#include +#include +#include +#include +#include +#include + +#include "Application.h" +#include "PathUtils.h" +#include "GLMHelpers.h" + +const int PRIVACY_SHIELD_VISIBLE_DURATION_MS = 3000; +const int PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS = 750; +const int PRIVACY_SHIELD_SOUND_RATE_LIMIT_MS = 15000; +const float PRIVACY_SHIELD_HEIGHT_SCALE = 0.15f; + +PrivacyShield::PrivacyShield() { + auto usersScriptingInterface = DependencyManager::get(); + //connect(usersScriptingInterface.data(), &UsersScriptingInterface::ignoreRadiusEnabledChanged, [this](bool enabled) { + // onPrivacyShieldToggled(enabled); + //}); + //connect(usersScriptingInterface.data(), &UsersScriptingInterface::enteredIgnoreRadius, this, &PrivacyShield::enteredIgnoreRadius); +} + +void PrivacyShield::createPrivacyShield() { + // Affects bubble height + auto myAvatar = DependencyManager::get()->getMyAvatar(); + auto avatarScale = myAvatar->getTargetScale(); + auto avatarSensorToWorldScale = myAvatar->getSensorToWorldScale(); + auto avatarWorldPosition = myAvatar->getWorldPosition(); + auto avatarWorldOrientation = myAvatar->getWorldOrientation(); + EntityItemProperties properties; + properties.setName("Privacy-Shield"); + properties.setModelURL(PathUtils::resourcesUrl("assets/models/Bubble-v14.fbx").toString()); + properties.setDimensions(glm::vec3(avatarSensorToWorldScale, 0.75 * avatarSensorToWorldScale, avatarSensorToWorldScale)); + properties.setPosition(glm::vec3(avatarWorldPosition.x, + -avatarScale * 2 + avatarWorldPosition.y + avatarScale * PRIVACY_SHIELD_HEIGHT_SCALE, avatarWorldPosition.z)); + properties.setRotation(avatarWorldOrientation * Quaternions::Y_180); + properties.setModelScale(glm::vec3(2.0, 0.5 * (avatarScale + 1.0), 2.0)); + properties.setVisible(false); + + _localPrivacyShieldID = DependencyManager::get()->addEntityInternal(properties, entity::HostType::LOCAL); + //_bubbleActivateSound = DependencyManager::get()->getSound(PathUtils::resourcesUrl() + "assets/sounds/bubble.wav"); + + //onPrivacyShieldToggled(DependencyManager::get()->getIgnoreRadiusEnabled(), true); +} + +void PrivacyShield::destroyPrivacyShield() { + DependencyManager::get()->deleteEntity(_localPrivacyShieldID); +} + +void PrivacyShield::update(float deltaTime) { + if (_updateConnected) { + auto now = usecTimestampNow(); + auto delay = (now - _privacyShieldTimestamp); + auto privacyShieldAlpha = 1.0 - (delay / PRIVACY_SHIELD_VISIBLE_DURATION_MS); + if (privacyShieldAlpha > 0) { + auto myAvatar = DependencyManager::get()->getMyAvatar(); + auto avatarScale = myAvatar->getTargetScale(); + auto avatarSensorToWorldScale = myAvatar->getSensorToWorldScale(); + auto avatarWorldPosition = myAvatar->getWorldPosition(); + auto avatarWorldOrientation = myAvatar->getWorldOrientation(); + EntityItemProperties properties; + properties.setDimensions(glm::vec3(avatarSensorToWorldScale, 0.75 * avatarSensorToWorldScale, avatarSensorToWorldScale)); + properties.setRotation(avatarWorldOrientation * Quaternions::Y_180); + if (delay < PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS) { + properties.setPosition(glm::vec3(avatarWorldPosition.x, + (-((PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS - delay) / PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS)) * avatarScale * 2.0 + + avatarWorldPosition.y + avatarScale * PRIVACY_SHIELD_HEIGHT_SCALE, avatarWorldPosition.z)); + properties.setModelScale(glm::vec3(2.0, + ((1 - ((PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS - delay) / PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS)) * + (0.5 * (avatarScale + 1.0))), 2.0)); + } else { + properties.setPosition(glm::vec3(avatarWorldPosition.x, avatarWorldPosition.y + avatarScale * PRIVACY_SHIELD_HEIGHT_SCALE, avatarWorldPosition.z)); + properties.setModelScale(glm::vec3(2.0, 0.5 * (avatarScale + 1.0), 2.0)); + } + DependencyManager::get()->editEntity(_localPrivacyShieldID, properties); + } + else { + hidePrivacyShield(); + if (_updateConnected) { + _updateConnected = false; + } + } + } +} + +void PrivacyShield::enteredIgnoreRadius() { + showPrivacyShield(); + DependencyManager::get()->privacyShieldActivated(); +} + +void PrivacyShield::onPrivacyShieldToggled(bool enabled, bool doNotLog) { + if (!doNotLog) { + DependencyManager::get()->privacyShieldToggled(enabled); + } + if (enabled) { + showPrivacyShield(); + } else { + hidePrivacyShield(); + if (_updateConnected) { + _updateConnected = false; + } + } +} + +void PrivacyShield::showPrivacyShield() { + auto now = usecTimestampNow(); + auto myAvatar = DependencyManager::get()->getMyAvatar(); + auto avatarScale = myAvatar->getTargetScale(); + auto avatarSensorToWorldScale = myAvatar->getSensorToWorldScale(); + auto avatarWorldPosition = myAvatar->getWorldPosition(); + auto avatarWorldOrientation = myAvatar->getWorldOrientation(); + if (now - _lastPrivacyShieldSoundTimestamp >= PRIVACY_SHIELD_SOUND_RATE_LIMIT_MS) { + AudioInjectorOptions options; + options.position = avatarWorldPosition; + options.localOnly = true; + options.volume = 0.2f; + AudioInjector::playSoundAndDelete(_bubbleActivateSound, options); + _lastPrivacyShieldSoundTimestamp = now; + } + hidePrivacyShield(); + if (_updateConnected) { + _updateConnected = false; + } + + EntityItemProperties properties; + properties.setDimensions(glm::vec3(avatarSensorToWorldScale, 0.75 * avatarSensorToWorldScale, avatarSensorToWorldScale)); + properties.setPosition(glm::vec3(avatarWorldPosition.x, + -avatarScale * 2 + avatarWorldPosition.y + avatarScale * PRIVACY_SHIELD_HEIGHT_SCALE, avatarWorldPosition.z)); + properties.setModelScale(glm::vec3(2.0, 0.5 * (avatarScale + 1.0), 2.0)); + properties.setVisible(true); + + DependencyManager::get()->editEntity(_localPrivacyShieldID, properties); + + _privacyShieldTimestamp = now; + _updateConnected = true; +} + +void PrivacyShield::hidePrivacyShield() { + EntityTreePointer entityTree = qApp->getEntities()->getTree(); + EntityItemPointer privacyShieldEntity = entityTree->findEntityByEntityItemID(EntityItemID(_localPrivacyShieldID)); + if (privacyShieldEntity) { + privacyShieldEntity->setVisible(false); + } +} diff --git a/interface/src/ui/PrivacyShield.h b/interface/src/ui/PrivacyShield.h index a609f2775b..5aecb661f7 100644 --- a/interface/src/ui/PrivacyShield.h +++ b/interface/src/ui/PrivacyShield.h @@ -10,3 +10,38 @@ // #pragma once + +#include +#include +#include + +#include +#include + +class PrivacyShield : public QObject, public Dependency { + Q_OBJECT + SINGLETON_DEPENDENCY + +public: + PrivacyShield(); + void createPrivacyShield(); + void destroyPrivacyShield(); + + bool isVisible() const { return _visible; } + void update(float deltaTime); + +protected slots: + void enteredIgnoreRadius(); + void onPrivacyShieldToggled(bool enabled, bool doNotLog = false); + +private: + void showPrivacyShield(); + void hidePrivacyShield(); + + SharedSoundPointer _bubbleActivateSound; + QUuid _localPrivacyShieldID; + quint64 _privacyShieldTimestamp; + quint64 _lastPrivacyShieldSoundTimestamp; + bool _visible { false }; + bool _updateConnected { false }; +}; diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 95bbcbeb16..32f53f77a3 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -1477,6 +1477,7 @@ protected: glm::vec3 _globalBoundingBoxOffset; AABox _defaultBubbleBox; + AABox _fitBoundingBox; mutable ReadWriteLockable _avatarEntitiesLock; AvatarEntityIDs _avatarEntityRemoved; // recently removed AvatarEntity ids diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp index c63170de75..2f47ef5e00 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp @@ -71,12 +71,12 @@ void UserActivityLoggerScriptingInterface::makeUserConnection(QString otherID, b doLogAction("makeUserConnection", payload); } -void UserActivityLoggerScriptingInterface::bubbleToggled(bool newValue) { - doLogAction(newValue ? "bubbleOn" : "bubbleOff"); +void UserActivityLoggerScriptingInterface::privacyShieldToggled(bool newValue) { + doLogAction(newValue ? "privacyShieldOn" : "privacyShieldOff"); } -void UserActivityLoggerScriptingInterface::bubbleActivated() { - doLogAction("bubbleActivated"); +void UserActivityLoggerScriptingInterface::privacyShieldActivated() { + doLogAction("privacyShieldActivated"); } void UserActivityLoggerScriptingInterface::logAction(QString action, QVariantMap details) { diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.h b/libraries/networking/src/UserActivityLoggerScriptingInterface.h index 71d411056d..1cda1235e9 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.h +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.h @@ -30,8 +30,8 @@ public: Q_INVOKABLE void palAction(QString action, QString target); Q_INVOKABLE void palOpened(float secondsOpen); Q_INVOKABLE void makeUserConnection(QString otherUser, bool success, QString details = ""); - Q_INVOKABLE void bubbleToggled(bool newValue); - Q_INVOKABLE void bubbleActivated(); + Q_INVOKABLE void privacyShieldToggled(bool newValue); + Q_INVOKABLE void privacyShieldActivated(); Q_INVOKABLE void logAction(QString action, QVariantMap details = QVariantMap{}); Q_INVOKABLE void commercePurchaseSuccess(QString marketplaceID, QString contentCreator, int cost, bool firstPurchaseOfThisItem); Q_INVOKABLE void commercePurchaseFailure(QString marketplaceID, QString contentCreator, int cost, bool firstPurchaseOfThisItem, QString errorDetails); diff --git a/scripts/system/bubble.js b/scripts/system/bubble.js index 6ca624872e..eca3b3dcd4 100644 --- a/scripts/system/bubble.js +++ b/scripts/system/bubble.js @@ -90,7 +90,7 @@ // Called from the C++ scripting interface to show the bubble overlay function enteredIgnoreRadius() { createOverlays(); - UserActivityLogger.bubbleActivated(); + UserActivityLogger.privacyShieldActivated(); } // Used to set the state of the bubble HUD button @@ -160,7 +160,7 @@ function onBubbleToggled(enabled, doNotLog) { writeButtonProperties(enabled); if (doNotLog !== true) { - UserActivityLogger.bubbleToggled(enabled); + UserActivityLogger.privacyShieldToggled(enabled); } if (enabled) { createOverlays(); @@ -174,7 +174,7 @@ } // Setup the bubble button - var buttonName = "BUBBLE"; + var buttonName = "SHIELD"; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); button = tablet.addButton({ icon: "icons/tablet-icons/bubble-i.svg", From 811fa8dcb29a3cb306fef95e49d98a5f824bd350 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Fri, 1 Mar 2019 09:12:03 -0800 Subject: [PATCH 287/446] allowing bubble icon to toggle --- interface/resources/qml/AvatarInputsBar.qml | 8 +- interface/src/ui/PrivacyShield.cpp | 96 ++++++++++----------- 2 files changed, 51 insertions(+), 53 deletions(-) diff --git a/interface/resources/qml/AvatarInputsBar.qml b/interface/resources/qml/AvatarInputsBar.qml index 615e260833..ead4fbc618 100644 --- a/interface/resources/qml/AvatarInputsBar.qml +++ b/interface/resources/qml/AvatarInputsBar.qml @@ -36,7 +36,7 @@ Item { width: bubbleIcon.width + 10 height: parent.height radius: 5; - opacity: 0.5; + opacity: AvatarInputs.ignoreRadiusEnabled ? 0.7 : 0.3; color: "#00000000"; border { @@ -56,14 +56,12 @@ Item { scrollGestureEnabled: false; onClicked: { Tablet.playSound(TabletEnums.ButtonClick); + Users.toggleIgnoreRadius(); } drag.target: root; onContainsMouseChanged: { if (containsMouse) { Tablet.playSound(TabletEnums.ButtonHover); - bubbleRect.opacity = 0.7; - } else { - bubbleRect.opacity = 0.5; } } } @@ -82,7 +80,7 @@ Item { id: bubbleIconOverlay anchors.fill: bubbleIcon source: bubbleIcon - color: AvatarInputs.ignoreRadiusEnabled ? "#1FC6A6" : "#FFFFFF"; + color: "#FFFFFF"; } } } diff --git a/interface/src/ui/PrivacyShield.cpp b/interface/src/ui/PrivacyShield.cpp index e8f61ff5bf..279c05ee2d 100644 --- a/interface/src/ui/PrivacyShield.cpp +++ b/interface/src/ui/PrivacyShield.cpp @@ -37,22 +37,22 @@ PrivacyShield::PrivacyShield() { void PrivacyShield::createPrivacyShield() { // Affects bubble height - auto myAvatar = DependencyManager::get()->getMyAvatar(); - auto avatarScale = myAvatar->getTargetScale(); - auto avatarSensorToWorldScale = myAvatar->getSensorToWorldScale(); - auto avatarWorldPosition = myAvatar->getWorldPosition(); - auto avatarWorldOrientation = myAvatar->getWorldOrientation(); - EntityItemProperties properties; - properties.setName("Privacy-Shield"); - properties.setModelURL(PathUtils::resourcesUrl("assets/models/Bubble-v14.fbx").toString()); - properties.setDimensions(glm::vec3(avatarSensorToWorldScale, 0.75 * avatarSensorToWorldScale, avatarSensorToWorldScale)); - properties.setPosition(glm::vec3(avatarWorldPosition.x, - -avatarScale * 2 + avatarWorldPosition.y + avatarScale * PRIVACY_SHIELD_HEIGHT_SCALE, avatarWorldPosition.z)); - properties.setRotation(avatarWorldOrientation * Quaternions::Y_180); - properties.setModelScale(glm::vec3(2.0, 0.5 * (avatarScale + 1.0), 2.0)); - properties.setVisible(false); + //auto myAvatar = DependencyManager::get()->getMyAvatar(); + //auto avatarScale = myAvatar->getTargetScale(); + //auto avatarSensorToWorldScale = myAvatar->getSensorToWorldScale(); + //auto avatarWorldPosition = myAvatar->getWorldPosition(); + //auto avatarWorldOrientation = myAvatar->getWorldOrientation(); + //EntityItemProperties properties; + //properties.setName("Privacy-Shield"); + //properties.setModelURL(PathUtils::resourcesUrl("assets/models/Bubble-v14.fbx").toString()); + //properties.setDimensions(glm::vec3(avatarSensorToWorldScale, 0.75 * avatarSensorToWorldScale, avatarSensorToWorldScale)); + //properties.setPosition(glm::vec3(avatarWorldPosition.x, + // -avatarScale * 2 + avatarWorldPosition.y + avatarScale * PRIVACY_SHIELD_HEIGHT_SCALE, avatarWorldPosition.z)); + //properties.setRotation(avatarWorldOrientation * Quaternions::Y_180); + //properties.setModelScale(glm::vec3(2.0, 0.5 * (avatarScale + 1.0), 2.0)); + //properties.setVisible(false); - _localPrivacyShieldID = DependencyManager::get()->addEntityInternal(properties, entity::HostType::LOCAL); + //_localPrivacyShieldID = DependencyManager::get()->addEntityInternal(properties, entity::HostType::LOCAL); //_bubbleActivateSound = DependencyManager::get()->getSound(PathUtils::resourcesUrl() + "assets/sounds/bubble.wav"); //onPrivacyShieldToggled(DependencyManager::get()->getIgnoreRadiusEnabled(), true); @@ -63,39 +63,39 @@ void PrivacyShield::destroyPrivacyShield() { } void PrivacyShield::update(float deltaTime) { - if (_updateConnected) { - auto now = usecTimestampNow(); - auto delay = (now - _privacyShieldTimestamp); - auto privacyShieldAlpha = 1.0 - (delay / PRIVACY_SHIELD_VISIBLE_DURATION_MS); - if (privacyShieldAlpha > 0) { - auto myAvatar = DependencyManager::get()->getMyAvatar(); - auto avatarScale = myAvatar->getTargetScale(); - auto avatarSensorToWorldScale = myAvatar->getSensorToWorldScale(); - auto avatarWorldPosition = myAvatar->getWorldPosition(); - auto avatarWorldOrientation = myAvatar->getWorldOrientation(); - EntityItemProperties properties; - properties.setDimensions(glm::vec3(avatarSensorToWorldScale, 0.75 * avatarSensorToWorldScale, avatarSensorToWorldScale)); - properties.setRotation(avatarWorldOrientation * Quaternions::Y_180); - if (delay < PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS) { - properties.setPosition(glm::vec3(avatarWorldPosition.x, - (-((PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS - delay) / PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS)) * avatarScale * 2.0 + - avatarWorldPosition.y + avatarScale * PRIVACY_SHIELD_HEIGHT_SCALE, avatarWorldPosition.z)); - properties.setModelScale(glm::vec3(2.0, - ((1 - ((PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS - delay) / PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS)) * - (0.5 * (avatarScale + 1.0))), 2.0)); - } else { - properties.setPosition(glm::vec3(avatarWorldPosition.x, avatarWorldPosition.y + avatarScale * PRIVACY_SHIELD_HEIGHT_SCALE, avatarWorldPosition.z)); - properties.setModelScale(glm::vec3(2.0, 0.5 * (avatarScale + 1.0), 2.0)); - } - DependencyManager::get()->editEntity(_localPrivacyShieldID, properties); - } - else { - hidePrivacyShield(); - if (_updateConnected) { - _updateConnected = false; - } - } - } + //if (_updateConnected) { + // auto now = usecTimestampNow(); + // auto delay = (now - _privacyShieldTimestamp); + // auto privacyShieldAlpha = 1.0 - (delay / PRIVACY_SHIELD_VISIBLE_DURATION_MS); + // if (privacyShieldAlpha > 0) { + // auto myAvatar = DependencyManager::get()->getMyAvatar(); + // auto avatarScale = myAvatar->getTargetScale(); + // auto avatarSensorToWorldScale = myAvatar->getSensorToWorldScale(); + // auto avatarWorldPosition = myAvatar->getWorldPosition(); + // auto avatarWorldOrientation = myAvatar->getWorldOrientation(); + // EntityItemProperties properties; + // properties.setDimensions(glm::vec3(avatarSensorToWorldScale, 0.75 * avatarSensorToWorldScale, avatarSensorToWorldScale)); + // properties.setRotation(avatarWorldOrientation * Quaternions::Y_180); + // if (delay < PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS) { + // properties.setPosition(glm::vec3(avatarWorldPosition.x, + // (-((PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS - delay) / PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS)) * avatarScale * 2.0 + + // avatarWorldPosition.y + avatarScale * PRIVACY_SHIELD_HEIGHT_SCALE, avatarWorldPosition.z)); + // properties.setModelScale(glm::vec3(2.0, + // ((1 - ((PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS - delay) / PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS)) * + // (0.5 * (avatarScale + 1.0))), 2.0)); + // } else { + // properties.setPosition(glm::vec3(avatarWorldPosition.x, avatarWorldPosition.y + avatarScale * PRIVACY_SHIELD_HEIGHT_SCALE, avatarWorldPosition.z)); + // properties.setModelScale(glm::vec3(2.0, 0.5 * (avatarScale + 1.0), 2.0)); + // } + // DependencyManager::get()->editEntity(_localPrivacyShieldID, properties); + // } + // else { + // hidePrivacyShield(); + // if (_updateConnected) { + // _updateConnected = false; + // } + // } + //} } void PrivacyShield::enteredIgnoreRadius() { From 5b6ce8736d3a46c0f12a40aff572f048e8b7c971 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Fri, 1 Mar 2019 11:28:00 -0800 Subject: [PATCH 288/446] adding new icons with gated icon --- .../icons/tablet-icons/mic-clip-i.svg | 5 ++- .../icons/tablet-icons/mic-gate-i.svg | 6 ++++ .../icons/tablet-icons/mic-mute-i.svg | 32 +++++-------------- .../icons/tablet-icons/mic-unmute-i.svg | 6 +++- .../qml/hifi/audio/MicBarApplication.qml | 3 +- 5 files changed, 25 insertions(+), 27 deletions(-) create mode 100644 interface/resources/icons/tablet-icons/mic-gate-i.svg diff --git a/interface/resources/icons/tablet-icons/mic-clip-i.svg b/interface/resources/icons/tablet-icons/mic-clip-i.svg index 8bc1b2b558..c1989db2d6 100644 --- a/interface/resources/icons/tablet-icons/mic-clip-i.svg +++ b/interface/resources/icons/tablet-icons/mic-clip-i.svg @@ -1,6 +1,9 @@ - + + + + diff --git a/interface/resources/icons/tablet-icons/mic-gate-i.svg b/interface/resources/icons/tablet-icons/mic-gate-i.svg new file mode 100644 index 0000000000..a5a5a621d7 --- /dev/null +++ b/interface/resources/icons/tablet-icons/mic-gate-i.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/interface/resources/icons/tablet-icons/mic-mute-i.svg b/interface/resources/icons/tablet-icons/mic-mute-i.svg index 9dc2c53443..0da394a5a2 100644 --- a/interface/resources/icons/tablet-icons/mic-mute-i.svg +++ b/interface/resources/icons/tablet-icons/mic-mute-i.svg @@ -1,25 +1,9 @@ - - - - - - - - - - - - - - - + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/mic-unmute-i.svg b/interface/resources/icons/tablet-icons/mic-unmute-i.svg index f577a65bc2..ebfcc21e37 100644 --- a/interface/resources/icons/tablet-icons/mic-unmute-i.svg +++ b/interface/resources/icons/tablet-icons/mic-unmute-i.svg @@ -1,3 +1,7 @@ - + + + + + diff --git a/interface/resources/qml/hifi/audio/MicBarApplication.qml b/interface/resources/qml/hifi/audio/MicBarApplication.qml index 2bf49aa83b..30cef44bce 100644 --- a/interface/resources/qml/hifi/audio/MicBarApplication.qml +++ b/interface/resources/qml/hifi/audio/MicBarApplication.qml @@ -29,6 +29,7 @@ Rectangle { readonly property string unmutedIcon: "../../../icons/tablet-icons/mic-unmute-i.svg"; readonly property string mutedIcon: "../../../icons/tablet-icons/mic-mute-i.svg"; readonly property string clippingIcon: "../../../icons/tablet-icons/mic-clip-i.svg"; + readonly property string gatedIcon: "../../../icons/tablet-icons/mic-gate-i.svg"; property bool standalone: false; property var dragTarget: null; @@ -118,7 +119,7 @@ Rectangle { Item { Image { id: image; - source: muted ? mutedIcon : clipping ? clippingIcon : unmutedIcon; + source: muted ? mutedIcon : clipping ? clippingIcon : gated ? gatedIcon : unmutedIcon; width: 29; height: 32; From 4dca4cbc40f421d63836a45b8bbe4d214fcffc45 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Fri, 1 Mar 2019 11:38:32 -0800 Subject: [PATCH 289/446] update icons again --- .../icons/tablet-icons/mic-clip-i.svg | 5 +- .../icons/tablet-icons/mic-gate-i.svg | 5 +- .../icons/tablet-icons/mic-mute-a.svg | 26 +------ .../icons/tablet-icons/mic-mute-i.svg | 8 +- .../icons/tablet-icons/mic-unmute-a.svg | 73 +------------------ .../icons/tablet-icons/mic-unmute-i.svg | 6 +- 6 files changed, 9 insertions(+), 114 deletions(-) diff --git a/interface/resources/icons/tablet-icons/mic-clip-i.svg b/interface/resources/icons/tablet-icons/mic-clip-i.svg index c1989db2d6..f912c1e744 100644 --- a/interface/resources/icons/tablet-icons/mic-clip-i.svg +++ b/interface/resources/icons/tablet-icons/mic-clip-i.svg @@ -1,9 +1,6 @@ - - - - + diff --git a/interface/resources/icons/tablet-icons/mic-gate-i.svg b/interface/resources/icons/tablet-icons/mic-gate-i.svg index a5a5a621d7..8255174532 100644 --- a/interface/resources/icons/tablet-icons/mic-gate-i.svg +++ b/interface/resources/icons/tablet-icons/mic-gate-i.svg @@ -1,6 +1,3 @@ - - - - + diff --git a/interface/resources/icons/tablet-icons/mic-mute-a.svg b/interface/resources/icons/tablet-icons/mic-mute-a.svg index 9dc2c53443..67eafc27c8 100644 --- a/interface/resources/icons/tablet-icons/mic-mute-a.svg +++ b/interface/resources/icons/tablet-icons/mic-mute-a.svg @@ -1,25 +1,3 @@ - - - - - - - - - - - - - - - + + diff --git a/interface/resources/icons/tablet-icons/mic-mute-i.svg b/interface/resources/icons/tablet-icons/mic-mute-i.svg index 0da394a5a2..63af1b0da8 100644 --- a/interface/resources/icons/tablet-icons/mic-mute-i.svg +++ b/interface/resources/icons/tablet-icons/mic-mute-i.svg @@ -1,9 +1,3 @@ - - - - - - - + diff --git a/interface/resources/icons/tablet-icons/mic-unmute-a.svg b/interface/resources/icons/tablet-icons/mic-unmute-a.svg index b1464f207d..0bf7677017 100644 --- a/interface/resources/icons/tablet-icons/mic-unmute-a.svg +++ b/interface/resources/icons/tablet-icons/mic-unmute-a.svg @@ -1,70 +1,3 @@ - - - -image/svg+xml \ No newline at end of file + + + diff --git a/interface/resources/icons/tablet-icons/mic-unmute-i.svg b/interface/resources/icons/tablet-icons/mic-unmute-i.svg index ebfcc21e37..0bf7677017 100644 --- a/interface/resources/icons/tablet-icons/mic-unmute-i.svg +++ b/interface/resources/icons/tablet-icons/mic-unmute-i.svg @@ -1,7 +1,3 @@ - - - - - + From 77b7cc2457cf9f9412116d51e59edc7c9c2461cb Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Thu, 14 Mar 2019 10:28:18 -0700 Subject: [PATCH 290/446] separating out bubble icon qml - adding support for transparency --- interface/resources/qml/AvatarInputsBar.qml | 53 +----- interface/resources/qml/BubbleIcon.qml | 82 +++++++++ interface/resources/qml/hifi/audio/MicBar.qml | 34 ++-- .../qml/hifi/audio/MicBarApplication.qml | 80 ++++----- interface/src/Application.cpp | 65 +++++-- interface/src/Application.h | 7 +- interface/src/scripting/Audio.cpp | 8 +- interface/src/ui/PrivacyShield.cpp | 159 ------------------ interface/src/ui/PrivacyShield.h | 47 ------ scripts/defaultScripts.js | 3 +- 10 files changed, 204 insertions(+), 334 deletions(-) create mode 100644 interface/resources/qml/BubbleIcon.qml delete mode 100644 interface/src/ui/PrivacyShield.cpp delete mode 100644 interface/src/ui/PrivacyShield.h diff --git a/interface/resources/qml/AvatarInputsBar.qml b/interface/resources/qml/AvatarInputsBar.qml index ead4fbc618..dfff103aa0 100644 --- a/interface/resources/qml/AvatarInputsBar.qml +++ b/interface/resources/qml/AvatarInputsBar.qml @@ -31,56 +31,7 @@ Item { standalone: true; dragTarget: parent; } - Rectangle { - id: bubbleRect - width: bubbleIcon.width + 10 - height: parent.height - radius: 5; - opacity: AvatarInputs.ignoreRadiusEnabled ? 0.7 : 0.3; - - color: "#00000000"; - border { - width: mouseArea.containsMouse || mouseArea.containsPress ? 2 : 0; - color: "#80FFFFFF"; - } - anchors { - left: root.right - top: root.top - } - - MouseArea { - id: mouseArea; - anchors.fill: parent - - hoverEnabled: true; - scrollGestureEnabled: false; - onClicked: { - Tablet.playSound(TabletEnums.ButtonClick); - Users.toggleIgnoreRadius(); - } - drag.target: root; - onContainsMouseChanged: { - if (containsMouse) { - Tablet.playSound(TabletEnums.ButtonHover); - } - } - } - Image { - id: bubbleIcon - source: "../icons/tablet-icons/bubble-i.svg"; - sourceSize: Qt.size(28, 28); - smooth: true; - visible: false - anchors.top: parent.top - anchors.topMargin: (parent.height - bubbleIcon.height) / 2 - anchors.left: parent.left - anchors.leftMargin: (parent.width - bubbleIcon.width) / 2 - } - ColorOverlay { - id: bubbleIconOverlay - anchors.fill: bubbleIcon - source: bubbleIcon - color: "#FFFFFF"; - } + BubbleIcon { + dragTarget: parent } } diff --git a/interface/resources/qml/BubbleIcon.qml b/interface/resources/qml/BubbleIcon.qml new file mode 100644 index 0000000000..f9c57697f0 --- /dev/null +++ b/interface/resources/qml/BubbleIcon.qml @@ -0,0 +1,82 @@ +// +// Created by Bradley Austin Davis on 2015/06/19 +// Copyright 2015 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 Hifi 1.0 as Hifi +import QtQuick 2.5 +import QtGraphicalEffects 1.0 + +import "./hifi/audio" as HifiAudio + +import TabletScriptingInterface 1.0 + +Rectangle { + id: bubbleRect + width: bubbleIcon.width + 10 + height: bubbleIcon.height + 10 + radius: 5; + opacity: AvatarInputs.ignoreRadiusEnabled ? 0.7 : 0.3; + property var dragTarget: null; + + color: "#00000000"; + border { + width: mouseArea.containsMouse || mouseArea.containsPress ? 2 : 0; + color: "#80FFFFFF"; + } + anchors { + left: dragTarget ? dragTarget.right : undefined + top: dragTarget ? dragTarget.top : undefined + } + + // borders are painted over fill, so reduce the fill to fit inside the border + Rectangle { + color: "#55000000"; + width: 40; + height: 40; + + radius: 5; + + anchors { + verticalCenter: parent.verticalCenter; + horizontalCenter: parent.horizontalCenter; + } + } + + MouseArea { + id: mouseArea; + anchors.fill: parent + + hoverEnabled: true; + scrollGestureEnabled: false; + onClicked: { + Tablet.playSound(TabletEnums.ButtonClick); + Users.toggleIgnoreRadius(); + } + drag.target: dragTarget; + onContainsMouseChanged: { + if (containsMouse) { + Tablet.playSound(TabletEnums.ButtonHover); + } + } + } + Image { + id: bubbleIcon + source: "../icons/tablet-icons/bubble-i.svg"; + sourceSize: Qt.size(28, 28); + smooth: true; + anchors.top: parent.top + anchors.topMargin: (parent.height - bubbleIcon.height) / 2 + anchors.left: parent.left + anchors.leftMargin: (parent.width - bubbleIcon.width) / 2 + } + ColorOverlay { + id: bubbleIconOverlay + anchors.fill: bubbleIcon + source: bubbleIcon + color: "#FFFFFF"; + } +} diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index 7ed9451a04..fba06ac987 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -19,13 +19,18 @@ Rectangle { HifiConstants { id: hifi; } readonly property var level: AudioScriptingInterface.inputLevel; - + readonly property var clipping: AudioScriptingInterface.clipping; + readonly property var muted: AudioScriptingInterface.muted; + readonly property var pushToTalk: AudioScriptingInterface.pushToTalk; + readonly property var pushingToTalk: AudioScriptingInterface.pushingToTalk; + + readonly property var userSpeakingLevel: 0.4; property bool gated: false; Component.onCompleted: { AudioScriptingInterface.noiseGateOpened.connect(function() { gated = false; }); AudioScriptingInterface.noiseGateClosed.connect(function() { gated = true; }); } - + property bool standalone: false; property var dragTarget: null; @@ -70,7 +75,7 @@ Rectangle { if (AudioScriptingInterface.pushToTalk) { return; } - AudioScriptingInterface.muted = !AudioScriptingInterface.muted; + muted = !muted; Tablet.playSound(TabletEnums.ButtonClick); } drag.target: dragTarget; @@ -113,9 +118,12 @@ Rectangle { readonly property string unmutedIcon: "../../../icons/tablet-icons/mic-unmute-i.svg"; readonly property string mutedIcon: "../../../icons/tablet-icons/mic-mute-i.svg"; readonly property string pushToTalkIcon: "../../../icons/tablet-icons/mic-ptt-i.svg"; + readonly property string clippingIcon: "../../../icons/tablet-icons/mic-clip-i.svg"; + readonly property string gatedIcon: "../../../icons/tablet-icons/mic-gate-i.svg"; id: image; - source: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) ? pushToTalkIcon : AudioScriptingInterface.muted ? mutedIcon : unmutedIcon; + source: (pushToTalk && !pushingToTalk) ? pushToTalkIcon : muted ? mutedIcon : + clipping ? clippingIcon : gated ? gatedIcon : unmutedIcon; width: 30; height: 30; @@ -138,9 +146,9 @@ Rectangle { Item { id: status; - readonly property string color: AudioScriptingInterface.muted ? colors.muted : colors.unmuted; + readonly property string color: muted ? colors.muted : colors.unmuted; - visible: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) || (AudioScriptingInterface.muted && (level >= userSpeakingLevel)); + visible: (pushToTalk && !pushingToTalk) || (muted && (level >= userSpeakingLevel)); anchors { left: parent.left; @@ -159,7 +167,7 @@ Rectangle { color: parent.color; - text: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) ? (HMD.active ? "MUTED PTT" : "MUTED PTT-(T)") : (AudioScriptingInterface.muted ? "MUTED" : "MUTE"); + text: (pushToTalk && !pushingToTalk) ? (HMD.active ? "MUTED PTT" : "MUTED PTT-(T)") : (muted ? "MUTED" : "MUTE"); font.pointSize: 12; } @@ -235,12 +243,12 @@ Rectangle { } } } - + Rectangle { id: gatedIndicator; visible: gated && !AudioScriptingInterface.clipping - - radius: 4; + + radius: 4; width: 2 * radius; height: 2 * radius; color: "#0080FF"; @@ -249,12 +257,12 @@ Rectangle { verticalCenter: parent.verticalCenter; } } - + Rectangle { id: clippingIndicator; visible: AudioScriptingInterface.clipping - - radius: 4; + + radius: 4; width: 2 * radius; height: 2 * radius; color: colors.red; diff --git a/interface/resources/qml/hifi/audio/MicBarApplication.qml b/interface/resources/qml/hifi/audio/MicBarApplication.qml index 30cef44bce..bfac278ee4 100644 --- a/interface/resources/qml/hifi/audio/MicBarApplication.qml +++ b/interface/resources/qml/hifi/audio/MicBarApplication.qml @@ -1,5 +1,5 @@ // -// MicBar.qml +// MicBarApplication.qml // qml/hifi/audio // // Created by Zach Pomerantz on 6/14/2017 @@ -16,9 +16,12 @@ import stylesUit 1.0 import TabletScriptingInterface 1.0 Rectangle { + id: micBar; readonly property var level: AudioScriptingInterface.inputLevel; readonly property var clipping: AudioScriptingInterface.clipping; readonly property var muted: AudioScriptingInterface.muted; + readonly property var pushToTalk: AudioScriptingInterface.pushToTalk; + readonly property var pushingToTalk: AudioScriptingInterface.pushingToTalk; readonly property var userSpeakingLevel: 0.4; property bool gated: false; Component.onCompleted: { @@ -28,6 +31,7 @@ Rectangle { readonly property string unmutedIcon: "../../../icons/tablet-icons/mic-unmute-i.svg"; readonly property string mutedIcon: "../../../icons/tablet-icons/mic-mute-i.svg"; + readonly property string pushToTalkIcon: "../../../icons/tablet-icons/mic-ptt-i.svg"; readonly property string clippingIcon: "../../../icons/tablet-icons/mic-clip-i.svg"; readonly property string gatedIcon: "../../../icons/tablet-icons/mic-gate-i.svg"; property bool standalone: false; @@ -37,13 +41,16 @@ Rectangle { height: 44; radius: 5; + opacity: 0.7 onLevelChanged: { var rectOpacity = muted && (level >= userSpeakingLevel) ? 0.9 : 0.3; - if (mouseArea.containsMouse && rectOpacity != 0.9) { + if (pushToTalk && !pushingToTalk) { + rectOpacity = (level >= userSpeakingLevel) ? 0.9 : 0.7; + } else if (mouseArea.containsMouse && rectOpacity != 0.9) { rectOpacity = 0.5; } - opacity = rectOpacity; + micBar.opacity = rectOpacity; } color: "#00000000"; @@ -94,15 +101,15 @@ Rectangle { id: colors; readonly property string unmutedColor: "#FFF"; + readonly property string gatedColor: "#00BDFF"; readonly property string mutedColor: "#E2334D"; readonly property string gutter: "#575757"; readonly property string greenStart: "#39A38F"; readonly property string greenEnd: "#1FC6A6"; readonly property string yellow: "#C0C000"; - readonly property string red: colors.muted; readonly property string fill: "#55000000"; readonly property string border: standalone ? "#80FFFFFF" : "#55FFFFFF"; - readonly property string icon: muted ? mutedColor : unmutedColor; + readonly property string icon: (muted || clipping) ? mutedColor : gated ? gatedColor : unmutedColor; } Item { @@ -110,7 +117,7 @@ Rectangle { anchors { left: parent.left; - verticalCenter: parent.verticalCenter; + top: parent.top; } width: 40; @@ -119,8 +126,8 @@ Rectangle { Item { Image { id: image; - source: muted ? mutedIcon : clipping ? clippingIcon : gated ? gatedIcon : unmutedIcon; - + source: (pushToTalk && !pushingToTalk) ? pushToTalkIcon : muted ? mutedIcon : + clipping ? clippingIcon : gated ? gatedIcon : unmutedIcon; width: 29; height: 32; anchors { @@ -134,7 +141,8 @@ Rectangle { id: imageOverlay anchors { fill: image } source: image; - color: colors.icon; + color: (pushToTalk && !pushingToTalk) ? ((level >= userSpeakingLevel) ? colors.mutedColor : + colors.unmutedColor) : colors.icon; } } } @@ -142,26 +150,34 @@ Rectangle { Item { id: status; - visible: muted && (level >= userSpeakingLevel); + visible: (pushToTalk && !pushingToTalk) || (muted && (level >= userSpeakingLevel)); anchors { left: parent.left; - top: parent.bottom - topMargin: 5 + top: icon.bottom; + topMargin: 5; } - width: icon.width; - height: 8 + width: parent.width; + height: statusTextMetrics.height; + + TextMetrics { + id: statusTextMetrics + text: statusText.text + font: statusText.font + } RalewaySemiBold { + id: statusText anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter; } - color: colors.mutedColor; + color: (level >= userSpeakingLevel && muted) ? colors.mutedColor : colors.unmutedColor; + font.bold: true - text: "MUTED"; + text: (pushToTalk && !pushingToTalk) ? (HMD.active ? "PTT" : "PTT-(T)") : (muted ? "MUTED" : "MUTE"); size: 12; } } @@ -172,7 +188,8 @@ Rectangle { anchors { right: parent.right; rightMargin: 7; - verticalCenter: parent.verticalCenter; + top: parent.top + topMargin: 5 } width: 8; @@ -219,34 +236,5 @@ Rectangle { } } } -/* - Rectangle { - id: gatedIndicator; - visible: gated && !clipping - - radius: 4; - width: 2 * radius; - height: 2 * radius; - color: "#0080FF"; - anchors { - right: parent.left; - verticalCenter: parent.verticalCenter; - } - } - - Rectangle { - id: clippingIndicator; - visible: clipping - - radius: 4; - width: 2 * radius; - height: 2 * radius; - color: colors.red; - anchors { - left: parent.right; - verticalCenter: parent.verticalCenter; - } - } -*/ } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e34ae4bcba..9fd0781eef 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -211,7 +211,6 @@ #include "ui/UpdateDialog.h" #include "ui/DomainConnectionModel.h" #include "ui/Keyboard.h" -#include "ui/PrivacyShield.h" #include "Util.h" #include "InterfaceParentFinder.h" #include "ui/OctreeStatsProvider.h" @@ -339,6 +338,10 @@ Setting::Handle maxOctreePacketsPerSecond{"maxOctreePPS", DEFAULT_MAX_OCTRE Setting::Handle loginDialogPoppedUp{"loginDialogPoppedUp", false}; +static const QUrl AVATAR_INPUTS_BAR_QML = PathUtils::qmlUrl("AvatarInputsBar.qml"); +static const QUrl MIC_BAR_ENTITY_QML = PathUtils::qmlUrl("hifi/audio/MicBarApplication.qml"); +static const QUrl BUBBLE_ICON_QML = PathUtils::qmlUrl("BubbleIcon.qml"); + static const QString STANDARD_TO_ACTION_MAPPING_NAME = "Standard to Action"; static const QString NO_MOVEMENT_MAPPING_NAME = "Standard to Action (No Movement)"; static const QString NO_MOVEMENT_MAPPING_JSON = PathUtils::resourcesPath() + "/controllers/standard_nomovement.json"; @@ -928,7 +931,6 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); - DependencyManager::set(); return previousSessionCrashed; } @@ -1293,6 +1295,21 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo setCrashAnnotation("display_plugin", displayPlugin->getName().toStdString()); setCrashAnnotation("hmd", displayPlugin->isHmd() ? "1" : "0"); }); + connect(this, &Application::activeDisplayPluginChanged, this, [&](){ +#if !defined(Q_OS_ANDROID) + if (!getLoginDialogPoppedUp() && _desktopRootItemCreated) { + if (isHMDMode()) { + createAvatarInputsBar(); + auto offscreenUi = getOffscreenUI(); + offscreenUi->hide(AVATAR_INPUTS_BAR_QML.toString()); + } else { + destroyAvatarInputsBar(); + auto offscreenUi = getOffscreenUI(); + offscreenUi->show(AVATAR_INPUTS_BAR_QML.toString(), "AvatarInputsBar"); + } + } +#endif + }); connect(this, &Application::activeDisplayPluginChanged, this, &Application::updateSystemTabletMode); connect(this, &Application::activeDisplayPluginChanged, this, [&](){ if (getLoginDialogPoppedUp()) { @@ -2378,7 +2395,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo }); }); auto rootItemLoadedFunctor = [webSurface, url, isTablet] { - Application::setupQmlSurface(webSurface->getSurfaceContext(), isTablet || url == LOGIN_DIALOG.toString()); + Application::setupQmlSurface(webSurface->getSurfaceContext(), isTablet || url == LOGIN_DIALOG.toString() || url == AVATAR_INPUTS_BAR_QML.toString() || + url == BUBBLE_ICON_QML.toString() || url == MIC_BAR_ENTITY_QML.toString() ); }; if (webSurface->getRootItem()) { rootItemLoadedFunctor(); @@ -2652,9 +2670,6 @@ void Application::cleanupBeforeQuit() { nodeList->getPacketReceiver().setShouldDropPackets(true); } - // destroy privacy shield before entity shutdown. - DependencyManager::get()->destroyPrivacyShield(); - getEntities()->shutdown(); // tell the entities system we're shutting down, so it will stop running scripts // Clear any queued processing (I/O, FBX/OBJ/Texture parsing) @@ -2733,7 +2748,6 @@ void Application::cleanupBeforeQuit() { DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); - DependencyManager::destroy(); DependencyManager::destroy(); qCDebug(interfaceapp) << "Application::cleanupBeforeQuit() complete"; @@ -3301,6 +3315,7 @@ void Application::onDesktopRootItemCreated(QQuickItem* rootItem) { auto qml = PathUtils::qmlUrl("AvatarInputsBar.qml"); offscreenUi->show(qml, "AvatarInputsBar"); #endif + _desktopRootItemCreated = true; } void Application::userKickConfirmation(const QUuid& nodeID) { @@ -5534,8 +5549,6 @@ void Application::resumeAfterLoginDialogActionTaken() { menu->getMenu("Developer")->setVisible(_developerMenuVisible); _myCamera.setMode(_previousCameraMode); cameraModeChanged(); - - DependencyManager::get()->createPrivacyShield(); } void Application::loadAvatarScripts(const QVector& urls) { @@ -6494,8 +6507,6 @@ void Application::update(float deltaTime) { updateLoginDialogPosition(); } - DependencyManager::get()->update(deltaTime); - { PROFILE_RANGE_EX(app, "Overlays", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount()); PerformanceTimer perfTimer("overlays"); @@ -8986,6 +8997,38 @@ void Application::updateLoginDialogPosition() { } } +void Application::createAvatarInputsBar() { + const glm::vec3 LOCAL_POSITION { 0.0, 0.0, -1.0 }; + // DEFAULT_DPI / tablet scale percentage + const float DPI = 31.0f / (75.0f / 100.0f); + + EntityItemProperties properties; + properties.setType(EntityTypes::Web); + properties.setName("AvatarInputsBarEntity"); + properties.setSourceUrl(AVATAR_INPUTS_BAR_QML.toString()); + properties.setParentID(getMyAvatar()->getSelfID()); + properties.setParentJointIndex(getMyAvatar()->getJointIndex("_CAMERA_MATRIX")); + properties.setPosition(LOCAL_POSITION); + properties.setLocalRotation(Quaternions::IDENTITY); + //properties.setDimensions(LOGIN_DIMENSIONS); + properties.setPrimitiveMode(PrimitiveMode::SOLID); + properties.getGrab().setGrabbable(false); + properties.setIgnorePickIntersection(false); + properties.setAlpha(1.0f); + properties.setDPI(DPI); + properties.setVisible(true); + + auto entityScriptingInterface = DependencyManager::get(); + _avatarInputsBarID = entityScriptingInterface->addEntityInternal(properties, entity::HostType::LOCAL); +} + +void Application::destroyAvatarInputsBar() { + auto entityScriptingInterface = DependencyManager::get(); + if (!_avatarInputsBarID.isNull()) { + entityScriptingInterface->deleteEntity(_avatarInputsBarID); + } +} + bool Application::hasRiftControllers() { return PluginUtils::isOculusTouchControllerAvailable(); } diff --git a/interface/src/Application.h b/interface/src/Application.h index 762ac9585a..99e57f1866 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -330,6 +330,9 @@ public: void createLoginDialog(); void updateLoginDialogPosition(); + void createAvatarInputsBar(); + void destroyAvatarInputsBar(); + // Check if a headset is connected bool hasRiftControllers(); bool hasViveControllers(); @@ -704,12 +707,14 @@ private: int _maxOctreePPS = DEFAULT_MAX_OCTREE_PPS; bool _interstitialModeEnabled{ false }; - bool _loginDialogPoppedUp = false; + bool _loginDialogPoppedUp{ false }; + bool _desktopRootItemCreated{ false }; bool _developerMenuVisible{ false }; QString _previousAvatarSkeletonModel; float _previousAvatarTargetScale; CameraMode _previousCameraMode; QUuid _loginDialogID; + QUuid _avatarInputsBarID; LoginStateManager _loginStateManager; quint64 _lastFaceTrackerUpdate; diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index bf43db3044..434688e474 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -224,10 +224,10 @@ void Audio::saveData() { } void Audio::loadData() { - _desktopMuted = _desktopMutedSetting.get(); - _hmdMuted = _hmdMutedSetting.get(); - _pttDesktop = _pttDesktopSetting.get(); - _pttHMD = _pttHMDSetting.get(); + setMutedDesktop(_desktopMutedSetting.get()); + setMutedHMD(_hmdMutedSetting.get()); + setPTTDesktop(_pttDesktopSetting.get()); + setPTTHMD(_pttHMDSetting.get()); } bool Audio::getPTTHMD() const { diff --git a/interface/src/ui/PrivacyShield.cpp b/interface/src/ui/PrivacyShield.cpp deleted file mode 100644 index 279c05ee2d..0000000000 --- a/interface/src/ui/PrivacyShield.cpp +++ /dev/null @@ -1,159 +0,0 @@ -// -// PrivacyShield.cpp -// interface/src/ui -// -// Created by Wayne Chen on 2/27/19. -// Copyright 2019 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 -// - -#include "PrivacyShield.h" - -#include -#include -#include -#include -#include -#include - -#include "Application.h" -#include "PathUtils.h" -#include "GLMHelpers.h" - -const int PRIVACY_SHIELD_VISIBLE_DURATION_MS = 3000; -const int PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS = 750; -const int PRIVACY_SHIELD_SOUND_RATE_LIMIT_MS = 15000; -const float PRIVACY_SHIELD_HEIGHT_SCALE = 0.15f; - -PrivacyShield::PrivacyShield() { - auto usersScriptingInterface = DependencyManager::get(); - //connect(usersScriptingInterface.data(), &UsersScriptingInterface::ignoreRadiusEnabledChanged, [this](bool enabled) { - // onPrivacyShieldToggled(enabled); - //}); - //connect(usersScriptingInterface.data(), &UsersScriptingInterface::enteredIgnoreRadius, this, &PrivacyShield::enteredIgnoreRadius); -} - -void PrivacyShield::createPrivacyShield() { - // Affects bubble height - //auto myAvatar = DependencyManager::get()->getMyAvatar(); - //auto avatarScale = myAvatar->getTargetScale(); - //auto avatarSensorToWorldScale = myAvatar->getSensorToWorldScale(); - //auto avatarWorldPosition = myAvatar->getWorldPosition(); - //auto avatarWorldOrientation = myAvatar->getWorldOrientation(); - //EntityItemProperties properties; - //properties.setName("Privacy-Shield"); - //properties.setModelURL(PathUtils::resourcesUrl("assets/models/Bubble-v14.fbx").toString()); - //properties.setDimensions(glm::vec3(avatarSensorToWorldScale, 0.75 * avatarSensorToWorldScale, avatarSensorToWorldScale)); - //properties.setPosition(glm::vec3(avatarWorldPosition.x, - // -avatarScale * 2 + avatarWorldPosition.y + avatarScale * PRIVACY_SHIELD_HEIGHT_SCALE, avatarWorldPosition.z)); - //properties.setRotation(avatarWorldOrientation * Quaternions::Y_180); - //properties.setModelScale(glm::vec3(2.0, 0.5 * (avatarScale + 1.0), 2.0)); - //properties.setVisible(false); - - //_localPrivacyShieldID = DependencyManager::get()->addEntityInternal(properties, entity::HostType::LOCAL); - //_bubbleActivateSound = DependencyManager::get()->getSound(PathUtils::resourcesUrl() + "assets/sounds/bubble.wav"); - - //onPrivacyShieldToggled(DependencyManager::get()->getIgnoreRadiusEnabled(), true); -} - -void PrivacyShield::destroyPrivacyShield() { - DependencyManager::get()->deleteEntity(_localPrivacyShieldID); -} - -void PrivacyShield::update(float deltaTime) { - //if (_updateConnected) { - // auto now = usecTimestampNow(); - // auto delay = (now - _privacyShieldTimestamp); - // auto privacyShieldAlpha = 1.0 - (delay / PRIVACY_SHIELD_VISIBLE_DURATION_MS); - // if (privacyShieldAlpha > 0) { - // auto myAvatar = DependencyManager::get()->getMyAvatar(); - // auto avatarScale = myAvatar->getTargetScale(); - // auto avatarSensorToWorldScale = myAvatar->getSensorToWorldScale(); - // auto avatarWorldPosition = myAvatar->getWorldPosition(); - // auto avatarWorldOrientation = myAvatar->getWorldOrientation(); - // EntityItemProperties properties; - // properties.setDimensions(glm::vec3(avatarSensorToWorldScale, 0.75 * avatarSensorToWorldScale, avatarSensorToWorldScale)); - // properties.setRotation(avatarWorldOrientation * Quaternions::Y_180); - // if (delay < PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS) { - // properties.setPosition(glm::vec3(avatarWorldPosition.x, - // (-((PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS - delay) / PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS)) * avatarScale * 2.0 + - // avatarWorldPosition.y + avatarScale * PRIVACY_SHIELD_HEIGHT_SCALE, avatarWorldPosition.z)); - // properties.setModelScale(glm::vec3(2.0, - // ((1 - ((PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS - delay) / PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS)) * - // (0.5 * (avatarScale + 1.0))), 2.0)); - // } else { - // properties.setPosition(glm::vec3(avatarWorldPosition.x, avatarWorldPosition.y + avatarScale * PRIVACY_SHIELD_HEIGHT_SCALE, avatarWorldPosition.z)); - // properties.setModelScale(glm::vec3(2.0, 0.5 * (avatarScale + 1.0), 2.0)); - // } - // DependencyManager::get()->editEntity(_localPrivacyShieldID, properties); - // } - // else { - // hidePrivacyShield(); - // if (_updateConnected) { - // _updateConnected = false; - // } - // } - //} -} - -void PrivacyShield::enteredIgnoreRadius() { - showPrivacyShield(); - DependencyManager::get()->privacyShieldActivated(); -} - -void PrivacyShield::onPrivacyShieldToggled(bool enabled, bool doNotLog) { - if (!doNotLog) { - DependencyManager::get()->privacyShieldToggled(enabled); - } - if (enabled) { - showPrivacyShield(); - } else { - hidePrivacyShield(); - if (_updateConnected) { - _updateConnected = false; - } - } -} - -void PrivacyShield::showPrivacyShield() { - auto now = usecTimestampNow(); - auto myAvatar = DependencyManager::get()->getMyAvatar(); - auto avatarScale = myAvatar->getTargetScale(); - auto avatarSensorToWorldScale = myAvatar->getSensorToWorldScale(); - auto avatarWorldPosition = myAvatar->getWorldPosition(); - auto avatarWorldOrientation = myAvatar->getWorldOrientation(); - if (now - _lastPrivacyShieldSoundTimestamp >= PRIVACY_SHIELD_SOUND_RATE_LIMIT_MS) { - AudioInjectorOptions options; - options.position = avatarWorldPosition; - options.localOnly = true; - options.volume = 0.2f; - AudioInjector::playSoundAndDelete(_bubbleActivateSound, options); - _lastPrivacyShieldSoundTimestamp = now; - } - hidePrivacyShield(); - if (_updateConnected) { - _updateConnected = false; - } - - EntityItemProperties properties; - properties.setDimensions(glm::vec3(avatarSensorToWorldScale, 0.75 * avatarSensorToWorldScale, avatarSensorToWorldScale)); - properties.setPosition(glm::vec3(avatarWorldPosition.x, - -avatarScale * 2 + avatarWorldPosition.y + avatarScale * PRIVACY_SHIELD_HEIGHT_SCALE, avatarWorldPosition.z)); - properties.setModelScale(glm::vec3(2.0, 0.5 * (avatarScale + 1.0), 2.0)); - properties.setVisible(true); - - DependencyManager::get()->editEntity(_localPrivacyShieldID, properties); - - _privacyShieldTimestamp = now; - _updateConnected = true; -} - -void PrivacyShield::hidePrivacyShield() { - EntityTreePointer entityTree = qApp->getEntities()->getTree(); - EntityItemPointer privacyShieldEntity = entityTree->findEntityByEntityItemID(EntityItemID(_localPrivacyShieldID)); - if (privacyShieldEntity) { - privacyShieldEntity->setVisible(false); - } -} diff --git a/interface/src/ui/PrivacyShield.h b/interface/src/ui/PrivacyShield.h deleted file mode 100644 index 5aecb661f7..0000000000 --- a/interface/src/ui/PrivacyShield.h +++ /dev/null @@ -1,47 +0,0 @@ -// -// PrivacyShield.h -// interface/src/ui -// -// Created by Wayne Chen on 2/27/19. -// Copyright 2019 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 -// - -#pragma once - -#include -#include -#include - -#include -#include - -class PrivacyShield : public QObject, public Dependency { - Q_OBJECT - SINGLETON_DEPENDENCY - -public: - PrivacyShield(); - void createPrivacyShield(); - void destroyPrivacyShield(); - - bool isVisible() const { return _visible; } - void update(float deltaTime); - -protected slots: - void enteredIgnoreRadius(); - void onPrivacyShieldToggled(bool enabled, bool doNotLog = false); - -private: - void showPrivacyShield(); - void hidePrivacyShield(); - - SharedSoundPointer _bubbleActivateSound; - QUuid _localPrivacyShieldID; - quint64 _privacyShieldTimestamp; - quint64 _lastPrivacyShieldSoundTimestamp; - bool _visible { false }; - bool _updateConnected { false }; -}; diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index e392680df9..bd7e79dffc 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -32,8 +32,7 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/firstPersonHMD.js", "system/tablet-ui/tabletUI.js", "system/emote.js", - "system/miniTablet.js", - "system/audioMuteOverlay.js" + "system/miniTablet.js" ]; var DEFAULT_SCRIPTS_SEPARATE = [ "system/controllers/controllerScripts.js", From d834c5aca1c7f7d98ff895f0e7356d50a876d998 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Thu, 14 Mar 2019 15:05:26 -0700 Subject: [PATCH 291/446] adding test script for creating avatar input bar --- scripts/defaultScripts.js | 3 +- scripts/system/createAvatarInputsBarEntity.js | 76 +++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 scripts/system/createAvatarInputsBarEntity.js diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index bd7e79dffc..0d9799a035 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -32,7 +32,8 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/firstPersonHMD.js", "system/tablet-ui/tabletUI.js", "system/emote.js", - "system/miniTablet.js" + "system/miniTablet.js", + "system/createAvatarInputsBarEntity.js" ]; var DEFAULT_SCRIPTS_SEPARATE = [ "system/controllers/controllerScripts.js", diff --git a/scripts/system/createAvatarInputsBarEntity.js b/scripts/system/createAvatarInputsBarEntity.js new file mode 100644 index 0000000000..babf519035 --- /dev/null +++ b/scripts/system/createAvatarInputsBarEntity.js @@ -0,0 +1,76 @@ +"use strict"; + +(function(){ + + var button; + var buttonName = "AVBAR"; + var onCreateAvatarInputsBarEntity = false; + var micBarEntity, bubbleIconEntity; + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + + function onClicked(){ + onCreateAvatarInputsBarEntity = !onCreateAvatarInputsBarEntity; + button.editProperties({isActive: onCreateAvatarInputsBarEntity}); + var micBarDimensions = {x: 0.036, y: 0.048, z: 0.3}; + var bubbleIconDimensions = {x: 0.036, y: 0.036, z: 0.3}; + var micBarLocalPosition = {x: (-(micBarDimensions.x / 2)) - 0.2, y: -0.125, z: -0.5}; + var bubbleIconLocalPosition = {x: (micBarDimensions.x * 1.2 / 2) - 0.2, y: ((micBarDimensions.y - bubbleIconDimensions.y) / 2 - 0.125), z: -0.5}; + if (onCreateAvatarInputsBarEntity) { + var props = { + type: "Web", + parentID: MyAvatar.SELF_ID, + parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX"), + localPosition: micBarLocalPosition, + localRotation: Quat.lookAtSimple(Camera.orientation, micBarLocalPosition), + sourceUrl: Script.resourcesPath() + "qml/hifi/audio/MicBarApplication.qml", + dimensions: micBarDimensions, + userData: { + grabbable: false + }, + }; + micBarEntity = Entities.addEntity(props, "local"); + var props = { + type: "Web", + parentID: MyAvatar.SELF_ID, + parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX"), + // y is 0.01 - (0.048 + 0.036) / 2 + // have 10% spacing separation between the entities + localPosition: bubbleIconLocalPosition, + localRotation: Quat.lookAtSimple(Camera.orientation, bubbleIconLocalPosition), + sourceUrl: Script.resourcesPath() + "qml/BubbleIcon.qml", + dimensions: bubbleIconDimensions, + userData: { + grabbable: false + }, + }; + bubbleIconEntity = Entities.addEntity(props, "local"); + console.log("creating entity"); + } else { + console.log("deleting entity"); + Entities.deleteEntity(micBarEntity); + Entities.deleteEntity(bubbleIconEntity); + } + }; + + function setup() { + button = tablet.addButton({ + icon: "icons/tablet-icons/edit-i.svg", + activeIcon: "icons/tablet-icons/edit-a.svg", + text: buttonName + }); + button.clicked.connect(onClicked); + }; + + setup(); + + Script.scriptEnding.connect(function() { + if (micBarEntity) { + Entities.deleteEntity(micBarEntity); + } + if (bubbleIconEntity) { + Entities.deleteEntity(bubbleIconEntity); + } + tablet.removeButton(button); + }); + +}()); From d063882d3747cb42d665f6b9a4d479dd5a3420a4 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Tue, 26 Feb 2019 18:14:56 -0800 Subject: [PATCH 292/446] adding user speaking level rotated vertically and muted symbol --- interface/resources/qml/hifi/audio/MicBar.qml | 53 ++++++++++--------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index fba06ac987..6d428d9ab8 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -13,6 +13,7 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 import stylesUit 1.0 +import stylesUit 1.0 import TabletScriptingInterface 1.0 Rectangle { @@ -34,8 +35,8 @@ Rectangle { property bool standalone: false; property var dragTarget: null; - width: 240; - height: 50; + width: 44; + height: 44; radius: 5; @@ -48,8 +49,8 @@ Rectangle { // borders are painted over fill, so reduce the fill to fit inside the border Rectangle { color: standalone ? colors.fill : "#00000000"; - width: 236; - height: 46; + width: 40; + height: 40; radius: 5; @@ -98,7 +99,7 @@ Rectangle { readonly property string red: colors.muted; readonly property string fill: "#55000000"; readonly property string border: standalone ? "#80FFFFFF" : "#55FFFFFF"; - readonly property string icon: AudioScriptingInterface.muted ? muted : unmuted; + readonly property string icon: muted ? muted : unmuted; } Item { @@ -106,7 +107,6 @@ Rectangle { anchors { left: parent.left; - leftMargin: 5; verticalCenter: parent.verticalCenter; } @@ -125,11 +125,11 @@ Rectangle { source: (pushToTalk && !pushingToTalk) ? pushToTalkIcon : muted ? mutedIcon : clipping ? clippingIcon : gated ? gatedIcon : unmutedIcon; - width: 30; - height: 30; + width: 21; + height: 24; anchors { left: parent.left; - leftMargin: 5; + leftMargin: 7; top: parent.top; topMargin: 5; } @@ -146,20 +146,20 @@ Rectangle { Item { id: status; - readonly property string color: muted ? colors.muted : colors.unmuted; + readonly property string color: colors.muted; visible: (pushToTalk && !pushingToTalk) || (muted && (level >= userSpeakingLevel)); anchors { left: parent.left; - leftMargin: 50; - verticalCenter: parent.verticalCenter; + top: parent.bottom + topMargin: 5 } - width: 170; + width: icon.width; height: 8 - Text { + RalewaySemiBold { anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter; @@ -197,11 +197,14 @@ Rectangle { Item { id: bar; - visible: !status.visible; + anchors { + right: parent.right; + rightMargin: 7; + verticalCenter: parent.verticalCenter; + } - anchors.fill: status; - - width: status.width; + width: 8; + height: 32; Rectangle { // base radius: 4; @@ -211,13 +214,12 @@ Rectangle { Rectangle { // mask id: mask; - width: gated ? 0 : parent.width * level; + height: parent.height * level; + width: parent.width; radius: 5; anchors { bottom: parent.bottom; bottomMargin: 0; - top: parent.top; - topMargin: 0; left: parent.left; leftMargin: 0; } @@ -227,10 +229,11 @@ Rectangle { anchors { fill: mask } source: mask start: Qt.point(0, 0); - end: Qt.point(170, 0); + end: Qt.point(0, bar.height); + rotation: 180 gradient: Gradient { GradientStop { - position: 0; + position: 0.0; color: colors.greenStart; } GradientStop { @@ -238,8 +241,8 @@ Rectangle { color: colors.greenEnd; } GradientStop { - position: 1; - color: colors.yellow; + position: 1.0; + color: colors.red; } } } From 8e8ceaac475e6affd5ca199b035be171db28b8d6 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Tue, 26 Feb 2019 18:19:23 -0800 Subject: [PATCH 293/446] moving file as MicBarApplication --- interface/resources/qml/hifi/audio/MicBar.qml | 65 +++++++++---------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index 6d428d9ab8..cad64b9b74 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -13,7 +13,6 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 import stylesUit 1.0 -import stylesUit 1.0 import TabletScriptingInterface 1.0 Rectangle { @@ -31,12 +30,12 @@ Rectangle { AudioScriptingInterface.noiseGateOpened.connect(function() { gated = false; }); AudioScriptingInterface.noiseGateClosed.connect(function() { gated = true; }); } - + property bool standalone: false; property var dragTarget: null; - width: 44; - height: 44; + width: 240; + height: 50; radius: 5; @@ -49,8 +48,8 @@ Rectangle { // borders are painted over fill, so reduce the fill to fit inside the border Rectangle { color: standalone ? colors.fill : "#00000000"; - width: 40; - height: 40; + width: 236; + height: 46; radius: 5; @@ -107,6 +106,7 @@ Rectangle { anchors { left: parent.left; + leftMargin: 5; verticalCenter: parent.verticalCenter; } @@ -125,11 +125,11 @@ Rectangle { source: (pushToTalk && !pushingToTalk) ? pushToTalkIcon : muted ? mutedIcon : clipping ? clippingIcon : gated ? gatedIcon : unmutedIcon; - width: 21; - height: 24; + width: 30; + height: 30; anchors { left: parent.left; - leftMargin: 7; + leftMargin: 5; top: parent.top; topMargin: 5; } @@ -146,20 +146,20 @@ Rectangle { Item { id: status; - readonly property string color: colors.muted; + readonly property string color: AudioScriptingInterface.muted ? colors.muted : colors.unmuted; visible: (pushToTalk && !pushingToTalk) || (muted && (level >= userSpeakingLevel)); anchors { left: parent.left; - top: parent.bottom - topMargin: 5 + leftMargin: 50; + verticalCenter: parent.verticalCenter; } - width: icon.width; + width: 170; height: 8 - RalewaySemiBold { + Text { anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter; @@ -197,14 +197,11 @@ Rectangle { Item { id: bar; - anchors { - right: parent.right; - rightMargin: 7; - verticalCenter: parent.verticalCenter; - } + visible: !status.visible; - width: 8; - height: 32; + anchors.fill: status; + + width: status.width; Rectangle { // base radius: 4; @@ -214,12 +211,13 @@ Rectangle { Rectangle { // mask id: mask; - height: parent.height * level; - width: parent.width; + width: gated ? 0 : parent.width * level; radius: 5; anchors { bottom: parent.bottom; bottomMargin: 0; + top: parent.top; + topMargin: 0; left: parent.left; leftMargin: 0; } @@ -229,11 +227,10 @@ Rectangle { anchors { fill: mask } source: mask start: Qt.point(0, 0); - end: Qt.point(0, bar.height); - rotation: 180 + end: Qt.point(170, 0); gradient: Gradient { GradientStop { - position: 0.0; + position: 0; color: colors.greenStart; } GradientStop { @@ -241,17 +238,17 @@ Rectangle { color: colors.greenEnd; } GradientStop { - position: 1.0; - color: colors.red; + position: 1; + color: colors.yellow; } } } - + Rectangle { id: gatedIndicator; visible: gated && !AudioScriptingInterface.clipping - - radius: 4; + + radius: 4; width: 2 * radius; height: 2 * radius; color: "#0080FF"; @@ -260,12 +257,12 @@ Rectangle { verticalCenter: parent.verticalCenter; } } - + Rectangle { id: clippingIndicator; visible: AudioScriptingInterface.clipping - - radius: 4; + + radius: 4; width: 2 * radius; height: 2 * radius; color: colors.red; From db6bf46ee7deb17c6c0f5988b0f95f90ad84a912 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 27 Feb 2019 17:38:11 -0800 Subject: [PATCH 294/446] adding more opacity, cleanup --- interface/resources/qml/hifi/audio/MicBarApplication.qml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/interface/resources/qml/hifi/audio/MicBarApplication.qml b/interface/resources/qml/hifi/audio/MicBarApplication.qml index bfac278ee4..592843467b 100644 --- a/interface/resources/qml/hifi/audio/MicBarApplication.qml +++ b/interface/resources/qml/hifi/audio/MicBarApplication.qml @@ -53,6 +53,14 @@ Rectangle { micBar.opacity = rectOpacity; } + onLevelChanged: { + var rectOpacity = AudioScriptingInterface.muted && (level >= userSpeakingLevel)? 0.9 : 0.3; + if (mouseArea.containsMouse) { + rectOpacity = 0.5; + } + opacity = rectOpacity; + } + color: "#00000000"; border { width: mouseArea.containsMouse || mouseArea.containsPress ? 2 : 0; From 3b6c0b5b18219a46c7c9baab95ccb3cf65497d46 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 27 Feb 2019 18:04:09 -0800 Subject: [PATCH 295/446] adding privacy shield files --- interface/src/ui/PrivacyShield.cpp | 12 ++++++++++++ interface/src/ui/PrivacyShield.h | 12 ++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 interface/src/ui/PrivacyShield.cpp create mode 100644 interface/src/ui/PrivacyShield.h diff --git a/interface/src/ui/PrivacyShield.cpp b/interface/src/ui/PrivacyShield.cpp new file mode 100644 index 0000000000..e1035ee5bb --- /dev/null +++ b/interface/src/ui/PrivacyShield.cpp @@ -0,0 +1,12 @@ +// +// PrivacyShield.h +// interface/src/ui +// +// Created by Wayne Chen on 2/27/19. +// Copyright 2019 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 +// + +#include "PrivacyShield.cpp" diff --git a/interface/src/ui/PrivacyShield.h b/interface/src/ui/PrivacyShield.h new file mode 100644 index 0000000000..a609f2775b --- /dev/null +++ b/interface/src/ui/PrivacyShield.h @@ -0,0 +1,12 @@ +// +// PrivacyShield.h +// interface/src/ui +// +// Created by Wayne Chen on 2/27/19. +// Copyright 2019 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 +// + +#pragma once From 49c3dfa52c67ede93e7cc30f03a23022a55e00f5 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 27 Feb 2019 18:04:58 -0800 Subject: [PATCH 296/446] fixing typos --- interface/src/ui/PrivacyShield.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/ui/PrivacyShield.cpp b/interface/src/ui/PrivacyShield.cpp index e1035ee5bb..12687afbea 100644 --- a/interface/src/ui/PrivacyShield.cpp +++ b/interface/src/ui/PrivacyShield.cpp @@ -1,5 +1,5 @@ // -// PrivacyShield.h +// PrivacyShield.cpp // interface/src/ui // // Created by Wayne Chen on 2/27/19. @@ -9,4 +9,4 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "PrivacyShield.cpp" +#include "PrivacyShield.h" From 48b4fe37b4ccb53ac35de6028377aa199bb23b65 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Thu, 28 Feb 2019 15:50:13 -0800 Subject: [PATCH 297/446] don't display gated state --- interface/resources/qml/hifi/audio/MicBarApplication.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/audio/MicBarApplication.qml b/interface/resources/qml/hifi/audio/MicBarApplication.qml index 592843467b..812e581fe1 100644 --- a/interface/resources/qml/hifi/audio/MicBarApplication.qml +++ b/interface/resources/qml/hifi/audio/MicBarApplication.qml @@ -54,8 +54,8 @@ Rectangle { } onLevelChanged: { - var rectOpacity = AudioScriptingInterface.muted && (level >= userSpeakingLevel)? 0.9 : 0.3; - if (mouseArea.containsMouse) { + var rectOpacity = muted && (level >= userSpeakingLevel) ? 0.9 : 0.3; + if (mouseArea.containsMouse && rectOpacity != 0.9) { rectOpacity = 0.5; } opacity = rectOpacity; From 20e181cd82f9b6d180617dad1ac8c05de7fca98f Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Thu, 28 Feb 2019 16:51:19 -0800 Subject: [PATCH 298/446] staging avatar inputs for ignore radius --- interface/src/Application.cpp | 10 ++ interface/src/ui/PrivacyShield.cpp | 147 +++++++++++++++++++++++++++++ interface/src/ui/PrivacyShield.h | 35 +++++++ 3 files changed, 192 insertions(+) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 9fd0781eef..bd8107793a 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -211,6 +211,7 @@ #include "ui/UpdateDialog.h" #include "ui/DomainConnectionModel.h" #include "ui/Keyboard.h" +#include "ui/PrivacyShield.h" #include "Util.h" #include "InterfaceParentFinder.h" #include "ui/OctreeStatsProvider.h" @@ -931,6 +932,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); return previousSessionCrashed; } @@ -2670,6 +2672,9 @@ void Application::cleanupBeforeQuit() { nodeList->getPacketReceiver().setShouldDropPackets(true); } + // destroy privacy shield before entity shutdown. + DependencyManager::get()->destroyPrivacyShield(); + getEntities()->shutdown(); // tell the entities system we're shutting down, so it will stop running scripts // Clear any queued processing (I/O, FBX/OBJ/Texture parsing) @@ -2748,6 +2753,7 @@ void Application::cleanupBeforeQuit() { DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); + DependencyManager::destroy(); DependencyManager::destroy(); qCDebug(interfaceapp) << "Application::cleanupBeforeQuit() complete"; @@ -5549,6 +5555,8 @@ void Application::resumeAfterLoginDialogActionTaken() { menu->getMenu("Developer")->setVisible(_developerMenuVisible); _myCamera.setMode(_previousCameraMode); cameraModeChanged(); + + DependencyManager::get()->createPrivacyShield(); } void Application::loadAvatarScripts(const QVector& urls) { @@ -6507,6 +6515,8 @@ void Application::update(float deltaTime) { updateLoginDialogPosition(); } + DependencyManager::get()->update(deltaTime); + { PROFILE_RANGE_EX(app, "Overlays", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount()); PerformanceTimer perfTimer("overlays"); diff --git a/interface/src/ui/PrivacyShield.cpp b/interface/src/ui/PrivacyShield.cpp index 12687afbea..e8f61ff5bf 100644 --- a/interface/src/ui/PrivacyShield.cpp +++ b/interface/src/ui/PrivacyShield.cpp @@ -10,3 +10,150 @@ // #include "PrivacyShield.h" + +#include +#include +#include +#include +#include +#include + +#include "Application.h" +#include "PathUtils.h" +#include "GLMHelpers.h" + +const int PRIVACY_SHIELD_VISIBLE_DURATION_MS = 3000; +const int PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS = 750; +const int PRIVACY_SHIELD_SOUND_RATE_LIMIT_MS = 15000; +const float PRIVACY_SHIELD_HEIGHT_SCALE = 0.15f; + +PrivacyShield::PrivacyShield() { + auto usersScriptingInterface = DependencyManager::get(); + //connect(usersScriptingInterface.data(), &UsersScriptingInterface::ignoreRadiusEnabledChanged, [this](bool enabled) { + // onPrivacyShieldToggled(enabled); + //}); + //connect(usersScriptingInterface.data(), &UsersScriptingInterface::enteredIgnoreRadius, this, &PrivacyShield::enteredIgnoreRadius); +} + +void PrivacyShield::createPrivacyShield() { + // Affects bubble height + auto myAvatar = DependencyManager::get()->getMyAvatar(); + auto avatarScale = myAvatar->getTargetScale(); + auto avatarSensorToWorldScale = myAvatar->getSensorToWorldScale(); + auto avatarWorldPosition = myAvatar->getWorldPosition(); + auto avatarWorldOrientation = myAvatar->getWorldOrientation(); + EntityItemProperties properties; + properties.setName("Privacy-Shield"); + properties.setModelURL(PathUtils::resourcesUrl("assets/models/Bubble-v14.fbx").toString()); + properties.setDimensions(glm::vec3(avatarSensorToWorldScale, 0.75 * avatarSensorToWorldScale, avatarSensorToWorldScale)); + properties.setPosition(glm::vec3(avatarWorldPosition.x, + -avatarScale * 2 + avatarWorldPosition.y + avatarScale * PRIVACY_SHIELD_HEIGHT_SCALE, avatarWorldPosition.z)); + properties.setRotation(avatarWorldOrientation * Quaternions::Y_180); + properties.setModelScale(glm::vec3(2.0, 0.5 * (avatarScale + 1.0), 2.0)); + properties.setVisible(false); + + _localPrivacyShieldID = DependencyManager::get()->addEntityInternal(properties, entity::HostType::LOCAL); + //_bubbleActivateSound = DependencyManager::get()->getSound(PathUtils::resourcesUrl() + "assets/sounds/bubble.wav"); + + //onPrivacyShieldToggled(DependencyManager::get()->getIgnoreRadiusEnabled(), true); +} + +void PrivacyShield::destroyPrivacyShield() { + DependencyManager::get()->deleteEntity(_localPrivacyShieldID); +} + +void PrivacyShield::update(float deltaTime) { + if (_updateConnected) { + auto now = usecTimestampNow(); + auto delay = (now - _privacyShieldTimestamp); + auto privacyShieldAlpha = 1.0 - (delay / PRIVACY_SHIELD_VISIBLE_DURATION_MS); + if (privacyShieldAlpha > 0) { + auto myAvatar = DependencyManager::get()->getMyAvatar(); + auto avatarScale = myAvatar->getTargetScale(); + auto avatarSensorToWorldScale = myAvatar->getSensorToWorldScale(); + auto avatarWorldPosition = myAvatar->getWorldPosition(); + auto avatarWorldOrientation = myAvatar->getWorldOrientation(); + EntityItemProperties properties; + properties.setDimensions(glm::vec3(avatarSensorToWorldScale, 0.75 * avatarSensorToWorldScale, avatarSensorToWorldScale)); + properties.setRotation(avatarWorldOrientation * Quaternions::Y_180); + if (delay < PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS) { + properties.setPosition(glm::vec3(avatarWorldPosition.x, + (-((PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS - delay) / PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS)) * avatarScale * 2.0 + + avatarWorldPosition.y + avatarScale * PRIVACY_SHIELD_HEIGHT_SCALE, avatarWorldPosition.z)); + properties.setModelScale(glm::vec3(2.0, + ((1 - ((PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS - delay) / PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS)) * + (0.5 * (avatarScale + 1.0))), 2.0)); + } else { + properties.setPosition(glm::vec3(avatarWorldPosition.x, avatarWorldPosition.y + avatarScale * PRIVACY_SHIELD_HEIGHT_SCALE, avatarWorldPosition.z)); + properties.setModelScale(glm::vec3(2.0, 0.5 * (avatarScale + 1.0), 2.0)); + } + DependencyManager::get()->editEntity(_localPrivacyShieldID, properties); + } + else { + hidePrivacyShield(); + if (_updateConnected) { + _updateConnected = false; + } + } + } +} + +void PrivacyShield::enteredIgnoreRadius() { + showPrivacyShield(); + DependencyManager::get()->privacyShieldActivated(); +} + +void PrivacyShield::onPrivacyShieldToggled(bool enabled, bool doNotLog) { + if (!doNotLog) { + DependencyManager::get()->privacyShieldToggled(enabled); + } + if (enabled) { + showPrivacyShield(); + } else { + hidePrivacyShield(); + if (_updateConnected) { + _updateConnected = false; + } + } +} + +void PrivacyShield::showPrivacyShield() { + auto now = usecTimestampNow(); + auto myAvatar = DependencyManager::get()->getMyAvatar(); + auto avatarScale = myAvatar->getTargetScale(); + auto avatarSensorToWorldScale = myAvatar->getSensorToWorldScale(); + auto avatarWorldPosition = myAvatar->getWorldPosition(); + auto avatarWorldOrientation = myAvatar->getWorldOrientation(); + if (now - _lastPrivacyShieldSoundTimestamp >= PRIVACY_SHIELD_SOUND_RATE_LIMIT_MS) { + AudioInjectorOptions options; + options.position = avatarWorldPosition; + options.localOnly = true; + options.volume = 0.2f; + AudioInjector::playSoundAndDelete(_bubbleActivateSound, options); + _lastPrivacyShieldSoundTimestamp = now; + } + hidePrivacyShield(); + if (_updateConnected) { + _updateConnected = false; + } + + EntityItemProperties properties; + properties.setDimensions(glm::vec3(avatarSensorToWorldScale, 0.75 * avatarSensorToWorldScale, avatarSensorToWorldScale)); + properties.setPosition(glm::vec3(avatarWorldPosition.x, + -avatarScale * 2 + avatarWorldPosition.y + avatarScale * PRIVACY_SHIELD_HEIGHT_SCALE, avatarWorldPosition.z)); + properties.setModelScale(glm::vec3(2.0, 0.5 * (avatarScale + 1.0), 2.0)); + properties.setVisible(true); + + DependencyManager::get()->editEntity(_localPrivacyShieldID, properties); + + _privacyShieldTimestamp = now; + _updateConnected = true; +} + +void PrivacyShield::hidePrivacyShield() { + EntityTreePointer entityTree = qApp->getEntities()->getTree(); + EntityItemPointer privacyShieldEntity = entityTree->findEntityByEntityItemID(EntityItemID(_localPrivacyShieldID)); + if (privacyShieldEntity) { + privacyShieldEntity->setVisible(false); + } +} diff --git a/interface/src/ui/PrivacyShield.h b/interface/src/ui/PrivacyShield.h index a609f2775b..5aecb661f7 100644 --- a/interface/src/ui/PrivacyShield.h +++ b/interface/src/ui/PrivacyShield.h @@ -10,3 +10,38 @@ // #pragma once + +#include +#include +#include + +#include +#include + +class PrivacyShield : public QObject, public Dependency { + Q_OBJECT + SINGLETON_DEPENDENCY + +public: + PrivacyShield(); + void createPrivacyShield(); + void destroyPrivacyShield(); + + bool isVisible() const { return _visible; } + void update(float deltaTime); + +protected slots: + void enteredIgnoreRadius(); + void onPrivacyShieldToggled(bool enabled, bool doNotLog = false); + +private: + void showPrivacyShield(); + void hidePrivacyShield(); + + SharedSoundPointer _bubbleActivateSound; + QUuid _localPrivacyShieldID; + quint64 _privacyShieldTimestamp; + quint64 _lastPrivacyShieldSoundTimestamp; + bool _visible { false }; + bool _updateConnected { false }; +}; From 84fcd50b3b3e0b3231b6fe80cc56b19af8895515 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Tue, 26 Feb 2019 18:14:56 -0800 Subject: [PATCH 299/446] adding user speaking level rotated vertically and muted symbol --- interface/resources/qml/hifi/audio/MicBar.qml | 51 ++++++++++--------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index cad64b9b74..4734da8771 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -13,6 +13,7 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 import stylesUit 1.0 +import stylesUit 1.0 import TabletScriptingInterface 1.0 Rectangle { @@ -34,8 +35,8 @@ Rectangle { property bool standalone: false; property var dragTarget: null; - width: 240; - height: 50; + width: 44; + height: 44; radius: 5; @@ -48,8 +49,8 @@ Rectangle { // borders are painted over fill, so reduce the fill to fit inside the border Rectangle { color: standalone ? colors.fill : "#00000000"; - width: 236; - height: 46; + width: 40; + height: 40; radius: 5; @@ -106,7 +107,6 @@ Rectangle { anchors { left: parent.left; - leftMargin: 5; verticalCenter: parent.verticalCenter; } @@ -125,11 +125,11 @@ Rectangle { source: (pushToTalk && !pushingToTalk) ? pushToTalkIcon : muted ? mutedIcon : clipping ? clippingIcon : gated ? gatedIcon : unmutedIcon; - width: 30; - height: 30; + width: 21; + height: 24; anchors { left: parent.left; - leftMargin: 5; + leftMargin: 7; top: parent.top; topMargin: 5; } @@ -146,20 +146,20 @@ Rectangle { Item { id: status; - readonly property string color: AudioScriptingInterface.muted ? colors.muted : colors.unmuted; + readonly property string color: colors.muted; visible: (pushToTalk && !pushingToTalk) || (muted && (level >= userSpeakingLevel)); anchors { left: parent.left; - leftMargin: 50; - verticalCenter: parent.verticalCenter; + top: parent.bottom + topMargin: 5 } - width: 170; + width: icon.width; height: 8 - Text { + RalewaySemiBold { anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter; @@ -197,11 +197,14 @@ Rectangle { Item { id: bar; - visible: !status.visible; + anchors { + right: parent.right; + rightMargin: 7; + verticalCenter: parent.verticalCenter; + } - anchors.fill: status; - - width: status.width; + width: 8; + height: 32; Rectangle { // base radius: 4; @@ -211,13 +214,12 @@ Rectangle { Rectangle { // mask id: mask; - width: gated ? 0 : parent.width * level; + height: parent.height * level; + width: parent.width; radius: 5; anchors { bottom: parent.bottom; bottomMargin: 0; - top: parent.top; - topMargin: 0; left: parent.left; leftMargin: 0; } @@ -227,10 +229,11 @@ Rectangle { anchors { fill: mask } source: mask start: Qt.point(0, 0); - end: Qt.point(170, 0); + end: Qt.point(0, bar.height); + rotation: 180 gradient: Gradient { GradientStop { - position: 0; + position: 0.0; color: colors.greenStart; } GradientStop { @@ -238,8 +241,8 @@ Rectangle { color: colors.greenEnd; } GradientStop { - position: 1; - color: colors.yellow; + position: 1.0; + color: colors.red; } } } From cd7b6e7ed08a6ac6d8c02a582ecb69c25b5c2e18 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Tue, 26 Feb 2019 18:19:23 -0800 Subject: [PATCH 300/446] moving file as MicBarApplication --- interface/resources/qml/hifi/audio/MicBar.qml | 51 +++++++++---------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index 4734da8771..cad64b9b74 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -13,7 +13,6 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 import stylesUit 1.0 -import stylesUit 1.0 import TabletScriptingInterface 1.0 Rectangle { @@ -35,8 +34,8 @@ Rectangle { property bool standalone: false; property var dragTarget: null; - width: 44; - height: 44; + width: 240; + height: 50; radius: 5; @@ -49,8 +48,8 @@ Rectangle { // borders are painted over fill, so reduce the fill to fit inside the border Rectangle { color: standalone ? colors.fill : "#00000000"; - width: 40; - height: 40; + width: 236; + height: 46; radius: 5; @@ -107,6 +106,7 @@ Rectangle { anchors { left: parent.left; + leftMargin: 5; verticalCenter: parent.verticalCenter; } @@ -125,11 +125,11 @@ Rectangle { source: (pushToTalk && !pushingToTalk) ? pushToTalkIcon : muted ? mutedIcon : clipping ? clippingIcon : gated ? gatedIcon : unmutedIcon; - width: 21; - height: 24; + width: 30; + height: 30; anchors { left: parent.left; - leftMargin: 7; + leftMargin: 5; top: parent.top; topMargin: 5; } @@ -146,20 +146,20 @@ Rectangle { Item { id: status; - readonly property string color: colors.muted; + readonly property string color: AudioScriptingInterface.muted ? colors.muted : colors.unmuted; visible: (pushToTalk && !pushingToTalk) || (muted && (level >= userSpeakingLevel)); anchors { left: parent.left; - top: parent.bottom - topMargin: 5 + leftMargin: 50; + verticalCenter: parent.verticalCenter; } - width: icon.width; + width: 170; height: 8 - RalewaySemiBold { + Text { anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter; @@ -197,14 +197,11 @@ Rectangle { Item { id: bar; - anchors { - right: parent.right; - rightMargin: 7; - verticalCenter: parent.verticalCenter; - } + visible: !status.visible; - width: 8; - height: 32; + anchors.fill: status; + + width: status.width; Rectangle { // base radius: 4; @@ -214,12 +211,13 @@ Rectangle { Rectangle { // mask id: mask; - height: parent.height * level; - width: parent.width; + width: gated ? 0 : parent.width * level; radius: 5; anchors { bottom: parent.bottom; bottomMargin: 0; + top: parent.top; + topMargin: 0; left: parent.left; leftMargin: 0; } @@ -229,11 +227,10 @@ Rectangle { anchors { fill: mask } source: mask start: Qt.point(0, 0); - end: Qt.point(0, bar.height); - rotation: 180 + end: Qt.point(170, 0); gradient: Gradient { GradientStop { - position: 0.0; + position: 0; color: colors.greenStart; } GradientStop { @@ -241,8 +238,8 @@ Rectangle { color: colors.greenEnd; } GradientStop { - position: 1.0; - color: colors.red; + position: 1; + color: colors.yellow; } } } From f80f4b2fd4db894e2b0f07d48a8ba9b98bdd2d0f Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 27 Feb 2019 14:54:13 -0800 Subject: [PATCH 301/446] adding working bubble icon --- interface/src/ui/AvatarInputs.cpp | 2 -- interface/src/ui/AvatarInputs.h | 23 ----------------------- 2 files changed, 25 deletions(-) diff --git a/interface/src/ui/AvatarInputs.cpp b/interface/src/ui/AvatarInputs.cpp index 80604f354b..352db37a78 100644 --- a/interface/src/ui/AvatarInputs.cpp +++ b/interface/src/ui/AvatarInputs.cpp @@ -32,9 +32,7 @@ AvatarInputs* AvatarInputs::getInstance() { AvatarInputs::AvatarInputs(QObject* parent) : QObject(parent) { _showAudioTools = showAudioToolsSetting.get(); auto nodeList = DependencyManager::get(); - auto usersScriptingInterface = DependencyManager::get(); connect(nodeList.data(), &NodeList::ignoreRadiusEnabledChanged, this, &AvatarInputs::ignoreRadiusEnabledChanged); - connect(usersScriptingInterface.data(), &UsersScriptingInterface::enteredIgnoreRadius, this, &AvatarInputs::enteredIgnoreRadiusChanged); } #define AI_UPDATE(name, src) \ diff --git a/interface/src/ui/AvatarInputs.h b/interface/src/ui/AvatarInputs.h index f53adc1749..1feb054147 100644 --- a/interface/src/ui/AvatarInputs.h +++ b/interface/src/ui/AvatarInputs.h @@ -43,7 +43,6 @@ class AvatarInputs : public QObject { Q_PROPERTY(bool showAudioTools READ showAudioTools WRITE setShowAudioTools NOTIFY showAudioToolsChanged) Q_PROPERTY(bool ignoreRadiusEnabled READ getIgnoreRadiusEnabled NOTIFY ignoreRadiusEnabledChanged) - //Q_PROPERTY(bool enteredIgnoreRadius READ getEnteredIgnoreRadius NOTIFY enteredIgnoreRadiusChanged) public: static AvatarInputs* getInstance(); @@ -59,7 +58,6 @@ public: void update(); bool showAudioTools() const { return _showAudioTools; } bool getIgnoreRadiusEnabled() const; - //bool getEnteredIgnoreRadius() const; public slots: @@ -97,20 +95,6 @@ signals: */ void showAudioToolsChanged(bool show); - /**jsdoc - * @function AvatarInputs.avatarEnteredIgnoreRadius - * @param {QUuid} avatarID - * @returns {Signal} - */ - void avatarEnteredIgnoreRadius(QUuid avatarID); - - /**jsdoc - * @function AvatarInputs.avatarLeftIgnoreRadius - * @param {QUuid} avatarID - * @returns {Signal} - */ - void avatarLeftIgnoreRadius(QUuid avatarID); - /**jsdoc * @function AvatarInputs.ignoreRadiusEnabledChanged * @param {boolean} enabled @@ -118,13 +102,6 @@ signals: */ void ignoreRadiusEnabledChanged(bool enabled); - /**jsdoc - * @function AvatarInputs.enteredIgnoreRadiusChanged - * @param {boolean} enabled - * @returns {Signal} - */ - void enteredIgnoreRadiusChanged(); - protected: /**jsdoc From e5133d6f4d3fbccba641e088a2be5e03fd91ef43 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Thu, 28 Feb 2019 16:48:17 -0800 Subject: [PATCH 302/446] fixing muted text --- interface/resources/icons/tablet-icons/mic.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/icons/tablet-icons/mic.svg b/interface/resources/icons/tablet-icons/mic.svg index 30b46d18dd..c961351b14 100644 --- a/interface/resources/icons/tablet-icons/mic.svg +++ b/interface/resources/icons/tablet-icons/mic.svg @@ -59,4 +59,4 @@ d="m 27.9,20.9 c 0,0 0,-3.6 0,-3.8 0,-0.7 -0.6,-1.2 -1.3,-1.2 -0.7,0 -1.2,0.6 -1.2,1.3 0,0.2 0,3.4 0,3.7 0,2.6 -2.4,4.8 -5.3,4.8 -2.9,0 -5.3,-2.1 -5.3,-4.8 0,-0.3 0,-3.5 0,-3.8 0,-0.7 -0.5,-1.3 -1.2,-1.3 -0.7,0 -1.3,0.5 -1.3,1.2 0,0.2 0,3.9 0,3.9 0,3.6 2.9,6.6 6.6,7.2 l 0,2.4 -3.1,0 c -0.7,0 -1.3,0.6 -1.3,1.3 0,0.7 0.6,1.3 1.3,1.3 l 8.8,0 c 0.7,0 1.3,-0.6 1.3,-1.3 0,-0.7 -0.6,-1.3 -1.3,-1.3 l -3.2,0 0,-2.4 c 3.6,-0.5 6.5,-3.5 6.5,-7.2 z" id="path6952" inkscape:connector-curvature="0" - style="fill:#ffffff" /> \ No newline at end of file + style="fill:#ffffff" /> From 8e6913dde1ae7b3803a829b041263cf698b1418d Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Thu, 28 Feb 2019 16:51:19 -0800 Subject: [PATCH 303/446] staging avatar inputs for ignore radius --- interface/src/ui/AvatarInputs.cpp | 2 ++ interface/src/ui/AvatarInputs.h | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/interface/src/ui/AvatarInputs.cpp b/interface/src/ui/AvatarInputs.cpp index 352db37a78..80604f354b 100644 --- a/interface/src/ui/AvatarInputs.cpp +++ b/interface/src/ui/AvatarInputs.cpp @@ -32,7 +32,9 @@ AvatarInputs* AvatarInputs::getInstance() { AvatarInputs::AvatarInputs(QObject* parent) : QObject(parent) { _showAudioTools = showAudioToolsSetting.get(); auto nodeList = DependencyManager::get(); + auto usersScriptingInterface = DependencyManager::get(); connect(nodeList.data(), &NodeList::ignoreRadiusEnabledChanged, this, &AvatarInputs::ignoreRadiusEnabledChanged); + connect(usersScriptingInterface.data(), &UsersScriptingInterface::enteredIgnoreRadius, this, &AvatarInputs::enteredIgnoreRadiusChanged); } #define AI_UPDATE(name, src) \ diff --git a/interface/src/ui/AvatarInputs.h b/interface/src/ui/AvatarInputs.h index 1feb054147..f53adc1749 100644 --- a/interface/src/ui/AvatarInputs.h +++ b/interface/src/ui/AvatarInputs.h @@ -43,6 +43,7 @@ class AvatarInputs : public QObject { Q_PROPERTY(bool showAudioTools READ showAudioTools WRITE setShowAudioTools NOTIFY showAudioToolsChanged) Q_PROPERTY(bool ignoreRadiusEnabled READ getIgnoreRadiusEnabled NOTIFY ignoreRadiusEnabledChanged) + //Q_PROPERTY(bool enteredIgnoreRadius READ getEnteredIgnoreRadius NOTIFY enteredIgnoreRadiusChanged) public: static AvatarInputs* getInstance(); @@ -58,6 +59,7 @@ public: void update(); bool showAudioTools() const { return _showAudioTools; } bool getIgnoreRadiusEnabled() const; + //bool getEnteredIgnoreRadius() const; public slots: @@ -95,6 +97,20 @@ signals: */ void showAudioToolsChanged(bool show); + /**jsdoc + * @function AvatarInputs.avatarEnteredIgnoreRadius + * @param {QUuid} avatarID + * @returns {Signal} + */ + void avatarEnteredIgnoreRadius(QUuid avatarID); + + /**jsdoc + * @function AvatarInputs.avatarLeftIgnoreRadius + * @param {QUuid} avatarID + * @returns {Signal} + */ + void avatarLeftIgnoreRadius(QUuid avatarID); + /**jsdoc * @function AvatarInputs.ignoreRadiusEnabledChanged * @param {boolean} enabled @@ -102,6 +118,13 @@ signals: */ void ignoreRadiusEnabledChanged(bool enabled); + /**jsdoc + * @function AvatarInputs.enteredIgnoreRadiusChanged + * @param {boolean} enabled + * @returns {Signal} + */ + void enteredIgnoreRadiusChanged(); + protected: /**jsdoc From d0116abc5271fccc15ae8e6f9371faf92120332c Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Thu, 14 Mar 2019 10:28:18 -0700 Subject: [PATCH 304/446] separating out bubble icon qml - adding support for transparency --- interface/resources/qml/hifi/audio/MicBar.qml | 16 +++---- .../qml/hifi/audio/MicBarApplication.qml | 6 ++- interface/src/Application.cpp | 10 ---- interface/src/ui/PrivacyShield.h | 47 ------------------- scripts/defaultScripts.js | 3 +- 5 files changed, 13 insertions(+), 69 deletions(-) delete mode 100644 interface/src/ui/PrivacyShield.h diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index cad64b9b74..fb52f8bc5e 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -30,7 +30,7 @@ Rectangle { AudioScriptingInterface.noiseGateOpened.connect(function() { gated = false; }); AudioScriptingInterface.noiseGateClosed.connect(function() { gated = true; }); } - + property bool standalone: false; property var dragTarget: null; @@ -146,7 +146,7 @@ Rectangle { Item { id: status; - readonly property string color: AudioScriptingInterface.muted ? colors.muted : colors.unmuted; + readonly property string color: muted ? colors.muted : colors.unmuted; visible: (pushToTalk && !pushingToTalk) || (muted && (level >= userSpeakingLevel)); @@ -243,12 +243,12 @@ Rectangle { } } } - + Rectangle { id: gatedIndicator; visible: gated && !AudioScriptingInterface.clipping - - radius: 4; + + radius: 4; width: 2 * radius; height: 2 * radius; color: "#0080FF"; @@ -257,12 +257,12 @@ Rectangle { verticalCenter: parent.verticalCenter; } } - + Rectangle { id: clippingIndicator; visible: AudioScriptingInterface.clipping - - radius: 4; + + radius: 4; width: 2 * radius; height: 2 * radius; color: colors.red; diff --git a/interface/resources/qml/hifi/audio/MicBarApplication.qml b/interface/resources/qml/hifi/audio/MicBarApplication.qml index 812e581fe1..f2839aee1a 100644 --- a/interface/resources/qml/hifi/audio/MicBarApplication.qml +++ b/interface/resources/qml/hifi/audio/MicBarApplication.qml @@ -55,10 +55,12 @@ Rectangle { onLevelChanged: { var rectOpacity = muted && (level >= userSpeakingLevel) ? 0.9 : 0.3; - if (mouseArea.containsMouse && rectOpacity != 0.9) { + if (pushToTalk && !pushingToTalk) { + rectOpacity = (level >= userSpeakingLevel) ? 0.9 : 0.7; + } else if (mouseArea.containsMouse && rectOpacity != 0.9) { rectOpacity = 0.5; } - opacity = rectOpacity; + micBar.opacity = rectOpacity; } color: "#00000000"; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index bd8107793a..9fd0781eef 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -211,7 +211,6 @@ #include "ui/UpdateDialog.h" #include "ui/DomainConnectionModel.h" #include "ui/Keyboard.h" -#include "ui/PrivacyShield.h" #include "Util.h" #include "InterfaceParentFinder.h" #include "ui/OctreeStatsProvider.h" @@ -932,7 +931,6 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); - DependencyManager::set(); return previousSessionCrashed; } @@ -2672,9 +2670,6 @@ void Application::cleanupBeforeQuit() { nodeList->getPacketReceiver().setShouldDropPackets(true); } - // destroy privacy shield before entity shutdown. - DependencyManager::get()->destroyPrivacyShield(); - getEntities()->shutdown(); // tell the entities system we're shutting down, so it will stop running scripts // Clear any queued processing (I/O, FBX/OBJ/Texture parsing) @@ -2753,7 +2748,6 @@ void Application::cleanupBeforeQuit() { DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); - DependencyManager::destroy(); DependencyManager::destroy(); qCDebug(interfaceapp) << "Application::cleanupBeforeQuit() complete"; @@ -5555,8 +5549,6 @@ void Application::resumeAfterLoginDialogActionTaken() { menu->getMenu("Developer")->setVisible(_developerMenuVisible); _myCamera.setMode(_previousCameraMode); cameraModeChanged(); - - DependencyManager::get()->createPrivacyShield(); } void Application::loadAvatarScripts(const QVector& urls) { @@ -6515,8 +6507,6 @@ void Application::update(float deltaTime) { updateLoginDialogPosition(); } - DependencyManager::get()->update(deltaTime); - { PROFILE_RANGE_EX(app, "Overlays", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount()); PerformanceTimer perfTimer("overlays"); diff --git a/interface/src/ui/PrivacyShield.h b/interface/src/ui/PrivacyShield.h deleted file mode 100644 index 5aecb661f7..0000000000 --- a/interface/src/ui/PrivacyShield.h +++ /dev/null @@ -1,47 +0,0 @@ -// -// PrivacyShield.h -// interface/src/ui -// -// Created by Wayne Chen on 2/27/19. -// Copyright 2019 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 -// - -#pragma once - -#include -#include -#include - -#include -#include - -class PrivacyShield : public QObject, public Dependency { - Q_OBJECT - SINGLETON_DEPENDENCY - -public: - PrivacyShield(); - void createPrivacyShield(); - void destroyPrivacyShield(); - - bool isVisible() const { return _visible; } - void update(float deltaTime); - -protected slots: - void enteredIgnoreRadius(); - void onPrivacyShieldToggled(bool enabled, bool doNotLog = false); - -private: - void showPrivacyShield(); - void hidePrivacyShield(); - - SharedSoundPointer _bubbleActivateSound; - QUuid _localPrivacyShieldID; - quint64 _privacyShieldTimestamp; - quint64 _lastPrivacyShieldSoundTimestamp; - bool _visible { false }; - bool _updateConnected { false }; -}; diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 0d9799a035..bd7e79dffc 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -32,8 +32,7 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/firstPersonHMD.js", "system/tablet-ui/tabletUI.js", "system/emote.js", - "system/miniTablet.js", - "system/createAvatarInputsBarEntity.js" + "system/miniTablet.js" ]; var DEFAULT_SCRIPTS_SEPARATE = [ "system/controllers/controllerScripts.js", From 87de95c63b47bf23b24cc71662db9e6dc7ddfbdd Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Thu, 14 Mar 2019 15:05:26 -0700 Subject: [PATCH 305/446] adding test script for creating avatar input bar --- scripts/defaultScripts.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index bd7e79dffc..0d9799a035 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -32,7 +32,8 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/firstPersonHMD.js", "system/tablet-ui/tabletUI.js", "system/emote.js", - "system/miniTablet.js" + "system/miniTablet.js", + "system/createAvatarInputsBarEntity.js" ]; var DEFAULT_SCRIPTS_SEPARATE = [ "system/controllers/controllerScripts.js", From 44ed9e607b24a59892ec7b066772ea85adae9998 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Tue, 19 Mar 2019 09:33:38 -0700 Subject: [PATCH 306/446] cancelling out roll and pitch --- scripts/system/createAvatarInputsBarEntity.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scripts/system/createAvatarInputsBarEntity.js b/scripts/system/createAvatarInputsBarEntity.js index babf519035..b32085da86 100644 --- a/scripts/system/createAvatarInputsBarEntity.js +++ b/scripts/system/createAvatarInputsBarEntity.js @@ -21,7 +21,7 @@ parentID: MyAvatar.SELF_ID, parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX"), localPosition: micBarLocalPosition, - localRotation: Quat.lookAtSimple(Camera.orientation, micBarLocalPosition), + localRotation: Quat.cancelOutRollAndPitch(Quat.lookAtSimple(Camera.orientation, micBarLocalPosition)), sourceUrl: Script.resourcesPath() + "qml/hifi/audio/MicBarApplication.qml", dimensions: micBarDimensions, userData: { @@ -36,7 +36,7 @@ // y is 0.01 - (0.048 + 0.036) / 2 // have 10% spacing separation between the entities localPosition: bubbleIconLocalPosition, - localRotation: Quat.lookAtSimple(Camera.orientation, bubbleIconLocalPosition), + localRotation: Quat.cancelOutRollAndPitch(Quat.lookAtSimple(Camera.orientation, bubbleIconLocalPosition)), sourceUrl: Script.resourcesPath() + "qml/BubbleIcon.qml", dimensions: bubbleIconDimensions, userData: { @@ -44,9 +44,7 @@ }, }; bubbleIconEntity = Entities.addEntity(props, "local"); - console.log("creating entity"); } else { - console.log("deleting entity"); Entities.deleteEntity(micBarEntity); Entities.deleteEntity(bubbleIconEntity); } From ee1a14505a67f2cbbfe10ba5d7f59063a252eb2f Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Tue, 19 Mar 2019 09:44:08 -0700 Subject: [PATCH 307/446] adding constants, removing entity creation in cpp --- interface/src/Application.cpp | 18 +++++++++--------- scripts/system/createAvatarInputsBarEntity.js | 16 ++++++++++------ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 9fd0781eef..5b0c379c64 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1298,15 +1298,15 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(this, &Application::activeDisplayPluginChanged, this, [&](){ #if !defined(Q_OS_ANDROID) if (!getLoginDialogPoppedUp() && _desktopRootItemCreated) { - if (isHMDMode()) { - createAvatarInputsBar(); - auto offscreenUi = getOffscreenUI(); - offscreenUi->hide(AVATAR_INPUTS_BAR_QML.toString()); - } else { - destroyAvatarInputsBar(); - auto offscreenUi = getOffscreenUI(); - offscreenUi->show(AVATAR_INPUTS_BAR_QML.toString(), "AvatarInputsBar"); - } +/* if (isHMDMode()) {*/ + //createAvatarInputsBar(); + //auto offscreenUi = getOffscreenUI(); + //offscreenUi->hide(AVATAR_INPUTS_BAR_QML.toString()); + //} else { + //destroyAvatarInputsBar(); + //auto offscreenUi = getOffscreenUI(); + //offscreenUi->show(AVATAR_INPUTS_BAR_QML.toString(), "AvatarInputsBar"); + /*}*/ } #endif }); diff --git a/scripts/system/createAvatarInputsBarEntity.js b/scripts/system/createAvatarInputsBarEntity.js index b32085da86..8c4d78d32d 100644 --- a/scripts/system/createAvatarInputsBarEntity.js +++ b/scripts/system/createAvatarInputsBarEntity.js @@ -11,10 +11,16 @@ function onClicked(){ onCreateAvatarInputsBarEntity = !onCreateAvatarInputsBarEntity; button.editProperties({isActive: onCreateAvatarInputsBarEntity}); - var micBarDimensions = {x: 0.036, y: 0.048, z: 0.3}; - var bubbleIconDimensions = {x: 0.036, y: 0.036, z: 0.3}; - var micBarLocalPosition = {x: (-(micBarDimensions.x / 2)) - 0.2, y: -0.125, z: -0.5}; - var bubbleIconLocalPosition = {x: (micBarDimensions.x * 1.2 / 2) - 0.2, y: ((micBarDimensions.y - bubbleIconDimensions.y) / 2 - 0.125), z: -0.5}; + // QML NATURAL DIMENSIONS + var MIC_BAR_DIMENSIONS = {x: 0.036, y: 0.048, z: 0.3}; + var BUBBLE_ICON_DIMENSIONS = {x: 0.036, y: 0.036, z: 0.3}; + // CONSTANTS + var LOCAL_POSITION_X_OFFSET = -0.2; + var LOCAL_POSITION_Y_OFFSET = -0.125; + var LOCAL_POSITION_Z_OFFSET = -0.5; + // POSITIONS + var micBarLocalPosition = {x: (-(micBarDimensions.x / 2)) + LOCAL_POSITION_X_OFFSET, y: LOCAL_POSITION_Y_OFFSET, z: LOCAL_POSITION_Z_OFFSET}; + var bubbleIconLocalPosition = {x: (micBarDimensions.x * 1.2 / 2) + LOCAL_POSITION_X_OFFSET, y: ((micBarDimensions.y - bubbleIconDimensions.y) / 2 + LOCAL_POSITION_Y_OFFSET), z: LOCAL_POSITION_Z_OFFSET}; if (onCreateAvatarInputsBarEntity) { var props = { type: "Web", @@ -33,8 +39,6 @@ type: "Web", parentID: MyAvatar.SELF_ID, parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX"), - // y is 0.01 - (0.048 + 0.036) / 2 - // have 10% spacing separation between the entities localPosition: bubbleIconLocalPosition, localRotation: Quat.cancelOutRollAndPitch(Quat.lookAtSimple(Camera.orientation, bubbleIconLocalPosition)), sourceUrl: Script.resourcesPath() + "qml/BubbleIcon.qml", From 4cbe8cd4d1b39989c4a3c5a81d1a9b445f1f41f6 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Tue, 19 Mar 2019 10:55:00 -0700 Subject: [PATCH 308/446] Adding alpha to entities to enable transparency --- scripts/system/createAvatarInputsBarEntity.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/system/createAvatarInputsBarEntity.js b/scripts/system/createAvatarInputsBarEntity.js index 8c4d78d32d..c14bf433d7 100644 --- a/scripts/system/createAvatarInputsBarEntity.js +++ b/scripts/system/createAvatarInputsBarEntity.js @@ -29,6 +29,7 @@ localPosition: micBarLocalPosition, localRotation: Quat.cancelOutRollAndPitch(Quat.lookAtSimple(Camera.orientation, micBarLocalPosition)), sourceUrl: Script.resourcesPath() + "qml/hifi/audio/MicBarApplication.qml", + alpha: 0.9, dimensions: micBarDimensions, userData: { grabbable: false @@ -42,6 +43,7 @@ localPosition: bubbleIconLocalPosition, localRotation: Quat.cancelOutRollAndPitch(Quat.lookAtSimple(Camera.orientation, bubbleIconLocalPosition)), sourceUrl: Script.resourcesPath() + "qml/BubbleIcon.qml", + alpha: 0.9, dimensions: bubbleIconDimensions, userData: { grabbable: false From b6ebdb5315fdc24f18255182175acdbd9791f104 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Tue, 19 Mar 2019 13:49:59 -0700 Subject: [PATCH 309/446] alpha to max value of detecting transparency --- scripts/system/createAvatarInputsBarEntity.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/system/createAvatarInputsBarEntity.js b/scripts/system/createAvatarInputsBarEntity.js index c14bf433d7..c64c8e35c5 100644 --- a/scripts/system/createAvatarInputsBarEntity.js +++ b/scripts/system/createAvatarInputsBarEntity.js @@ -29,7 +29,8 @@ localPosition: micBarLocalPosition, localRotation: Quat.cancelOutRollAndPitch(Quat.lookAtSimple(Camera.orientation, micBarLocalPosition)), sourceUrl: Script.resourcesPath() + "qml/hifi/audio/MicBarApplication.qml", - alpha: 0.9, + // cutoff alpha for detecting transparency + alpha: 0.98, dimensions: micBarDimensions, userData: { grabbable: false @@ -43,7 +44,8 @@ localPosition: bubbleIconLocalPosition, localRotation: Quat.cancelOutRollAndPitch(Quat.lookAtSimple(Camera.orientation, bubbleIconLocalPosition)), sourceUrl: Script.resourcesPath() + "qml/BubbleIcon.qml", - alpha: 0.9, + // cutoff alpha for detecting transparency + alpha: 0.98, dimensions: bubbleIconDimensions, userData: { grabbable: false From cb715c3e55ee84f8ab2047853f34f7e031b1bf24 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 20 Mar 2019 09:39:14 -0700 Subject: [PATCH 310/446] fixing typos --- scripts/system/createAvatarInputsBarEntity.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/system/createAvatarInputsBarEntity.js b/scripts/system/createAvatarInputsBarEntity.js index c64c8e35c5..064bfa33e8 100644 --- a/scripts/system/createAvatarInputsBarEntity.js +++ b/scripts/system/createAvatarInputsBarEntity.js @@ -19,8 +19,8 @@ var LOCAL_POSITION_Y_OFFSET = -0.125; var LOCAL_POSITION_Z_OFFSET = -0.5; // POSITIONS - var micBarLocalPosition = {x: (-(micBarDimensions.x / 2)) + LOCAL_POSITION_X_OFFSET, y: LOCAL_POSITION_Y_OFFSET, z: LOCAL_POSITION_Z_OFFSET}; - var bubbleIconLocalPosition = {x: (micBarDimensions.x * 1.2 / 2) + LOCAL_POSITION_X_OFFSET, y: ((micBarDimensions.y - bubbleIconDimensions.y) / 2 + LOCAL_POSITION_Y_OFFSET), z: LOCAL_POSITION_Z_OFFSET}; + var micBarLocalPosition = {x: (-(MIC_BAR_DIMENSIONS.x / 2)) + LOCAL_POSITION_X_OFFSET, y: LOCAL_POSITION_Y_OFFSET, z: LOCAL_POSITION_Z_OFFSET}; + var bubbleIconLocalPosition = {x: (MIC_BAR_DIMENSIONS.x * 1.2 / 2) + LOCAL_POSITION_X_OFFSET, y: ((MIC_BAR_DIMENSIONS.y - BUBBLE_ICON_DIMENSIONS.y) / 2 + LOCAL_POSITION_Y_OFFSET), z: LOCAL_POSITION_Z_OFFSET}; if (onCreateAvatarInputsBarEntity) { var props = { type: "Web", @@ -31,7 +31,7 @@ sourceUrl: Script.resourcesPath() + "qml/hifi/audio/MicBarApplication.qml", // cutoff alpha for detecting transparency alpha: 0.98, - dimensions: micBarDimensions, + dimensions: MIC_BAR_DIMENSIONS, userData: { grabbable: false }, @@ -46,7 +46,7 @@ sourceUrl: Script.resourcesPath() + "qml/BubbleIcon.qml", // cutoff alpha for detecting transparency alpha: 0.98, - dimensions: bubbleIconDimensions, + dimensions: BUBBLE_ICON_DIMENSIONS, userData: { grabbable: false }, From 6cf8b06c6daa06c18e64b36af439ab6b64b45112 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 20 Mar 2019 17:39:32 -0700 Subject: [PATCH 311/446] more wip on adding qml to adjusting ui position --- .../qml/hifi/EditAvatarInputsBar.qml | 37 +++++++++++++++++++ scripts/system/createAvatarInputsBarEntity.js | 13 +++++++ 2 files changed, 50 insertions(+) create mode 100644 interface/resources/qml/hifi/EditAvatarInputsBar.qml diff --git a/interface/resources/qml/hifi/EditAvatarInputsBar.qml b/interface/resources/qml/hifi/EditAvatarInputsBar.qml new file mode 100644 index 0000000000..d9ba3a6fcc --- /dev/null +++ b/interface/resources/qml/hifi/EditAvatarInputsBar.qml @@ -0,0 +1,37 @@ +// +// EditAvatarInputsBar.qml +// qml/hifi +// +// Audio setup +// +// Created by Wayne Chen on 3/20/2019 +// Copyright 2019 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.7 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 + +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit +import "../windows" + +Rectangle { + id: editRect + + HifiConstants { id: hifi; } + + color: hifi.colors.baseGray; + + signal sendToScript(var message); + function emitSendToScript(message) { + sendToScript(message); + } + + function fromScript(message) { + } + +} diff --git a/scripts/system/createAvatarInputsBarEntity.js b/scripts/system/createAvatarInputsBarEntity.js index 064bfa33e8..0b8fde4bb3 100644 --- a/scripts/system/createAvatarInputsBarEntity.js +++ b/scripts/system/createAvatarInputsBarEntity.js @@ -1,12 +1,20 @@ "use strict"; (function(){ + var AppUi = Script.require("appUi"); + + var ui; var button; var buttonName = "AVBAR"; var onCreateAvatarInputsBarEntity = false; var micBarEntity, bubbleIconEntity; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var AVATAR_INPUTS_EDIT_QML_SOURCE = "hifi/EditAvatarInputsBar.qml"; + + function fromQml(message) { + console.log("message from QML: " + JSON.stringify(message)); + }; function onClicked(){ onCreateAvatarInputsBarEntity = !onCreateAvatarInputsBarEntity; @@ -24,6 +32,7 @@ if (onCreateAvatarInputsBarEntity) { var props = { type: "Web", + name: "AvatarInputsMicBarEntity", parentID: MyAvatar.SELF_ID, parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX"), localPosition: micBarLocalPosition, @@ -32,6 +41,7 @@ // cutoff alpha for detecting transparency alpha: 0.98, dimensions: MIC_BAR_DIMENSIONS, + drawInFront: true, userData: { grabbable: false }, @@ -39,6 +49,7 @@ micBarEntity = Entities.addEntity(props, "local"); var props = { type: "Web", + name: "AvatarInputsBubbleIconEntity", parentID: MyAvatar.SELF_ID, parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX"), localPosition: bubbleIconLocalPosition, @@ -47,11 +58,13 @@ // cutoff alpha for detecting transparency alpha: 0.98, dimensions: BUBBLE_ICON_DIMENSIONS, + drawInFront: true, userData: { grabbable: false }, }; bubbleIconEntity = Entities.addEntity(props, "local"); + tablet.loadQMLSource(AVATAR_INPUTS_EDIT_QML_SOURCE); } else { Entities.deleteEntity(micBarEntity); Entities.deleteEntity(bubbleIconEntity); From 584fa1f17b1c8b98c8b9d9324b2ab51fe0982a50 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 20 Mar 2019 18:07:50 -0700 Subject: [PATCH 312/446] more wip --- .../qml/hifi/EditAvatarInputsBar.qml | 83 +++++++++++++++++++ scripts/system/createAvatarInputsBarEntity.js | 17 ++-- 2 files changed, 95 insertions(+), 5 deletions(-) diff --git a/interface/resources/qml/hifi/EditAvatarInputsBar.qml b/interface/resources/qml/hifi/EditAvatarInputsBar.qml index d9ba3a6fcc..d34537bb0e 100644 --- a/interface/resources/qml/hifi/EditAvatarInputsBar.qml +++ b/interface/resources/qml/hifi/EditAvatarInputsBar.qml @@ -28,10 +28,93 @@ Rectangle { signal sendToScript(var message); function emitSendToScript(message) { + console.log("sending to script"); + console.log(JSON.stringify(message)); sendToScript(message); } function fromScript(message) { } + RalewayRegular { + id: title; + color: hifi.colors.white; + text: qsTr("Avatar Inputs Persistent UI Settings") + size: 20 + font.bold: true + anchors { + top: parent.top + left: parent.left + leftMargin: (parent.width - width) / 2 + } + } + + HifiControlsUit.Slider { + id: xSlider + anchors { + top: title.bottom + topMargin: 50 + left: parent.left + } + label: "X OFFSET" + maximumValue: 1.0 + minimumValue: -1.0 + stepSize: 0.05 + value: -0.2 + width: 300 + onValueChanged: { + emitSendToScript({ + "method": "reposition", + "x": value + }); + } + } + + HifiControlsUit.Slider { + id: ySlider + anchors { + top: xSlider.bottom + topMargin: 50 + left: parent.left + } + label: "Y OFFSET" + maximumValue: 1.0 + minimumValue: -1.0 + stepSize: 0.05 + value: -0.125 + width: 300 + onValueChanged: { + emitSendToScript({ + "method": "reposition", + "y": value + }); + } + } + + HifiControlsUit.Slider { + anchors { + top: ySlider.bottom + topMargin: 50 + left: parent.left + } + label: "Y OFFSET" + maximumValue: 0.0 + minimumValue: -1.0 + stepSize: 0.05 + value: -0.5 + width: 300 + onValueChanged: { + emitSendToScript({ + "method": "reposition", + "z": value + }); + } + } + + HifiControlsUit.Button { + id: setVisibleButton + } + + HifiControlsUit.Button { + } } diff --git a/scripts/system/createAvatarInputsBarEntity.js b/scripts/system/createAvatarInputsBarEntity.js index 0b8fde4bb3..5ffba2c029 100644 --- a/scripts/system/createAvatarInputsBarEntity.js +++ b/scripts/system/createAvatarInputsBarEntity.js @@ -13,7 +13,7 @@ var AVATAR_INPUTS_EDIT_QML_SOURCE = "hifi/EditAvatarInputsBar.qml"; function fromQml(message) { - console.log("message from QML: " + JSON.stringify(message)); + print("message from QML: " + JSON.stringify(message)); }; function onClicked(){ @@ -72,10 +72,17 @@ }; function setup() { - button = tablet.addButton({ - icon: "icons/tablet-icons/edit-i.svg", - activeIcon: "icons/tablet-icons/edit-a.svg", - text: buttonName + // button = tablet.addButton({ + // icon: "icons/tablet-icons/edit-i.svg", + // activeIcon: "icons/tablet-icons/edit-a.svg", + // text: buttonName + // }); + ui = new AppUi({ + buttonName: "AVBAR", + home: Script.resourcesPath() + "qml/hifi/EditAvatarInputsBar.qml", + onMessage: fromQml, + // normalButton: "icons/tablet-icons/avatar-i.svg", + // activeButton: "icons/tablet-icons/avatar-a.svg", }); button.clicked.connect(onClicked); }; From 554a144b0efc43ed435fae9c2011502dddc4cfc4 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 20 Mar 2019 21:55:36 -0700 Subject: [PATCH 313/446] adding tool to change avatar inputs properties --- .../qml/hifi/EditAvatarInputsBar.qml | 38 +++- scripts/system/createAvatarInputsBarEntity.js | 175 ++++++++++-------- 2 files changed, 138 insertions(+), 75 deletions(-) diff --git a/interface/resources/qml/hifi/EditAvatarInputsBar.qml b/interface/resources/qml/hifi/EditAvatarInputsBar.qml index d34537bb0e..bbf3652a92 100644 --- a/interface/resources/qml/hifi/EditAvatarInputsBar.qml +++ b/interface/resources/qml/hifi/EditAvatarInputsBar.qml @@ -55,6 +55,7 @@ Rectangle { top: title.bottom topMargin: 50 left: parent.left + leftMargin: 20 } label: "X OFFSET" maximumValue: 1.0 @@ -76,6 +77,7 @@ Rectangle { top: xSlider.bottom topMargin: 50 left: parent.left + leftMargin: 20 } label: "Y OFFSET" maximumValue: 1.0 @@ -92,12 +94,14 @@ Rectangle { } HifiControlsUit.Slider { + id: zSlider anchors { top: ySlider.bottom topMargin: 50 left: parent.left + leftMargin: 20 } - label: "Y OFFSET" + label: "Z OFFSET" maximumValue: 0.0 minimumValue: -1.0 stepSize: 0.05 @@ -112,9 +116,39 @@ Rectangle { } HifiControlsUit.Button { - id: setVisibleButton + id: setVisibleButton; + text: setVisible ? "SET INVISIBLE" : "SET VISIBLE"; + width: 300; + property bool setVisible: true; + anchors { + top: zSlider.bottom + topMargin: 50 + left: parent.left + leftMargin: 20 + } + onClicked: { + setVisible = !setVisible; + emitSendToScript({ + "method": "setVisible", + "visible": setVisible + }); + } } HifiControlsUit.Button { + id: printButton; + text: "PRINT POSITIONS"; + width: 300; + anchors { + top: setVisibleButton.bottom + topMargin: 50 + left: parent.left + leftMargin: 20 + } + onClicked: { + emitSendToScript({ + "method": "print", + }); + } } } diff --git a/scripts/system/createAvatarInputsBarEntity.js b/scripts/system/createAvatarInputsBarEntity.js index 5ffba2c029..35c50416ed 100644 --- a/scripts/system/createAvatarInputsBarEntity.js +++ b/scripts/system/createAvatarInputsBarEntity.js @@ -5,98 +5,127 @@ var ui; - var button; - var buttonName = "AVBAR"; var onCreateAvatarInputsBarEntity = false; var micBarEntity, bubbleIconEntity; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var AVATAR_INPUTS_EDIT_QML_SOURCE = "hifi/EditAvatarInputsBar.qml"; - function fromQml(message) { - print("message from QML: " + JSON.stringify(message)); - }; + // QML NATURAL DIMENSIONS + var MIC_BAR_DIMENSIONS = {x: 0.036, y: 0.048, z: 0.3}; + var BUBBLE_ICON_DIMENSIONS = {x: 0.036, y: 0.036, z: 0.3}; + // CONSTANTS + var LOCAL_POSITION_X_OFFSET = -0.2; + var LOCAL_POSITION_Y_OFFSET = -0.125; + var LOCAL_POSITION_Z_OFFSET = -0.5; - function onClicked(){ - onCreateAvatarInputsBarEntity = !onCreateAvatarInputsBarEntity; - button.editProperties({isActive: onCreateAvatarInputsBarEntity}); - // QML NATURAL DIMENSIONS - var MIC_BAR_DIMENSIONS = {x: 0.036, y: 0.048, z: 0.3}; - var BUBBLE_ICON_DIMENSIONS = {x: 0.036, y: 0.036, z: 0.3}; - // CONSTANTS - var LOCAL_POSITION_X_OFFSET = -0.2; - var LOCAL_POSITION_Y_OFFSET = -0.125; - var LOCAL_POSITION_Z_OFFSET = -0.5; - // POSITIONS - var micBarLocalPosition = {x: (-(MIC_BAR_DIMENSIONS.x / 2)) + LOCAL_POSITION_X_OFFSET, y: LOCAL_POSITION_Y_OFFSET, z: LOCAL_POSITION_Z_OFFSET}; - var bubbleIconLocalPosition = {x: (MIC_BAR_DIMENSIONS.x * 1.2 / 2) + LOCAL_POSITION_X_OFFSET, y: ((MIC_BAR_DIMENSIONS.y - BUBBLE_ICON_DIMENSIONS.y) / 2 + LOCAL_POSITION_Y_OFFSET), z: LOCAL_POSITION_Z_OFFSET}; - if (onCreateAvatarInputsBarEntity) { - var props = { - type: "Web", - name: "AvatarInputsMicBarEntity", - parentID: MyAvatar.SELF_ID, - parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX"), - localPosition: micBarLocalPosition, - localRotation: Quat.cancelOutRollAndPitch(Quat.lookAtSimple(Camera.orientation, micBarLocalPosition)), - sourceUrl: Script.resourcesPath() + "qml/hifi/audio/MicBarApplication.qml", - // cutoff alpha for detecting transparency - alpha: 0.98, - dimensions: MIC_BAR_DIMENSIONS, - drawInFront: true, - userData: { - grabbable: false - }, + function fromQml(message) { + if (message.method === "reposition") { + var micBarLocalPosition = Entities.getEntityProperties(micBarEntity).localPosition; + var bubbleIconLocalPosition = Entities.getEntityProperties(bubbleIconEntity).localPosition; + var newMicBarLocalPosition, newBubbleIconLocalPosition; + if (message.x !== undefined) { + newMicBarLocalPosition = { x: -((MIC_BAR_DIMENSIONS.x) / 2) - message.x, y: micBarLocalPosition.y, z: micBarLocalPosition.z }; + newBubbleIconLocalPosition = { x: ((MIC_BAR_DIMENSIONS.x) * 1.2 / 2) - message.x, y: bubbleIconLocalPosition.y, z: bubbleIconLocalPosition.z }; + } else if (message.y !== undefined) { + newMicBarLocalPosition = { x: micBarLocalPosition.x, y: message.y, z: micBarLocalPosition.z }; + newBubbleIconLocalPosition = { x: bubbleIconLocalPosition.x, y: ((MIC_BAR_DIMENSIONS.y - BUBBLE_ICON_DIMENSIONS.y) / 2 + message.y), z: bubbleIconLocalPosition.z }; + } else if (message.z !== undefined) { + newMicBarLocalPosition = { x: micBarLocalPosition.x, y: micBarLocalPosition.y, z: message.z }; + newBubbleIconLocalPosition = { x: bubbleIconLocalPosition.x, y: bubbleIconLocalPosition.y, z: message.z }; + } + var micBarProps = { + localPosition: newMicBarLocalPosition }; - micBarEntity = Entities.addEntity(props, "local"); - var props = { - type: "Web", - name: "AvatarInputsBubbleIconEntity", - parentID: MyAvatar.SELF_ID, - parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX"), - localPosition: bubbleIconLocalPosition, - localRotation: Quat.cancelOutRollAndPitch(Quat.lookAtSimple(Camera.orientation, bubbleIconLocalPosition)), - sourceUrl: Script.resourcesPath() + "qml/BubbleIcon.qml", - // cutoff alpha for detecting transparency - alpha: 0.98, - dimensions: BUBBLE_ICON_DIMENSIONS, - drawInFront: true, - userData: { - grabbable: false - }, + var bubbleIconProps = { + localPosition: newBubbleIconLocalPosition }; - bubbleIconEntity = Entities.addEntity(props, "local"); - tablet.loadQMLSource(AVATAR_INPUTS_EDIT_QML_SOURCE); - } else { + + Entities.editEntity(micBarEntity, micBarProps); + Entities.editEntity(bubbleIconEntity, bubbleIconProps); + } else if (message.method === "setVisible") { + if (message.visible !== undefined) { + var props = { + visible: message.visible + }; + Entities.editEntity(micBarEntity, props); + Entities.editEntity(bubbleIconEntity, props); + } + } else if (message.method === "print") { + // prints the local position into the hifi log. + var micBarLocalPosition = Entities.getEntityProperties(micBarEntity).localPosition; + var bubbleIconLocalPosition = Entities.getEntityProperties(bubbleIconEntity).localPosition; + console.log("mic bar local position is at " + JSON.stringify(micBarLocalPosition)); + console.log("bubble icon local position is at " + JSON.stringify(bubbleIconLocalPosition)); + } else if (message.method === "destroy") { + console.log("destroying"); Entities.deleteEntity(micBarEntity); Entities.deleteEntity(bubbleIconEntity); } }; - function setup() { - // button = tablet.addButton({ - // icon: "icons/tablet-icons/edit-i.svg", - // activeIcon: "icons/tablet-icons/edit-a.svg", - // text: buttonName - // }); - ui = new AppUi({ - buttonName: "AVBAR", - home: Script.resourcesPath() + "qml/hifi/EditAvatarInputsBar.qml", - onMessage: fromQml, - // normalButton: "icons/tablet-icons/avatar-i.svg", - // activeButton: "icons/tablet-icons/avatar-a.svg", - }); - button.clicked.connect(onClicked); + function createEntities(){ + // POSITIONS + var micBarLocalPosition = {x: (-(MIC_BAR_DIMENSIONS.x / 2)) + LOCAL_POSITION_X_OFFSET, y: LOCAL_POSITION_Y_OFFSET, z: LOCAL_POSITION_Z_OFFSET}; + var bubbleIconLocalPosition = {x: (MIC_BAR_DIMENSIONS.x * 1.2 / 2) + LOCAL_POSITION_X_OFFSET, y: ((MIC_BAR_DIMENSIONS.y - BUBBLE_ICON_DIMENSIONS.y) / 2 + LOCAL_POSITION_Y_OFFSET), z: LOCAL_POSITION_Z_OFFSET}; + var props = { + type: "Web", + name: "AvatarInputsMicBarEntity", + parentID: MyAvatar.SELF_ID, + parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX"), + localPosition: micBarLocalPosition, + localRotation: Quat.cancelOutRollAndPitch(Quat.lookAtSimple(Camera.orientation, micBarLocalPosition)), + sourceUrl: Script.resourcesPath() + "qml/hifi/audio/MicBarApplication.qml", + // cutoff alpha for detecting transparency + alpha: 0.98, + dimensions: MIC_BAR_DIMENSIONS, + drawInFront: true, + userData: { + grabbable: false + }, + }; + micBarEntity = Entities.addEntity(props, "local"); + var props = { + type: "Web", + name: "AvatarInputsBubbleIconEntity", + parentID: MyAvatar.SELF_ID, + parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX"), + localPosition: bubbleIconLocalPosition, + localRotation: Quat.cancelOutRollAndPitch(Quat.lookAtSimple(Camera.orientation, bubbleIconLocalPosition)), + sourceUrl: Script.resourcesPath() + "qml/BubbleIcon.qml", + // cutoff alpha for detecting transparency + alpha: 0.98, + dimensions: BUBBLE_ICON_DIMENSIONS, + drawInFront: true, + userData: { + grabbable: false + }, + }; + bubbleIconEntity = Entities.addEntity(props, "local"); + tablet.loadQMLSource(AVATAR_INPUTS_EDIT_QML_SOURCE); }; - - setup(); - - Script.scriptEnding.connect(function() { + function cleanup() { if (micBarEntity) { Entities.deleteEntity(micBarEntity); } if (bubbleIconEntity) { Entities.deleteEntity(bubbleIconEntity); } - tablet.removeButton(button); - }); + }; + + function setup() { + ui = new AppUi({ + buttonName: "AVBAR", + home: Script.resourcesPath() + "qml/hifi/EditAvatarInputsBar.qml", + onMessage: fromQml, + onOpened: createEntities, + onClosed: cleanup, + // normalButton: "icons/tablet-icons/avatar-i.svg", + // activeButton: "icons/tablet-icons/avatar-a.svg", + }); + }; + + setup(); + + Script.scriptEnding.connect(cleanup); }()); From 1f71a291a5fe968f6b35dd1e8accc1876044d4c9 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Thu, 21 Mar 2019 08:38:31 -0700 Subject: [PATCH 314/446] displaying offset values --- .../qml/hifi/EditAvatarInputsBar.qml | 8 +++--- scripts/system/createAvatarInputsBarEntity.js | 25 +++++++++++-------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/interface/resources/qml/hifi/EditAvatarInputsBar.qml b/interface/resources/qml/hifi/EditAvatarInputsBar.qml index bbf3652a92..b27b0c8db2 100644 --- a/interface/resources/qml/hifi/EditAvatarInputsBar.qml +++ b/interface/resources/qml/hifi/EditAvatarInputsBar.qml @@ -28,8 +28,6 @@ Rectangle { signal sendToScript(var message); function emitSendToScript(message) { - console.log("sending to script"); - console.log(JSON.stringify(message)); sendToScript(message); } @@ -57,7 +55,7 @@ Rectangle { left: parent.left leftMargin: 20 } - label: "X OFFSET" + label: "X OFFSET: " + value.toFixed(2); maximumValue: 1.0 minimumValue: -1.0 stepSize: 0.05 @@ -79,7 +77,7 @@ Rectangle { left: parent.left leftMargin: 20 } - label: "Y OFFSET" + label: "Y OFFSET: " + value.toFixed(2); maximumValue: 1.0 minimumValue: -1.0 stepSize: 0.05 @@ -101,7 +99,7 @@ Rectangle { left: parent.left leftMargin: 20 } - label: "Z OFFSET" + label: "Z OFFSET: " + value.toFixed(2); maximumValue: 0.0 minimumValue: -1.0 stepSize: 0.05 diff --git a/scripts/system/createAvatarInputsBarEntity.js b/scripts/system/createAvatarInputsBarEntity.js index 35c50416ed..500f8563fb 100644 --- a/scripts/system/createAvatarInputsBarEntity.js +++ b/scripts/system/createAvatarInputsBarEntity.js @@ -6,13 +6,17 @@ var ui; var onCreateAvatarInputsBarEntity = false; - var micBarEntity, bubbleIconEntity; + var micBarEntity = null; + var bubbleIconEntity = null; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var AVATAR_INPUTS_EDIT_QML_SOURCE = "hifi/EditAvatarInputsBar.qml"; // QML NATURAL DIMENSIONS var MIC_BAR_DIMENSIONS = {x: 0.036, y: 0.048, z: 0.3}; var BUBBLE_ICON_DIMENSIONS = {x: 0.036, y: 0.036, z: 0.3}; + // ENTITY NAMES + var MIC_BAR_NAME = "AvatarInputsMicBarEntity"; + var BUBBLE_ICON_NAME = "AvatarInputsBubbleIconEntity"; // CONSTANTS var LOCAL_POSITION_X_OFFSET = -0.2; var LOCAL_POSITION_Y_OFFSET = -0.125; @@ -56,20 +60,19 @@ var bubbleIconLocalPosition = Entities.getEntityProperties(bubbleIconEntity).localPosition; console.log("mic bar local position is at " + JSON.stringify(micBarLocalPosition)); console.log("bubble icon local position is at " + JSON.stringify(bubbleIconLocalPosition)); - } else if (message.method === "destroy") { - console.log("destroying"); - Entities.deleteEntity(micBarEntity); - Entities.deleteEntity(bubbleIconEntity); } }; - function createEntities(){ + function createEntities() { + if (micBarEntity != null && bubbleIconEntity != null) { + return; + } // POSITIONS var micBarLocalPosition = {x: (-(MIC_BAR_DIMENSIONS.x / 2)) + LOCAL_POSITION_X_OFFSET, y: LOCAL_POSITION_Y_OFFSET, z: LOCAL_POSITION_Z_OFFSET}; var bubbleIconLocalPosition = {x: (MIC_BAR_DIMENSIONS.x * 1.2 / 2) + LOCAL_POSITION_X_OFFSET, y: ((MIC_BAR_DIMENSIONS.y - BUBBLE_ICON_DIMENSIONS.y) / 2 + LOCAL_POSITION_Y_OFFSET), z: LOCAL_POSITION_Z_OFFSET}; var props = { type: "Web", - name: "AvatarInputsMicBarEntity", + name: MIC_BAR_NAME, parentID: MyAvatar.SELF_ID, parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX"), localPosition: micBarLocalPosition, @@ -86,7 +89,7 @@ micBarEntity = Entities.addEntity(props, "local"); var props = { type: "Web", - name: "AvatarInputsBubbleIconEntity", + name: BUBBLE_ICON_NAME, parentID: MyAvatar.SELF_ID, parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX"), localPosition: bubbleIconLocalPosition, @@ -118,9 +121,9 @@ home: Script.resourcesPath() + "qml/hifi/EditAvatarInputsBar.qml", onMessage: fromQml, onOpened: createEntities, - onClosed: cleanup, - // normalButton: "icons/tablet-icons/avatar-i.svg", - // activeButton: "icons/tablet-icons/avatar-a.svg", + // onClosed: cleanup, + normalButton: "icons/tablet-icons/edit-i.svg", + activeButton: "icons/tablet-icons/edit-a.svg", }); }; From 7b5deb692c7aff78675cb9a4239a1446b8026ed7 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Thu, 21 Mar 2019 09:57:31 -0700 Subject: [PATCH 315/446] fixing x value change --- scripts/system/createAvatarInputsBarEntity.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/scripts/system/createAvatarInputsBarEntity.js b/scripts/system/createAvatarInputsBarEntity.js index 500f8563fb..deb0cfdd89 100644 --- a/scripts/system/createAvatarInputsBarEntity.js +++ b/scripts/system/createAvatarInputsBarEntity.js @@ -11,9 +11,11 @@ var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var AVATAR_INPUTS_EDIT_QML_SOURCE = "hifi/EditAvatarInputsBar.qml"; + // DPI + var ENTITY_DPI = 60.0; // QML NATURAL DIMENSIONS - var MIC_BAR_DIMENSIONS = {x: 0.036, y: 0.048, z: 0.3}; - var BUBBLE_ICON_DIMENSIONS = {x: 0.036, y: 0.036, z: 0.3}; + var MIC_BAR_DIMENSIONS = Vec3.multiply(30.0 / ENTITY_DPI, {x: 0.036, y: 0.048, z: 0.3}); + var BUBBLE_ICON_DIMENSIONS = Vec3.multiply(30.0 / ENTITY_DPI, {x: 0.036, y: 0.036, z: 0.3}); // ENTITY NAMES var MIC_BAR_NAME = "AvatarInputsMicBarEntity"; var BUBBLE_ICON_NAME = "AvatarInputsBubbleIconEntity"; @@ -28,8 +30,8 @@ var bubbleIconLocalPosition = Entities.getEntityProperties(bubbleIconEntity).localPosition; var newMicBarLocalPosition, newBubbleIconLocalPosition; if (message.x !== undefined) { - newMicBarLocalPosition = { x: -((MIC_BAR_DIMENSIONS.x) / 2) - message.x, y: micBarLocalPosition.y, z: micBarLocalPosition.z }; - newBubbleIconLocalPosition = { x: ((MIC_BAR_DIMENSIONS.x) * 1.2 / 2) - message.x, y: bubbleIconLocalPosition.y, z: bubbleIconLocalPosition.z }; + newMicBarLocalPosition = { x: -((MIC_BAR_DIMENSIONS.x) / 2) + message.x, y: micBarLocalPosition.y, z: micBarLocalPosition.z }; + newBubbleIconLocalPosition = { x: ((MIC_BAR_DIMENSIONS.x) * 1.2 / 2) + message.x, y: bubbleIconLocalPosition.y, z: bubbleIconLocalPosition.z }; } else if (message.y !== undefined) { newMicBarLocalPosition = { x: micBarLocalPosition.x, y: message.y, z: micBarLocalPosition.z }; newBubbleIconLocalPosition = { x: bubbleIconLocalPosition.x, y: ((MIC_BAR_DIMENSIONS.y - BUBBLE_ICON_DIMENSIONS.y) / 2 + message.y), z: bubbleIconLocalPosition.z }; @@ -81,6 +83,7 @@ // cutoff alpha for detecting transparency alpha: 0.98, dimensions: MIC_BAR_DIMENSIONS, + dpi: ENTITY_DPI, drawInFront: true, userData: { grabbable: false @@ -98,6 +101,7 @@ // cutoff alpha for detecting transparency alpha: 0.98, dimensions: BUBBLE_ICON_DIMENSIONS, + dpi: ENTITY_DPI, drawInFront: true, userData: { grabbable: false From 0edbf12fa373dfb415aa8ed0f0d45a009fd8409d Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Fri, 22 Mar 2019 15:26:57 -0700 Subject: [PATCH 316/446] removing extra onLevelChanged --- .../qml/hifi/audio/MicBarApplication.qml | 10 -- interface/src/ui/PrivacyShield.cpp | 159 ------------------ 2 files changed, 169 deletions(-) delete mode 100644 interface/src/ui/PrivacyShield.cpp diff --git a/interface/resources/qml/hifi/audio/MicBarApplication.qml b/interface/resources/qml/hifi/audio/MicBarApplication.qml index f2839aee1a..bfac278ee4 100644 --- a/interface/resources/qml/hifi/audio/MicBarApplication.qml +++ b/interface/resources/qml/hifi/audio/MicBarApplication.qml @@ -53,16 +53,6 @@ Rectangle { micBar.opacity = rectOpacity; } - onLevelChanged: { - var rectOpacity = muted && (level >= userSpeakingLevel) ? 0.9 : 0.3; - if (pushToTalk && !pushingToTalk) { - rectOpacity = (level >= userSpeakingLevel) ? 0.9 : 0.7; - } else if (mouseArea.containsMouse && rectOpacity != 0.9) { - rectOpacity = 0.5; - } - micBar.opacity = rectOpacity; - } - color: "#00000000"; border { width: mouseArea.containsMouse || mouseArea.containsPress ? 2 : 0; diff --git a/interface/src/ui/PrivacyShield.cpp b/interface/src/ui/PrivacyShield.cpp deleted file mode 100644 index e8f61ff5bf..0000000000 --- a/interface/src/ui/PrivacyShield.cpp +++ /dev/null @@ -1,159 +0,0 @@ -// -// PrivacyShield.cpp -// interface/src/ui -// -// Created by Wayne Chen on 2/27/19. -// Copyright 2019 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 -// - -#include "PrivacyShield.h" - -#include -#include -#include -#include -#include -#include - -#include "Application.h" -#include "PathUtils.h" -#include "GLMHelpers.h" - -const int PRIVACY_SHIELD_VISIBLE_DURATION_MS = 3000; -const int PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS = 750; -const int PRIVACY_SHIELD_SOUND_RATE_LIMIT_MS = 15000; -const float PRIVACY_SHIELD_HEIGHT_SCALE = 0.15f; - -PrivacyShield::PrivacyShield() { - auto usersScriptingInterface = DependencyManager::get(); - //connect(usersScriptingInterface.data(), &UsersScriptingInterface::ignoreRadiusEnabledChanged, [this](bool enabled) { - // onPrivacyShieldToggled(enabled); - //}); - //connect(usersScriptingInterface.data(), &UsersScriptingInterface::enteredIgnoreRadius, this, &PrivacyShield::enteredIgnoreRadius); -} - -void PrivacyShield::createPrivacyShield() { - // Affects bubble height - auto myAvatar = DependencyManager::get()->getMyAvatar(); - auto avatarScale = myAvatar->getTargetScale(); - auto avatarSensorToWorldScale = myAvatar->getSensorToWorldScale(); - auto avatarWorldPosition = myAvatar->getWorldPosition(); - auto avatarWorldOrientation = myAvatar->getWorldOrientation(); - EntityItemProperties properties; - properties.setName("Privacy-Shield"); - properties.setModelURL(PathUtils::resourcesUrl("assets/models/Bubble-v14.fbx").toString()); - properties.setDimensions(glm::vec3(avatarSensorToWorldScale, 0.75 * avatarSensorToWorldScale, avatarSensorToWorldScale)); - properties.setPosition(glm::vec3(avatarWorldPosition.x, - -avatarScale * 2 + avatarWorldPosition.y + avatarScale * PRIVACY_SHIELD_HEIGHT_SCALE, avatarWorldPosition.z)); - properties.setRotation(avatarWorldOrientation * Quaternions::Y_180); - properties.setModelScale(glm::vec3(2.0, 0.5 * (avatarScale + 1.0), 2.0)); - properties.setVisible(false); - - _localPrivacyShieldID = DependencyManager::get()->addEntityInternal(properties, entity::HostType::LOCAL); - //_bubbleActivateSound = DependencyManager::get()->getSound(PathUtils::resourcesUrl() + "assets/sounds/bubble.wav"); - - //onPrivacyShieldToggled(DependencyManager::get()->getIgnoreRadiusEnabled(), true); -} - -void PrivacyShield::destroyPrivacyShield() { - DependencyManager::get()->deleteEntity(_localPrivacyShieldID); -} - -void PrivacyShield::update(float deltaTime) { - if (_updateConnected) { - auto now = usecTimestampNow(); - auto delay = (now - _privacyShieldTimestamp); - auto privacyShieldAlpha = 1.0 - (delay / PRIVACY_SHIELD_VISIBLE_DURATION_MS); - if (privacyShieldAlpha > 0) { - auto myAvatar = DependencyManager::get()->getMyAvatar(); - auto avatarScale = myAvatar->getTargetScale(); - auto avatarSensorToWorldScale = myAvatar->getSensorToWorldScale(); - auto avatarWorldPosition = myAvatar->getWorldPosition(); - auto avatarWorldOrientation = myAvatar->getWorldOrientation(); - EntityItemProperties properties; - properties.setDimensions(glm::vec3(avatarSensorToWorldScale, 0.75 * avatarSensorToWorldScale, avatarSensorToWorldScale)); - properties.setRotation(avatarWorldOrientation * Quaternions::Y_180); - if (delay < PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS) { - properties.setPosition(glm::vec3(avatarWorldPosition.x, - (-((PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS - delay) / PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS)) * avatarScale * 2.0 + - avatarWorldPosition.y + avatarScale * PRIVACY_SHIELD_HEIGHT_SCALE, avatarWorldPosition.z)); - properties.setModelScale(glm::vec3(2.0, - ((1 - ((PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS - delay) / PRIVACY_SHIELD_RAISE_ANIMATION_DURATION_MS)) * - (0.5 * (avatarScale + 1.0))), 2.0)); - } else { - properties.setPosition(glm::vec3(avatarWorldPosition.x, avatarWorldPosition.y + avatarScale * PRIVACY_SHIELD_HEIGHT_SCALE, avatarWorldPosition.z)); - properties.setModelScale(glm::vec3(2.0, 0.5 * (avatarScale + 1.0), 2.0)); - } - DependencyManager::get()->editEntity(_localPrivacyShieldID, properties); - } - else { - hidePrivacyShield(); - if (_updateConnected) { - _updateConnected = false; - } - } - } -} - -void PrivacyShield::enteredIgnoreRadius() { - showPrivacyShield(); - DependencyManager::get()->privacyShieldActivated(); -} - -void PrivacyShield::onPrivacyShieldToggled(bool enabled, bool doNotLog) { - if (!doNotLog) { - DependencyManager::get()->privacyShieldToggled(enabled); - } - if (enabled) { - showPrivacyShield(); - } else { - hidePrivacyShield(); - if (_updateConnected) { - _updateConnected = false; - } - } -} - -void PrivacyShield::showPrivacyShield() { - auto now = usecTimestampNow(); - auto myAvatar = DependencyManager::get()->getMyAvatar(); - auto avatarScale = myAvatar->getTargetScale(); - auto avatarSensorToWorldScale = myAvatar->getSensorToWorldScale(); - auto avatarWorldPosition = myAvatar->getWorldPosition(); - auto avatarWorldOrientation = myAvatar->getWorldOrientation(); - if (now - _lastPrivacyShieldSoundTimestamp >= PRIVACY_SHIELD_SOUND_RATE_LIMIT_MS) { - AudioInjectorOptions options; - options.position = avatarWorldPosition; - options.localOnly = true; - options.volume = 0.2f; - AudioInjector::playSoundAndDelete(_bubbleActivateSound, options); - _lastPrivacyShieldSoundTimestamp = now; - } - hidePrivacyShield(); - if (_updateConnected) { - _updateConnected = false; - } - - EntityItemProperties properties; - properties.setDimensions(glm::vec3(avatarSensorToWorldScale, 0.75 * avatarSensorToWorldScale, avatarSensorToWorldScale)); - properties.setPosition(glm::vec3(avatarWorldPosition.x, - -avatarScale * 2 + avatarWorldPosition.y + avatarScale * PRIVACY_SHIELD_HEIGHT_SCALE, avatarWorldPosition.z)); - properties.setModelScale(glm::vec3(2.0, 0.5 * (avatarScale + 1.0), 2.0)); - properties.setVisible(true); - - DependencyManager::get()->editEntity(_localPrivacyShieldID, properties); - - _privacyShieldTimestamp = now; - _updateConnected = true; -} - -void PrivacyShield::hidePrivacyShield() { - EntityTreePointer entityTree = qApp->getEntities()->getTree(); - EntityItemPointer privacyShieldEntity = entityTree->findEntityByEntityItemID(EntityItemID(_localPrivacyShieldID)); - if (privacyShieldEntity) { - privacyShieldEntity->setVisible(false); - } -} From 390ce9bb26951a825942e6589af8f0ef0e66d491 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Fri, 22 Mar 2019 15:34:06 -0700 Subject: [PATCH 317/446] wip for getting AvatarInputsBar to switch accordingly --- interface/resources/qml/AvatarInputsBar.qml | 21 ++++++++++++++----- interface/resources/qml/BubbleIcon.qml | 14 +++++++++++-- interface/resources/qml/hifi/audio/MicBar.qml | 15 ++++++------- .../qml/hifi/audio/MicBarApplication.qml | 15 ++++++------- .../resources/qml/hifi/tablet/TabletHome.qml | 2 +- interface/src/Application.cpp | 4 ++-- scripts/defaultScripts.js | 3 +-- .../createAvatarInputsBarEntity.js | 0 scripts/system/away.js | 4 ++-- 9 files changed, 50 insertions(+), 28 deletions(-) rename scripts/{system => developer}/createAvatarInputsBarEntity.js (100%) diff --git a/interface/resources/qml/AvatarInputsBar.qml b/interface/resources/qml/AvatarInputsBar.qml index dfff103aa0..adeb74242d 100644 --- a/interface/resources/qml/AvatarInputsBar.qml +++ b/interface/resources/qml/AvatarInputsBar.qml @@ -18,20 +18,31 @@ Item { id: root; objectName: "AvatarInputsBar" property int modality: Qt.NonModal - readonly property bool ignoreRadiusEnabled: AvatarInputs.ignoreRadiusEnabled - width: audio.width; - height: audio.height; + readonly property bool ignoreRadiusEnabled: AvatarInputs.ignoreRadiusEnabled; + width: HMD.active ? audio.width : audioApplication.width; + height: HMD.active ? audio.height : audioApplication.height; x: 10; y: 5; readonly property bool shouldReposition: true; - HifiAudio.MicBarApplication { + HifiAudio.MicBar { id: audio; - visible: AvatarInputs.showAudioTools; + visible: AvatarInputs.showAudioTools && HMD.active; + standalone: true; + dragTarget: parent; + } + + HifiAudio.MicBarApplication { + id: audioApplication; + visible: AvatarInputs.showAudioTools && !HMD.active; + onVisibleChanged: { + console.log("visible changed: " + visible); + } standalone: true; dragTarget: parent; } BubbleIcon { dragTarget: parent + visible: !HMD.active; } } diff --git a/interface/resources/qml/BubbleIcon.qml b/interface/resources/qml/BubbleIcon.qml index f9c57697f0..1ad73f6179 100644 --- a/interface/resources/qml/BubbleIcon.qml +++ b/interface/resources/qml/BubbleIcon.qml @@ -19,8 +19,16 @@ Rectangle { width: bubbleIcon.width + 10 height: bubbleIcon.height + 10 radius: 5; - opacity: AvatarInputs.ignoreRadiusEnabled ? 0.7 : 0.3; property var dragTarget: null; + property bool ignoreRadiusEnabled: AvatarInputs.ignoreRadiusEnabled; + + onIgnoreRadiusEnabledChanged: { + if (ignoreRadiusEnabled) { + bubbleRect.opacity = 0.7; + } else { + bubbleRect.opacity = 0.3; + } + } color: "#00000000"; border { @@ -58,15 +66,17 @@ Rectangle { } drag.target: dragTarget; onContainsMouseChanged: { + var rectOpacity = ignoreRadiusEnabled ? (containsMouse ? 0.9 : 0.7) : (containsMouse ? 0.5 : 0.3); if (containsMouse) { Tablet.playSound(TabletEnums.ButtonHover); } + bubbleRect.opacity = rectOpacity; } } Image { id: bubbleIcon source: "../icons/tablet-icons/bubble-i.svg"; - sourceSize: Qt.size(28, 28); + sourceSize: Qt.size(32, 32); smooth: true; anchors.top: parent.top anchors.topMargin: (parent.height - bubbleIcon.height) / 2 diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index fb52f8bc5e..71e3764826 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -16,6 +16,7 @@ import stylesUit 1.0 import TabletScriptingInterface 1.0 Rectangle { + id: micBar HifiConstants { id: hifi; } readonly property var level: AudioScriptingInterface.inputLevel; @@ -72,7 +73,7 @@ Rectangle { hoverEnabled: true; scrollGestureEnabled: false; onClicked: { - if (AudioScriptingInterface.pushToTalk) { + if (pushToTalk) { return; } muted = !muted; @@ -98,7 +99,7 @@ Rectangle { readonly property string red: colors.muted; readonly property string fill: "#55000000"; readonly property string border: standalone ? "#80FFFFFF" : "#55FFFFFF"; - readonly property string icon: muted ? muted : unmuted; + readonly property string icon: micBar.muted ? muted : unmuted; } Item { @@ -122,7 +123,7 @@ Rectangle { readonly property string gatedIcon: "../../../icons/tablet-icons/mic-gate-i.svg"; id: image; - source: (pushToTalk && !pushingToTalk) ? pushToTalkIcon : muted ? mutedIcon : + source: (pushToTalk && !pushingToTalk) ? pushToTalkIcon : muted ? mutedIcon : clipping ? clippingIcon : gated ? gatedIcon : unmutedIcon; width: 30; @@ -146,9 +147,9 @@ Rectangle { Item { id: status; - readonly property string color: muted ? colors.muted : colors.unmuted; + readonly property string color: colors.icon; - visible: (pushToTalk && !pushingToTalk) || (muted && (level >= userSpeakingLevel)); + visible: (pushToTalk && !pushingToTalk) || muted; anchors { left: parent.left; @@ -177,7 +178,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - width: AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk ? (HMD.active ? 27 : 25) : 50; + width: pushToTalk && !pushingToTalk ? (HMD.active ? 27 : 25) : 50; height: 4; color: parent.color; } @@ -188,7 +189,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - width: AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk ? (HMD.active ? 27 : 25) : 50; + width: pushToTalk && !pushingToTalk ? (HMD.active ? 27 : 25) : 50; height: 4; color: parent.color; } diff --git a/interface/resources/qml/hifi/audio/MicBarApplication.qml b/interface/resources/qml/hifi/audio/MicBarApplication.qml index bfac278ee4..f89ada6e49 100644 --- a/interface/resources/qml/hifi/audio/MicBarApplication.qml +++ b/interface/resources/qml/hifi/audio/MicBarApplication.qml @@ -126,7 +126,7 @@ Rectangle { Item { Image { id: image; - source: (pushToTalk && !pushingToTalk) ? pushToTalkIcon : muted ? mutedIcon : + source: (pushToTalk) ? pushToTalkIcon : muted ? mutedIcon : clipping ? clippingIcon : gated ? gatedIcon : unmutedIcon; width: 29; height: 32; @@ -141,8 +141,7 @@ Rectangle { id: imageOverlay anchors { fill: image } source: image; - color: (pushToTalk && !pushingToTalk) ? ((level >= userSpeakingLevel) ? colors.mutedColor : - colors.unmutedColor) : colors.icon; + color: pushToTalk ? (pushingToTalk ? colors.unmutedColor : colors.mutedColor) : colors.icon; } } } @@ -150,12 +149,12 @@ Rectangle { Item { id: status; - visible: (pushToTalk && !pushingToTalk) || (muted && (level >= userSpeakingLevel)); + visible: pushToTalk || (muted && (level >= userSpeakingLevel)); anchors { left: parent.left; top: icon.bottom; - topMargin: 5; + topMargin: 2; } width: parent.width; @@ -174,10 +173,10 @@ Rectangle { verticalCenter: parent.verticalCenter; } - color: (level >= userSpeakingLevel && muted) ? colors.mutedColor : colors.unmutedColor; + color: pushToTalk ? (pushingToTalk ? colors.unmutedColor : colors.mutedColor) : (level >= userSpeakingLevel && muted) ? colors.mutedColor : colors.unmutedColor; font.bold: true - text: (pushToTalk && !pushingToTalk) ? (HMD.active ? "PTT" : "PTT-(T)") : (muted ? "MUTED" : "MUTE"); + text: pushToTalk ? (HMD.active ? "PTT" : "PTT-(T)") : (muted ? "MUTED" : "MUTE"); size: 12; } } @@ -204,6 +203,7 @@ Rectangle { Rectangle { // mask id: mask; + visible: (!(pushToTalk && !pushingToTalk)) height: parent.height * level; width: parent.width; radius: 5; @@ -217,6 +217,7 @@ Rectangle { LinearGradient { anchors { fill: mask } + visible: (!(pushToTalk && !pushingToTalk)) source: mask start: Qt.point(0, 0); end: Qt.point(0, bar.height); diff --git a/interface/resources/qml/hifi/tablet/TabletHome.qml b/interface/resources/qml/hifi/tablet/TabletHome.qml index a1da69a44a..1a1e0a96ff 100644 --- a/interface/resources/qml/hifi/tablet/TabletHome.qml +++ b/interface/resources/qml/hifi/tablet/TabletHome.qml @@ -40,7 +40,7 @@ Item { } } - HifiAudio.MicBar { + HifiAudio.MicBarApplication { anchors { left: parent.left leftMargin: 30 diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 5b0c379c64..734eb7221b 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -339,7 +339,7 @@ Setting::Handle maxOctreePacketsPerSecond{"maxOctreePPS", DEFAULT_MAX_OCTRE Setting::Handle loginDialogPoppedUp{"loginDialogPoppedUp", false}; static const QUrl AVATAR_INPUTS_BAR_QML = PathUtils::qmlUrl("AvatarInputsBar.qml"); -static const QUrl MIC_BAR_ENTITY_QML = PathUtils::qmlUrl("hifi/audio/MicBarApplication.qml"); +static const QUrl MIC_BAR_APPLICATION_QML = PathUtils::qmlUrl("hifi/audio/MicBarApplication.qml"); static const QUrl BUBBLE_ICON_QML = PathUtils::qmlUrl("BubbleIcon.qml"); static const QString STANDARD_TO_ACTION_MAPPING_NAME = "Standard to Action"; @@ -2396,7 +2396,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo }); auto rootItemLoadedFunctor = [webSurface, url, isTablet] { Application::setupQmlSurface(webSurface->getSurfaceContext(), isTablet || url == LOGIN_DIALOG.toString() || url == AVATAR_INPUTS_BAR_QML.toString() || - url == BUBBLE_ICON_QML.toString() || url == MIC_BAR_ENTITY_QML.toString() ); + url == BUBBLE_ICON_QML.toString()); }; if (webSurface->getRootItem()) { rootItemLoadedFunctor(); diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 0d9799a035..bd7e79dffc 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -32,8 +32,7 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/firstPersonHMD.js", "system/tablet-ui/tabletUI.js", "system/emote.js", - "system/miniTablet.js", - "system/createAvatarInputsBarEntity.js" + "system/miniTablet.js" ]; var DEFAULT_SCRIPTS_SEPARATE = [ "system/controllers/controllerScripts.js", diff --git a/scripts/system/createAvatarInputsBarEntity.js b/scripts/developer/createAvatarInputsBarEntity.js similarity index 100% rename from scripts/system/createAvatarInputsBarEntity.js rename to scripts/developer/createAvatarInputsBarEntity.js diff --git a/scripts/system/away.js b/scripts/system/away.js index 2af43b2055..e45041139a 100644 --- a/scripts/system/away.js +++ b/scripts/system/away.js @@ -171,7 +171,7 @@ function goAway(fromStartup) { if (!previousBubbleState) { Users.toggleIgnoreRadius(); } - UserActivityLogger.bubbleToggled(Users.getIgnoreRadiusEnabled()); + UserActivityLogger.privacyShieldToggled(Users.getIgnoreRadiusEnabled()); UserActivityLogger.toggledAway(true); MyAvatar.isAway = true; } @@ -186,7 +186,7 @@ function goActive() { if (Users.getIgnoreRadiusEnabled() !== previousBubbleState) { Users.toggleIgnoreRadius(); - UserActivityLogger.bubbleToggled(Users.getIgnoreRadiusEnabled()); + UserActivityLogger.privacyShieldToggled(Users.getIgnoreRadiusEnabled()); } if (!Window.hasFocus()) { From 137c25f907711cc3f926d06b40c463d06518e004 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Fri, 22 Mar 2019 15:40:13 -0700 Subject: [PATCH 318/446] Use a 1 m offset for position test; call-out nonavatars in stats web page --- assignment-client/src/avatars/AvatarMixer.cpp | 8 +++++++- assignment-client/src/avatars/AvatarMixerClientData.cpp | 8 ++++---- assignment-client/src/avatars/AvatarMixerClientData.h | 1 + 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index ffe084bc33..c6cd12f30a 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -843,7 +843,7 @@ void AvatarMixer::sendStatsPacket() { QJsonObject avatarsObject; auto nodeList = DependencyManager::get(); - // add stats for each listerner + // add stats for each listener nodeList->eachNode([&](const SharedNodePointer& node) { QJsonObject avatarStats; @@ -867,6 +867,12 @@ void AvatarMixer::sendStatsPacket() { avatarStats["delta_full_vs_avatar_data_kbps"] = (double)outboundAvatarDataKbps - avatarStats[OUTBOUND_AVATAR_DATA_STATS_KEY].toDouble(); } + + if (node->getType() != NodeType::Agent) { // Nodes that aren't avatars + const QString displayName + { node->getType() == NodeType::EntityScriptServer ? "ENTITY SCRIPT SERVER" : "ENTITY SERVER" }; + avatarStats["display_name"] = displayName; + } } avatarsObject[uuidStringWithoutCurlyBraces(node->getUUID())] = avatarStats; diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index a6b675efa4..4880f73226 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -23,6 +23,9 @@ #include "AvatarMixerSlave.h" +// Offset from reported position for priority-zone purposes: +const glm::vec3 AvatarMixerClientData::AVATAR_CENTER_OFFSET { 0.0f, 1.0f, 0.0 }; + AvatarMixerClientData::AvatarMixerClientData(const QUuid& nodeID, Node::LocalID nodeLocalID) : NodeData(nodeID, nodeLocalID) { // in case somebody calls getSessionUUID on the AvatarData instance, make sure it has the right ID @@ -143,13 +146,10 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared auto newPosition = getPosition(); if (newPosition != oldPosition || _avatar->getNeedsHeroCheck()) { EntityTree& entityTree = *slaveSharedData.entityTree; - FindPriorityZone findPriorityZone { newPosition, false } ; + FindPriorityZone findPriorityZone { newPosition + AVATAR_CENTER_OFFSET } ; entityTree.recurseTreeWithOperation(&FindPriorityZone::operation, &findPriorityZone); _avatar->setHasPriority(findPriorityZone.isInPriorityZone); _avatar->setNeedsHeroCheck(false); - if (findPriorityZone.isInPriorityZone) { - qCWarning(avatars) << "Avatar" << _avatar->getSessionDisplayName() << "in hero zone"; - } } return true; diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index 98c8d7e15b..492dfc4720 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -223,6 +223,7 @@ private: PerNodeTraitVersions _perNodeSentTraitVersions; std::atomic_bool _isIgnoreRadiusEnabled { false }; + static const glm::vec3 AVATAR_CENTER_OFFSET; }; #endif // hifi_AvatarMixerClientData_h From 55b3b5034a0dad81c70dc499033ac478fc3d6a4a Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 22 Mar 2019 15:47:13 -0700 Subject: [PATCH 319/446] always update OwingAvatarID of AvatarEntities --- interface/src/avatar/MyAvatar.cpp | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 298e661f24..4ea0d37710 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3454,11 +3454,6 @@ void MyAvatar::setSessionUUID(const QUuid& sessionUUID) { QUuid oldSessionID = getSessionUUID(); Avatar::setSessionUUID(sessionUUID); QUuid newSessionID = getSessionUUID(); - if (DependencyManager::get()->getSessionUUID().isNull()) { - // we don't actually have a connection to a domain right now - // so there is no need to queue AvatarEntity messages --> bail early - return; - } if (newSessionID != oldSessionID) { auto treeRenderer = DependencyManager::get(); EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; @@ -3467,6 +3462,7 @@ void MyAvatar::setSessionUUID(const QUuid& sessionUUID) { _avatarEntitiesLock.withReadLock([&] { avatarEntityIDs = _packedAvatarEntityData.keys(); }); + bool sendPackets = DependencyManager::get()->getSessionUUID().isNull(); EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender(); entityTree->withWriteLock([&] { for (const auto& entityID : avatarEntityIDs) { @@ -3474,12 +3470,14 @@ void MyAvatar::setSessionUUID(const QUuid& sessionUUID) { if (!entity) { continue; } + // update OwningAvatarID so entity can be identified as "ours" later entity->setOwningAvatarID(newSessionID); - // NOTE: each attached AvatarEntity should already have the correct updated parentID - // via magic in SpatiallyNestable, but when an AvatarEntity IS parented to MyAvatar - // we need to update the "packedAvatarEntityData" we send to the avatar-mixer - // so that others will get the updated state. - if (entity->getParentID() == newSessionID) { + // NOTE: each attached AvatarEntity already have the correct updated parentID + // via magic in SpatiallyNestable, hence we check agains newSessionID + if (sendPackets && entity->getParentID() == newSessionID) { + // but when we have a real session and the AvatarEntity is parented to MyAvatar + // we need to update the "packedAvatarEntityData" sent to the avatar-mixer + // because it contains a stale parentID somewhere deep inside packetSender->queueEditAvatarEntityMessage(entityTree, entityID); } } From 7db498a1308d9147613ae8b1be79845d0647cb67 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 22 Mar 2019 16:20:54 -0700 Subject: [PATCH 320/446] fix shadows when tablet is open and misc threading issues --- .../src/RenderableMaterialEntityItem.cpp | 6 +++++- .../src/RenderableShapeEntityItem.cpp | 18 ++++++++++++++---- .../src/RenderableTextEntityItem.cpp | 8 +++----- libraries/render-utils/src/GeometryCache.h | 11 ++++++----- .../render-utils/src/RenderCommonTask.cpp | 6 +++++- .../render-utils/src/RenderDeferredTask.cpp | 8 ++++---- .../render-utils/src/RenderDeferredTask.h | 10 +++++----- .../render-utils/src/RenderForwardTask.cpp | 10 +++++++--- libraries/render-utils/src/RenderForwardTask.h | 3 ++- .../src/simple_transparent_textured.slf | 4 ++-- 10 files changed, 53 insertions(+), 31 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp index 2eb877b0e1..15842336f5 100644 --- a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp @@ -186,7 +186,11 @@ void MaterialEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo if (_networkMaterial->isLoaded()) { onMaterialRequestFinished(!_networkMaterial->isFailed()); } else { - connect(_networkMaterial.data(), &Resource::finished, this, onMaterialRequestFinished); + connect(_networkMaterial.data(), &Resource::finished, this, [&](bool success) { + withWriteLock([&] { + onMaterialRequestFinished(success); + }); + }); } } } else if (materialDataChanged && usingMaterialData) { diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index b33eb619c8..28b0bb8f23 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -249,10 +249,14 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) { graphics::MultiMaterial materials; auto geometryCache = DependencyManager::get(); GeometryCache::Shape geometryShape; + PrimitiveMode primitiveMode; + RenderLayer renderLayer; bool proceduralRender = false; glm::vec4 outColor; withReadLock([&] { geometryShape = geometryCache->getShapeForEntityShape(_shape); + primitiveMode = _primitiveMode; + renderLayer = _renderLayer; batch.setModelTransform(_renderTransform); // use a transform with scale, rotation, registration point and translation materials = _materials["0"]; auto& schema = materials.getSchemaBuffer().get(); @@ -267,7 +271,7 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) { }); if (proceduralRender) { - if (render::ShapeKey(args->_globalShapeKey).isWireframe()) { + if (render::ShapeKey(args->_globalShapeKey).isWireframe() || primitiveMode == PrimitiveMode::LINES) { geometryCache->renderWireShape(batch, geometryShape, outColor); } else { geometryCache->renderShape(batch, geometryShape, outColor); @@ -275,10 +279,16 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) { } else if (!useMaterialPipeline(materials)) { // FIXME, support instanced multi-shape rendering using multidraw indirect outColor.a *= _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; - if (render::ShapeKey(args->_globalShapeKey).isWireframe() || _primitiveMode == PrimitiveMode::LINES) { - geometryCache->renderWireShapeInstance(args, batch, geometryShape, outColor, args->_shapePipeline); + render::ShapePipelinePointer pipeline; + if (renderLayer == RenderLayer::WORLD) { + pipeline = GeometryCache::getShapePipeline(false, outColor.a < 1.0f, true, false); } else { - geometryCache->renderSolidShapeInstance(args, batch, geometryShape, outColor, args->_shapePipeline); + pipeline = GeometryCache::getShapePipeline(false, outColor.a < 1.0f, true, false, false, true); + } + if (render::ShapeKey(args->_globalShapeKey).isWireframe() || primitiveMode == PrimitiveMode::LINES) { + geometryCache->renderWireShapeInstance(args, batch, geometryShape, outColor, pipeline); + } else { + geometryCache->renderSolidShapeInstance(args, batch, geometryShape, outColor, pipeline); } } else { if (args->_renderMode != render::Args::RenderMode::SHADOW_RENDER_MODE) { diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp index a3e1a2f56d..81f367a956 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp @@ -162,10 +162,12 @@ void TextEntityRenderer::doRender(RenderArgs* args) { glm::vec4 backgroundColor; Transform modelTransform; glm::vec3 dimensions; + BillboardMode billboardMode; bool layered; withReadLock([&] { modelTransform = _renderTransform; dimensions = _dimensions; + billboardMode = _billboardMode; float fadeRatio = _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; textColor = glm::vec4(_textColor, fadeRatio * _textAlpha); @@ -190,7 +192,7 @@ void TextEntityRenderer::doRender(RenderArgs* args) { } auto transformToTopLeft = modelTransform; - transformToTopLeft.setRotation(EntityItem::getBillboardRotation(transformToTopLeft.getTranslation(), transformToTopLeft.getRotation(), _billboardMode, args->getViewFrustum().getPosition())); + transformToTopLeft.setRotation(EntityItem::getBillboardRotation(transformToTopLeft.getTranslation(), transformToTopLeft.getRotation(), billboardMode, args->getViewFrustum().getPosition())); transformToTopLeft.postTranslate(dimensions * glm::vec3(-0.5f, 0.5f, 0.0f)); // Go to the top left transformToTopLeft.setScale(1.0f); // Use a scale of one so that the text is not deformed @@ -210,10 +212,6 @@ void TextEntityRenderer::doRender(RenderArgs* args) { glm::vec2 bounds = glm::vec2(dimensions.x - (_leftMargin + _rightMargin), dimensions.y - (_topMargin + _bottomMargin)); _textRenderer->draw(batch, _leftMargin / scale, -_topMargin / scale, _text, textColor, bounds / scale, layered); } - - if (layered) { - DependencyManager::get()->unsetKeyLightBatch(batch); - } } QSizeF TextEntityRenderer::textSize(const QString& text) const { diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index cd3454bf38..e84f2e25a4 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -368,6 +368,12 @@ public: const ShapeData * getShapeData(Shape shape) const; graphics::MeshPointer meshFromShape(Shape geometryShape, glm::vec3 color); + + static render::ShapePipelinePointer getShapePipeline(bool textured = false, bool transparent = false, bool culled = true, + bool unlit = false, bool depthBias = false, bool forward = false); + static render::ShapePipelinePointer getFadingShapePipeline(bool textured = false, bool transparent = false, bool culled = true, + bool unlit = false, bool depthBias = false); + private: GeometryCache(); @@ -471,11 +477,6 @@ private: gpu::PipelinePointer _simpleOpaqueWebBrowserPipeline; gpu::ShaderPointer _simpleTransparentWebBrowserShader; gpu::PipelinePointer _simpleTransparentWebBrowserPipeline; - - static render::ShapePipelinePointer getShapePipeline(bool textured = false, bool transparent = false, bool culled = true, - bool unlit = false, bool depthBias = false, bool forward = false); - static render::ShapePipelinePointer getFadingShapePipeline(bool textured = false, bool transparent = false, bool culled = true, - bool unlit = false, bool depthBias = false); }; #endif // hifi_GeometryCache_h diff --git a/libraries/render-utils/src/RenderCommonTask.cpp b/libraries/render-utils/src/RenderCommonTask.cpp index 385e384efe..b1a62625b2 100644 --- a/libraries/render-utils/src/RenderCommonTask.cpp +++ b/libraries/render-utils/src/RenderCommonTask.cpp @@ -95,7 +95,11 @@ void DrawLayered3D::run(const RenderContextPointer& renderContext, const Inputs& // Setup lighting model for all items; batch.setUniformBuffer(ru::Buffer::LightModel, lightingModel->getParametersBuffer()); - renderShapes(renderContext, _shapePlumber, inItems, _maxDrawn); + if (_opaquePass) { + renderStateSortShapes(renderContext, _shapePlumber, inItems, _maxDrawn); + } else { + renderShapes(renderContext, _shapePlumber, inItems, _maxDrawn); + } args->_batch = nullptr; }); } diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 089d267711..ea2b05a6fa 100644 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -216,8 +216,8 @@ void RenderDeferredTask::build(JobModel& task, const render::Varying& input, ren task.addJob("DrawHazeDeferred", drawHazeInputs); // Render transparent objects forward in LightingBuffer - const auto transparentsInputs = DrawDeferred::Inputs(transparents, hazeFrame, lightFrame, lightingModel, lightClusters, shadowFrame, jitter).asVarying(); - task.addJob("DrawTransparentDeferred", transparentsInputs, shapePlumber); + const auto transparentsInputs = RenderTransparentDeferred::Inputs(transparents, hazeFrame, lightFrame, lightingModel, lightClusters, shadowFrame, jitter).asVarying(); + task.addJob("DrawTransparentDeferred", transparentsInputs, shapePlumber); const auto outlineRangeTimer = task.addJob("BeginHighlightRangeTimer", "Highlight"); @@ -436,7 +436,7 @@ void RenderDeferredTaskDebug::build(JobModel& task, const render::Varying& input } -void DrawDeferred::run(const RenderContextPointer& renderContext, const Inputs& inputs) { +void RenderTransparentDeferred::run(const RenderContextPointer& renderContext, const Inputs& inputs) { assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); @@ -453,7 +453,7 @@ void DrawDeferred::run(const RenderContextPointer& renderContext, const Inputs& RenderArgs* args = renderContext->args; - gpu::doInBatch("DrawDeferred::run", args->_context, [&](gpu::Batch& batch) { + gpu::doInBatch("RenderTransparentDeferred::run", args->_context, [&](gpu::Batch& batch) { args->_batch = &batch; // Setup camera, projection and viewport for all items diff --git a/libraries/render-utils/src/RenderDeferredTask.h b/libraries/render-utils/src/RenderDeferredTask.h index 0a188ec3a6..3eb1153928 100644 --- a/libraries/render-utils/src/RenderDeferredTask.h +++ b/libraries/render-utils/src/RenderDeferredTask.h @@ -19,7 +19,7 @@ #include "LightClusters.h" #include "RenderShadowTask.h" -class DrawDeferredConfig : public render::Job::Config { +class RenderTransparentDeferredConfig : public render::Job::Config { Q_OBJECT Q_PROPERTY(int numDrawn READ getNumDrawn NOTIFY newStats) Q_PROPERTY(int maxDrawn MEMBER maxDrawn NOTIFY dirty) @@ -41,13 +41,13 @@ protected: int _numDrawn{ 0 }; }; -class DrawDeferred { +class RenderTransparentDeferred { public: using Inputs = render::VaryingSet7; - using Config = DrawDeferredConfig; - using JobModel = render::Job::ModelI; + using Config = RenderTransparentDeferredConfig; + using JobModel = render::Job::ModelI; - DrawDeferred(render::ShapePlumberPointer shapePlumber) + RenderTransparentDeferred(render::ShapePlumberPointer shapePlumber) : _shapePlumber{ shapePlumber } {} void configure(const Config& config) { _maxDrawn = config.maxDrawn; } diff --git a/libraries/render-utils/src/RenderForwardTask.cpp b/libraries/render-utils/src/RenderForwardTask.cpp index 73692b41c2..0bc117bdb9 100755 --- a/libraries/render-utils/src/RenderForwardTask.cpp +++ b/libraries/render-utils/src/RenderForwardTask.cpp @@ -98,7 +98,7 @@ void RenderForwardTask::build(JobModel& task, const render::Varying& input, rend // Draw opaques forward const auto opaqueInputs = DrawForward::Inputs(opaques, lightingModel).asVarying(); - task.addJob("DrawOpaques", opaqueInputs, shapePlumber); + task.addJob("DrawOpaques", opaqueInputs, shapePlumber, true); // Similar to light stage, background stage has been filled by several potential render items and resolved for the frame in this job const auto backgroundInputs = DrawBackgroundStage::Inputs(lightingModel, backgroundFrame).asVarying(); @@ -106,7 +106,7 @@ void RenderForwardTask::build(JobModel& task, const render::Varying& input, rend // Draw transparent objects forward const auto transparentInputs = DrawForward::Inputs(transparents, lightingModel).asVarying(); - task.addJob("DrawTransparents", transparentInputs, shapePlumber); + task.addJob("DrawTransparents", transparentInputs, shapePlumber, false); // Layered const auto nullJitter = Varying(glm::vec2(0.0f, 0.0f)); @@ -261,7 +261,11 @@ void DrawForward::run(const RenderContextPointer& renderContext, const Inputs& i args->_globalShapeKey = globalKey._flags.to_ulong(); // Render items - renderStateSortShapes(renderContext, _shapePlumber, inItems, -1, globalKey); + if (_opaquePass) { + renderStateSortShapes(renderContext, _shapePlumber, inItems, -1, globalKey); + } else { + renderShapes(renderContext, _shapePlumber, inItems, -1, globalKey); + } args->_batch = nullptr; args->_globalShapeKey = 0; diff --git a/libraries/render-utils/src/RenderForwardTask.h b/libraries/render-utils/src/RenderForwardTask.h index 85b51ad5fa..40d004ddb2 100755 --- a/libraries/render-utils/src/RenderForwardTask.h +++ b/libraries/render-utils/src/RenderForwardTask.h @@ -76,12 +76,13 @@ public: using Inputs = render::VaryingSet2; using JobModel = render::Job::ModelI; - DrawForward(const render::ShapePlumberPointer& shapePlumber) : _shapePlumber(shapePlumber) {} + DrawForward(const render::ShapePlumberPointer& shapePlumber, bool opaquePass) : _shapePlumber(shapePlumber), _opaquePass(opaquePass) {} void run(const render::RenderContextPointer& renderContext, const Inputs& inputs); private: render::ShapePlumberPointer _shapePlumber; + bool _opaquePass; }; #endif // hifi_RenderForwardTask_h diff --git a/libraries/render-utils/src/simple_transparent_textured.slf b/libraries/render-utils/src/simple_transparent_textured.slf index f1bb2b1ea2..9f8a88c7c2 100644 --- a/libraries/render-utils/src/simple_transparent_textured.slf +++ b/libraries/render-utils/src/simple_transparent_textured.slf @@ -16,8 +16,8 @@ <@include gpu/Color.slh@> <@include render-utils/ShaderConstants.h@> -<@include ForwardGlobalLight.slh@> -<$declareEvalGlobalLightingAlphaBlended()$> +<@include DeferredGlobalLight.slh@> +<$declareEvalGlobalLightingAlphaBlendedWithHaze()$> <@include gpu/Transform.slh@> <$declareStandardCameraTransform()$> From 3ea45de7c73b87e34bea7707f9c36368a011cebc Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Fri, 22 Mar 2019 13:16:56 -0700 Subject: [PATCH 321/446] fixing qml bug in MicBar --- interface/resources/qml/AvatarInputsBar.qml | 35 ++++++++++++++----- interface/resources/qml/hifi/audio/MicBar.qml | 2 +- interface/src/scripting/Audio.cpp | 3 ++ 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/interface/resources/qml/AvatarInputsBar.qml b/interface/resources/qml/AvatarInputsBar.qml index adeb74242d..d975312aad 100644 --- a/interface/resources/qml/AvatarInputsBar.qml +++ b/interface/resources/qml/AvatarInputsBar.qml @@ -19,30 +19,49 @@ Item { objectName: "AvatarInputsBar" property int modality: Qt.NonModal readonly property bool ignoreRadiusEnabled: AvatarInputs.ignoreRadiusEnabled; - width: HMD.active ? audio.width : audioApplication.width; - height: HMD.active ? audio.height : audioApplication.height; x: 10; y: 5; readonly property bool shouldReposition: true; + property bool hmdActive: HMD.active; + width: hmdActive ? audio.width : audioApplication.width; + height: hmdActive ? audio.height : audioApplication.height; + + onHmdActiveChanged: { + console.log("hmd active = " + hmdActive); + } + + Timer { + id: hmdActiveCheckTimer; + interval: 500; + repeat: true; + onTriggered: { + root.hmdActive = HMD.active; + } + + } HifiAudio.MicBar { id: audio; - visible: AvatarInputs.showAudioTools && HMD.active; + visible: AvatarInputs.showAudioTools && root.hmdActive; standalone: true; dragTarget: parent; } HifiAudio.MicBarApplication { id: audioApplication; - visible: AvatarInputs.showAudioTools && !HMD.active; - onVisibleChanged: { - console.log("visible changed: " + visible); - } + visible: AvatarInputs.showAudioTools && !root.hmdActive; standalone: true; dragTarget: parent; } + + Component.onCompleted: { + HMD.displayModeChanged.connect(function(isHmdMode) { + root.hmdActive = isHmdMode; + }); + } + BubbleIcon { dragTarget: parent - visible: !HMD.active; + visible: !root.hmdActive; } } diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index 71e3764826..4b243e033a 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -19,9 +19,9 @@ Rectangle { id: micBar HifiConstants { id: hifi; } + property var muted: AudioScriptingInterface.muted; readonly property var level: AudioScriptingInterface.inputLevel; readonly property var clipping: AudioScriptingInterface.clipping; - readonly property var muted: AudioScriptingInterface.muted; readonly property var pushToTalk: AudioScriptingInterface.pushToTalk; readonly property var pushingToTalk: AudioScriptingInterface.pushingToTalk; diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 434688e474..0e0d13ae45 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -228,6 +228,9 @@ void Audio::loadData() { setMutedHMD(_hmdMutedSetting.get()); setPTTDesktop(_pttDesktopSetting.get()); setPTTHMD(_pttHMDSetting.get()); + + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted()), Q_ARG(bool, false)); } bool Audio::getPTTHMD() const { From b98eda4674944f6d8eb210c1aad11aa743cf9716 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Fri, 22 Mar 2019 16:42:37 -0700 Subject: [PATCH 322/446] removing debug statemeng --- interface/resources/qml/AvatarInputsBar.qml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/interface/resources/qml/AvatarInputsBar.qml b/interface/resources/qml/AvatarInputsBar.qml index d975312aad..3f1f598991 100644 --- a/interface/resources/qml/AvatarInputsBar.qml +++ b/interface/resources/qml/AvatarInputsBar.qml @@ -26,10 +26,6 @@ Item { width: hmdActive ? audio.width : audioApplication.width; height: hmdActive ? audio.height : audioApplication.height; - onHmdActiveChanged: { - console.log("hmd active = " + hmdActive); - } - Timer { id: hmdActiveCheckTimer; interval: 500; From d7a1ecdbb3ee5b808acb4f2d95b104e72eb568ca Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Fri, 22 Mar 2019 17:19:39 -0700 Subject: [PATCH 323/446] Expose hero-status to scripts --- interface/src/avatar/MyAvatar.h | 1 + libraries/avatars/src/AvatarData.cpp | 7 ++++--- libraries/avatars/src/AvatarData.h | 2 ++ libraries/avatars/src/ScriptAvatarData.cpp | 8 ++++++++ libraries/avatars/src/ScriptAvatarData.h | 4 ++++ 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index aadc8ee268..ccfa629fea 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -184,6 +184,7 @@ class MyAvatar : public Avatar { * @property {Mat4} controllerLeftHandMatrix Read-only. * @property {Mat4} controllerRightHandMatrix Read-only. * @property {number} sensorToWorldScale Read-only. + * @property {boolean} hasPriority - is the avatar in a Hero zone? Read-only */ // FIXME: `glm::vec3 position` is not accessible from QML, so this exposes position in a QML-native type Q_PROPERTY(QVector3D qmlPosition READ getQmlPosition) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 26407c3564..3355228eea 100755 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1143,10 +1143,11 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { // we store the hand state as well as other items in a shared bitset. The hand state is an octal, but is split // into two sections to maintain backward compatibility. The bits are ordered as such (0-7 left to right). // AA 6/1/18 added three more flags bits 8,9, and 10 for procedural audio, blink, and eye saccade enabled - // +---+-----+-----+--+--+--+--+-----+ - // |x,x|H0,H1|x,x,x|H2|Au|Bl|Ey|xxxxx| - // +---+-----+-----+--+--+--+--+-----+ + // +---+-----+-----+--+--+--+--+--+----+ + // |x,x|H0,H1|x,x,x|H2|Au|Bl|Ey|He|xxxx| + // +---+-----+-----+--+--+--+--+--+----+ // Hand state - H0,H1,H2 is found in the 3rd, 4th, and 8th bits + // Hero-avatar status (He) - 12th bit auto newHandState = getSemiNibbleAt(bitItems, HAND_STATE_START_BIT) + (oneAtBit16(bitItems, HAND_STATE_FINGER_POINTING_BIT) ? IS_FINGER_POINTING_FLAG : 0); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 95bbcbeb16..424384c15e 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -453,6 +453,8 @@ class AvatarData : public QObject, public SpatiallyNestable { Q_PROPERTY(float sensorToWorldScale READ getSensorToWorldScale) + Q_PROPERTY(bool hasPriority READ getHasPriority) + public: virtual QString getName() const override { return QString("Avatar:") + _displayName; } diff --git a/libraries/avatars/src/ScriptAvatarData.cpp b/libraries/avatars/src/ScriptAvatarData.cpp index a716a40ad8..18717c8ca3 100644 --- a/libraries/avatars/src/ScriptAvatarData.cpp +++ b/libraries/avatars/src/ScriptAvatarData.cpp @@ -343,6 +343,14 @@ glm::mat4 ScriptAvatarData::getControllerRightHandMatrix() const { // END // +bool ScriptAvatarData::getHasPriority() const { + if (AvatarSharedPointer sharedAvatarData = _avatarData.lock()) { + return sharedAvatarData->getHasPriority(); + } else { + return false; + } +} + glm::quat ScriptAvatarData::getAbsoluteJointRotationInObjectFrame(int index) const { if (AvatarSharedPointer sharedAvatarData = _avatarData.lock()) { return sharedAvatarData->getAbsoluteJointRotationInObjectFrame(index); diff --git a/libraries/avatars/src/ScriptAvatarData.h b/libraries/avatars/src/ScriptAvatarData.h index 91bac61728..01f7ff360a 100644 --- a/libraries/avatars/src/ScriptAvatarData.h +++ b/libraries/avatars/src/ScriptAvatarData.h @@ -68,6 +68,8 @@ class ScriptAvatarData : public QObject { Q_PROPERTY(glm::mat4 controllerLeftHandMatrix READ getControllerLeftHandMatrix) Q_PROPERTY(glm::mat4 controllerRightHandMatrix READ getControllerRightHandMatrix) + Q_PROPERTY(bool hasPriority READ getHasPriority) + public: ScriptAvatarData(AvatarSharedPointer avatarData); @@ -133,6 +135,8 @@ public: glm::mat4 getControllerLeftHandMatrix() const; glm::mat4 getControllerRightHandMatrix() const; + bool getHasPriority() const; + signals: void displayNameChanged(); void sessionDisplayNameChanged(); From e671a124c3c5d6a14510111417f6346c81d8efe4 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 22 Mar 2019 17:58:17 -0700 Subject: [PATCH 324/446] Cleanup --- assignment-client/src/audio/AudioMixerClientData.cpp | 6 +++--- interface/src/scripting/Audio.cpp | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index b8d3ec62a6..41b72c04d2 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -200,11 +200,11 @@ void AudioMixerClientData::parsePerAvatarGainSet(ReceivedMessage& message, const if (avatarUUID.isNull()) { // set the MASTER avatar gain setMasterAvatarGain(gain); - qCDebug(audio) << "Setting MASTER avatar gain for " << uuid << " to " << gain; + qCDebug(audio) << "Setting MASTER avatar gain for" << uuid << "to" << gain; } else { // set the per-source avatar gain setGainForAvatar(avatarUUID, gain); - qCDebug(audio) << "Setting avatar gain adjustment for hrtf[" << uuid << "][" << avatarUUID << "] to " << gain; + qCDebug(audio) << "Setting avatar gain adjustment for hrtf[" << uuid << "][" << avatarUUID << "] to" << gain; } } @@ -216,7 +216,7 @@ void AudioMixerClientData::parseInjectorGainSet(ReceivedMessage& message, const float gain = unpackFloatGainFromByte(packedGain); setMasterInjectorGain(gain); - qCDebug(audio) << "Setting MASTER injector gain for " << uuid << " to " << gain; + qCDebug(audio) << "Setting MASTER injector gain for" << uuid << "to" << gain; } void AudioMixerClientData::setGainForAvatar(QUuid nodeID, float gain) { diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 6dd1c40ef5..ac5ddd6a6a 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -401,13 +401,13 @@ void Audio::setReverbOptions(const AudioEffectOptions* options) { void Audio::setAvatarGain(float gain) { withWriteLock([&] { // ask the NodeList to set the master avatar gain - DependencyManager::get()->setAvatarGain("", gain); + DependencyManager::get()->setAvatarGain(QUuid(), gain); }); } float Audio::getAvatarGain() { return resultWithReadLock([&] { - return DependencyManager::get()->getAvatarGain(""); + return DependencyManager::get()->getAvatarGain(QUuid()); }); } From cbeb4b0b208fc40bc00d32e31bcce3b6903cc7e0 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Sat, 23 Mar 2019 06:48:37 -0700 Subject: [PATCH 325/446] Persist the audio-mixer settings across domain changes and server resets --- libraries/networking/src/NodeList.cpp | 38 ++++++++++++++++++--------- libraries/networking/src/NodeList.h | 3 ++- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index eec710322e..0021a594bc 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -265,8 +265,6 @@ void NodeList::reset(bool skipDomainHandlerReset) { _avatarGainMap.clear(); _avatarGainMapLock.unlock(); - _injectorGain = 0.0f; - if (!skipDomainHandlerReset) { // clear the domain connection information, unless they're the ones that asked us to reset _domainHandler.softReset(); @@ -1018,6 +1016,14 @@ void NodeList::maybeSendIgnoreSetToNode(SharedNodePointer newNode) { // also send them the current ignore radius state. sendIgnoreRadiusStateToNode(newNode); + + // also send the current avatar and injector gains + if (_avatarGain != 0.0f) { + setAvatarGain(QUuid(), _avatarGain); + } + if (_injectorGain != 0.0f) { + setInjectorGain(_injectorGain); + } } if (newNode->getType() == NodeType::AvatarMixer) { // this is a mixer that we just added - it's unlikely it knows who we were previously ignoring in this session, @@ -1064,13 +1070,17 @@ void NodeList::setAvatarGain(const QUuid& nodeID, float gain) { if (nodeID.isNull()) { qCDebug(networking) << "Sending Set MASTER Avatar Gain packet with Gain:" << gain; - } else { - qCDebug(networking) << "Sending Set Avatar Gain packet with UUID: " << uuidStringWithoutCurlyBraces(nodeID) << "Gain:" << gain; - } - sendPacket(std::move(setAvatarGainPacket), *audioMixer); - QWriteLocker lock{ &_avatarGainMapLock }; - _avatarGainMap[nodeID] = gain; + sendPacket(std::move(setAvatarGainPacket), *audioMixer); + _avatarGain = gain; + + } else { + qCDebug(networking) << "Sending Set Avatar Gain packet with UUID:" << uuidStringWithoutCurlyBraces(nodeID) << "Gain:" << gain; + + sendPacket(std::move(setAvatarGainPacket), *audioMixer); + QWriteLocker lock{ &_avatarGainMapLock }; + _avatarGainMap[nodeID] = gain; + } } else { qWarning() << "Couldn't find audio mixer to send set gain request"; @@ -1081,10 +1091,14 @@ void NodeList::setAvatarGain(const QUuid& nodeID, float gain) { } float NodeList::getAvatarGain(const QUuid& nodeID) { - QReadLocker lock{ &_avatarGainMapLock }; - auto it = _avatarGainMap.find(nodeID); - if (it != _avatarGainMap.cend()) { - return it->second; + if (nodeID.isNull()) { + return _avatarGain; + } else { + QReadLocker lock{ &_avatarGainMapLock }; + auto it = _avatarGainMap.find(nodeID); + if (it != _avatarGainMap.cend()) { + return it->second; + } } return 0.0f; } diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index d2a1212d64..f871560fba 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -183,7 +183,8 @@ private: mutable QReadWriteLock _avatarGainMapLock; tbb::concurrent_unordered_map _avatarGainMap; - std::atomic _injectorGain { 0.0f }; + std::atomic _avatarGain { 0.0f }; // in dB + std::atomic _injectorGain { 0.0f }; // in dB void sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode); #if defined(Q_OS_ANDROID) From 649bb92e6c8d628ce4825e2dc946cdf624b981e0 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Sat, 23 Mar 2019 16:00:02 -0700 Subject: [PATCH 326/446] Prototype an updated Audio tab with 3 independent volume controls --- interface/resources/qml/hifi/audio/Audio.qml | 176 +++++++++++++++--- .../qml/hifi/audio/LoopbackAudio.qml | 16 +- .../qml/hifi/audio/PlaySampleSound.qml | 16 +- 3 files changed, 169 insertions(+), 39 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index da306f911b..92dfcb4117 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -85,8 +85,19 @@ Rectangle { } function updateMyAvatarGainFromQML(sliderValue, isReleased) { - if (Users.getAvatarGain(myAvatarUuid) != sliderValue) { - Users.setAvatarGain(myAvatarUuid, sliderValue); + if (AudioScriptingInterface.getAvatarGain() != sliderValue) { + AudioScriptingInterface.setAvatarGain(sliderValue); + } + } + function updateInjectorGainFromQML(sliderValue, isReleased) { + if (AudioScriptingInterface.getInjectorGain() != sliderValue) { + AudioScriptingInterface.setInjectorGain(sliderValue); // server side + AudioScriptingInterface.setLocalInjectorGain(sliderValue); // client side + } + } + function updateSystemInjectorGainFromQML(sliderValue, isReleased) { + if (AudioScriptingInterface.getSystemInjectorGain() != sliderValue) { + AudioScriptingInterface.setSystemInjectorGain(sliderValue); } } @@ -254,6 +265,14 @@ Rectangle { color: hifi.colors.white; text: qsTr("Choose input device"); } + + AudioControls.LoopbackAudio { + x: margins.paddings + + visible: (bar.currentIndex === 1 && isVR) || + (bar.currentIndex === 0 && !isVR); + anchors { right: parent.right } + } } ListView { @@ -301,13 +320,6 @@ Rectangle { } } } - AudioControls.LoopbackAudio { - x: margins.paddings - - visible: (bar.currentIndex === 1 && isVR) || - (bar.currentIndex === 0 && !isVR); - anchors { left: parent.left; leftMargin: margins.paddings } - } Separator {} @@ -335,6 +347,14 @@ Rectangle { color: hifi.colors.white; text: qsTr("Choose output device"); } + + AudioControls.PlaySampleSound { + x: margins.paddings + + visible: (bar.currentIndex === 1 && isVR) || + (bar.currentIndex === 0 && !isVR); + anchors { right: parent.right } + } } ListView { @@ -370,20 +390,20 @@ Rectangle { } Item { - id: gainContainer + id: avatarGainContainer x: margins.paddings; width: parent.width - margins.paddings*2 - height: gainSliderTextMetrics.height + height: avatarGainSliderTextMetrics.height HifiControlsUit.Slider { - id: gainSlider + id: avatarGainSlider anchors.right: parent.right height: parent.height width: 200 minimumValue: -60.0 maximumValue: 20.0 stepSize: 5 - value: Users.getAvatarGain(myAvatarUuid) + value: AudioScriptingInterface.getAvatarGain() onValueChanged: { updateMyAvatarGainFromQML(value, false); } @@ -399,7 +419,7 @@ Rectangle { // Do nothing. } onDoubleClicked: { - gainSlider.value = 0.0 + avatarGainSlider.value = 0.0 } onPressed: { // Pass through to Slider @@ -413,13 +433,13 @@ Rectangle { } } TextMetrics { - id: gainSliderTextMetrics - text: gainSliderText.text - font: gainSliderText.font + id: avatarGainSliderTextMetrics + text: avatarGainSliderText.text + font: avatarGainSliderText.font } RalewayRegular { // The slider for my card is special, it controls the master gain - id: gainSliderText; + id: avatarGainSliderText; text: "Avatar volume"; size: 16; anchors.left: parent.left; @@ -429,12 +449,122 @@ Rectangle { } } - AudioControls.PlaySampleSound { - x: margins.paddings + Item { + id: injectorGainContainer + x: margins.paddings; + width: parent.width - margins.paddings*2 + height: injectorGainSliderTextMetrics.height - visible: (bar.currentIndex === 1 && isVR) || - (bar.currentIndex === 0 && !isVR); - anchors { left: parent.left; leftMargin: margins.paddings } + HifiControlsUit.Slider { + id: injectorGainSlider + anchors.right: parent.right + height: parent.height + width: 200 + minimumValue: -60.0 + maximumValue: 20.0 + stepSize: 5 + value: AudioScriptingInterface.getInjectorGain() + onValueChanged: { + updateInjectorGainFromQML(value, false); + } + onPressedChanged: { + if (!pressed) { + updateInjectorGainFromQML(value, false); + } + } + + MouseArea { + anchors.fill: parent + onWheel: { + // Do nothing. + } + onDoubleClicked: { + injectorGainSlider.value = 0.0 + } + onPressed: { + // Pass through to Slider + mouse.accepted = false + } + onReleased: { + // the above mouse.accepted seems to make this + // never get called, nonetheless... + mouse.accepted = false + } + } + } + TextMetrics { + id: injectorGainSliderTextMetrics + text: injectorGainSliderText.text + font: injectorGainSliderText.font + } + RalewayRegular { + id: injectorGainSliderText; + text: "Environment volume"; + size: 16; + anchors.left: parent.left; + color: hifi.colors.white; + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignTop; + } + } + + Item { + id: systemInjectorGainContainer + x: margins.paddings; + width: parent.width - margins.paddings*2 + height: systemInjectorGainSliderTextMetrics.height + + HifiControlsUit.Slider { + id: systemInjectorGainSlider + anchors.right: parent.right + height: parent.height + width: 200 + minimumValue: -60.0 + maximumValue: 20.0 + stepSize: 5 + value: AudioScriptingInterface.getSystemInjectorGain() + onValueChanged: { + updateSystemInjectorGainFromQML(value, false); + } + onPressedChanged: { + if (!pressed) { + updateSystemInjectorGainFromQML(value, false); + } + } + + MouseArea { + anchors.fill: parent + onWheel: { + // Do nothing. + } + onDoubleClicked: { + systemInjectorGainSlider.value = 0.0 + } + onPressed: { + // Pass through to Slider + mouse.accepted = false + } + onReleased: { + // the above mouse.accepted seems to make this + // never get called, nonetheless... + mouse.accepted = false + } + } + } + TextMetrics { + id: systemInjectorGainSliderTextMetrics + text: systemInjectorGainSliderText.text + font: systemInjectorGainSliderText.font + } + RalewayRegular { + id: systemInjectorGainSliderText; + text: "System Sound volume"; + size: 16; + anchors.left: parent.left; + color: hifi.colors.white; + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignTop; + } } } } diff --git a/interface/resources/qml/hifi/audio/LoopbackAudio.qml b/interface/resources/qml/hifi/audio/LoopbackAudio.qml index 8ec0ffc496..74bc0f67dc 100644 --- a/interface/resources/qml/hifi/audio/LoopbackAudio.qml +++ b/interface/resources/qml/hifi/audio/LoopbackAudio.qml @@ -44,7 +44,7 @@ RowLayout { } HifiControlsUit.Button { - text: audioLoopedBack ? qsTr("STOP TESTING YOUR VOICE") : qsTr("TEST YOUR VOICE"); + text: audioLoopedBack ? qsTr("STOP TESTING") : qsTr("TEST YOUR VOICE"); color: audioLoopedBack ? hifi.buttons.red : hifi.buttons.blue; onClicked: { if (audioLoopedBack) { @@ -57,11 +57,11 @@ RowLayout { } } - RalewayRegular { - Layout.leftMargin: 2; - size: 14; - color: "white"; - font.italic: true - text: audioLoopedBack ? qsTr("Speak in your input") : ""; - } +// RalewayRegular { +// Layout.leftMargin: 2; +// size: 14; +// color: "white"; +// font.italic: true +// text: audioLoopedBack ? qsTr("Speak in your input") : ""; +// } } diff --git a/interface/resources/qml/hifi/audio/PlaySampleSound.qml b/interface/resources/qml/hifi/audio/PlaySampleSound.qml index b9d9727dab..0eb78f3efe 100644 --- a/interface/resources/qml/hifi/audio/PlaySampleSound.qml +++ b/interface/resources/qml/hifi/audio/PlaySampleSound.qml @@ -56,16 +56,16 @@ RowLayout { HifiConstants { id: hifi; } HifiControlsUit.Button { - text: isPlaying ? qsTr("STOP TESTING YOUR SOUND") : qsTr("TEST YOUR SOUND"); + text: isPlaying ? qsTr("STOP TESTING") : qsTr("TEST YOUR SOUND"); color: isPlaying ? hifi.buttons.red : hifi.buttons.blue; onClicked: isPlaying ? stopSound() : playSound(); } - RalewayRegular { - Layout.leftMargin: 2; - size: 14; - color: "white"; - font.italic: true - text: isPlaying ? qsTr("Listen to your output") : ""; - } +// RalewayRegular { +// Layout.leftMargin: 2; +// size: 14; +// color: "white"; +// font.italic: true +// text: isPlaying ? qsTr("Listen to your output") : ""; +// } } From 8872e9e4e7b318fef033ca8b174fc12d80125fcc Mon Sep 17 00:00:00 2001 From: Simon Walton <36682372+SimonWalton-HiFi@users.noreply.github.com> Date: Sat, 23 Mar 2019 20:05:26 -0900 Subject: [PATCH 327/446] Remove unused debugging variable --- assignment-client/src/avatars/AvatarMixer.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index c6cd12f30a..32fb5b9b1a 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -1005,7 +1005,6 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { { // Fraction of downstream bandwidth reserved for 'hero' avatars: static const QString PRIORITY_FRACTION_KEY = "priority_fraction"; if (avatarMixerGroupObject.contains(PRIORITY_FRACTION_KEY)) { - bool isDouble = avatarMixerGroupObject[PRIORITY_FRACTION_KEY].isDouble(); float priorityFraction = float(avatarMixerGroupObject[PRIORITY_FRACTION_KEY].toDouble()); _slavePool.setPriorityReservedFraction(std::min(std::max(0.0f, priorityFraction), 1.0f)); qCDebug(avatars) << "Avatar mixer reserving" << priorityFraction << "of bandwidth for priority avatars"; From de5643b5b99a257c9fbe3f2475fed384c25899fa Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 25 Mar 2019 10:33:32 -0700 Subject: [PATCH 328/446] quint64 -> uint64_t --- libraries/procedural/src/procedural/Procedural.cpp | 4 ++-- libraries/procedural/src/procedural/Procedural.h | 10 +++++----- .../procedural/src/procedural/ProceduralSkybox.cpp | 2 +- libraries/procedural/src/procedural/ProceduralSkybox.h | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index c5bfa43e75..9940da0b9a 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -225,7 +225,7 @@ void Procedural::prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation, - const quint64& created, + const uint64_t& created, const ProceduralProgramKey key) { std::lock_guard lock(_mutex); _entityDimensions = size; @@ -233,7 +233,7 @@ void Procedural::prepare(gpu::Batch& batch, _entityOrientation = glm::mat3_cast(orientation); _entityCreated = created; if (!_shaderPath.isEmpty()) { - auto lastModified = (quint64)QFileInfo(_shaderPath).lastModified().toMSecsSinceEpoch(); + auto lastModified = (uint64_t)QFileInfo(_shaderPath).lastModified().toMSecsSinceEpoch(); if (lastModified > _shaderModified) { QFile file(_shaderPath); file.open(QIODevice::ReadOnly); diff --git a/libraries/procedural/src/procedural/Procedural.h b/libraries/procedural/src/procedural/Procedural.h index b8fd77b052..956cef368f 100644 --- a/libraries/procedural/src/procedural/Procedural.h +++ b/libraries/procedural/src/procedural/Procedural.h @@ -83,10 +83,10 @@ public: bool isReady() const; bool isEnabled() const { return _enabled; } void prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation, - const quint64& created, const ProceduralProgramKey key = ProceduralProgramKey()); + const uint64_t& created, const ProceduralProgramKey key = ProceduralProgramKey()); glm::vec4 getColor(const glm::vec4& entityColor) const; - quint64 getFadeStartTime() const { return _fadeStartTime; } + uint64_t getFadeStartTime() const { return _fadeStartTime; } bool isFading() const { return _doesFade && _isFading; } void setIsFading(bool isFading) { _isFading = isFading; } void setDoesFade(bool doesFade) { _doesFade = doesFade; } @@ -136,7 +136,7 @@ protected: // Rendering object descriptions, from userData QString _shaderSource; QString _shaderPath; - quint64 _shaderModified { 0 }; + uint64_t _shaderModified { 0 }; NetworkShaderPointer _networkShader; bool _shaderDirty { true }; bool _uniformsDirty { true }; @@ -156,12 +156,12 @@ protected: glm::vec3 _entityDimensions; glm::vec3 _entityPosition; glm::mat3 _entityOrientation; - quint64 _entityCreated; + uint64_t _entityCreated; private: void setupUniforms(); - mutable quint64 _fadeStartTime { 0 }; + mutable uint64_t _fadeStartTime { 0 }; mutable bool _hasStartedFade { false }; mutable bool _isFading { false }; bool _doesFade { true }; diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.cpp b/libraries/procedural/src/procedural/ProceduralSkybox.cpp index bf8e408e70..53df1532dc 100644 --- a/libraries/procedural/src/procedural/ProceduralSkybox.cpp +++ b/libraries/procedural/src/procedural/ProceduralSkybox.cpp @@ -17,7 +17,7 @@ #include #include -ProceduralSkybox::ProceduralSkybox(quint64 created) : graphics::Skybox(), _created(created) { +ProceduralSkybox::ProceduralSkybox(uint64_t created) : graphics::Skybox(), _created(created) { _procedural._vertexSource = gpu::Shader::createVertex(shader::graphics::vertex::skybox)->getSource(); _procedural._opaqueFragmentSource = shader::Source::get(shader::procedural::fragment::proceduralSkybox); // Adjust the pipeline state for background using the stencil test diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.h b/libraries/procedural/src/procedural/ProceduralSkybox.h index 1b01b891d3..a1d7ea8fa7 100644 --- a/libraries/procedural/src/procedural/ProceduralSkybox.h +++ b/libraries/procedural/src/procedural/ProceduralSkybox.h @@ -19,7 +19,7 @@ class ProceduralSkybox: public graphics::Skybox { public: - ProceduralSkybox(quint64 created = 0); + ProceduralSkybox(uint64_t created = 0); void parse(const QString& userData) { _procedural.setProceduralData(ProceduralData::parse(userData)); } @@ -29,11 +29,11 @@ public: void render(gpu::Batch& batch, const ViewFrustum& frustum) const override; static void render(gpu::Batch& batch, const ViewFrustum& frustum, const ProceduralSkybox& skybox); - quint64 getCreated() const { return _created; } + uint64_t getCreated() const { return _created; } protected: mutable Procedural _procedural; - quint64 _created; + uint64_t _created; }; typedef std::shared_ptr< ProceduralSkybox > ProceduralSkyboxPointer; From 57a02bc1d57584de4de914ffc8c960bbe653658a Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 25 Mar 2019 11:29:25 -0700 Subject: [PATCH 329/446] capture lambda by value --- .../entities-renderer/src/RenderableMaterialEntityItem.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp index 15842336f5..b474fb2266 100644 --- a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp @@ -169,7 +169,7 @@ void MaterialEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo if (urlChanged && !usingMaterialData) { _networkMaterial = MaterialCache::instance().getMaterial(_materialURL); - auto onMaterialRequestFinished = [&, oldParentID, oldParentMaterialName, newCurrentMaterialName](bool success) { + auto onMaterialRequestFinished = [this, oldParentID, oldParentMaterialName, newCurrentMaterialName](bool success) { if (success) { deleteMaterial(oldParentID, oldParentMaterialName); _texturesLoaded = false; @@ -186,7 +186,7 @@ void MaterialEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo if (_networkMaterial->isLoaded()) { onMaterialRequestFinished(!_networkMaterial->isFailed()); } else { - connect(_networkMaterial.data(), &Resource::finished, this, [&](bool success) { + connect(_networkMaterial.data(), &Resource::finished, this, [this, onMaterialRequestFinished](bool success) { withWriteLock([&] { onMaterialRequestFinished(success); }); From 4658e34b4be6bd1340e56d320f775fb1d2ee3d7b Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 25 Mar 2019 11:29:27 -0700 Subject: [PATCH 330/446] update indent spacing --- interface/resources/qml/hifi/audio/Audio.qml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index ded28506b8..46ea64a323 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -669,10 +669,10 @@ Rectangle { } } AudioControls.PlaySampleSound { - id: playSampleSound - x: margins.paddings - anchors.top: systemInjectorGainContainer.bottom; - anchors.topMargin: 10; + id: playSampleSound + x: margins.paddings + anchors.top: systemInjectorGainContainer.bottom; + anchors.topMargin: 10; } } } From afe46cd78bfcaf759bf3099c8a37d7d7bb4406fb Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 25 Mar 2019 11:29:34 -0700 Subject: [PATCH 331/446] update indent spacing From 7f5d9cbd40ae95518ef9ae5cfc6cb64e05698d71 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Fri, 22 Mar 2019 13:16:56 -0700 Subject: [PATCH 332/446] adding push to talk fix for loadData --- interface/resources/qml/hifi/audio/MicBar.qml | 19 +++++++++++-------- interface/src/scripting/Audio.cpp | 8 ++++---- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index f51da9c381..e1b3e05d43 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -18,7 +18,10 @@ import TabletScriptingInterface 1.0 Rectangle { HifiConstants { id: hifi; } + property var muted: AudioScriptingInterface.muted; readonly property var level: AudioScriptingInterface.inputLevel; + readonly property var pushToTalk: AudioScriptingInterface.pushToTalk; + readonly property var pushingToTalk: AudioScriptingInterface.pushingToTalk; property bool gated: false; Component.onCompleted: { @@ -67,10 +70,10 @@ Rectangle { hoverEnabled: true; scrollGestureEnabled: false; onClicked: { - if (AudioScriptingInterface.pushToTalk) { + if (pushToTalk) { return; } - AudioScriptingInterface.muted = !AudioScriptingInterface.muted; + muted = !muted; Tablet.playSound(TabletEnums.ButtonClick); } drag.target: dragTarget; @@ -115,7 +118,7 @@ Rectangle { readonly property string pushToTalkIcon: "../../../icons/tablet-icons/mic-ptt-i.svg"; id: image; - source: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) ? pushToTalkIcon : AudioScriptingInterface.muted ? mutedIcon : unmutedIcon; + source: (pushToTalk && !pushingToTalk) ? pushToTalkIcon : muted ? mutedIcon : unmutedIcon; width: 30; height: 30; @@ -138,9 +141,9 @@ Rectangle { Item { id: status; - readonly property string color: AudioScriptingInterface.muted ? colors.muted : colors.unmuted; + readonly property string color: muted ? colors.muted : colors.unmuted; - visible: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) || AudioScriptingInterface.muted; + visible: (pushToTalk && !pushingToTalk) || muted; anchors { left: parent.left; @@ -159,7 +162,7 @@ Rectangle { color: parent.color; - text: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) ? (HMD.active ? "MUTED PTT" : "MUTED PTT-(T)") : (AudioScriptingInterface.muted ? "MUTED" : "MUTE"); + text: (pushToTalk && !pushingToTalk) ? (HMD.active ? "MUTED PTT" : "MUTED PTT-(T)") : (muted ? "MUTED" : "MUTE"); font.pointSize: 12; } @@ -169,7 +172,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - width: AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk ? (HMD.active ? 27 : 25) : 50; + width: pushToTalk && !pushingToTalk ? (HMD.active ? 27 : 25) : 50; height: 4; color: parent.color; } @@ -180,7 +183,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - width: AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk ? (HMD.active ? 27 : 25) : 50; + width: pushToTalk && !pushingToTalk ? (HMD.active ? 27 : 25) : 50; height: 4; color: parent.color; } diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index b1b5077e60..0e0d13ae45 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -224,10 +224,10 @@ void Audio::saveData() { } void Audio::loadData() { - _desktopMuted = _desktopMutedSetting.get(); - _hmdMuted = _hmdMutedSetting.get(); - _pttDesktop = _pttDesktopSetting.get(); - _pttHMD = _pttHMDSetting.get(); + setMutedDesktop(_desktopMutedSetting.get()); + setMutedHMD(_hmdMutedSetting.get()); + setPTTDesktop(_pttDesktopSetting.get()); + setPTTHMD(_pttHMDSetting.get()); auto client = DependencyManager::get().data(); QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted()), Q_ARG(bool, false)); From f551d49f4f0ab549fbb1a7c086012d12b4aed87e Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 25 Mar 2019 11:31:05 -0700 Subject: [PATCH 333/446] adding signal for updating push to talk variables --- interface/resources/qml/hifi/audio/MicBar.qml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index e1b3e05d43..c154eabfaa 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -20,13 +20,20 @@ Rectangle { property var muted: AudioScriptingInterface.muted; readonly property var level: AudioScriptingInterface.inputLevel; - readonly property var pushToTalk: AudioScriptingInterface.pushToTalk; - readonly property var pushingToTalk: AudioScriptingInterface.pushingToTalk; + property var pushToTalk: AudioScriptingInterface.pushToTalk; + property var pushingToTalk: AudioScriptingInterface.pushingToTalk; property bool gated: false; Component.onCompleted: { AudioScriptingInterface.noiseGateOpened.connect(function() { gated = false; }); AudioScriptingInterface.noiseGateClosed.connect(function() { gated = true; }); + AudioScriptingInterface.pushToTalkChanged.connect(function() { + pushToTalk = AudioScriptingInterface.pushToTalk; + }); + AudioScriptingInterface.pushingToTalkChanged.connect(function() { + pushingToTalk = AudioScriptingInterface.pushingToTalk; + }); + } property bool standalone: false; From cd2dbbb955012431b92886756ca11d0d6d6e91d4 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 25 Mar 2019 11:52:52 -0700 Subject: [PATCH 334/446] fix logic typo --- interface/src/avatar/MyAvatar.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 4ea0d37710..b204588774 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3462,7 +3462,7 @@ void MyAvatar::setSessionUUID(const QUuid& sessionUUID) { _avatarEntitiesLock.withReadLock([&] { avatarEntityIDs = _packedAvatarEntityData.keys(); }); - bool sendPackets = DependencyManager::get()->getSessionUUID().isNull(); + bool sendPackets = !DependencyManager::get()->getSessionUUID().isNull(); EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender(); entityTree->withWriteLock([&] { for (const auto& entityID : avatarEntityIDs) { @@ -3473,7 +3473,7 @@ void MyAvatar::setSessionUUID(const QUuid& sessionUUID) { // update OwningAvatarID so entity can be identified as "ours" later entity->setOwningAvatarID(newSessionID); // NOTE: each attached AvatarEntity already have the correct updated parentID - // via magic in SpatiallyNestable, hence we check agains newSessionID + // via magic in SpatiallyNestable, hence we check against newSessionID if (sendPackets && entity->getParentID() == newSessionID) { // but when we have a real session and the AvatarEntity is parented to MyAvatar // we need to update the "packedAvatarEntityData" sent to the avatar-mixer From 59a420b1602d2bc8eb9a494601658993bc295e0c Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 25 Mar 2019 12:24:13 -0700 Subject: [PATCH 335/446] adding signals + slots to when muted + display mode changes --- interface/resources/qml/hifi/audio/MicBar.qml | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index c154eabfaa..d161f12d70 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -27,6 +27,13 @@ Rectangle { Component.onCompleted: { AudioScriptingInterface.noiseGateOpened.connect(function() { gated = false; }); AudioScriptingInterface.noiseGateClosed.connect(function() { gated = true; }); + HMD.displayModeChanged.connect(function() { + muted = AudioScriptingInterface.muted; + pushToTalk = AudioScriptingInterface.pushToTalk; + }); + AudioScriptingInterface.mutedChanged.connect(function() { + muted = AudioScriptingInterface.muted; + }); AudioScriptingInterface.pushToTalkChanged.connect(function() { pushToTalk = AudioScriptingInterface.pushToTalk; }); @@ -94,16 +101,16 @@ Rectangle { QtObject { id: colors; - readonly property string unmuted: "#FFF"; - readonly property string muted: "#E2334D"; + readonly property string unmutedColor: "#FFF"; + readonly property string mutedColor: "#E2334D"; readonly property string gutter: "#575757"; readonly property string greenStart: "#39A38F"; readonly property string greenEnd: "#1FC6A6"; readonly property string yellow: "#C0C000"; - readonly property string red: colors.muted; + readonly property string red: colors.mutedColor; readonly property string fill: "#55000000"; readonly property string border: standalone ? "#80FFFFFF" : "#55FFFFFF"; - readonly property string icon: AudioScriptingInterface.muted ? muted : unmuted; + readonly property string icon: muted ? colors.mutedColor : unmutedColor; } Item { @@ -148,8 +155,6 @@ Rectangle { Item { id: status; - readonly property string color: muted ? colors.muted : colors.unmuted; - visible: (pushToTalk && !pushingToTalk) || muted; anchors { @@ -167,7 +172,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - color: parent.color; + color: colors.icon; text: (pushToTalk && !pushingToTalk) ? (HMD.active ? "MUTED PTT" : "MUTED PTT-(T)") : (muted ? "MUTED" : "MUTE"); font.pointSize: 12; @@ -181,7 +186,7 @@ Rectangle { width: pushToTalk && !pushingToTalk ? (HMD.active ? 27 : 25) : 50; height: 4; - color: parent.color; + color: colors.icon; } Rectangle { @@ -192,7 +197,7 @@ Rectangle { width: pushToTalk && !pushingToTalk ? (HMD.active ? 27 : 25) : 50; height: 4; - color: parent.color; + color: colors.icon; } } From dc996c267f5af7a0203a1d9a4de84a630b68f2b7 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 25 Mar 2019 12:24:13 -0700 Subject: [PATCH 336/446] adding signals + slots to when muted + display mode changes From b78ae80ae621fc657f6882faa8ff8e84e6f282a9 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 26 Mar 2019 09:40:47 +1300 Subject: [PATCH 337/446] Address review comments and add further examples --- .../src/AgentScriptingInterface.h | 33 ++- .../src/avatars/ScriptableAvatar.h | 49 ++-- interface/src/avatar/MyAvatar.cpp | 21 +- interface/src/avatar/MyAvatar.h | 225 ++++++++++-------- .../animation/src/AnimInverseKinematics.h | 23 +- libraries/animation/src/AnimOverlay.h | 16 +- .../src/avatars-renderer/Avatar.h | 22 +- libraries/avatars/src/AvatarData.cpp | 6 +- libraries/avatars/src/AvatarData.h | 104 +++++--- 9 files changed, 310 insertions(+), 189 deletions(-) diff --git a/assignment-client/src/AgentScriptingInterface.h b/assignment-client/src/AgentScriptingInterface.h index 1242634dd5..b1a8aaff96 100644 --- a/assignment-client/src/AgentScriptingInterface.h +++ b/assignment-client/src/AgentScriptingInterface.h @@ -18,8 +18,8 @@ #include "Agent.h" /**jsdoc - * The Agent API enables an assignment client to emulate an avatar. In particular, setting isAvatar = - * true connects the assignment client to the avatar and audio mixers and enables the {@link Avatar} API to be used. + * The Agent API enables an assignment client to emulate an avatar. Setting isAvatar = true connects + * the assignment client to the avatar and audio mixers, and enables the {@link Avatar} API to be used. * * @namespace Agent * @@ -29,13 +29,13 @@ * false. * @property {boolean} isPlayingAvatarSound - true if the script has a sound to play, otherwise false. * Sounds are played when isAvatar is true, from the position and with the orientation of the - * scripted avatar's head.Read-only. + * scripted avatar's head. Read-only. * @property {boolean} isListeningToAudioStream - true if the agent is "listening" to the audio stream from the * domain, otherwise false. * @property {boolean} isNoiseGateEnabled - true if the noise gate is enabled, otherwise false. When * enabled, the input audio stream is blocked (fully attenuated) if it falls below an adaptive threshold. - * @property {number} lastReceivedAudioLoudness - The current loudness of the audio input, nominal range 0.0 (no - * sound) – 1.0 (the onset of clipping). Read-only. + * @property {number} lastReceivedAudioLoudness - The current loudness of the audio input. Nominal range [0.0 (no + * sound) – 1.0 (the onset of clipping)]. Read-only. * @property {Uuid} sessionUUID - The unique ID associated with the agent's current session in the domain. Read-only. */ class AgentScriptingInterface : public QObject { @@ -63,9 +63,9 @@ public: public slots: /**jsdoc - * Sets whether or not the script should emulate an avatar. + * Sets whether the script should emulate an avatar. * @function Agent.setIsAvatar - * @param {boolean} isAvatar - true if the script should act as if an avatar, otherwise false. + * @param {boolean} isAvatar - true if the script emulates an avatar, otherwise false. * @example
Make an assignment client script emulate an avatar.Check whether the agent is emulating an avatar.Play a sound from an emulated avatar.Create a scriptable avatar.Report the current animation details.Report the current avatar entities.
8PITCHRotate the user's avatar head and attached camera about its negative * x-axis (i.e., positive values pitch down) at a rate proportional to the control value, if the camera isn't in HMD, * independent, or mirror modes.
9ZOOMZooms the camera in or out.
9ZOOMZoom the camera in or out.
10DELTA_YAWRotate the user's avatar about its y-axis by an amount proportional * to the control value, if the camera isn't in independent or mirror modes.
11DELTA_PITCHRotate the user's avatar head and attached camera about its @@ -471,13 +470,14 @@ public: void setCollisionWithOtherAvatarsFlags() override; /**jsdoc - * Resets the sensor positioning of your HMD (if used) and recenters your avatar body and head. + * Resets the sensor positioning of your HMD (if in use) and recenters your avatar body and head. * @function MyAvatar.resetSensorsAndBody */ Q_INVOKABLE void resetSensorsAndBody(); /**jsdoc - * Moves and orients the avatar, such that it is directly underneath the HMD, with toes pointed forward. + * Moves and orients the avatar, such that it is directly underneath the HMD, with toes pointed forward in the direction of + * the HMD. * @function MyAvatar.centerBody */ Q_INVOKABLE void centerBody(); // thread-safe @@ -486,7 +486,7 @@ public: /**jsdoc * Clears inverse kinematics joint limit history. *

The internal inverse-kinematics system maintains a record of which joints are "locked". Sometimes it is useful to - * forget this history, to prevent contorted joints.

+ * forget this history to prevent contorted joints, e.g., after finishing with an override animation.

* @function MyAvatar.clearIKJointLimitHistory */ Q_INVOKABLE void clearIKJointLimitHistory(); // thread-safe @@ -502,14 +502,14 @@ public: /**jsdoc * Gets the avatar orientation. Suitable for use in QML. * @function MyAvatar.setOrientationVar - * @param {object} newOrientationVar - The avatar orientation. + * @param {object} newOrientationVar - The avatar's orientation. */ Q_INVOKABLE void setOrientationVar(const QVariant& newOrientationVar); /**jsdoc * Gets the avatar orientation. Suitable for use in QML. * @function MyAvatar.getOrientationVar - * @returns {object} The avatar orientation. + * @returns {object} The avatar's orientation. */ Q_INVOKABLE QVariant getOrientationVar() const; @@ -558,9 +558,9 @@ public: *

Note: When using pre-built animation data, it's critical that the joint orientation of the source animation and target * rig are equivalent, since the animation data applies absolute values onto the joints. If the orientations are different, * the avatar will move in unpredictable ways. For more information about avatar joint orientation standards, see - * Avatar Standards.

+ * Avatar Standards.

* @function MyAvatar.overrideAnimation - * @param url {string} The URL to the animation file. Animation files need to be .FBX format, but only need to contain the + * @param url {string} The URL to the animation file. Animation files need to be FBX format, but only need to contain the * avatar skeleton and animation data. * @param fps {number} The frames per second (FPS) rate for the animation playback. 30 FPS is normal speed. * @param loop {boolean} Set to true if the animation should loop. @@ -572,6 +572,7 @@ public: * MyAvatar.overrideAnimation(ANIM_URL, 30, true, 0, 53); * Script.setTimeout(function () { * MyAvatar.restoreAnimation(); + * MyAvatar.clearIKJointLimitHistory(); * }, 3000); */ Q_INVOKABLE void overrideAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame); @@ -617,18 +618,18 @@ public: *

Each avatar has an avatar-animation.json file that defines a set of animation roles. Animation roles map to easily * understandable actions that the avatar can perform, such as "idleStand", "idleTalk", or * "walkFwd". To get the full list of roles, use {@ link MyAvatar.getAnimationRoles}. - * For each role, the avatar-animation.json defines when the animation is used, the animation clip (.FBX) used, and how + * For each role, the avatar-animation.json defines when the animation is used, the animation clip (FBX) used, and how * animations are blended together with procedural data (such as look at vectors, hand sensors etc.). - * overrideRoleAnimation() is used to change the animation clip (.FBX) associated with a specified animation + * overrideRoleAnimation() is used to change the animation clip (FBX) associated with a specified animation * role. To end the role animation and restore the default, use {@link MyAvatar.restoreRoleAnimation}.

*

Note: Hand roles only affect the hand. Other 'main' roles, like 'idleStand', 'idleTalk', 'takeoffStand' are full body.

*

Note: When using pre-built animation data, it's critical that the joint orientation of the source animation and target * rig are equivalent, since the animation data applies absolute values onto the joints. If the orientations are different, * the avatar will move in unpredictable ways. For more information about avatar joint orientation standards, see - * Avatar Standards. + * Avatar Standards. * @function MyAvatar.overrideRoleAnimation * @param role {string} The animation role to override - * @param url {string} The URL to the animation file. Animation files need to be .FBX format, but only need to contain the avatar skeleton and animation data. + * @param url {string} The URL to the animation file. Animation files need to be in FBX format, but only need to contain the avatar skeleton and animation data. * @param fps {number} The frames per second (FPS) rate for the animation playback. 30 FPS is normal speed. * @param loop {boolean} Set to true if the animation should loop * @param firstFrame {number} The frame the animation should start at @@ -653,9 +654,9 @@ public: *

Each avatar has an avatar-animation.json file that defines a set of animation roles. Animation roles map to easily * understandable actions that the avatar can perform, such as "idleStand", "idleTalk", or * "walkFwd". To get the full list of roles, use {@link MyAvatar.getAnimationRoles}. For each role, - * the avatar-animation.json defines when the animation is used, the animation clip (.FBX) used, and how animations are + * the avatar-animation.json defines when the animation is used, the animation clip (FBX) used, and how animations are * blended together with procedural data (such as look-at vectors, hand sensors etc.). You can change the animation clip - * (.FBX) associated with a specified animation role using {@link MyAvatar.overrideRoleAnimation}. + * (FBX) associated with a specified animation role using {@link MyAvatar.overrideRoleAnimation}. * restoreRoleAnimation() is used to restore a specified animation role's default animation clip. If you have * not specified an override animation for the specified role, this function has no effect. * @function MyAvatar.restoreRoleAnimation @@ -688,10 +689,9 @@ public: * @function MyAvatar.addAnimationStateHandler * @param {function} handler - The animation state handler function to add. * @param {Array|null} propertiesList - The list of {@link MyAvatar.AnimStateDictionary|AnimStateDictionary} - * properties that should be included the in parameter that the handler function is called with. If null + * properties that should be included in the parameter that the handler function is called with. If null * then all properties are included in the call parameter. - * @returns {number} The ID of the animation state handler function if successfully added, undefined if not - * successfully added. + * @returns {number} The ID of the animation state handler function if successfully added, undefined if not. * @example

Log all the animation state dictionary parameters for a short while.Report the rotation of your avatar's head joint relative to your avatar.Report the translation of your avatar's head joint relative to your avatar.Create and grab an entity for a short while.Report the current avatar entities.Make your avatar invisible for 10s.Report the current avatar animation JSON being used.Report when the current avatar animation JSON being used changes.
ValueName

Description
0RelaxToUnderPosesThis is a blend between PreviousSolution and - * UnderPoses: it is 15/16 PreviousSolution and 1/16 UnderPoses. This - * provides some of the benefits of using UnderPoses so that the underlying animation is still visible, - * while at the same time converging faster then using the UnderPoses only initial solution.
1RelaxToLimitCenterPosesThis is a blend between - * Previous Solution and LimitCenterPoses: it is 15/16 PreviousSolution and - * 1/16 LimitCenterPoses. This should converge quickly because it is close to the previous solution, but - * still provides the benefits of avoiding limb locking.
0RelaxToUnderPosesThis is a blend: it is 15/16 PreviousSolution + * and 1/16 UnderPoses. This provides some of the benefits of using UnderPoses so that the + * underlying animation is still visible, while at the same time converging faster then using the + * UnderPoses as the only initial solution.
1RelaxToLimitCenterPosesThis is a blend: it is 15/16 + * PreviousSolution and 1/16 LimitCenterPoses. This should converge quickly because it is + * close to the previous solution, but still provides the benefits of avoiding limb locking.
2PreviousSolutionThe IK system will begin to solve from the same position and * orientations for each joint that was the result from the previous frame.
- * Pros: because the end effectors typically do not move much from frame to frame, this is likely to converge quickly + * Pros: As the end effectors typically do not move much from frame to frame, this is likely to converge quickly * to a valid solution.
* Cons: If the previous solution resulted in an awkward or uncomfortable posture, the next frame will also be * awkward and uncomfortable. It can also result in locked elbows and knees.
3UnderPosesThe IK occurs at one of the top-most layers, it has access to the + *
3UnderPosesThe IK occurs at one of the top-most layers. It has access to the * full posture that was computed via canned animations and blends. We call this animated set of poses the "under * pose". The under poses are what would be visible if IK was completely disabled. Using the under poses as the - * initial conditions of the CCD solve will cause some of the animated motion to be blended in to the result of the + * initial conditions of the CCD solve will cause some of the animated motion to be blended into the result of the * IK. This can result in very natural results, especially if there are only a few IK targets enabled. On the other * hand, because the under poses might be quite far from the desired end effector, it can converge slowly in some * cases, causing it to never reach the IK target in the allotted number of iterations. Also, in situations where all @@ -93,7 +92,7 @@ public: * constraints. This can prevent the IK solution from getting locked or stuck at a particular constraint. For * example, if the arm is pointing straight outward from the body, as the end effector moves towards the body, at * some point the elbow should bend to accommodate. However, because the CCD solver is stuck at a local maximum, it - * will not rotate the elbow, unless the initial conditions already has the elbow bent, which is the case for + * will not rotate the elbow, unless the initial conditions already have the elbow bent, which is the case for * LimitCenterPoses. When all the IK targets are enabled, this result will provide a consistent starting * point for each IK solve, hopefully resulting in a consistent, natural result.
0FullBodyBoneSetAll joints.
1UpperBodyBoneSetOnly the "Spine" joint and its children.
2LowerBodyBoneSetOnly the leg joints and their children.
3LeftArmBoneSetJoints that are children of the "LeftShoulder" joint.
4RightArmBoneSetJoints that are children of the "RightShoulder" + *
3LeftArmBoneSetJoints that are the children of the "LeftShoulder" * joint.
5AboveTheHeadBoneSetJoints that are children of the "Head" joint.
6BelowTheHeadBoneSetJoints that are NOT children of the "head" + *
4RightArmBoneSetJoints that are the children of the "RightShoulder" + * joint.
5AboveTheHeadBoneSetJoints that are the children of the "Head" + * joint.
6BelowTheHeadBoneSetJoints that are NOT the children of the "head" * joint.
7HeadOnlyBoneSetThe "Head" joint.
8SpineOnlyBoneSetThe "Spine" joint.
9EmptyBoneSetNo joints.
10LeftHandBoneSetjoints that are children of the "LeftHand" joint.
11RightHandBoneSetJoints that are children of the "RightHand" joint.
10LeftHandBoneSetjoints that are the children of the "LeftHand" + * joint.
11RightHandBoneSetJoints that are the children of the "RightHand" + * joint.
12HipsOnlyBoneSetThe "Hips" joint.
13BothFeetBoneSetThe "LeftFoot" and "RightFoot" joints.
Report the default rotation of your avatar's head joint relative to your avatar.Report the default translation of your avatar's head joint relative to your avatar.Open your avatar's mouth wide.Report the URLs of all current attachments.Report the sensor to world matrix.Report the left hand controller matrix.Report when your avatar display name changes.Report when your avatar's session display name changes.Report when your avatar's skeleton model changes.Report when your look-at snapping setting changes.Report when your avatar's session UUID changes.Create a scriptable avatar.