diff --git a/interface/resources/qml/controls/WebView.qml b/interface/resources/qml/controls/WebView.qml index bba91c64f6..7285db22d2 100644 --- a/interface/resources/qml/controls/WebView.qml +++ b/interface/resources/qml/controls/WebView.qml @@ -55,9 +55,11 @@ WebEngineView { } onNewViewRequested:{ + if (desktop) { var component = Qt.createComponent("../Browser.qml"); var newWindow = component.createObject(desktop); - request.openIn(newWindow.webView) + request.openIn(newWindow.webView); + } } // This breaks the webchannel used for passing messages. Fixed in Qt 5.6 diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index d3a38271a0..31fb2abe53 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -59,6 +59,8 @@ const float DISPLAYNAME_ALPHA = 1.0f; const float DISPLAYNAME_BACKGROUND_ALPHA = 0.4f; const glm::vec3 HAND_TO_PALM_OFFSET(0.0f, 0.12f, 0.08f); +const int SENSOR_TO_WORLD_MATRIX_INDEX = 65534; + namespace render { template <> const ItemKey payloadGetKey(const AvatarSharedPointer& avatar) { return ItemKey::Builder::opaqueShape(); @@ -851,15 +853,33 @@ glm::vec3 Avatar::getDefaultJointTranslation(int index) const { } glm::quat Avatar::getAbsoluteJointRotationInObjectFrame(int index) const { - glm::quat rotation; - _skeletonModel->getAbsoluteJointRotationInRigFrame(index, rotation); - return Quaternions::Y_180 * rotation; + if (index == SENSOR_TO_WORLD_MATRIX_INDEX) { + glm::mat4 sensorToWorldMatrix = getSensorToWorldMatrix(); + bool success; + Transform avatarTransform; + Transform::mult(avatarTransform, getParentTransform(success), getLocalTransform()); + glm::mat4 invAvatarMat = avatarTransform.getInverseMatrix(); + return glmExtractRotation(invAvatarMat * sensorToWorldMatrix); + } else { + glm::quat rotation; + _skeletonModel->getAbsoluteJointRotationInRigFrame(index, rotation); + return Quaternions::Y_180 * rotation; + } } glm::vec3 Avatar::getAbsoluteJointTranslationInObjectFrame(int index) const { - glm::vec3 translation; - _skeletonModel->getAbsoluteJointTranslationInRigFrame(index, translation); - return Quaternions::Y_180 * translation; + if (index == SENSOR_TO_WORLD_MATRIX_INDEX) { + glm::mat4 sensorToWorldMatrix = getSensorToWorldMatrix(); + bool success; + Transform avatarTransform; + Transform::mult(avatarTransform, getParentTransform(success), getLocalTransform()); + glm::mat4 invAvatarMat = avatarTransform.getInverseMatrix(); + return extractTranslation(invAvatarMat * sensorToWorldMatrix); + } else { + glm::vec3 translation; + _skeletonModel->getAbsoluteJointTranslationInRigFrame(index, translation); + return Quaternions::Y_180 * translation; + } } int Avatar::getJointIndex(const QString& name) const { diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 782ecbcc64..5687b5025c 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -107,7 +107,6 @@ MyAvatar::MyAvatar(RigPointer rig) : _hmdSensorOrientation(), _hmdSensorPosition(), _bodySensorMatrix(), - _sensorToWorldMatrix(), _goToPending(false), _goToPosition(), _goToOrientation(), @@ -511,13 +510,9 @@ void MyAvatar::simulate(float deltaTime) { updateAvatarEntities(); } -// thread-safe -glm::mat4 MyAvatar::getSensorToWorldMatrix() const { - return _sensorToWorldMatrixCache.get(); -} - -// As far as I know no HMD system supports a play area of a kilometer in radius. +// As far as I know no HMD system supports a play area of a kilometer in radius. static const float MAX_HMD_ORIGIN_DISTANCE = 1000.0f; + // Pass a recent sample of the HMD to the avatar. // This can also update the avatar's position to follow the HMD // as it moves through the world. @@ -526,7 +521,7 @@ void MyAvatar::updateFromHMDSensorMatrix(const glm::mat4& hmdSensorMatrix) { _hmdSensorMatrix = hmdSensorMatrix; auto newHmdSensorPosition = extractTranslation(hmdSensorMatrix); - if (newHmdSensorPosition != _hmdSensorPosition && + if (newHmdSensorPosition != _hmdSensorPosition && glm::length(newHmdSensorPosition) > MAX_HMD_ORIGIN_DISTANCE) { qWarning() << "Invalid HMD sensor position " << newHmdSensorPosition; // Ignore unreasonable HMD sensor data diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index d8ff679db6..1f212a1fec 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -79,8 +79,6 @@ class MyAvatar : public Avatar { Q_PROPERTY(controller::Pose leftHandTipPose READ getLeftHandTipPose) Q_PROPERTY(controller::Pose rightHandTipPose READ getRightHandTipPose) - Q_PROPERTY(glm::mat4 sensorToWorldMatrix READ getSensorToWorldMatrix) - Q_PROPERTY(float energy READ getEnergy WRITE setEnergy) Q_PROPERTY(bool hmdLeanRecenterEnabled READ getHMDLeanRecenterEnabled WRITE setHMDLeanRecenterEnabled) @@ -110,9 +108,6 @@ public: const glm::quat& getHMDSensorOrientation() const { return _hmdSensorOrientation; } const glm::vec2& getHMDSensorFacingMovingAverage() const { return _hmdSensorFacingMovingAverage; } - // thread safe - Q_INVOKABLE glm::mat4 getSensorToWorldMatrix() const; - Q_INVOKABLE void setOrientationVar(const QVariant& newOrientationVar); Q_INVOKABLE QVariant getOrientationVar() const; @@ -415,6 +410,10 @@ private: bool _useSnapTurn { true }; bool _clearOverlayWhenMoving { true }; + // working copy of sensorToWorldMatrix. + // See AvatarData for thread-safe _sensorToWorldMatrixCache, used for outward facing access + glm::mat4 _sensorToWorldMatrix; + // cache of the current HMD sensor position and orientation // in sensor space. glm::mat4 _hmdSensorMatrix; @@ -427,10 +426,6 @@ private: // in sensor space. glm::mat4 _bodySensorMatrix; - // used to transform any sensor into world space, including the _hmdSensorMat, or hand controllers. - glm::mat4 _sensorToWorldMatrix; - ThreadSafeValueCache _sensorToWorldMatrixCache { glm::mat4() }; - struct FollowHelper { FollowHelper(); diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index d0c7b3912c..9088ee0577 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -53,15 +53,18 @@ namespace AvatarDataPacket { // NOTE: AvatarDataPackets start with a uint16_t sequence number that is not reflected in the Header structure. PACKED_BEGIN struct Header { - float position[3]; // skeletal model's position - float globalPosition[3]; // avatar's position - uint16_t localOrientation[3]; // avatar's local euler angles (degrees, compressed) relative to the thing it's attached to - uint16_t scale; // (compressed) 'ratio' encoding uses sign bit as flag. - float lookAtPosition[3]; // world space position that eyes are focusing on. - float audioLoudness; // current loundess of microphone + float position[3]; // skeletal model's position + float globalPosition[3]; // avatar's position + uint16_t localOrientation[3]; // avatar's local euler angles (degrees, compressed) relative to the thing it's attached to + uint16_t scale; // (compressed) 'ratio' encoding uses sign bit as flag. + float lookAtPosition[3]; // world space position that eyes are focusing on. + float audioLoudness; // current loundess of microphone + uint8_t sensorToWorldQuat[6]; // 6 byte compressed quaternion part of sensor to world matrix + uint16_t sensorToWorldScale; // uniform scale of sensor to world matrix + float sensorToWorldTrans[3]; // fourth column of sensor to world matrix uint8_t flags; } PACKED_END; - const size_t HEADER_SIZE = 49; + const size_t HEADER_SIZE = 69; // only present if HAS_REFERENTIAL flag is set in header.flags PACKED_BEGIN struct ParentInfo { @@ -93,6 +96,9 @@ namespace AvatarDataPacket { */ } +static const int TRANSLATION_COMPRESSION_RADIX = 12; +static const int SENSOR_TO_WORLD_SCALE_RADIX = 10; + #define ASSERT(COND) do { if (!(COND)) { abort(); } } while(0) AvatarData::AvatarData() : @@ -210,6 +216,14 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { header->lookAtPosition[2] = _headData->_lookAtPosition.z; header->audioLoudness = _headData->_audioLoudness; + glm::mat4 sensorToWorldMatrix = getSensorToWorldMatrix(); + packOrientationQuatToSixBytes(header->sensorToWorldQuat, glmExtractRotation(sensorToWorldMatrix)); + glm::vec3 scale = extractScale(sensorToWorldMatrix); + packFloatScalarToSignedTwoByteFixed((uint8_t*)&header->sensorToWorldScale, scale.x, SENSOR_TO_WORLD_SCALE_RADIX); + header->sensorToWorldTrans[0] = sensorToWorldMatrix[3][0]; + header->sensorToWorldTrans[1] = sensorToWorldMatrix[3][1]; + header->sensorToWorldTrans[2] = sensorToWorldMatrix[3][2]; + setSemiNibbleAt(header->flags, KEY_STATE_START_BIT, _keyState); // hand state bool isFingerPointing = _handState & IS_FINGER_POINTING_FLAG; @@ -346,8 +360,6 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { *destinationBuffer++ = validity; } - const int TRANSLATION_COMPRESSION_RADIX = 12; - validityBit = 0; validity = *validityPosition++; for (int i = 0; i < _jointData.size(); i ++) { @@ -500,6 +512,15 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { } _headData->_audioLoudness = audioLoudness; + glm::quat sensorToWorldQuat; + unpackOrientationQuatFromSixBytes(header->sensorToWorldQuat, sensorToWorldQuat); + float sensorToWorldScale; + unpackFloatScalarFromSignedTwoByteFixed((int16_t*)&header->sensorToWorldScale, &sensorToWorldScale, SENSOR_TO_WORLD_SCALE_RADIX); + glm::vec3 sensorToWorldTrans(header->sensorToWorldTrans[0], header->sensorToWorldTrans[1], header->sensorToWorldTrans[2]); + glm::mat4 sensorToWorldMatrix = createMatFromScaleQuatAndPos(glm::vec3(sensorToWorldScale), sensorToWorldQuat, sensorToWorldTrans); + + _sensorToWorldMatrixCache.set(sensorToWorldMatrix); + { // bitFlags and face data uint8_t bitItems = header->flags; @@ -616,7 +637,6 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { // each joint translation component is stored in 6 bytes. const int COMPRESSED_TRANSLATION_SIZE = 6; PACKET_READ_CHECK(JointTranslation, numValidJointTranslations * COMPRESSED_TRANSLATION_SIZE); - const int TRANSLATION_COMPRESSION_RADIX = 12; for (int i = 0; i < numJoints; i++) { JointData& data = _jointData[i]; @@ -1718,6 +1738,11 @@ AvatarEntityIDs AvatarData::getAndClearRecentlyDetachedIDs() { return result; } +// thread-safe +glm::mat4 AvatarData::getSensorToWorldMatrix() const { + return _sensorToWorldMatrixCache.get(); +} + QScriptValue RayToAvatarIntersectionResultToScriptValue(QScriptEngine* engine, const RayToAvatarIntersectionResult& value) { QScriptValue obj = engine->newObject(); obj.setProperty("intersects", value.intersects); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index b88cec3e05..572657e921 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -54,6 +54,7 @@ typedef unsigned long long quint64; #include #include #include +#include #include "AABox.h" #include "HeadData.h" @@ -171,6 +172,8 @@ class AvatarData : public QObject, public SpatiallyNestable { Q_PROPERTY(QUuid sessionUUID READ getSessionUUID) + Q_PROPERTY(glm::mat4 sensorToWorldMatrix READ getSensorToWorldMatrix) + public: static const QString FRAME_NAME; @@ -351,6 +354,9 @@ public: void setAvatarEntityDataChanged(bool value) { _avatarEntityDataChanged = value; } AvatarEntityIDs getAndClearRecentlyDetachedIDs(); + // thread safe + Q_INVOKABLE glm::mat4 getSensorToWorldMatrix() const; + public slots: void sendAvatarDataPacket(); void sendIdentityPacket(); @@ -425,6 +431,9 @@ protected: bool _avatarEntityDataLocallyEdited { false }; bool _avatarEntityDataChanged { false }; + // used to transform any sensor into world space, including the _hmdSensorMat, or hand controllers. + ThreadSafeValueCache _sensorToWorldMatrixCache { glm::mat4() }; + private: friend void avatarStateFromFrame(const QByteArray& frameData, AvatarData* _avatar); static QUrl _defaultFullAvatarModelUrl; diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 79af620661..9221fec140 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -26,7 +27,6 @@ #include "EntityTreeRenderer.h" -const float DPI = 30.47f; const float METERS_TO_INCHES = 39.3701f; static uint32_t _currentWebCount { 0 }; // Don't allow more than 100 concurrent web views @@ -34,6 +34,8 @@ static const uint32_t MAX_CONCURRENT_WEB_VIEWS = 100; // If a web-view hasn't been rendered for 30 seconds, de-allocate the framebuffer static uint64_t MAX_NO_RENDER_INTERVAL = 30 * USECS_PER_SECOND; +static int MAX_WINDOW_SIZE = 4096; + EntityItemPointer RenderableWebEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { EntityItemPointer entity{ new RenderableWebEntityItem(entityID) }; entity->setProperties(properties); @@ -67,6 +69,7 @@ bool RenderableWebEntityItem::buildWebSurface(EntityTreeRenderer* renderer) { _webSurface->load("WebView.qml"); _webSurface->resume(); _webSurface->getRootItem()->setProperty("url", _sourceUrl); + _webSurface->getRootContext()->setContextProperty("desktop", QVariant()); _connection = QObject::connect(_webSurface, &OffscreenQmlSurface::textureUpdated, [&](GLuint textureId) { _texture = textureId; }); @@ -87,7 +90,7 @@ bool RenderableWebEntityItem::buildWebSurface(EntityTreeRenderer* renderer) { QTouchEvent::TouchPoint point; point.setId(event.getID()); point.setState(Qt::TouchPointReleased); - glm::vec2 windowPos = event.getPos2D() * (METERS_TO_INCHES * DPI); + glm::vec2 windowPos = event.getPos2D() * (METERS_TO_INCHES * _dpi); QPointF windowPoint(windowPos.x, windowPos.y); point.setPos(windowPoint); QList touchPoints; @@ -99,6 +102,19 @@ bool RenderableWebEntityItem::buildWebSurface(EntityTreeRenderer* renderer) { return true; } +glm::vec2 RenderableWebEntityItem::getWindowSize() const { + glm::vec2 dims = glm::vec2(getDimensions()); + dims *= METERS_TO_INCHES * _dpi; + + // ensure no side is never larger then MAX_WINDOW_SIZE + float max = (dims.x > dims.y) ? dims.x : dims.y; + if (max > MAX_WINDOW_SIZE) { + dims *= MAX_WINDOW_SIZE / max; + } + + return dims; +} + void RenderableWebEntityItem::render(RenderArgs* args) { checkFading(); @@ -124,12 +140,13 @@ void RenderableWebEntityItem::render(RenderArgs* args) { } _lastRenderTime = usecTimestampNow(); - glm::vec2 dims = glm::vec2(getDimensions()); - dims *= METERS_TO_INCHES * DPI; + + glm::vec2 windowSize = getWindowSize(); + // The offscreen surface is idempotent for resizes (bails early // if it's a no-op), so it's safe to just call resize every frame // without worrying about excessive overhead. - _webSurface->resize(QSize(dims.x, dims.y)); + _webSurface->resize(QSize(windowSize.x, windowSize.y)); PerformanceTimer perfTimer("RenderableWebEntityItem::render"); Q_ASSERT(getType() == EntityTypes::Web); @@ -185,7 +202,7 @@ void RenderableWebEntityItem::handlePointerEvent(const PointerEvent& event) { return; } - glm::vec2 windowPos = event.getPos2D() * (METERS_TO_INCHES * DPI); + glm::vec2 windowPos = event.getPos2D() * (METERS_TO_INCHES * _dpi); QPointF windowPoint(windowPos.x, windowPos.y); if (event.getType() == PointerEvent::Move) { diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.h b/libraries/entities-renderer/src/RenderableWebEntityItem.h index ee8a484109..7bfd40864b 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.h +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.h @@ -47,6 +47,7 @@ public: private: bool buildWebSurface(EntityTreeRenderer* renderer); void destroyWebSurface(); + glm::vec2 getWindowSize() const; OffscreenQmlSurface* _webSurface{ nullptr }; QMetaObject::Connection _connection; diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index b3d810c0eb..06c91b4f32 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -335,6 +335,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_OWNING_AVATAR_ID, owningAvatarID); CHECK_PROPERTY_CHANGE(PROP_SHAPE, shape); + CHECK_PROPERTY_CHANGE(PROP_DPI, dpi); changedProperties += _animation.getChangedProperties(); changedProperties += _keyLight.getChangedProperties(); @@ -504,6 +505,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool // Web only if (_type == EntityTypes::Web) { COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SOURCE_URL, sourceUrl); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_DPI, dpi); } // PolyVoxel only @@ -726,6 +728,8 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(clientOnly, bool, setClientOnly); COPY_PROPERTY_FROM_QSCRIPTVALUE(owningAvatarID, QUuid, setOwningAvatarID); + COPY_PROPERTY_FROM_QSCRIPTVALUE(dpi, uint16_t, setDPI); + _lastEdited = usecTimestampNow(); } @@ -903,6 +907,8 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue ADD_PROPERTY_TO_MAP(PROP_FLYING_ALLOWED, FlyingAllowed, flyingAllowed, bool); ADD_PROPERTY_TO_MAP(PROP_GHOSTING_ALLOWED, GhostingAllowed, ghostingAllowed, bool); + ADD_PROPERTY_TO_MAP(PROP_DPI, DPI, dpi, uint16_t); + // FIXME - these are not yet handled //ADD_PROPERTY_TO_MAP(PROP_CREATED, Created, created, quint64); @@ -1065,6 +1071,7 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem if (properties.getType() == EntityTypes::Web) { APPEND_ENTITY_PROPERTY(PROP_SOURCE_URL, properties.getSourceUrl()); + APPEND_ENTITY_PROPERTY(PROP_DPI, properties.getDPI()); } if (properties.getType() == EntityTypes::Text) { @@ -1364,6 +1371,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int if (properties.getType() == EntityTypes::Web) { READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SOURCE_URL, QString, setSourceUrl); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_DPI, uint16_t, setDPI); } if (properties.getType() == EntityTypes::Text) { @@ -1642,6 +1650,8 @@ void EntityItemProperties::markAllChanged() { _clientOnlyChanged = true; _owningAvatarIDChanged = true; + + _dpiChanged = true; } // The minimum bounding box for the entity. @@ -1977,6 +1987,10 @@ QList EntityItemProperties::listChangedProperties() { out += "ghostingAllowed"; } + if (dpiChanged()) { + out += "dpi"; + } + if (shapeChanged()) { out += "shape"; } diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 22d740e0dd..4591dabc51 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -215,6 +215,8 @@ public: DEFINE_PROPERTY(PROP_CLIENT_ONLY, ClientOnly, clientOnly, bool, false); DEFINE_PROPERTY_REF(PROP_OWNING_AVATAR_ID, OwningAvatarID, owningAvatarID, QUuid, UNKNOWN_ENTITY_ID); + DEFINE_PROPERTY_REF(PROP_DPI, DPI, dpi, uint16_t, ENTITY_ITEM_DEFAULT_DPI); + static QString getBackgroundModeString(BackgroundMode mode); diff --git a/libraries/entities/src/EntityItemPropertiesDefaults.h b/libraries/entities/src/EntityItemPropertiesDefaults.h index 4ec0bea7df..3ab827a222 100644 --- a/libraries/entities/src/EntityItemPropertiesDefaults.h +++ b/libraries/entities/src/EntityItemPropertiesDefaults.h @@ -73,4 +73,6 @@ const bool ENTITY_ITEM_DEFAULT_BILLBOARDED = false; const QString ENTITY_ITEM_DEFAULT_NAME = QString(""); +const uint16_t ENTITY_ITEM_DEFAULT_DPI = 30; + #endif // hifi_EntityItemPropertiesDefaults_h diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index 0baef0fa25..e09db1e867 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -176,6 +176,7 @@ enum EntityPropertyList { PROP_OWNING_AVATAR_ID, // doesn't go over wire PROP_SHAPE, + PROP_DPI, PROP_LOCAL_VELOCITY, // only used to convert values to and from scripts PROP_LOCAL_ANGULAR_VELOCITY, // only used to convert values to and from scripts diff --git a/libraries/entities/src/WebEntityItem.cpp b/libraries/entities/src/WebEntityItem.cpp index b31e47608f..1a8bf074d2 100644 --- a/libraries/entities/src/WebEntityItem.cpp +++ b/libraries/entities/src/WebEntityItem.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -30,6 +31,7 @@ EntityItemPointer WebEntityItem::factory(const EntityItemID& entityID, const Ent WebEntityItem::WebEntityItem(const EntityItemID& entityItemID) : EntityItem(entityItemID) { _type = EntityTypes::Web; + _dpi = ENTITY_ITEM_DEFAULT_DPI; } const float WEB_ENTITY_ITEM_FIXED_DEPTH = 0.01f; @@ -42,6 +44,7 @@ void WebEntityItem::setDimensions(const glm::vec3& value) { EntityItemProperties WebEntityItem::getProperties(EntityPropertyFlags desiredProperties) const { EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class COPY_ENTITY_PROPERTY_TO_PROPERTIES(sourceUrl, getSourceUrl); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(dpi, getDPI); return properties; } @@ -50,6 +53,7 @@ bool WebEntityItem::setProperties(const EntityItemProperties& properties) { somethingChanged = EntityItem::setProperties(properties); // set the properties in our base class SET_ENTITY_PROPERTY_FROM_PROPERTIES(sourceUrl, setSourceUrl); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(dpi, setDPI); if (somethingChanged) { bool wantDebug = false; @@ -74,6 +78,7 @@ int WebEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, i const unsigned char* dataAt = data; READ_ENTITY_PROPERTY(PROP_SOURCE_URL, QString, setSourceUrl); + READ_ENTITY_PROPERTY(PROP_DPI, uint16_t, setDPI); return bytesRead; } @@ -83,6 +88,7 @@ int WebEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, i EntityPropertyFlags WebEntityItem::getEntityProperties(EncodeBitstreamParams& params) const { EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); requestedProperties += PROP_SOURCE_URL; + requestedProperties += PROP_DPI; return requestedProperties; } @@ -96,6 +102,7 @@ void WebEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBitst bool successPropertyFits = true; APPEND_ENTITY_PROPERTY(PROP_SOURCE_URL, _sourceUrl); + APPEND_ENTITY_PROPERTY(PROP_DPI, _dpi); } bool WebEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, @@ -123,3 +130,11 @@ void WebEntityItem::setSourceUrl(const QString& value) { } const QString& WebEntityItem::getSourceUrl() const { return _sourceUrl; } + +void WebEntityItem::setDPI(uint16_t value) { + _dpi = value; +} + +uint16_t WebEntityItem::getDPI() const { + return _dpi; +} diff --git a/libraries/entities/src/WebEntityItem.h b/libraries/entities/src/WebEntityItem.h index 7c4e9f801a..5a8097a9f1 100644 --- a/libraries/entities/src/WebEntityItem.h +++ b/libraries/entities/src/WebEntityItem.h @@ -54,8 +54,14 @@ public: virtual void setSourceUrl(const QString& value); const QString& getSourceUrl() const; + virtual bool wantsHandControllerPointerEvents() const override { return true; } + + void setDPI(uint16_t value); + uint16_t getDPI() const; + protected: QString _sourceUrl; + uint16_t _dpi; }; #endif // hifi_WebEntityItem_h diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index 366bc96ca3..c47312f9f6 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -166,7 +166,7 @@ private: QMyQuickRenderControl* _renderControl{ nullptr }; FramebufferPtr _fbo; RenderbufferPtr _depthStencil; - TextureRecycler _textures; + TextureRecycler _textures { true }; GLTextureEscrow _escrow; uint64_t _lastRenderTime{ 0 }; @@ -399,6 +399,8 @@ void OffscreenQmlRenderThread::render() { glGetError(); } + Context::Bound(oglplus::Texture::Target::_2D, *texture).GenerateMipmap(); + // FIXME probably unecessary DefaultFramebuffer().Bind(Framebuffer::Target::Draw); _quickWindow->resetOpenGLState(); diff --git a/libraries/gl/src/gl/OglplusHelpers.cpp b/libraries/gl/src/gl/OglplusHelpers.cpp index 1154042b4a..d31800dc4c 100644 --- a/libraries/gl/src/gl/OglplusHelpers.cpp +++ b/libraries/gl/src/gl/OglplusHelpers.cpp @@ -509,16 +509,28 @@ TexturePtr TextureRecycler::getNextTexture() { using namespace oglplus; if (_readyTextures.empty()) { TexturePtr newTexture(new Texture()); - Context::Bound(oglplus::Texture::Target::_2D, *newTexture) - .MinFilter(TextureMinFilter::Linear) - .MagFilter(TextureMagFilter::Linear) - .WrapS(TextureWrap::ClampToEdge) - .WrapT(TextureWrap::ClampToEdge) - .Image2D( - 0, PixelDataInternalFormat::RGBA8, - _size.x, _size.y, - 0, PixelDataFormat::RGB, PixelDataType::UnsignedByte, nullptr - ); + + if (_useMipmaps) { + Context::Bound(oglplus::Texture::Target::_2D, *newTexture) + .MinFilter(TextureMinFilter::LinearMipmapLinear) + .MagFilter(TextureMagFilter::Linear) + .WrapS(TextureWrap::ClampToEdge) + .WrapT(TextureWrap::ClampToEdge) + .Anisotropy(8.0f) + .LODBias(-0.2f) + .Image2D(0, PixelDataInternalFormat::RGBA8, + _size.x, _size.y, + 0, PixelDataFormat::RGB, PixelDataType::UnsignedByte, nullptr); + } else { + Context::Bound(oglplus::Texture::Target::_2D, *newTexture) + .MinFilter(TextureMinFilter::Linear) + .MagFilter(TextureMagFilter::Linear) + .WrapS(TextureWrap::ClampToEdge) + .WrapT(TextureWrap::ClampToEdge) + .Image2D(0, PixelDataInternalFormat::RGBA8, + _size.x, _size.y, + 0, PixelDataFormat::RGB, PixelDataType::UnsignedByte, nullptr); + } GLuint texId = GetName(*newTexture); _allTextures[texId] = TexInfo{ newTexture, _size }; _readyTextures.push(newTexture); diff --git a/libraries/gl/src/gl/OglplusHelpers.h b/libraries/gl/src/gl/OglplusHelpers.h index fe5822c4be..ab47689312 100644 --- a/libraries/gl/src/gl/OglplusHelpers.h +++ b/libraries/gl/src/gl/OglplusHelpers.h @@ -190,6 +190,7 @@ using BasicFramebufferWrapperPtr = std::shared_ptr; class TextureRecycler { public: + TextureRecycler(bool useMipmaps) : _useMipmaps(useMipmaps) {} void setSize(const uvec2& size); void clear(); TexturePtr getNextTexture(); @@ -212,4 +213,5 @@ private: Map _allTextures; Queue _readyTextures; uvec2 _size{ 1920, 1080 }; + bool _useMipmaps; }; diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index e9d61a827a..0d25d4f1be 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -47,12 +47,12 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityAdd: case PacketType::EntityEdit: case PacketType::EntityData: - return VERSION_MODEL_ENTITIES_SUPPORT_SIMPLE_HULLS; + return VERSION_WEB_ENTITIES_SUPPORT_DPI; case PacketType::AvatarIdentity: case PacketType::AvatarData: case PacketType::BulkAvatarData: case PacketType::KillAvatar: - return static_cast(AvatarMixerPacketVersion::AbsoluteSixByteRotations); + return static_cast(AvatarMixerPacketVersion::SensorToWorldMat); case PacketType::ICEServerHeartbeat: return 18; // ICE Server Heartbeat signing case PacketType::AssetGetInfo: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 40524e2288..3ecdb75a18 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -186,12 +186,14 @@ const PacketVersion VERSION_ENTITIES_MORE_SHAPES = 59; const PacketVersion VERSION_ENTITIES_PROPERLY_ENCODE_SHAPE_EDITS = 60; const PacketVersion VERSION_MODEL_ENTITIES_SUPPORT_STATIC_MESH = 61; const PacketVersion VERSION_MODEL_ENTITIES_SUPPORT_SIMPLE_HULLS = 62; +const PacketVersion VERSION_WEB_ENTITIES_SUPPORT_DPI = 63; enum class AvatarMixerPacketVersion : PacketVersion { TranslationSupport = 17, SoftAttachmentSupport, AvatarEntities, - AbsoluteSixByteRotations + AbsoluteSixByteRotations, + SensorToWorldMat }; enum class DomainConnectRequestVersion : PacketVersion { diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index ab1fe76a91..ac1c844cc9 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -234,6 +234,10 @@ CONTROLLER_STATE_MACHINE[STATE_ENTITY_TOUCHING] = { updateMethod: "entityTouching" }; +function angleBetween(a, b) { + return Math.acos(Vec3.dot(Vec3.normalize(a), Vec3.normalize(b))); +} + function projectOntoEntityXYPlane(entityID, worldPos) { var props = entityPropertiesCache.getProps(entityID); var invRot = Quat.inverse(props.rotation); @@ -1975,7 +1979,8 @@ function MyController(hand) { var handPosition = this.getHandPosition(); // the center of the equipped object being far from the hand isn't enough to auto-unequip -- we also // need to fail the findEntities test. - var nearPickedCandidateEntities = Entities.findEntities(handPosition, NEAR_GRAB_RADIUS); + var TEAR_AWAY_DISTANCE = 0.04; + var nearPickedCandidateEntities = Entities.findEntities(handPosition, NEAR_GRAB_RADIUS + TEAR_AWAY_DISTANCE); if (nearPickedCandidateEntities.indexOf(this.grabbedEntity) == -1) { // for whatever reason, the held/equipped entity has been pulled away. ungrab or unequip. print("handControllerGrab -- autoreleasing held or equipped item because it is far from hand." + @@ -2128,6 +2133,11 @@ function MyController(hand) { Entities.sendMousePressOnEntity(this.grabbedEntity, pointerEvent); Entities.sendClickDownOnEntity(this.grabbedEntity, pointerEvent); + + this.touchingEnterTimer = 0; + this.touchingEnterPointerEvent = pointerEvent; + this.touchingEnterPointerEvent.button = "None"; + this.deadspotExpired = false; } }; @@ -2135,27 +2145,37 @@ function MyController(hand) { // test for intersection between controller laser and web entity plane. var intersectInfo = handLaserIntersectEntity(this.grabbedEntity, this.hand); if (intersectInfo) { - var pointerEvent = { - type: "Release", - id: this.hand + 1, // 0 is reserved for hardware mouse - pos2D: projectOntoEntityXYPlane(this.grabbedEntity, intersectInfo.point), - pos3D: intersectInfo.point, - normal: intersectInfo.normal, - direction: intersectInfo.searchRay.direction, - button: "Primary", - isPrimaryButton: true, - isSecondaryButton: false, - isTertiaryButton: false - }; + var pointerEvent; + if (this.deadspotExpired) { + pointerEvent = { + type: "Release", + id: this.hand + 1, // 0 is reserved for hardware mouse + pos2D: projectOntoEntityXYPlane(this.grabbedEntity, intersectInfo.point), + pos3D: intersectInfo.point, + normal: intersectInfo.normal, + direction: intersectInfo.searchRay.direction, + button: "Primary", + isPrimaryButton: false, + isSecondaryButton: false, + isTertiaryButton: false + }; + } else { + pointerEvent = this.touchingEnterPointerEvent; + pointerEvent.button = "Primary"; + pointerEvent.isPrimaryButton = false; + } Entities.sendMouseReleaseOnEntity(this.grabbedEntity, pointerEvent); Entities.sendClickReleaseOnEntity(this.grabbedEntity, pointerEvent); Entities.sendHoverLeaveEntity(this.grabbedEntity, pointerEvent); - this.focusedEntity = null; } + this.focusedEntity = null; }; - this.entityTouching = function() { + this.entityTouching = function(dt) { + + this.touchingEnterTimer += dt; + entityPropertiesCache.addEntity(this.grabbedEntity); if (!this.triggerSmoothedGrab()) { @@ -2184,8 +2204,14 @@ function MyController(hand) { isTertiaryButton: false }; - Entities.sendMouseMoveOnEntity(this.grabbedEntity, pointerEvent); - Entities.sendHoldingClickOnEntity(this.grabbedEntity, pointerEvent); + var POINTER_PRESS_TO_MOVE_DELAY = 0.15; // seconds + var POINTER_PRESS_TO_MOVE_DEADSPOT_ANGLE = 0.05; // radians ~ 3 degrees + if (this.deadspotExpired || this.touchingEnterTimer > POINTER_PRESS_TO_MOVE_DELAY || + angleBetween(pointerEvent.direction, this.touchingEnterPointerEvent.direction) > POINTER_PRESS_TO_MOVE_DEADSPOT_ANGLE) { + Entities.sendMouseMoveOnEntity(this.grabbedEntity, pointerEvent); + Entities.sendHoldingClickOnEntity(this.grabbedEntity, pointerEvent); + this.deadspotExpired = true; + } this.intersectionDistance = intersectInfo.distance; this.searchIndicatorOn(intersectInfo.searchRay); diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index 4d6e9d7f60..5888b53a27 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -121,7 +121,7 @@ function ignoreMouseActivity() { return true; } var pos = Reticle.position; - if (pos.x == -1 && pos.y == -1) { + if (!pos || (pos.x == -1 && pos.y == -1)) { return true; } // Only we know if we moved it, which is why this script has to replace depthReticle.js diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 26c7f53997..6de1eec7d0 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -525,6 +525,10 @@ +
+ + +
M
@@ -557,4 +561,4 @@ - \ No newline at end of file + diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index a20906fe34..41a1266434 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -630,6 +630,7 @@ function loaded() { var elWebSections = document.querySelectorAll(".web-section"); allSections.push(elWebSections); var elWebSourceURL = document.getElementById("property-web-source-url"); + var elWebDPI = document.getElementById("property-web-dpi"); var elDescription = document.getElementById("property-description"); @@ -931,6 +932,7 @@ function loaded() { } elWebSourceURL.value = properties.sourceUrl; + elWebDPI.value = properties.dpi; } else if (properties.type == "Text") { for (var i = 0; i < elTextSections.length; i++) { elTextSections[i].style.display = 'table'; @@ -1228,6 +1230,7 @@ function loaded() { elShape.addEventListener('change', createEmitTextPropertyUpdateFunction('shape')); elWebSourceURL.addEventListener('change', createEmitTextPropertyUpdateFunction('sourceUrl')); + elWebDPI.addEventListener('change', createEmitNumberPropertyUpdateFunction('dpi')); elModelURL.addEventListener('change', createEmitTextPropertyUpdateFunction('modelURL')); elShapeType.addEventListener('change', createEmitTextPropertyUpdateFunction('shapeType')); diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js new file mode 100644 index 0000000000..34368e475b --- /dev/null +++ b/scripts/system/libraries/WebTablet.js @@ -0,0 +1,93 @@ +// +// WebTablet.js +// +// Created by Anthony J. Thibault on 8/8/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var RAD_TO_DEG = 180 / Math.PI; +var X_AXIS = {x: 1, y: 0, z: 0}; +var Y_AXIS = {x: 0, y: 1, z: 0}; + +var TABLET_URL = "https://s3.amazonaws.com/hifi-public/tony/tablet.fbx"; + +// returns object with two fields: +// * position - position in front of the user +// * rotation - rotation of entity so it faces the user. +function calcSpawnInfo() { + var front; + var pitchBackRotation = Quat.angleAxis(20.0, X_AXIS); + if (HMD.active) { + front = Quat.getFront(HMD.orientation); + var yawOnlyRotation = Quat.angleAxis(Math.atan2(front.x, front.z) * RAD_TO_DEG, Y_AXIS); + return { + position: Vec3.sum(Vec3.sum(HMD.position, Vec3.multiply(0.6, front)), Vec3.multiply(-0.5, Y_AXIS)), + rotation: Quat.multiply(yawOnlyRotation, pitchBackRotation) + }; + } else { + front = Quat.getFront(MyAvatar.orientation); + return { + position: Vec3.sum(Vec3.sum(MyAvatar.position, Vec3.multiply(0.6, front)), {x: 0, y: 0.6, z: 0}), + rotation: Quat.multiply(MyAvatar.orientation, pitchBackRotation) + }; + } +} + +// ctor +WebTablet = function (url) { + + var ASPECT = 4.0 / 3.0; + var WIDTH = 0.4; + var HEIGHT = WIDTH * ASPECT; + var DEPTH = 0.025; + + var spawnInfo = calcSpawnInfo(); + + var tabletEntityPosition = spawnInfo.position; + var tabletEntityRotation = spawnInfo.rotation; + this.tabletEntityID = Entities.addEntity({ + name: "tablet", + type: "Model", + modelURL: TABLET_URL, + position: tabletEntityPosition, + rotation: tabletEntityRotation, + userData: JSON.stringify({ + "grabbableKey": {"grabbable": true} + }), + dimensions: {x: WIDTH, y: HEIGHT, z: DEPTH}, + parentID: MyAvatar.sessionUUID, + parentJointIndex: -2 + }); + + var WEB_ENTITY_REDUCTION_FACTOR = {x: 0.78, y: 0.85}; + var WEB_ENTITY_Z_OFFSET = -0.01; + + var webEntityRotation = Quat.multiply(spawnInfo.rotation, Quat.angleAxis(180, Y_AXIS)); + var webEntityPosition = Vec3.sum(spawnInfo.position, Vec3.multiply(WEB_ENTITY_Z_OFFSET, Quat.getFront(webEntityRotation))); + + this.webEntityID = Entities.addEntity({ + name: "web", + type: "Web", + sourceUrl: url, + dimensions: {x: WIDTH * WEB_ENTITY_REDUCTION_FACTOR.x, + y: HEIGHT * WEB_ENTITY_REDUCTION_FACTOR.y, + z: 0.1}, + position: webEntityPosition, + rotation: webEntityRotation, + shapeType: "box", + dpi: 45, + parentID: this.tabletEntityID, + parentJointIndex: -1 + }); + + this.state = "idle"; +}; + +WebTablet.prototype.destroy = function () { + Entities.deleteEntity(this.webEntityID); + Entities.deleteEntity(this.tabletEntityID); +}; + diff --git a/scripts/system/marketplaces/marketplace.js b/scripts/system/marketplaces/marketplace.js index a43ef6c977..2bd6033e62 100644 --- a/scripts/system/marketplaces/marketplace.js +++ b/scripts/system/marketplaces/marketplace.js @@ -8,6 +8,9 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +/* global WebTablet */ +Script.include("../libraries/WebTablet.js"); + var toolIconUrl = Script.resolvePath("../assets/images/tools/"); var MARKETPLACE_URL = "https://metaverse.highfidelity.com/marketplace"; @@ -22,26 +25,45 @@ var marketplaceWindow = new OverlayWebWindow({ var toolHeight = 50; var toolWidth = 50; var TOOLBAR_MARGIN_Y = 0; +var marketplaceVisible = false; +var marketplaceWebTablet; +function shouldShowWebTablet() { + var rightPose = Controller.getPoseValue(Controller.Standard.RightHand); + var leftPose = Controller.getPoseValue(Controller.Standard.LeftHand); + var hasHydra = !!Controller.Hardware.Hydra; + return HMD.active && (leftPose.valid || rightPose.valid || hasHydra); +} function showMarketplace(marketplaceID) { - var url = MARKETPLACE_URL; - if (marketplaceID) { - url = url + "/items/" + marketplaceID; + if (shouldShowWebTablet()) { + marketplaceWebTablet = new WebTablet("https://metaverse.highfidelity.com/marketplace"); + } else { + var url = MARKETPLACE_URL; + if (marketplaceID) { + url = url + "/items/" + marketplaceID; + } + marketplaceWindow.setURL(url); + marketplaceWindow.setVisible(true); } - marketplaceWindow.setURL(url); - marketplaceWindow.setVisible(true); + marketplaceVisible = true; UserActivityLogger.openedMarketplace(); } function hideMarketplace() { - marketplaceWindow.setVisible(false); - marketplaceWindow.setURL("about:blank"); + if (marketplaceWindow.visible) { + marketplaceWindow.setVisible(false); + marketplaceWindow.setURL("about:blank"); + } else if (marketplaceWebTablet) { + marketplaceWebTablet.destroy(); + marketplaceWebTablet = null; + } + marketplaceVisible = false; } function toggleMarketplace() { - if (marketplaceWindow.visible) { + if (marketplaceVisible) { hideMarketplace(); } else { showMarketplace(); @@ -59,19 +81,22 @@ var browseExamplesButton = toolBar.addButton({ alpha: 0.9 }); -function onExamplesWindowVisibilityChanged() { +function onMarketplaceWindowVisibilityChanged() { browseExamplesButton.writeProperty('buttonState', marketplaceWindow.visible ? 0 : 1); browseExamplesButton.writeProperty('defaultState', marketplaceWindow.visible ? 0 : 1); browseExamplesButton.writeProperty('hoverState', marketplaceWindow.visible ? 2 : 3); + marketplaceVisible = marketplaceWindow.visible; } + function onClick() { toggleMarketplace(); } + browseExamplesButton.clicked.connect(onClick); -marketplaceWindow.visibleChanged.connect(onExamplesWindowVisibilityChanged); +marketplaceWindow.visibleChanged.connect(onMarketplaceWindowVisibilityChanged); Script.scriptEnding.connect(function () { toolBar.removeButton("marketplace"); browseExamplesButton.clicked.disconnect(onClick); - marketplaceWindow.visibleChanged.disconnect(onExamplesWindowVisibilityChanged); -}); \ No newline at end of file + marketplaceWindow.visibleChanged.disconnect(onMarketplaceWindowVisibilityChanged); +});