diff --git a/assignment-client/src/AvatarAudioTimer.cpp b/assignment-client/src/AvatarAudioTimer.cpp index 77dd61043e..d031b9d9f6 100644 --- a/assignment-client/src/AvatarAudioTimer.cpp +++ b/assignment-client/src/AvatarAudioTimer.cpp @@ -15,7 +15,6 @@ // this should send a signal every 10ms, with pretty good precision. Hardcoding // to 10ms since that's what you'd want for audio. void AvatarAudioTimer::start() { - qDebug() << __FUNCTION__; auto startTime = usecTimestampNow(); quint64 frameCounter = 0; const int TARGET_INTERVAL_USEC = 10000; // 10ms diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index ffd7cc703b..3dba1ce1c2 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -95,7 +95,7 @@ AudioMixer::AudioMixer(ReceivedMessage& message) : packetReceiver.registerListener(PacketType::NodeIgnoreRequest, this, "handleNodeIgnoreRequestPacket"); packetReceiver.registerListener(PacketType::KillAvatar, this, "handleKillAvatarPacket"); packetReceiver.registerListener(PacketType::NodeMuteRequest, this, "handleNodeMuteRequestPacket"); - + packetReceiver.registerListener(PacketType::RadiusIgnoreRequest, this, "handleRadiusIgnoreRequestPacket"); connect(nodeList.data(), &NodeList::nodeKilled, this, &AudioMixer::handleNodeKilled); } @@ -393,16 +393,26 @@ bool AudioMixer::prepareMixForListeningNode(Node* node) { && !node->isIgnoringNodeWithID(otherNode->getUUID()) && !otherNode->isIgnoringNodeWithID(node->getUUID())) { AudioMixerClientData* otherNodeClientData = (AudioMixerClientData*) otherNode->getLinkedData(); - // enumerate the ARBs attached to the otherNode and add all that should be added to mix - auto streamsCopy = otherNodeClientData->getAudioStreams(); + // check to see if we're ignoring in radius + bool insideIgnoreRadius = false; + if (node->isIgnoreRadiusEnabled() || otherNode->isIgnoreRadiusEnabled()) { + AudioMixerClientData* otherData = reinterpret_cast(otherNode->getLinkedData()); + AudioMixerClientData* nodeData = reinterpret_cast(node->getLinkedData()); + float ignoreRadius = glm::min(node->getIgnoreRadius(), otherNode->getIgnoreRadius()); + if (glm::distance(nodeData->getPosition(), otherData->getPosition()) < ignoreRadius) { + insideIgnoreRadius = true; + } + } - for (auto& streamPair : streamsCopy) { - - auto otherNodeStream = streamPair.second; - - if (*otherNode != *node || otherNodeStream->shouldLoopbackForNode()) { - addStreamToMixForListeningNodeWithStream(*listenerNodeData, *otherNodeStream, otherNode->getUUID(), - *nodeAudioStream); + if (!insideIgnoreRadius) { + // enumerate the ARBs attached to the otherNode and add all that should be added to mix + auto streamsCopy = otherNodeClientData->getAudioStreams(); + for (auto& streamPair : streamsCopy) { + auto otherNodeStream = streamPair.second; + if (*otherNode != *node || otherNodeStream->shouldLoopbackForNode()) { + addStreamToMixForListeningNodeWithStream(*listenerNodeData, *otherNodeStream, otherNode->getUUID(), + *nodeAudioStream); + } } } } @@ -634,11 +644,14 @@ void AudioMixer::handleKillAvatarPacket(QSharedPointer packet, } } - void AudioMixer::handleNodeIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode) { sendingNode->parseIgnoreRequestMessage(packet); } +void AudioMixer::handleRadiusIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode) { + sendingNode->parseIgnoreRadiusRequestMessage(packet); +} + void AudioMixer::removeHRTFsForFinishedInjector(const QUuid& streamID) { auto injectorClientData = qobject_cast(sender()); if (injectorClientData) { diff --git a/assignment-client/src/audio/AudioMixer.h b/assignment-client/src/audio/AudioMixer.h index 91eafadd9d..9bf337fe60 100644 --- a/assignment-client/src/audio/AudioMixer.h +++ b/assignment-client/src/audio/AudioMixer.h @@ -48,6 +48,7 @@ private slots: void handleNegotiateAudioFormat(QSharedPointer message, SharedNodePointer sendingNode); void handleNodeKilled(SharedNodePointer killedNode); void handleNodeIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode); + void handleRadiusIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode); void handleKillAvatarPacket(QSharedPointer packet, SharedNodePointer sendingNode); void handleNodeMuteRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode); diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 5b8c4aa105..70d6a67b5b 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -365,10 +365,6 @@ QJsonObject AudioMixerClientData::getAudioStreamStats() { } void AudioMixerClientData::handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec) { - qDebug() << __FUNCTION__ << - "sendingNode:" << *node << - "currentCodec:" << currentCodec << - "receivedCodec:" << recievedCodec; sendSelectAudioFormat(node, currentCodec); } diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index c74461a444..a8b6b6606d 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -89,6 +89,7 @@ public: bool shouldMuteClient() { return _shouldMuteClient; } void setShouldMuteClient(bool shouldMuteClient) { _shouldMuteClient = shouldMuteClient; } + glm::vec3 getPosition() { return getAvatarAudioStream() ? getAvatarAudioStream()->getPosition() : glm::vec3(0); } signals: void injectorStreamFinished(const QUuid& streamIdentifier); diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 2c9fadc7b1..63cda4a4ff 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -46,6 +46,7 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) : packetReceiver.registerListener(PacketType::AvatarIdentity, this, "handleAvatarIdentityPacket"); packetReceiver.registerListener(PacketType::KillAvatar, this, "handleKillAvatarPacket"); packetReceiver.registerListener(PacketType::NodeIgnoreRequest, this, "handleNodeIgnoreRequestPacket"); + packetReceiver.registerListener(PacketType::RadiusIgnoreRequest, this, "handleRadiusIgnoreRequestPacket"); auto nodeList = DependencyManager::get(); connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &AvatarMixer::handlePacketVersionMismatch); @@ -237,6 +238,20 @@ void AvatarMixer::broadcastAvatarData() { || otherNode->isIgnoringNodeWithID(node->getUUID())) { return false; } else { + AvatarMixerClientData* otherData = reinterpret_cast(otherNode->getLinkedData()); + AvatarMixerClientData* nodeData = reinterpret_cast(node->getLinkedData()); + // check to see if we're ignoring in radius + if (node->isIgnoreRadiusEnabled() || otherNode->isIgnoreRadiusEnabled()) { + float ignoreRadius = glm::min(node->getIgnoreRadius(), otherNode->getIgnoreRadius()); + if (glm::distance(nodeData->getPosition(), otherData->getPosition()) < ignoreRadius) { + nodeData->ignoreOther(node, otherNode); + otherData->ignoreOther(otherNode, node); + return false; + } + } + // not close enough to ignore + nodeData->removeFromRadiusIgnoringSet(otherNode->getUUID()); + otherData->removeFromRadiusIgnoringSet(node->getUUID()); return true; } }, @@ -442,6 +457,10 @@ void AvatarMixer::handleNodeIgnoreRequestPacket(QSharedPointer senderNode->parseIgnoreRequestMessage(message); } +void AvatarMixer::handleRadiusIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode) { + sendingNode->parseIgnoreRadiusRequestMessage(packet); +} + void AvatarMixer::sendStatsPacket() { QJsonObject statsObject; statsObject["average_listeners_last_second"] = (float) _sumListeners / (float) _numStatFrames; diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h index 6e1d722145..f537cc9244 100644 --- a/assignment-client/src/avatars/AvatarMixer.h +++ b/assignment-client/src/avatars/AvatarMixer.h @@ -38,6 +38,7 @@ private slots: void handleAvatarIdentityPacket(QSharedPointer message, SharedNodePointer senderNode); void handleKillAvatarPacket(QSharedPointer message); void handleNodeIgnoreRequestPacket(QSharedPointer message, SharedNodePointer senderNode); + void handleRadiusIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode); void domainSettingsRequestComplete(); void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID); diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index 4b7a696d58..60d03f8930 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -11,6 +11,9 @@ #include +#include +#include + #include "AvatarMixerClientData.h" int AvatarMixerClientData::parseData(ReceivedMessage& message) { @@ -39,6 +42,16 @@ uint16_t AvatarMixerClientData::getLastBroadcastSequenceNumber(const QUuid& node } } +void AvatarMixerClientData::ignoreOther(SharedNodePointer self, SharedNodePointer other) { + if (!isRadiusIgnoring(other->getUUID())) { + addToRadiusIgnoringSet(other->getUUID()); + auto killPacket = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID); + killPacket->write(other->getUUID().toRfc4122()); + DependencyManager::get()->sendUnreliablePacket(*killPacket, *self); + _hasReceivedFirstPacketsFrom.erase(other->getUUID()); + } +} + void AvatarMixerClientData::loadJSONStats(QJsonObject& jsonObject) const { jsonObject["display_name"] = _avatar->getDisplayName(); jsonObject["full_rate_distance"] = _fullRateDistance; diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index 4a816291f4..96bc275a13 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -79,6 +79,13 @@ public: { return _avgOtherAvatarDataRate.getAverageSampleValuePerSecond() / (float) BYTES_PER_KILOBIT; } void loadJSONStats(QJsonObject& jsonObject) const; + + glm::vec3 getPosition() { return _avatar ? _avatar->getPosition() : glm::vec3(0); } + bool isRadiusIgnoring(const QUuid& other) { return _radiusIgnoredOthers.find(other) != _radiusIgnoredOthers.end(); } + void addToRadiusIgnoringSet(const QUuid& other) { _radiusIgnoredOthers.insert(other); } + void removeFromRadiusIgnoringSet(const QUuid& other) { _radiusIgnoredOthers.erase(other); } + void ignoreOther(SharedNodePointer self, SharedNodePointer other); + private: AvatarSharedPointer _avatar { new AvatarData() }; @@ -99,6 +106,7 @@ private: int _numOutOfOrderSends = 0; SimpleMovingAverage _avgOtherAvatarDataRate; + std::unordered_set _radiusIgnoredOthers; }; #endif // hifi_AvatarMixerClientData_h diff --git a/cmake/externals/GifCreator/CMakeLists.txt b/cmake/externals/GifCreator/CMakeLists.txt new file mode 100644 index 0000000000..127bdf28f5 --- /dev/null +++ b/cmake/externals/GifCreator/CMakeLists.txt @@ -0,0 +1,20 @@ +set(EXTERNAL_NAME GifCreator) + +include(ExternalProject) +ExternalProject_Add( + ${EXTERNAL_NAME} + URL https://hifi-public.s3.amazonaws.com/dependencies/GifCreator.zip + URL_MD5 8ac8ef5196f47c658dce784df5ecdb70 + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND "" + LOG_DOWNLOAD 1 +) + +# Hide this external target (for ide users) +set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals") + +ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR) + +string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) +set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${INSTALL_DIR}/src/${EXTERNAL_NAME} CACHE PATH "List of GifCreator include directories") \ No newline at end of file diff --git a/cmake/externals/wasapi/CMakeLists.txt b/cmake/externals/wasapi/CMakeLists.txt index bacdb5b0b7..67f47d68fc 100644 --- a/cmake/externals/wasapi/CMakeLists.txt +++ b/cmake/externals/wasapi/CMakeLists.txt @@ -6,8 +6,8 @@ if (WIN32) include(ExternalProject) ExternalProject_Add( ${EXTERNAL_NAME} - URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi3.zip - URL_MD5 1a2433f80a788a54c70f505ff4f43ac1 + URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi4.zip + URL_MD5 2abde5340a64d387848f12b9536a7e85 CONFIGURE_COMMAND "" BUILD_COMMAND "" INSTALL_COMMAND "" diff --git a/cmake/modules/FindGifCreator.cmake b/cmake/modules/FindGifCreator.cmake new file mode 100644 index 0000000000..def9f1d131 --- /dev/null +++ b/cmake/modules/FindGifCreator.cmake @@ -0,0 +1,26 @@ +# +# FindGifCreator.cmake +# +# Try to find GifCreator include path. +# Once done this will define +# +# GIFCREATOR_INCLUDE_DIRS +# +# Created on 11/15/2016 by Zach Fox +# 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 +# + +# setup hints for GifCreator search +include("${MACRO_DIR}/HifiLibrarySearchHints.cmake") +hifi_library_search_hints("GIFCREATOR") + +# locate header +find_path(GIFCREATOR_INCLUDE_DIRS "GifCreator/GifCreator.h" HINTS ${GIFCREATOR_SEARCH_DIRS}) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(GIFCREATOR DEFAULT_MSG GIFCREATOR_INCLUDE_DIRS) + +mark_as_advanced(GIFCREATOR_INCLUDE_DIRS GIFCREATOR_SEARCH_DIRS) \ No newline at end of file diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 911732fcef..8cd9136895 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -684,6 +684,79 @@ } ] }, + { + "name": "permissions", + "type": "table", + "caption": "Permissions for Specific Users", + "can_add_new_rows": true, + + "groups": [ + { + "label": "User", + "span": 1 + }, + { + "label": "Permissions ?", + "span": 7 + } + ], + + "columns": [ + { + "name": "permissions_id", + "label": "" + }, + { + "name": "id_can_connect", + "label": "Connect", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_adjust_locks", + "label": "Lock / Unlock", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_rez", + "label": "Rez", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_rez_tmp", + "label": "Rez Temporary", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_write_to_asset_server", + "label": "Write Assets", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_connect_past_max_capacity", + "label": "Ignore Max Capacity", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_kick", + "label": "Kick Users", + "type": "checkbox", + "editable": true, + "default": false + } + ] + }, { "name": "ip_permissions", "type": "table", @@ -757,18 +830,17 @@ ] }, { - "name": "permissions", + "name": "mac_permissions", "type": "table", - "caption": "Permissions for Specific Users", + "caption": "Permissions for Users with MAC Addresses", "can_add_new_rows": true, - "groups": [ { - "label": "User", + "label": "MAC Address", "span": 1 }, { - "label": "Permissions ?", + "label": "Permissions ?", "span": 7 } ], diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 051465efd2..f55a2073d1 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -119,15 +119,20 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointersetNodeInterestSet(safeInterestSet); nodeData->setPlaceName(nodeConnection.placeName); + qDebug() << "Allowed connection from node" << uuidStringWithoutCurlyBraces(node->getUUID()) + << "on" << message->getSenderSockAddr() << "with MAC" << nodeConnection.hardwareAddress; + // signal that we just connected a node so the DomainServer can get it a list // and broadcast its presence right away emit connectedNode(node); } else { - qDebug() << "Refusing connection from node at" << message->getSenderSockAddr(); + qDebug() << "Refusing connection from node at" << message->getSenderSockAddr() + << "with hardware address" << nodeConnection.hardwareAddress; } } -NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QString verifiedUsername, const QHostAddress& senderAddress) { +NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QString verifiedUsername, + const QHostAddress& senderAddress, const QString& hardwareAddress) { NodePermissions userPerms; userPerms.setAll(false); @@ -144,8 +149,14 @@ NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QStrin #ifdef WANT_DEBUG qDebug() << "| user-permissions: unverified or no username for" << userPerms.getID() << ", so:" << userPerms; #endif + if (!hardwareAddress.isEmpty() && _server->_settingsManager.hasPermissionsForMAC(hardwareAddress)) { + // this user comes from a MAC we have in our permissions table, apply those permissions + userPerms = _server->_settingsManager.getPermissionsForMAC(hardwareAddress); - if (_server->_settingsManager.hasPermissionsForIP(senderAddress)) { +#ifdef WANT_DEBUG + qDebug() << "| user-permissions: specific MAC matches, so:" << userPerms; +#endif + } else if (_server->_settingsManager.hasPermissionsForIP(senderAddress)) { // this user comes from an IP we have in our permissions table, apply those permissions userPerms = _server->_settingsManager.getPermissionsForIP(senderAddress); @@ -158,6 +169,13 @@ NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QStrin userPerms = _server->_settingsManager.getPermissionsForName(verifiedUsername); #ifdef WANT_DEBUG qDebug() << "| user-permissions: specific user matches, so:" << userPerms; +#endif + } else if (!hardwareAddress.isEmpty() && _server->_settingsManager.hasPermissionsForMAC(hardwareAddress)) { + // this user comes from a MAC we have in our permissions table, apply those permissions + userPerms = _server->_settingsManager.getPermissionsForMAC(hardwareAddress); + +#ifdef WANT_DEBUG + qDebug() << "| user-permissions: specific MAC matches, so:" << userPerms; #endif } else if (_server->_settingsManager.hasPermissionsForIP(senderAddress)) { // this user comes from an IP we have in our permissions table, apply those permissions @@ -255,7 +273,14 @@ void DomainGatekeeper::updateNodePermissions() { // or the public socket if we haven't activated a socket for the node yet HifiSockAddr connectingAddr = node->getActiveSocket() ? *node->getActiveSocket() : node->getPublicSocket(); - userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, connectingAddr.getAddress()); + QString hardwareAddress; + + DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); + if (nodeData) { + hardwareAddress = nodeData->getHardwareAddress(); + } + + userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, connectingAddr.getAddress(), hardwareAddress); } node->setPermissions(userPerms); @@ -308,6 +333,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo nodeData->setAssignmentUUID(matchingQueuedAssignment->getUUID()); nodeData->setWalletUUID(it->second.getWalletUUID()); nodeData->setNodeVersion(it->second.getNodeVersion()); + nodeData->setHardwareAddress(nodeConnection.hardwareAddress); nodeData->setWasAssigned(true); // cleanup the PendingAssignedNodeData for this assignment now that it's connecting @@ -369,7 +395,8 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect } } - userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, nodeConnection.senderSockAddr.getAddress()); + userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, nodeConnection.senderSockAddr.getAddress(), + nodeConnection.hardwareAddress); if (!userPerms.can(NodePermissions::Permission::canConnectToDomain)) { sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.", @@ -425,6 +452,9 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect // if we have a username from the connect request, set it on the DomainServerNodeData nodeData->setUsername(username); + // set the hardware address passed in the connect request + nodeData->setHardwareAddress(nodeConnection.hardwareAddress); + // also add an interpolation to DomainServerNodeData so that servers can get username in stats nodeData->addOverrideForKey(USERNAME_UUID_REPLACEMENT_STATS_KEY, uuidStringWithoutCurlyBraces(newNode->getUUID()), username); diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index b7d2a03af6..b17d0f61a4 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -107,7 +107,8 @@ private: QSet _domainOwnerFriends; // keep track of friends of the domain owner QSet _inFlightGroupMembershipsRequests; // keep track of which we've already asked for - NodePermissions setPermissionsForUser(bool isLocalUser, QString verifiedUsername, const QHostAddress& senderAddress); + NodePermissions setPermissionsForUser(bool isLocalUser, QString verifiedUsername, + const QHostAddress& senderAddress, const QString& hardwareAddress); void getGroupMemberships(const QString& username); // void getIsGroupMember(const QString& username, const QUuid groupID); diff --git a/domain-server/src/DomainServerNodeData.h b/domain-server/src/DomainServerNodeData.h index f95403c779..ff637844f1 100644 --- a/domain-server/src/DomainServerNodeData.h +++ b/domain-server/src/DomainServerNodeData.h @@ -53,6 +53,9 @@ public: void setNodeVersion(const QString& nodeVersion) { _nodeVersion = nodeVersion; } const QString& getNodeVersion() { return _nodeVersion; } + + void setHardwareAddress(const QString& hardwareAddress) { _hardwareAddress = hardwareAddress; } + const QString& getHardwareAddress() { return _hardwareAddress; } void addOverrideForKey(const QString& key, const QString& value, const QString& overrideValue); void removeOverrideForKey(const QString& key, const QString& value); @@ -81,6 +84,7 @@ private: bool _isAuthenticated = true; NodeSet _nodeInterestSet; QString _nodeVersion; + QString _hardwareAddress; QString _placeName; diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index fbc5fd4bd5..21214ed5f6 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -29,6 +29,8 @@ #include #include +#include "DomainServerNodeData.h" + #include "DomainServerSettingsManager.h" const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/describe-settings.json"; @@ -439,6 +441,9 @@ void DomainServerSettingsManager::packPermissions() { // save settings for IP addresses packPermissionsForMap("permissions", _ipPermissions, IP_PERMISSIONS_KEYPATH); + // save settings for MAC addresses + packPermissionsForMap("permissions", _macPermissions, MAC_PERMISSIONS_KEYPATH); + // save settings for groups packPermissionsForMap("permissions", _groupPermissions, GROUP_PERMISSIONS_KEYPATH); @@ -506,6 +511,17 @@ void DomainServerSettingsManager::unpackPermissions() { } }); + needPack |= unpackPermissionsForKeypath(MAC_PERMISSIONS_KEYPATH, &_macPermissions, + [&](NodePermissionsPointer perms){ + // make sure that this permission row is for a non-empty hardware + if (perms->getKey().first.isEmpty()) { + _macPermissions.remove(perms->getKey()); + + // we removed a row from the MAC permissions, we'll need a re-pack + needPack = true; + } + }); + needPack |= unpackPermissionsForKeypath(GROUP_PERMISSIONS_KEYPATH, &_groupPermissions, [&](NodePermissionsPointer perms){ @@ -558,7 +574,8 @@ void DomainServerSettingsManager::unpackPermissions() { qDebug() << "--------------- permissions ---------------------"; QList> permissionsSets; permissionsSets << _standardAgentPermissions.get() << _agentPermissions.get() - << _groupPermissions.get() << _groupForbiddens.get() << _ipPermissions.get(); + << _groupPermissions.get() << _groupForbiddens.get() + << _ipPermissions.get() << _macPermissions.get(); foreach (auto permissionSet, permissionsSets) { QHashIterator i(permissionSet); while (i.hasNext()) { @@ -653,19 +670,25 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointergetPermissions().getVerifiedUserName(); - bool hadExistingPermissions = false; + bool newPermissions = false; if (!verifiedUsername.isEmpty()) { // if we have a verified user name for this user, we apply the kick to the username // check if there were already permissions - hadExistingPermissions = havePermissionsForName(verifiedUsername); + bool hadPermissions = havePermissionsForName(verifiedUsername); // grab or create permissions for the given username - destinationPermissions = _agentPermissions[matchingNode->getPermissions().getKey()]; + auto userPermissions = _agentPermissions[matchingNode->getPermissions().getKey()]; + + newPermissions = !hadPermissions || userPermissions->can(NodePermissions::Permission::canConnectToDomain); + + // ensure that the connect permission is clear + userPermissions->clear(NodePermissions::Permission::canConnectToDomain); } else { - // otherwise we apply the kick to the IP from active socket for this node - // (falling back to the public socket if not yet active) + // otherwise we apply the kick to the IP from active socket for this node and the MAC address + + // remove connect permissions for the IP (falling back to the public socket if not yet active) auto& kickAddress = matchingNode->getActiveSocket() ? matchingNode->getActiveSocket()->getAddress() : matchingNode->getPublicSocket().getAddress(); @@ -673,32 +696,41 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointercan(NodePermissions::Permission::canConnectToDomain)) { + newPermissions = true; + + ipPermissions->clear(NodePermissions::Permission::canConnectToDomain); + } + + // potentially remove connect permissions for the MAC address + DomainServerNodeData* nodeData = reinterpret_cast(matchingNode->getLinkedData()); + if (nodeData) { + NodePermissionsKey macAddressKey(nodeData->getHardwareAddress(), 0); + + bool hadMACPermissions = hasPermissionsForMAC(nodeData->getHardwareAddress()); + + auto macPermissions = _macPermissions[macAddressKey]; + + if (!hadMACPermissions || macPermissions->can(NodePermissions::Permission::canConnectToDomain)) { + newPermissions = true; + + macPermissions->clear(NodePermissions::Permission::canConnectToDomain); + } + } } - // make sure we didn't already have existing permissions that disallowed connect - if (!hadExistingPermissions - || destinationPermissions->can(NodePermissions::Permission::canConnectToDomain)) { - + if (newPermissions) { qDebug() << "Removing connect permission for node" << uuidStringWithoutCurlyBraces(matchingNode->getUUID()) - << "after kick request"; - - // ensure that the connect permission is clear - destinationPermissions->clear(NodePermissions::Permission::canConnectToDomain); + << "after kick request from" << uuidStringWithoutCurlyBraces(sendingNode->getUUID()); // we've changed permissions, time to store them to disk and emit our signal to say they have changed packPermissions(); - - emit updateNodePermissions(); } else { - qWarning() << "Received kick request for node" << uuidStringWithoutCurlyBraces(matchingNode->getUUID()) - << "that already did not have permission to connect"; - - // in this case, though we don't expect the node to be connected to the domain, it is - // emit updateNodePermissions so that the DomainGatekeeper kicks it out emit updateNodePermissions(); } @@ -753,6 +785,16 @@ NodePermissions DomainServerSettingsManager::getPermissionsForIP(const QHostAddr return nullPermissions; } +NodePermissions DomainServerSettingsManager::getPermissionsForMAC(const QString& macAddress) const { + NodePermissionsKey macKey = NodePermissionsKey(macAddress, 0); + if (_macPermissions.contains(macKey)) { + return *(_macPermissions[macKey].get()); + } + NodePermissions nullPermissions; + nullPermissions.setAll(false); + return nullPermissions; +} + NodePermissions DomainServerSettingsManager::getPermissionsForGroup(const QString& groupName, QUuid rankID) const { NodePermissionsKey groupRankKey = NodePermissionsKey(groupName, rankID); if (_groupPermissions.contains(groupRankKey)) { diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index c067377ffc..fcc3e9d91d 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -28,6 +28,7 @@ const QString SETTINGS_PATH_JSON = SETTINGS_PATH + ".json"; const QString AGENT_STANDARD_PERMISSIONS_KEYPATH = "security.standard_permissions"; const QString AGENT_PERMISSIONS_KEYPATH = "security.permissions"; const QString IP_PERMISSIONS_KEYPATH = "security.ip_permissions"; +const QString MAC_PERMISSIONS_KEYPATH = "security.mac_permissions"; const QString GROUP_PERMISSIONS_KEYPATH = "security.group_permissions"; const QString GROUP_FORBIDDENS_KEYPATH = "security.group_forbiddens"; @@ -62,6 +63,10 @@ public: bool hasPermissionsForIP(const QHostAddress& address) const { return _ipPermissions.contains(address.toString(), 0); } NodePermissions getPermissionsForIP(const QHostAddress& address) const; + // these give access to permissions for specific MACs from the domain-server settings page + bool hasPermissionsForMAC(const QString& macAddress) const { return _macPermissions.contains(macAddress, 0); } + NodePermissions getPermissionsForMAC(const QString& macAddress) const; + // these give access to permissions for specific groups from the domain-server settings page bool havePermissionsForGroup(const QString& groupName, QUuid rankID) const { return _groupPermissions.contains(groupName, rankID); @@ -142,6 +147,7 @@ private: NodePermissionsMap _agentPermissions; // specific account-names NodePermissionsMap _ipPermissions; // permissions granted by node IP address + NodePermissionsMap _macPermissions; // permissions granted by node MAC address NodePermissionsMap _groupPermissions; // permissions granted by membership to specific groups NodePermissionsMap _groupForbiddens; // permissions denied due to membership in a specific group diff --git a/domain-server/src/NodeConnectionData.cpp b/domain-server/src/NodeConnectionData.cpp index 13bb9123d8..93d6802d84 100644 --- a/domain-server/src/NodeConnectionData.cpp +++ b/domain-server/src/NodeConnectionData.cpp @@ -29,6 +29,9 @@ NodeConnectionData NodeConnectionData::fromDataStream(QDataStream& dataStream, c // NOTE: QDataStream::readBytes() - The buffer is allocated using new []. Destroy it with the delete [] operator. delete[] rawBytes; + + // read the hardware address sent by the client + dataStream >> newHeader.hardwareAddress; } dataStream >> newHeader.nodeType diff --git a/domain-server/src/NodeConnectionData.h b/domain-server/src/NodeConnectionData.h index 9264db637e..bcbbdf0a40 100644 --- a/domain-server/src/NodeConnectionData.h +++ b/domain-server/src/NodeConnectionData.h @@ -28,6 +28,7 @@ public: HifiSockAddr senderSockAddr; QList interestList; QString placeName; + QString hardwareAddress; QByteArray protocolVersion; }; diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 131c4ee509..e32df6bc62 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -351,3 +351,7 @@ if (ANDROID) qt_create_apk() endif () + +add_dependency_external_projects(GifCreator) +find_package(GifCreator REQUIRED) +target_include_directories(${TARGET_NAME} PUBLIC ${GIFCREATOR_INCLUDE_DIRS}) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 089983d8ca..82007c4f06 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -152,6 +152,7 @@ #include "ui/LoginDialog.h" #include "ui/overlays/Cube3DOverlay.h" #include "ui/Snapshot.h" +#include "ui/SnapshotAnimated.h" #include "ui/StandAloneJSConsole.h" #include "ui/Stats.h" #include "ui/UpdateDialog.h" @@ -5428,19 +5429,27 @@ void Application::toggleLogDialog() { } } -void Application::takeSnapshot(bool notify, float aspectRatio) { - postLambdaEvent([notify, aspectRatio, this] { + +void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRatio) { + postLambdaEvent([notify, includeAnimated, aspectRatio, this] { QMediaPlayer* player = new QMediaPlayer(); QFileInfo inf = QFileInfo(PathUtils::resourcesPath() + "sounds/snap.wav"); player->setMedia(QUrl::fromLocalFile(inf.absoluteFilePath())); player->play(); + // Get a screenshot and save it QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); - emit DependencyManager::get()->snapshotTaken(path, notify); + // If we're not doing an animated snapshot as well... + if (!includeAnimated || !(SnapshotAnimated::alsoTakeAnimatedSnapshot.get())) { + // Tell the dependency manager that the capture of the still snapshot has taken place. + emit DependencyManager::get()->snapshotTaken(path, "", notify); + } else { + // Get an animated GIF snapshot and save it + SnapshotAnimated::saveSnapshotAnimated(path, aspectRatio, qApp, DependencyManager::get()); + } }); } - void Application::shareSnapshot(const QString& path) { postLambdaEvent([path] { // not much to do here, everything is done in snapshot code... diff --git a/interface/src/Application.h b/interface/src/Application.h index 4c98be9c2d..8f8b42d66a 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -266,7 +266,7 @@ public: float getAvatarSimrate() const { return _avatarSimCounter.rate(); } float getAverageSimsPerSecond() const { return _simCounter.rate(); } - void takeSnapshot(bool notify, float aspectRatio = 0.0f); + void takeSnapshot(bool notify, bool includeAnimated = false, float aspectRatio = 0.0f); void shareSnapshot(const QString& filename); model::SkyboxPointer getDefaultSkybox() const { return _defaultSkybox; } diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 0f9dd698fd..0cb574c1f6 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -199,8 +199,8 @@ void WindowScriptingInterface::copyToClipboard(const QString& text) { QApplication::clipboard()->setText(text); } -void WindowScriptingInterface::takeSnapshot(bool notify, float aspectRatio) { - qApp->takeSnapshot(notify, aspectRatio); +void WindowScriptingInterface::takeSnapshot(bool notify, bool includeAnimated, float aspectRatio) { + qApp->takeSnapshot(notify, includeAnimated, aspectRatio); } void WindowScriptingInterface::shareSnapshot(const QString& path) { diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index f4a89ae221..7246dc0927 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -52,7 +52,7 @@ public slots: QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); void showAssetServer(const QString& upload = ""); void copyToClipboard(const QString& text); - void takeSnapshot(bool notify = true, float aspectRatio = 0.0f); + void takeSnapshot(bool notify = true, bool includeAnimated = false, float aspectRatio = 0.0f); void shareSnapshot(const QString& path); bool isPhysicsEnabled(); @@ -60,7 +60,7 @@ signals: void domainChanged(const QString& domainHostname); void svoImportRequested(const QString& url); void domainConnectionRefused(const QString& reasonMessage, int reasonCode, const QString& extraInfo); - void snapshotTaken(const QString& path, bool notify); + void snapshotTaken(const QString& pathStillSnapshot, const QString& pathAnimatedSnapshot, bool notify); void snapshotShared(const QString& error); private: diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 7d3261aa78..dea1c49346 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -23,6 +23,7 @@ #include "LODManager.h" #include "Menu.h" #include "Snapshot.h" +#include "SnapshotAnimated.h" #include "UserActivityLogger.h" #include "AmbientOcclusionEffect.h" @@ -31,7 +32,7 @@ void setupPreferences() { auto preferences = DependencyManager::get(); - + auto nodeList = DependencyManager::get(); auto myAvatar = DependencyManager::get()->getMyAvatar(); static const QString AVATAR_BASICS { "Avatar Basics" }; { @@ -67,6 +68,18 @@ void setupPreferences() { auto setter = [=](bool value) { myAvatar->setClearOverlayWhenMoving(value); }; preferences->addPreference(new CheckPreference(AVATAR_BASICS, "Clear overlays when moving", getter, setter)); } + { + auto getter = [=]()->float { return nodeList->getIgnoreRadius(); }; + auto setter = [=](float value) { + nodeList->ignoreNodesInRadius(value, nodeList->getIgnoreRadiusEnabled()); + }; + auto preference = new SpinnerPreference(AVATAR_BASICS, "Personal space bubble radius (default is 1m)", getter, setter); + preference->setMin(0.01f); + preference->setMax(99.9f); + preference->setDecimals(2); + preference->setStep(0.25); + preferences->addPreference(preference); + } // UI { @@ -83,6 +96,20 @@ void setupPreferences() { auto preference = new BrowsePreference(SNAPSHOTS, "Put my snapshots here", getter, setter); preferences->addPreference(preference); } + { + auto getter = []()->bool { return SnapshotAnimated::alsoTakeAnimatedSnapshot.get(); }; + auto setter = [](bool value) { SnapshotAnimated::alsoTakeAnimatedSnapshot.set(value); }; + preferences->addPreference(new CheckPreference(SNAPSHOTS, "Take Animated GIF Snapshot with HUD Button", getter, setter)); + } + { + auto getter = []()->float { return SnapshotAnimated::snapshotAnimatedDuration.get(); }; + auto setter = [](float value) { SnapshotAnimated::snapshotAnimatedDuration.set(value); }; + auto preference = new SpinnerPreference(SNAPSHOTS, "Animated Snapshot Duration", getter, setter); + preference->setMin(3); + preference->setMax(10); + preference->setStep(1); + preferences->addPreference(preference); + } // Scripts { diff --git a/interface/src/ui/Snapshot.cpp b/interface/src/ui/Snapshot.cpp index 1bf5f5de4e..5df0d4575b 100644 --- a/interface/src/ui/Snapshot.cpp +++ b/interface/src/ui/Snapshot.cpp @@ -51,16 +51,24 @@ SnapshotMetaData* Snapshot::parseSnapshotData(QString snapshotPath) { return NULL; } - QImage shot(snapshotPath); + QUrl url; - // no location data stored - if (shot.text(URL).isEmpty()) { + if (snapshotPath.right(3) == "jpg") { + QImage shot(snapshotPath); + + // no location data stored + if (shot.text(URL).isEmpty()) { + return NULL; + } + + // parsing URL + url = QUrl(shot.text(URL), QUrl::ParsingMode::StrictMode); + } else if (snapshotPath.right(3) == "gif") { + url = QUrl(DependencyManager::get()->currentShareableAddress()); + } else { return NULL; } - // parsing URL - QUrl url = QUrl(shot.text(URL), QUrl::ParsingMode::StrictMode); - SnapshotMetaData* data = new SnapshotMetaData(); data->setURL(url); @@ -156,7 +164,11 @@ void Snapshot::uploadSnapshot(const QString& filename) { file->open(QIODevice::ReadOnly); QHttpPart imagePart; - imagePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/jpeg")); + if (filename.right(3) == "gif") { + imagePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/gif")); + } else { + imagePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/jpeg")); + } imagePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"image\"; filename=\"" + file->fileName() + "\"")); imagePart.setBodyDevice(file); diff --git a/interface/src/ui/SnapshotAnimated.cpp b/interface/src/ui/SnapshotAnimated.cpp new file mode 100644 index 0000000000..c8edbfc028 --- /dev/null +++ b/interface/src/ui/SnapshotAnimated.cpp @@ -0,0 +1,139 @@ +// +// SnapshotAnimated.cpp +// interface/src/ui +// +// Created by Zach Fox on 11/14/16. +// 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 +// + +#include +#include +#include +#include + +#include +#include "SnapshotAnimated.h" + +QTimer* SnapshotAnimated::snapshotAnimatedTimer = NULL; +qint64 SnapshotAnimated::snapshotAnimatedTimestamp = 0; +qint64 SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = 0; +bool SnapshotAnimated::snapshotAnimatedTimerRunning = false; +QString SnapshotAnimated::snapshotAnimatedPath; +QString SnapshotAnimated::snapshotStillPath; +QVector SnapshotAnimated::snapshotAnimatedFrameVector; +QVector SnapshotAnimated::snapshotAnimatedFrameDelayVector; +Application* SnapshotAnimated::app; +float SnapshotAnimated::aspectRatio; +QSharedPointer SnapshotAnimated::snapshotAnimatedDM; +GifWriter SnapshotAnimated::snapshotAnimatedGifWriter; + + +Setting::Handle SnapshotAnimated::alsoTakeAnimatedSnapshot("alsoTakeAnimatedSnapshot", true); +Setting::Handle SnapshotAnimated::snapshotAnimatedDuration("snapshotAnimatedDuration", SNAPSNOT_ANIMATED_DURATION_SECS); + +void SnapshotAnimated::saveSnapshotAnimated(QString pathStill, float aspectRatio, Application* app, QSharedPointer dm) { + // If we're not in the middle of capturing an animated snapshot... + if (SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp == 0) { + SnapshotAnimated::snapshotAnimatedTimer = new QTimer(); + SnapshotAnimated::aspectRatio = aspectRatio; + SnapshotAnimated::app = app; + SnapshotAnimated::snapshotAnimatedDM = dm; + // Define the output location of the still and animated snapshots. + SnapshotAnimated::snapshotStillPath = pathStill; + SnapshotAnimated::snapshotAnimatedPath = pathStill; + SnapshotAnimated::snapshotAnimatedPath.replace("jpg", "gif"); + + // Ensure the snapshot timer is Precise (attempted millisecond precision) + SnapshotAnimated::snapshotAnimatedTimer->setTimerType(Qt::PreciseTimer); + + // Connect the snapshotAnimatedTimer QTimer to the lambda slot function + QObject::connect((SnapshotAnimated::snapshotAnimatedTimer), &QTimer::timeout, captureFrames); + + // Start the snapshotAnimatedTimer QTimer - argument for this is in milliseconds + SnapshotAnimated::snapshotAnimatedTimerRunning = true; + SnapshotAnimated::snapshotAnimatedTimer->start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC); + // If we're already in the middle of capturing an animated snapshot... + } else { + // Just tell the dependency manager that the capture of the still snapshot has taken place. + emit dm->snapshotTaken(pathStill, "", false); + } +} + +void SnapshotAnimated::captureFrames() { + if (SnapshotAnimated::snapshotAnimatedTimerRunning) { + // Get a screenshot from the display, then scale the screenshot down, + // then convert it to the image format the GIF library needs, + // then save all that to the QImage named "frame" + QImage frame(SnapshotAnimated::app->getActiveDisplayPlugin()->getScreenshot(SnapshotAnimated::aspectRatio)); + frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH); + SnapshotAnimated::snapshotAnimatedFrameVector.append(frame); + + // If that was the first frame... + if (SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp == 0) { + // Record the current frame timestamp + SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); + // Record the first frame timestamp + SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = SnapshotAnimated::snapshotAnimatedTimestamp; + SnapshotAnimated::snapshotAnimatedFrameDelayVector.append(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); + // If this is an intermediate or the final frame... + } else { + // Push the current frame delay onto the vector + SnapshotAnimated::snapshotAnimatedFrameDelayVector.append(round(((float)(QDateTime::currentMSecsSinceEpoch() - SnapshotAnimated::snapshotAnimatedTimestamp)) / 10)); + // Record the current frame timestamp + SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); + + // If that was the last frame... + if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SnapshotAnimated::snapshotAnimatedDuration.get() * MSECS_PER_SECOND)) { + SnapshotAnimated::snapshotAnimatedTimerRunning = false; + // Reset the current frame timestamp + SnapshotAnimated::snapshotAnimatedTimestamp = 0; + SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = 0; + + // Kick off the thread that'll pack the frames into the GIF + QtConcurrent::run(processFrames); + // Stop the snapshot QTimer. This action by itself DOES NOT GUARANTEE + // that the slot will not be called again in the future. + // See: http://lists.qt-project.org/pipermail/qt-interest-old/2009-October/013926.html + SnapshotAnimated::snapshotAnimatedTimer->stop(); + delete SnapshotAnimated::snapshotAnimatedTimer; + } + } + } +} + +void SnapshotAnimated::processFrames() { + uint32_t width = SnapshotAnimated::snapshotAnimatedFrameVector[0].width(); + uint32_t height = SnapshotAnimated::snapshotAnimatedFrameVector[0].height(); + + // Create the GIF from the temporary files + // Write out the header and beginning of the GIF file + GifBegin( + &(SnapshotAnimated::snapshotAnimatedGifWriter), + qPrintable(SnapshotAnimated::snapshotAnimatedPath), + width, + height, + 1); // "1" means "yes there is a delay" with this GifCreator library. + for (int itr = 0; itr < SnapshotAnimated::snapshotAnimatedFrameVector.size(); itr++) { + // Write each frame to the GIF + GifWriteFrame(&(SnapshotAnimated::snapshotAnimatedGifWriter), + (uint8_t*)SnapshotAnimated::snapshotAnimatedFrameVector[itr].convertToFormat(QImage::Format_RGBA8888).bits(), + width, + height, + SnapshotAnimated::snapshotAnimatedFrameDelayVector[itr]); + } + // Write out the end of the GIF + GifEnd(&(SnapshotAnimated::snapshotAnimatedGifWriter)); + + // Clear out the frame and frame delay vectors. + // Also release the memory not required to store the items. + SnapshotAnimated::snapshotAnimatedFrameVector.clear(); + SnapshotAnimated::snapshotAnimatedFrameVector.squeeze(); + SnapshotAnimated::snapshotAnimatedFrameDelayVector.clear(); + SnapshotAnimated::snapshotAnimatedFrameDelayVector.squeeze(); + + // Let the dependency manager know that the snapshots have been taken. + emit SnapshotAnimated::snapshotAnimatedDM->snapshotTaken(SnapshotAnimated::snapshotStillPath, SnapshotAnimated::snapshotAnimatedPath, false); +} diff --git a/interface/src/ui/SnapshotAnimated.h b/interface/src/ui/SnapshotAnimated.h new file mode 100644 index 0000000000..78b1529ab4 --- /dev/null +++ b/interface/src/ui/SnapshotAnimated.h @@ -0,0 +1,58 @@ +// +// SnapshotAnimated.h +// interface/src/ui +// +// Created by Zach Fox on 11/14/16. +// 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 +// + +#ifndef hifi_SnapshotAnimated_h +#define hifi_SnapshotAnimated_h + +#include +#include +#include +#include +#include +#include +#include "scripting/WindowScriptingInterface.h" + +// If the snapshot width or the framerate are too high for the +// application to handle, the framerate of the output GIF will drop. +#define SNAPSNOT_ANIMATED_WIDTH (480) +// This value should divide evenly into 100. Snapshot framerate is NOT guaranteed. +#define SNAPSNOT_ANIMATED_TARGET_FRAMERATE (25) +#define SNAPSNOT_ANIMATED_DURATION_SECS (3) +#define SNAPSNOT_ANIMATED_DURATION_MSEC (SNAPSNOT_ANIMATED_DURATION_SECS*1000) + +#define SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC (1000/SNAPSNOT_ANIMATED_TARGET_FRAMERATE) + +class SnapshotAnimated { +private: + static QTimer* snapshotAnimatedTimer; + static qint64 snapshotAnimatedTimestamp; + static qint64 snapshotAnimatedFirstFrameTimestamp; + static bool snapshotAnimatedTimerRunning; + static QString snapshotStillPath; + + static QString snapshotAnimatedPath; + static QVector snapshotAnimatedFrameVector; + static QVector snapshotAnimatedFrameDelayVector; + static QSharedPointer snapshotAnimatedDM; + static Application* app; + static float aspectRatio; + + static GifWriter snapshotAnimatedGifWriter; + + static void captureFrames(); + static void processFrames(); +public: + static void saveSnapshotAnimated(QString pathStill, float aspectRatio, Application* app, QSharedPointer dm); + static Setting::Handle alsoTakeAnimatedSnapshot; + static Setting::Handle snapshotAnimatedDuration; +}; + +#endif // hifi_SnapshotAnimated_h diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 062991c187..a05d550fd8 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -61,6 +61,10 @@ static const auto DEFAULT_ORIENTATION_GETTER = [] { return Quaternions::IDENTITY static const int DEFAULT_BUFFER_FRAMES = 1; +// OUTPUT_CHANNEL_COUNT is audio pipeline output format, which is always 2 channel. +// _outputFormat.channelCount() is device output format, which may be 1 or multichannel. +static const int OUTPUT_CHANNEL_COUNT = 2; + static const bool DEFAULT_STARVE_DETECTION_ENABLED = true; static const int STARVE_DETECTION_THRESHOLD = 3; static const int STARVE_DETECTION_PERIOD = 10 * 1000; // 10 Seconds @@ -140,7 +144,7 @@ AudioClient::AudioClient() : _reverbOptions(&_scriptReverbOptions), _inputToNetworkResampler(NULL), _networkToOutputResampler(NULL), - _audioLimiter(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO), + _audioLimiter(AudioConstants::SAMPLE_RATE, OUTPUT_CHANNEL_COUNT), _outgoingAvatarAudioSequenceNumber(0), _audioOutputIODevice(_receivedAudioStream, this), _stats(&_receivedAudioStream), @@ -237,14 +241,6 @@ QAudioDeviceInfo getNamedAudioDeviceForMode(QAudio::Mode mode, const QString& de return result; } -int numDestinationSamplesRequired(const QAudioFormat& sourceFormat, const QAudioFormat& destinationFormat, - int numSourceSamples) { - float ratio = (float) destinationFormat.channelCount() / sourceFormat.channelCount(); - ratio *= (float) destinationFormat.sampleRate() / sourceFormat.sampleRate(); - - return (numSourceSamples * ratio) + 0.5f; -} - #ifdef Q_OS_WIN QString friendlyNameForAudioDevice(IMMDevice* pEndpoint) { QString deviceName; @@ -387,14 +383,36 @@ bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, adjustedAudioFormat = desiredAudioFormat; -#ifdef Q_OS_ANDROID +#if defined(Q_OS_WIN) + + // On Windows, using WASAPI shared mode, the sample rate and channel count must + // exactly match the internal mix format. Any other format will fail to open. + + adjustedAudioFormat = audioDevice.preferredFormat(); // returns mixFormat + + adjustedAudioFormat.setCodec("audio/pcm"); + adjustedAudioFormat.setSampleSize(16); + adjustedAudioFormat.setSampleType(QAudioFormat::SignedInt); + adjustedAudioFormat.setByteOrder(QAudioFormat::LittleEndian); + + if (!audioDevice.isFormatSupported(adjustedAudioFormat)) { + qCDebug(audioclient) << "WARNING: The mix format is" << adjustedAudioFormat << "but isFormatSupported() failed."; + return false; + } + // converting to/from this rate must produce an integral number of samples + if (adjustedAudioFormat.sampleRate() * AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL % AudioConstants::SAMPLE_RATE != 0) { + qCDebug(audioclient) << "WARNING: The current sample rate [" << adjustedAudioFormat.sampleRate() << "] is not supported."; + return false; + } + return true; + +#elif defined(Q_OS_ANDROID) // FIXME: query the native sample rate of the device? adjustedAudioFormat.setSampleRate(48000); #else // // Attempt the device sample rate in decreasing order of preference. - // On Windows, using WASAPI shared mode, only a match with the hardware sample rate will succeed. // if (audioDevice.supportedSampleRates().contains(48000)) { adjustedAudioFormat.setSampleRate(48000); @@ -427,15 +445,15 @@ bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, } bool sampleChannelConversion(const int16_t* sourceSamples, int16_t* destinationSamples, unsigned int numSourceSamples, - const QAudioFormat& sourceAudioFormat, const QAudioFormat& destinationAudioFormat) { - if (sourceAudioFormat.channelCount() == 2 && destinationAudioFormat.channelCount() == 1) { + const int sourceChannelCount, const int destinationChannelCount) { + if (sourceChannelCount == 2 && destinationChannelCount == 1) { // loop through the stereo input audio samples and average every two samples for (uint i = 0; i < numSourceSamples; i += 2) { destinationSamples[i / 2] = (sourceSamples[i] / 2) + (sourceSamples[i + 1] / 2); } return true; - } else if (sourceAudioFormat.channelCount() == 1 && destinationAudioFormat.channelCount() == 2) { + } else if (sourceChannelCount == 1 && destinationChannelCount == 2) { // loop through the mono input audio and repeat each sample twice for (uint i = 0; i < numSourceSamples; ++i) { @@ -451,26 +469,24 @@ bool sampleChannelConversion(const int16_t* sourceSamples, int16_t* destinationS void possibleResampling(AudioSRC* resampler, const int16_t* sourceSamples, int16_t* destinationSamples, unsigned int numSourceSamples, unsigned int numDestinationSamples, - const QAudioFormat& sourceAudioFormat, const QAudioFormat& destinationAudioFormat) { + const int sourceChannelCount, const int destinationChannelCount) { if (numSourceSamples > 0) { if (!resampler) { if (!sampleChannelConversion(sourceSamples, destinationSamples, numSourceSamples, - sourceAudioFormat, destinationAudioFormat)) { + sourceChannelCount, destinationChannelCount)) { // no conversion, we can copy the samples directly across memcpy(destinationSamples, sourceSamples, numSourceSamples * AudioConstants::SAMPLE_SIZE); } } else { - if (sourceAudioFormat.channelCount() != destinationAudioFormat.channelCount()) { - float channelCountRatio = (float)destinationAudioFormat.channelCount() / sourceAudioFormat.channelCount(); + if (sourceChannelCount != destinationChannelCount) { - int numChannelCoversionSamples = (int)(numSourceSamples * channelCountRatio); + int numChannelCoversionSamples = (numSourceSamples * destinationChannelCount) / sourceChannelCount; int16_t* channelConversionSamples = new int16_t[numChannelCoversionSamples]; - sampleChannelConversion(sourceSamples, channelConversionSamples, - numSourceSamples, - sourceAudioFormat, destinationAudioFormat); + sampleChannelConversion(sourceSamples, channelConversionSamples, numSourceSamples, + sourceChannelCount, destinationChannelCount); resampler->render(channelConversionSamples, destinationSamples, numChannelCoversionSamples); @@ -480,7 +496,7 @@ void possibleResampling(AudioSRC* resampler, unsigned int numAdjustedSourceSamples = numSourceSamples; unsigned int numAdjustedDestinationSamples = numDestinationSamples; - if (sourceAudioFormat.channelCount() == 2 && destinationAudioFormat.channelCount() == 2) { + if (sourceChannelCount == 2 && destinationChannelCount == 2) { numAdjustedSourceSamples /= 2; numAdjustedDestinationSamples /= 2; } @@ -502,7 +518,7 @@ void AudioClient::start() { _desiredInputFormat.setChannelCount(1); _desiredOutputFormat = _desiredInputFormat; - _desiredOutputFormat.setChannelCount(2); + _desiredOutputFormat.setChannelCount(OUTPUT_CHANNEL_COUNT); QAudioDeviceInfo inputDeviceInfo = defaultAudioDeviceForMode(QAudio::AudioInput); qCDebug(audioclient) << "The default audio input device is" << inputDeviceInfo.deviceName(); @@ -824,6 +840,36 @@ void AudioClient::setReverbOptions(const AudioEffectOptions* options) { } } +static void channelUpmix(int16_t* source, int16_t* dest, int numSamples, int numExtraChannels) { + + for (int i = 0; i < numSamples/2; i++) { + + // read 2 samples + int16_t left = *source++; + int16_t right = *source++; + + // write 2 + N samples + *dest++ = left; + *dest++ = right; + for (int n = 0; n < numExtraChannels; n++) { + *dest++ = 0; + } + } +} + +static void channelDownmix(int16_t* source, int16_t* dest, int numSamples) { + + for (int i = 0; i < numSamples/2; i++) { + + // read 2 samples + int16_t left = *source++; + int16_t right = *source++; + + // write 1 sample + *dest++ = (int16_t)((left + right) / 2); + } +} + 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(); @@ -857,7 +903,7 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { static QByteArray loopBackByteArray; int numInputSamples = inputByteArray.size() / AudioConstants::SAMPLE_SIZE; - int numLoopbackSamples = numDestinationSamplesRequired(_inputFormat, _outputFormat, numInputSamples); + int numLoopbackSamples = (numInputSamples * OUTPUT_CHANNEL_COUNT) / _inputFormat.channelCount(); loopBackByteArray.resize(numLoopbackSamples * AudioConstants::SAMPLE_SIZE); @@ -865,7 +911,7 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { int16_t* loopbackSamples = reinterpret_cast(loopBackByteArray.data()); // upmix mono to stereo - if (!sampleChannelConversion(inputSamples, loopbackSamples, numInputSamples, _inputFormat, _outputFormat)) { + if (!sampleChannelConversion(inputSamples, loopbackSamples, numInputSamples, _inputFormat.channelCount(), OUTPUT_CHANNEL_COUNT)) { // no conversion, just copy the samples memcpy(loopbackSamples, inputSamples, numInputSamples * AudioConstants::SAMPLE_SIZE); } @@ -876,7 +922,29 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { _sourceReverb.render(loopbackSamples, loopbackSamples, numLoopbackSamples/2); } - _loopbackOutputDevice->write(loopBackByteArray); + // if required, upmix or downmix to deviceChannelCount + int deviceChannelCount = _outputFormat.channelCount(); + if (deviceChannelCount == OUTPUT_CHANNEL_COUNT) { + + _loopbackOutputDevice->write(loopBackByteArray); + + } else { + + static QByteArray deviceByteArray; + + int numDeviceSamples = (numLoopbackSamples * deviceChannelCount) / OUTPUT_CHANNEL_COUNT; + + deviceByteArray.resize(numDeviceSamples * AudioConstants::SAMPLE_SIZE); + + int16_t* deviceSamples = reinterpret_cast(deviceByteArray.data()); + + if (deviceChannelCount > OUTPUT_CHANNEL_COUNT) { + channelUpmix(loopbackSamples, deviceSamples, numLoopbackSamples, deviceChannelCount - OUTPUT_CHANNEL_COUNT); + } else { + channelDownmix(loopbackSamples, deviceSamples, numLoopbackSamples); + } + _loopbackOutputDevice->write(deviceByteArray); + } } void AudioClient::handleAudioInput() { @@ -923,7 +991,7 @@ void AudioClient::handleAudioInput() { possibleResampling(_inputToNetworkResampler, inputAudioSamples.get(), networkAudioSamples, inputSamplesRequired, numNetworkSamples, - _inputFormat, _desiredInputFormat); + _inputFormat.channelCount(), _desiredInputFormat.channelCount()); // Remove DC offset if (!_isStereoInput) { @@ -1170,9 +1238,9 @@ bool AudioClient::outputLocalInjector(bool isStereo, AudioInjector* injector) { } void AudioClient::outputFormatChanged() { - _outputFrameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * _outputFormat.channelCount() * _outputFormat.sampleRate()) / + _outputFrameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * OUTPUT_CHANNEL_COUNT * _outputFormat.sampleRate()) / _desiredOutputFormat.sampleRate(); - _receivedAudioStream.outputFormatChanged(_outputFormat.sampleRate(), _outputFormat.channelCount()); + _receivedAudioStream.outputFormatChanged(_outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); } bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo& inputDeviceInfo) { @@ -1316,9 +1384,8 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice assert(_desiredOutputFormat.sampleSize() == 16); assert(_outputFormat.sampleSize() == 16); - int channelCount = (_desiredOutputFormat.channelCount() == 2 && _outputFormat.channelCount() == 2) ? 2 : 1; - _networkToOutputResampler = new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), channelCount); + _networkToOutputResampler = new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); } else { qCDebug(audioclient) << "No resampling required for network output to match actual output format."; @@ -1328,8 +1395,11 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice // setup our general output device for audio-mixer audio _audioOutput = new QAudioOutput(outputDeviceInfo, _outputFormat, this); + int osDefaultBufferSize = _audioOutput->bufferSize(); - int requestedSize = _sessionOutputBufferSizeFrames *_outputFrameSize * AudioConstants::SAMPLE_SIZE; + int deviceChannelCount = _outputFormat.channelCount(); + int deviceFrameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * deviceChannelCount * _outputFormat.sampleRate()) / _desiredOutputFormat.sampleRate(); + int requestedSize = _sessionOutputBufferSizeFrames * deviceFrameSize * AudioConstants::SAMPLE_SIZE; _audioOutput->setBufferSize(requestedSize); connect(_audioOutput, &QAudioOutput::notify, this, &AudioClient::outputNotify); @@ -1341,14 +1411,13 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice _audioOutput->start(&_audioOutputIODevice); lock.unlock(); - qCDebug(audioclient) << "Output Buffer capacity in frames: " << _audioOutput->bufferSize() / AudioConstants::SAMPLE_SIZE / (float)_outputFrameSize << + qCDebug(audioclient) << "Output Buffer capacity in frames: " << _audioOutput->bufferSize() / AudioConstants::SAMPLE_SIZE / (float)deviceFrameSize << "requested bytes:" << requestedSize << "actual bytes:" << _audioOutput->bufferSize() << "os default:" << osDefaultBufferSize << "period size:" << _audioOutput->periodSize(); // setup a loopback audio output device _loopbackAudioOutput = new QAudioOutput(outputDeviceInfo, _outputFormat, this); - _timeSinceLastReceived.start(); supportedFormat = true; @@ -1447,15 +1516,27 @@ float AudioClient::gainForSource(float distance, float volume) { } qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { - auto samplesRequested = maxSize / AudioConstants::SAMPLE_SIZE; + + // samples requested from OUTPUT_CHANNEL_COUNT + int deviceChannelCount = _audio->_outputFormat.channelCount(); + int samplesRequested = (int)(maxSize / AudioConstants::SAMPLE_SIZE) * OUTPUT_CHANNEL_COUNT / deviceChannelCount; + int samplesPopped; int bytesWritten; - if ((samplesPopped = _receivedAudioStream.popSamples((int)samplesRequested, false)) > 0) { + if ((samplesPopped = _receivedAudioStream.popSamples(samplesRequested, false)) > 0) { qCDebug(audiostream, "Read %d samples from buffer (%d available)", samplesPopped, _receivedAudioStream.getSamplesAvailable()); AudioRingBuffer::ConstIterator lastPopOutput = _receivedAudioStream.getLastPopOutput(); - lastPopOutput.readSamples((int16_t*)data, samplesPopped); - bytesWritten = samplesPopped * AudioConstants::SAMPLE_SIZE; + + // if required, upmix or downmix to deviceChannelCount + if (deviceChannelCount == OUTPUT_CHANNEL_COUNT) { + lastPopOutput.readSamples((int16_t*)data, samplesPopped); + } else if (deviceChannelCount > OUTPUT_CHANNEL_COUNT) { + lastPopOutput.readSamplesWithUpmix((int16_t*)data, samplesPopped, deviceChannelCount - OUTPUT_CHANNEL_COUNT); + } else { + lastPopOutput.readSamplesWithDownmix((int16_t*)data, samplesPopped); + } + bytesWritten = (samplesPopped * AudioConstants::SAMPLE_SIZE) * deviceChannelCount / OUTPUT_CHANNEL_COUNT; } else { // nothing on network, don't grab anything from injectors, and just return 0s // this will flood the log: qCDebug(audioclient, "empty/partial network buffer"); diff --git a/libraries/audio/src/AudioRingBuffer.h b/libraries/audio/src/AudioRingBuffer.h index 7ccb32ce10..29e7a9e998 100644 --- a/libraries/audio/src/AudioRingBuffer.h +++ b/libraries/audio/src/AudioRingBuffer.h @@ -105,6 +105,8 @@ public: void readSamples(int16_t* dest, int numSamples); void readSamplesWithFade(int16_t* dest, int numSamples, float fade); + void readSamplesWithUpmix(int16_t* dest, int numSamples, int numExtraChannels); + void readSamplesWithDownmix(int16_t* dest, int numSamples); private: int16_t* atShiftedBy(int i); @@ -225,6 +227,40 @@ inline void AudioRingBuffer::ConstIterator::readSamplesWithFade(int16_t* dest, i } } +inline void AudioRingBuffer::ConstIterator::readSamplesWithUpmix(int16_t* dest, int numSamples, int numExtraChannels) { + int16_t* at = _at; + for (int i = 0; i < numSamples/2; i++) { + + // read 2 samples + int16_t left = *at; + at = (at == _bufferLast) ? _bufferFirst : at + 1; + int16_t right = *at; + at = (at == _bufferLast) ? _bufferFirst : at + 1; + + // write 2 + N samples + *dest++ = left; + *dest++ = right; + for (int n = 0; n < numExtraChannels; n++) { + *dest++ = 0; + } + } +} + +inline void AudioRingBuffer::ConstIterator::readSamplesWithDownmix(int16_t* dest, int numSamples) { + int16_t* at = _at; + for (int i = 0; i < numSamples/2; i++) { + + // read 2 samples + int16_t left = *at; + at = (at == _bufferLast) ? _bufferFirst : at + 1; + int16_t right = *at; + at = (at == _bufferLast) ? _bufferFirst : at + 1; + + // write 1 sample + *dest++ = (int16_t)((left + right) / 2); + } +} + inline AudioRingBuffer::ConstIterator AudioRingBuffer::nextOutput() const { return ConstIterator(_buffer, _bufferLength, _nextOutput); } diff --git a/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp index fa267e2c68..5a3e5afc86 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp @@ -39,11 +39,11 @@ bool DebugHmdDisplayPlugin::beginFrameRender(uint32_t frameIndex) { _uiModelTransform = DependencyManager::get()->getModelTransform(); _frameInfos[frameIndex] = _currentRenderFrameInfo; - _handPoses[0] = glm::translate(mat4(), vec3(-0.3f, 0.0f, 0.0f)); + _handPoses[0] = glm::translate(mat4(), vec3(0.3f * cosf(secTimestampNow() * 3.0f), -0.3f * sinf(secTimestampNow() * 5.0f), 0.0f)); _handLasers[0].color = vec4(1, 0, 0, 1); _handLasers[0].mode = HandLaserMode::Overlay; - _handPoses[1] = glm::translate(mat4(), vec3(0.3f, 0.0f, 0.0f)); + _handPoses[1] = glm::translate(mat4(), vec3(0.3f * sinf(secTimestampNow() * 3.0f), -0.3f * cosf(secTimestampNow() * 5.0f), 0.0f)); _handLasers[1].color = vec4(0, 1, 1, 1); _handLasers[1].mode = HandLaserMode::Overlay; }); diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index c5d7ac5690..d01f2407eb 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -23,7 +23,6 @@ #include #include #include -#include #include #include #include @@ -32,6 +31,9 @@ #include "../Logging.h" #include "../CompositorHelper.h" +#include <../render-utils/shaders/render-utils/glowLine_vert.h> +#include <../render-utils/shaders/render-utils/glowLine_frag.h> + static const QString MONO_PREVIEW = "Mono Preview"; static const QString DISABLE_PREVIEW = "Disable Preview"; @@ -47,6 +49,12 @@ static const size_t NUMBER_OF_HANDS = 2; //#define LIVE_SHADER_RELOAD 1 extern glm::vec3 getPoint(float yaw, float pitch); +struct HandLaserData { + vec4 p1; + vec4 p2; + vec4 color; +}; + static QString readFile(const QString& filename) { QFile file(filename); file.open(QFile::Text | QFile::ReadOnly); @@ -112,11 +120,28 @@ void HmdDisplayPlugin::internalDeactivate() { void HmdDisplayPlugin::customizeContext() { Parent::customizeContext(); _overlayRenderer.build(); - auto geometryCache = DependencyManager::get(); - for (size_t i = 0; i < _geometryIds.size(); ++i) { - _geometryIds[i] = geometryCache->allocateID(); - } - _extraLaserID = geometryCache->allocateID(); + + { + auto state = std::make_shared(); + auto VS = gpu::Shader::createVertex(std::string(glowLine_vert)); + auto PS = gpu::Shader::createPixel(std::string(glowLine_frag)); + auto program = gpu::Shader::createProgram(VS, PS); + 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); + gpu::Shader::makeProgram(*program, gpu::Shader::BindingSet()); + _glowLinePipeline = gpu::Pipeline::create(program, state); + for (const auto& buffer : program->getBuffers()) { + if (buffer._name == "lineData") { + _handLaserUniformSlot = buffer._location; + } + } + _handLaserUniforms = std::array{ { std::make_shared(), std::make_shared() } }; + _extraLaserUniforms = std::make_shared(); + }; + } void HmdDisplayPlugin::uncustomizeContext() { @@ -131,12 +156,10 @@ void HmdDisplayPlugin::uncustomizeContext() { }); _overlayRenderer = OverlayRenderer(); _previewTexture.reset(); - - auto geometryCache = DependencyManager::get(); - for (size_t i = 0; i < _geometryIds.size(); ++i) { - geometryCache->releaseID(_geometryIds[i]); - } - geometryCache->releaseID(_extraLaserID); + _handLaserUniforms[0].reset(); + _handLaserUniforms[1].reset(); + _extraLaserUniforms.reset(); + _glowLinePipeline.reset(); Parent::uncustomizeContext(); } @@ -682,12 +705,16 @@ void HmdDisplayPlugin::compositeExtra() { if (_presentHandPoses[0] == IDENTITY_MATRIX && _presentHandPoses[1] == IDENTITY_MATRIX && !_presentExtraLaser.valid()) { return; } - - auto geometryCache = DependencyManager::get(); + render([&](gpu::Batch& batch) { batch.setFramebuffer(_compositeFramebuffer); + batch.setModelTransform(Transform()); batch.setViewportTransform(ivec4(uvec2(0), _renderTargetSize)); batch.setViewTransform(_currentPresentFrameInfo.presentPose, false); + // Compile the shaders + batch.setPipeline(_glowLinePipeline); + + bilateral::for_each_side([&](bilateral::Side side){ auto index = bilateral::index(side); if (_presentHandPoses[index] == IDENTITY_MATRIX) { @@ -696,13 +723,19 @@ void HmdDisplayPlugin::compositeExtra() { const auto& laser = _presentHandLasers[index]; if (laser.valid()) { const auto& points = _presentHandLaserPoints[index]; - geometryCache->renderGlowLine(batch, points.first, points.second, laser.color, _geometryIds[index]); + _handLaserUniforms[index]->resize(sizeof(HandLaserData)); + _handLaserUniforms[index]->setSubData(0, HandLaserData { vec4(points.first, 1.0f), vec4(points.second, 1.0f), _handLasers[index].color }); + batch.setUniformBuffer(_handLaserUniformSlot, _handLaserUniforms[index]); + batch.draw(gpu::TRIANGLE_STRIP, 4, 0); } }); if (_presentExtraLaser.valid()) { const auto& points = _presentExtraLaserPoints; - geometryCache->renderGlowLine(batch, points.first, points.second, _presentExtraLaser.color, _extraLaserID); + _extraLaserUniforms->resize(sizeof(HandLaserData)); + _extraLaserUniforms->setSubData(0, HandLaserData { vec4(points.first, 1.0f), vec4(points.second, 1.0f), _presentExtraLaser.color }); + batch.setUniformBuffer(_handLaserUniformSlot, _extraLaserUniforms); + batch.draw(gpu::TRIANGLE_STRIP, 4, 0); } }); } diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index 435f547899..5443403364 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -80,8 +80,6 @@ protected: Transform _presentUiModelTransform; std::array _presentHandLasers; - std::array _geometryIds; - int _extraLaserID; std::array _presentHandPoses; std::array, 2> _presentHandLaserPoints; @@ -120,6 +118,10 @@ private: bool _disablePreviewItemAdded { false }; bool _monoPreview { true }; bool _clearPreviewFlag { false }; + std::array _handLaserUniforms; + uint32_t _handLaserUniformSlot { 0 }; + gpu::BufferPointer _extraLaserUniforms; + gpu::PipelinePointer _glowLinePipeline; gpu::TexturePointer _previewTexture; glm::vec2 _lastWindowSize; diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index 7defe347ca..e0c068ea6b 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -580,6 +580,11 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { voxelVolumeSize = _voxelVolumeSize; }); + if (!mesh || + !mesh->getIndexBuffer()._buffer) { + return; + } + if (!_pipeline) { gpu::ShaderPointer vertexShader = gpu::Shader::createVertex(std::string(polyvox_vert)); gpu::ShaderPointer pixelShader = gpu::Shader::createPixel(std::string(polyvox_frag)); @@ -600,17 +605,23 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { _pipeline = gpu::Pipeline::create(program, state); } + if (!_vertexFormat) { + auto vf = std::make_shared(); + vf->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); + vf->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 12); + _vertexFormat = vf; + } + gpu::Batch& batch = *args->_batch; batch.setPipeline(_pipeline); Transform transform(voxelToWorldMatrix()); batch.setModelTransform(transform); - batch.setInputFormat(mesh->getVertexFormat()); - batch.setInputBuffer(gpu::Stream::POSITION, mesh->getVertexBuffer()); - batch.setInputBuffer(gpu::Stream::NORMAL, - mesh->getVertexBuffer()._buffer, - sizeof(float) * 3, - mesh->getVertexBuffer()._stride); + batch.setInputFormat(_vertexFormat); + batch.setInputBuffer(gpu::Stream::POSITION, mesh->getVertexBuffer()._buffer, + 0, + sizeof(PolyVox::PositionMaterialNormal)); + batch.setIndexBuffer(gpu::UINT32, mesh->getIndexBuffer()._buffer, 0); if (!_xTextureURL.isEmpty() && !_xTexture) { @@ -1097,7 +1108,6 @@ void RenderablePolyVoxEntityItem::getMesh() { auto entity = std::static_pointer_cast(getThisPointer()); - QtConcurrent::run([entity, voxelSurfaceStyle] { model::MeshPointer mesh(new model::Mesh()); @@ -1146,18 +1156,14 @@ void RenderablePolyVoxEntityItem::getMesh() { auto vertexBuffer = std::make_shared(vecVertices.size() * sizeof(PolyVox::PositionMaterialNormal), (gpu::Byte*)vecVertices.data()); auto vertexBufferPtr = gpu::BufferPointer(vertexBuffer); - gpu::Resource::Size vertexBufferSize = 0; - if (vertexBufferPtr->getSize() > sizeof(float) * 3) { - vertexBufferSize = vertexBufferPtr->getSize() - sizeof(float) * 3; - } - gpu::BufferView vertexBufferView(vertexBufferPtr, 0, vertexBufferSize, + gpu::BufferView vertexBufferView(vertexBufferPtr, 0, + vertexBufferPtr->getSize(), sizeof(PolyVox::PositionMaterialNormal), gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RAW)); mesh->setVertexBuffer(vertexBufferView); mesh->addAttribute(gpu::Stream::NORMAL, - gpu::BufferView(vertexBufferPtr, - sizeof(float) * 3, - vertexBufferPtr->getSize() - sizeof(float) * 3, + gpu::BufferView(vertexBufferPtr, sizeof(float) * 3, + vertexBufferPtr->getSize() , sizeof(PolyVox::PositionMaterialNormal), gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RAW))); entity->setMesh(mesh); diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h index 1b6ea34bda..ee4c3b318f 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h @@ -149,6 +149,7 @@ private: // may not match _voxelVolumeSize. model::MeshPointer _mesh; + gpu::Stream::FormatPointer _vertexFormat; bool _meshDirty { true }; // does collision-shape need to be recomputed? bool _meshInitialized { false }; diff --git a/libraries/networking/src/Node.cpp b/libraries/networking/src/Node.cpp index 406498b025..36e7cc961b 100644 --- a/libraries/networking/src/Node.cpp +++ b/libraries/networking/src/Node.cpp @@ -64,6 +64,7 @@ Node::Node(const QUuid& uuid, NodeType_t type, const HifiSockAddr& publicSocket, { // Update socket's object name setType(_type); + _ignoreRadiusEnabled = false; } void Node::setType(char type) { @@ -101,6 +102,15 @@ void Node::addIgnoredNode(const QUuid& otherNodeID) { } } +void Node::parseIgnoreRadiusRequestMessage(QSharedPointer message) { + bool enabled; + float radius; + message->readPrimitive(&enabled); + message->readPrimitive(&radius); + _ignoreRadiusEnabled = enabled; + _ignoreRadius = radius; +} + QDataStream& operator<<(QDataStream& out, const Node& node) { out << node._type; out << node._uuid; diff --git a/libraries/networking/src/Node.h b/libraries/networking/src/Node.h index 18088c6cea..ab8cdb3a41 100644 --- a/libraries/networking/src/Node.h +++ b/libraries/networking/src/Node.h @@ -74,10 +74,14 @@ public: void parseIgnoreRequestMessage(QSharedPointer message); void addIgnoredNode(const QUuid& otherNodeID); bool isIgnoringNodeWithID(const QUuid& nodeID) const { return _ignoredNodeIDSet.find(nodeID) != _ignoredNodeIDSet.cend(); } + void parseIgnoreRadiusRequestMessage(QSharedPointer message); friend QDataStream& operator<<(QDataStream& out, const Node& node); friend QDataStream& operator>>(QDataStream& in, Node& node); + bool isIgnoreRadiusEnabled() const { return _ignoreRadiusEnabled; } + float getIgnoreRadius() { return _ignoreRadiusEnabled ? _ignoreRadius.load() : std::numeric_limits::max(); } + private: // privatize copy and assignment operator to disallow Node copying Node(const Node &otherNode); @@ -94,6 +98,9 @@ private: MovingPercentile _clockSkewMovingPercentile; NodePermissions _permissions; tbb::concurrent_unordered_set _ignoredNodeIDSet; + + std::atomic_bool _ignoreRadiusEnabled; + std::atomic _ignoreRadius { 0.0f }; }; Q_DECLARE_METATYPE(Node*) diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 7a778edaad..86b9bc1794 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -346,6 +347,28 @@ void NodeList::sendDomainServerCheckIn() { // include the protocol version signature in our connect request QByteArray protocolVersionSig = protocolVersionsSignature(); packetStream.writeBytes(protocolVersionSig.constData(), protocolVersionSig.size()); + + // if possible, include the MAC address for the current interface in our connect request + QString hardwareAddress; + + for (auto networkInterface : QNetworkInterface::allInterfaces()) { + for (auto interfaceAddress : networkInterface.addressEntries()) { + if (interfaceAddress.ip() == _localSockAddr.getAddress()) { + // this is the interface whose local IP matches what we've detected the current IP to be + hardwareAddress = networkInterface.hardwareAddress(); + + // stop checking interfaces and addresses + break; + } + } + + // stop looping if this was the current interface + if (!hardwareAddress.isEmpty()) { + break; + } + } + + packetStream << hardwareAddress; } // pack our data to send to the domain-server including @@ -727,9 +750,26 @@ bool NodeList::sockAddrBelongsToDomainOrNode(const HifiSockAddr& sockAddr) { return _domainHandler.getSockAddr() == sockAddr || LimitedNodeList::sockAddrBelongsToNode(sockAddr); } +void NodeList::ignoreNodesInRadius(float radiusToIgnore, bool enabled) { + _ignoreRadiusEnabled.set(enabled); + _ignoreRadius.set(radiusToIgnore); + + eachMatchingNode([](const SharedNodePointer& node)->bool { + return (node->getType() == NodeType::AudioMixer || node->getType() == NodeType::AvatarMixer); + }, [this](const SharedNodePointer& destinationNode) { + sendIgnoreRadiusStateToNode(destinationNode); + }); +} + +void NodeList::sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode) { + auto ignorePacket = NLPacket::create(PacketType::RadiusIgnoreRequest, sizeof(bool) + sizeof(float), true); + ignorePacket->writePrimitive(_ignoreRadiusEnabled.get()); + ignorePacket->writePrimitive(_ignoreRadius.get()); + sendPacket(std::move(ignorePacket), *destinationNode); +} + void NodeList::ignoreNodeBySessionID(const QUuid& nodeID) { // enumerate the nodes to send a reliable ignore packet to each that can leverage it - if (!nodeID.isNull() && _sessionUUID != nodeID) { eachMatchingNode([&nodeID](const SharedNodePointer& node)->bool { if (node->getType() == NodeType::AudioMixer || node->getType() == NodeType::AvatarMixer) { @@ -788,6 +828,9 @@ void NodeList::maybeSendIgnoreSetToNode(SharedNodePointer newNode) { // send this NLPacketList to the new node sendPacketList(std::move(ignorePacketList), *newNode); } + + // also send them the current ignore radius state. + sendIgnoreRadiusStateToNode(newNode); } } diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index 4c06a13469..f30283f3c2 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -30,6 +30,7 @@ #include #include +#include #include "DomainHandler.h" #include "LimitedNodeList.h" @@ -70,6 +71,12 @@ public: void setIsShuttingDown(bool isShuttingDown) { _isShuttingDown = isShuttingDown; } + void ignoreNodesInRadius(float radiusToIgnore, bool enabled = true); + float getIgnoreRadius() const { return _ignoreRadius.get(); } + bool getIgnoreRadiusEnabled() const { return _ignoreRadiusEnabled.get(); } + void toggleIgnoreRadius() { ignoreNodesInRadius(getIgnoreRadius(), !getIgnoreRadiusEnabled()); } + void enableIgnoreRadius() { ignoreNodesInRadius(getIgnoreRadius(), true); } + void disableIgnoreRadius() { ignoreNodesInRadius(getIgnoreRadius(), false); } void ignoreNodeBySessionID(const QUuid& nodeID); bool isIgnoringNode(const QUuid& nodeID) const; @@ -101,7 +108,7 @@ signals: void limitOfSilentDomainCheckInsReached(); void receivedDomainServerList(); void ignoredNode(const QUuid& nodeID); - + private slots: void stopKeepalivePingTimer(); void sendPendingDSPathQuery(); @@ -146,6 +153,10 @@ private: mutable QReadWriteLock _ignoredSetLock; tbb::concurrent_unordered_set _ignoredNodeIDs; + void sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode); + Setting::Handle _ignoreRadiusEnabled { "IgnoreRadiusEnabled", false }; + Setting::Handle _ignoreRadius { "IgnoreRadius", 1.0f }; + #if (PR_BUILD || DEV_BUILD) bool _shouldSendNewerVersion { false }; #endif diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 88285602e1..b2fca69b03 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -67,7 +67,7 @@ PacketVersion versionForPacketType(PacketType packetType) { return static_cast(DomainConnectionDeniedVersion::IncludesExtraInfo); case PacketType::DomainConnectRequest: - return static_cast(DomainConnectRequestVersion::HasProtocolVersions); + return static_cast(DomainConnectRequestVersion::HasMACAddress); case PacketType::DomainServerAddedNode: return static_cast(DomainServerAddedNodeVersion::PermissionsGrid); diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 502ecc3951..2b17aa7d57 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -100,7 +100,8 @@ public: MoreEntityShapes, NodeKickRequest, NodeMuteRequest, - LAST_PACKET_TYPE = NodeMuteRequest + RadiusIgnoreRequest, + LAST_PACKET_TYPE = RadiusIgnoreRequest }; }; @@ -207,7 +208,8 @@ enum class AvatarMixerPacketVersion : PacketVersion { enum class DomainConnectRequestVersion : PacketVersion { NoHostname = 17, HasHostname, - HasProtocolVersions + HasProtocolVersions, + HasMACAddress }; enum class DomainConnectionDeniedVersion : PacketVersion { diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 3f5dc26db2..a19f1844f0 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -38,7 +38,6 @@ #include "simple_opaque_web_browser_frag.h" #include "simple_transparent_web_browser_frag.h" #include "glowLine_vert.h" -#include "glowLine_geom.h" #include "glowLine_frag.h" #include "grid_frag.h" @@ -1405,6 +1404,7 @@ GeometryCache::BatchItemDetails::~BatchItemDetails() { void GeometryCache::BatchItemDetails::clear() { isCreated = false; + uniformBuffer.reset(); verticesBuffer.reset(); colorBuffer.reset(); streamFormat.reset(); @@ -1593,8 +1593,6 @@ void GeometryCache::renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const glowIntensity = 0.0f; #endif - glowIntensity = 0.0f; - if (glowIntensity <= 0) { bindSimpleProgram(batch, false, false, false, true, false); renderLine(batch, p1, p2, color, id); @@ -1602,20 +1600,20 @@ void GeometryCache::renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const } // Compile the shaders + static const uint32_t LINE_DATA_SLOT = 1; static std::once_flag once; std::call_once(once, [&] { auto state = std::make_shared(); auto VS = gpu::Shader::createVertex(std::string(glowLine_vert)); - auto GS = gpu::Shader::createGeometry(std::string(glowLine_geom)); auto PS = gpu::Shader::createPixel(std::string(glowLine_frag)); - auto program = gpu::Shader::createProgram(VS, GS, PS); + auto program = gpu::Shader::createProgram(VS, PS); 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); gpu::Shader::BindingSet slotBindings; - slotBindings.insert(gpu::Shader::Binding(std::string("normalFittingMap"), render::ShapePipeline::Slot::MAP::NORMAL_FITTING)); + slotBindings.insert(gpu::Shader::Binding(std::string("lineData"), LINE_DATA_SLOT)); gpu::Shader::makeProgram(*program, slotBindings); _glowLinePipeline = gpu::Pipeline::create(program, state); }); @@ -1626,11 +1624,6 @@ void GeometryCache::renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const bool registered = (id != UNKNOWN_ID); BatchItemDetails& details = _registeredLine3DVBOs[id]; - int compactColor = ((int(color.x * 255.0f) & 0xFF)) | - ((int(color.y * 255.0f) & 0xFF) << 8) | - ((int(color.z * 255.0f) & 0xFF) << 16) | - ((int(color.w * 255.0f) & 0xFF) << 24); - // 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]; @@ -1640,47 +1633,25 @@ void GeometryCache::renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const } } - const int FLOATS_PER_VERTEX = 3 + 3; // vertices + normals - const int NUM_POS_COORDS = 3; - const int VERTEX_NORMAL_OFFSET = NUM_POS_COORDS * sizeof(float); - const int vertices = 2; + const int NUM_VERTICES = 4; if (!details.isCreated) { details.isCreated = true; - details.vertices = vertices; - details.vertexSize = FLOATS_PER_VERTEX; + details.uniformBuffer = std::make_shared(); - auto verticesBuffer = std::make_shared(); - auto colorBuffer = std::make_shared(); - auto streamFormat = std::make_shared(); - auto stream = std::make_shared(); + struct LineData { + vec4 p1; + vec4 p2; + vec4 color; + }; - details.verticesBuffer = verticesBuffer; - details.colorBuffer = colorBuffer; - details.streamFormat = streamFormat; - details.stream = stream; - - details.streamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); - details.streamFormat->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), VERTEX_NORMAL_OFFSET); - details.streamFormat->setAttribute(gpu::Stream::COLOR, 1, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA)); - - details.stream->addBuffer(details.verticesBuffer, 0, details.streamFormat->getChannels().at(0)._stride); - details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(1)._stride); - - const glm::vec3 NORMAL(1.0f, 0.0f, 0.0f); - float vertexBuffer[vertices * FLOATS_PER_VERTEX] = { - p1.x, p1.y, p1.z, NORMAL.x, NORMAL.y, NORMAL.z, - p2.x, p2.y, p2.z, NORMAL.x, NORMAL.y, NORMAL.z }; - - const int NUM_COLOR_SCALARS = 2; - int colors[NUM_COLOR_SCALARS] = { compactColor, compactColor }; - details.verticesBuffer->append(sizeof(vertexBuffer), (gpu::Byte*) vertexBuffer); - details.colorBuffer->append(sizeof(colors), (gpu::Byte*) colors); + LineData lineData { vec4(p1, 1.0f), vec4(p2, 1.0f), color }; + details.uniformBuffer->resize(sizeof(LineData)); + details.uniformBuffer->setSubData(0, lineData); } - // this is what it takes to render a quad - batch.setInputFormat(details.streamFormat); - batch.setInputStream(0, *details.stream); - batch.draw(gpu::LINES, 2, 0); + // The shader requires no vertices, only uniforms. + batch.setUniformBuffer(LINE_DATA_SLOT, details.uniformBuffer); + batch.draw(gpu::TRIANGLE_STRIP, NUM_VERTICES, 0); } void GeometryCache::useSimpleDrawPipeline(gpu::Batch& batch, bool noBlend) { diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index 6e6ac89a8f..84dfd8ccc3 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -369,6 +369,7 @@ private: static int population; gpu::BufferPointer verticesBuffer; gpu::BufferPointer colorBuffer; + gpu::BufferPointer uniformBuffer; gpu::Stream::FormatPointer streamFormat; gpu::BufferStreamPointer stream; diff --git a/libraries/render-utils/src/glowLine.slf b/libraries/render-utils/src/glowLine.slf index edebc99c81..c0af97930a 100644 --- a/libraries/render-utils/src/glowLine.slf +++ b/libraries/render-utils/src/glowLine.slf @@ -10,26 +10,24 @@ // layout(location = 0) in vec4 inColor; -layout(location = 1) in vec3 inLineDistance; out vec4 _fragColor; void main(void) { - vec2 d = inLineDistance.xy; - d.y = abs(d.y); - d.x = abs(d.x); - if (d.x > 1.0) { - d.x = (d.x - 1.0) / 0.02; - } else { - d.x = 0.0; - } - float alpha = 1.0 - length(d); - if (alpha <= 0.0) { - discard; - } - alpha = pow(alpha, 10.0); - if (alpha < 0.05) { + // The incoming value actually ranges from -1 to 1, so modify it + // so that it goes from 0 -> 1 -> 0 with the solid alpha being at + // the center of the line + float alpha = 1.0 - abs(inColor.a); + + // Convert from a linear alpha curve to a sharp peaked one + alpha = pow(alpha, 10); + + // Drop everything where the curve falls off to nearly nothing + if (alpha <= 0.05) { discard; } + + // Emit the color _fragColor = vec4(inColor.rgb, alpha); + return; } diff --git a/libraries/render-utils/src/glowLine.slg b/libraries/render-utils/src/glowLine.slg deleted file mode 100644 index 9af8eaa4d0..0000000000 --- a/libraries/render-utils/src/glowLine.slg +++ /dev/null @@ -1,102 +0,0 @@ -<@include gpu/Config.slh@> -<$VERSION_HEADER$> -// Generated on <$_SCRIBE_DATE$> -// -// Created by Bradley Austin Davis on 2016/07/05 -// Copyright 2013-2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#extension GL_EXT_geometry_shader4 : enable - -<@include gpu/Transform.slh@> -<$declareStandardCameraTransform()$> - -layout(location = 0) in vec4 inColor[]; - -layout(location = 0) out vec4 outColor; -layout(location = 1) out vec3 outLineDistance; - -layout(lines) in; -layout(triangle_strip, max_vertices = 24) out; - -vec3 ndcToEyeSpace(in vec4 v) { - TransformCamera cam = getTransformCamera(); - vec4 u = cam._projectionInverse * v; - return u.xyz / u.w; -} - -vec2 toScreenSpace(in vec4 v) -{ - TransformCamera cam = getTransformCamera(); - vec4 u = cam._projection * cam._view * v; - return u.xy / u.w; -} - -vec3[2] getOrthogonals(in vec3 n, float scale) { - float yDot = abs(dot(n, vec3(0, 1, 0))); - - vec3 result[2]; - if (yDot < 0.9) { - result[0] = normalize(cross(n, vec3(0, 1, 0))); - } else { - result[0] = normalize(cross(n, vec3(1, 0, 0))); - } - // The cross of result[0] and n is orthogonal to both, which are orthogonal to each other - result[1] = cross(result[0], n); - result[0] *= scale; - result[1] *= scale; - return result; -} - - -vec2 orthogonal(vec2 v) { - vec2 result = v.yx; - result.y *= -1.0; - return result; -} - -void main() { - vec2 endpoints[2]; - vec3 eyeSpace[2]; - TransformCamera cam = getTransformCamera(); - for (int i = 0; i < 2; ++i) { - eyeSpace[i] = ndcToEyeSpace(gl_PositionIn[i]); - endpoints[i] = gl_PositionIn[i].xy / gl_PositionIn[i].w; - } - vec2 lineNormal = normalize(endpoints[1] - endpoints[0]); - vec2 lineOrthogonal = orthogonal(lineNormal); - lineNormal *= 0.02; - lineOrthogonal *= 0.02; - - gl_Position = gl_PositionIn[0]; - gl_Position.xy -= lineOrthogonal; - outColor = inColor[0]; - outLineDistance = vec3(-1.02, -1, gl_Position.z); - EmitVertex(); - - gl_Position = gl_PositionIn[0]; - gl_Position.xy += lineOrthogonal; - outColor = inColor[0]; - outLineDistance = vec3(-1.02, 1, gl_Position.z); - EmitVertex(); - - gl_Position = gl_PositionIn[1]; - gl_Position.xy -= lineOrthogonal; - outColor = inColor[1]; - outLineDistance = vec3(1.02, -1, gl_Position.z); - EmitVertex(); - - gl_Position = gl_PositionIn[1]; - gl_Position.xy += lineOrthogonal; - outColor = inColor[1]; - outLineDistance = vec3(1.02, 1, gl_Position.z); - EmitVertex(); - - EndPrimitive(); -} - - - - diff --git a/libraries/render-utils/src/glowLine.slv b/libraries/render-utils/src/glowLine.slv index aa126fe31a..e856edc787 100644 --- a/libraries/render-utils/src/glowLine.slv +++ b/libraries/render-utils/src/glowLine.slv @@ -9,18 +9,50 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -<@include gpu/Inputs.slh@> -<@include gpu/Color.slh@> <@include gpu/Transform.slh@> <$declareStandardTransform()$> +layout(std140) uniform lineData { + vec4 p1; + vec4 p2; + vec4 color; +}; + layout(location = 0) out vec4 _color; void main(void) { - _color = inColor; + _color = color; - // standard transform TransformCamera cam = getTransformCamera(); TransformObject obj = getTransformObject(); - <$transformModelToClipPos(cam, obj, inPosition, gl_Position)$> + + vec4 p1eye, p2eye; + <$transformModelToEyePos(cam, obj, p1, p1eye)$> + <$transformModelToEyePos(cam, obj, p2, p2eye)$> + p1eye /= p1eye.w; + p2eye /= p2eye.w; + + // Find the line direction + vec3 v1 = normalize(p1eye.xyz - p2eye.xyz); + // Find the vector from the eye to one of the points + vec3 v2 = normalize(p1eye.xyz); + // The orthogonal vector is the cross product of these two + vec3 orthogonal = cross(v1, v2) * 0.02; + + // Deteremine which end to emit based on the vertex id (even / odd) + vec4 eye = (0 == gl_VertexID % 2) ? p1eye : p2eye; + + // Add or subtract the orthogonal vector based on a different vertex ID + // calculation + if (gl_VertexID < 2) { + // Use the alpha channel to store the distance from the center in 'quad space' + _color.a = -1.0; + eye.xyz -= orthogonal; + } else { + _color.a = 1.0; + eye.xyz += orthogonal; + } + + // Finally, put the eyespace vertex into clip space + <$transformEyeToClipPos(cam, eye, gl_Position)$> } \ No newline at end of file diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp index 702368c2b3..c809617995 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.cpp +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -38,3 +38,27 @@ bool UsersScriptingInterface::getCanKick() { // ask the NodeList to return our ability to kick return DependencyManager::get()->getThisNodeCanKick(); } + +void UsersScriptingInterface::toggleIgnoreRadius() { + DependencyManager::get()->toggleIgnoreRadius(); +} + +void UsersScriptingInterface::enableIgnoreRadius() { + DependencyManager::get()->enableIgnoreRadius(); +} + +void UsersScriptingInterface::disableIgnoreRadius() { + DependencyManager::get()->disableIgnoreRadius(); +} + +void UsersScriptingInterface::setIgnoreRadius(float radius, bool enabled) { + DependencyManager::get()->ignoreNodesInRadius(radius, enabled); +} + + float UsersScriptingInterface::getIgnoreRadius() { + return DependencyManager::get()->getIgnoreRadius(); +} + +bool UsersScriptingInterface::getIgnoreRadiusEnabled() { + return DependencyManager::get()->getIgnoreRadiusEnabled(); +} diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index 3c98d0a393..07398558e5 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -16,6 +16,9 @@ #include +/**jsdoc +* @namespace Users +*/ class UsersScriptingInterface : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY @@ -26,12 +29,75 @@ public: UsersScriptingInterface(); public slots: + + /**jsdoc + * Ignore another user. + * @function Users.ignore + * @param {nodeID} nodeID The node or session ID of the user you want to ignore. + */ void ignore(const QUuid& nodeID); + + /**jsdoc + * Kick another user. + * @function Users.kick + * @param {nodeID} nodeID The node or session ID of the user you want to kick. + */ void kick(const QUuid& nodeID); + + /**jsdoc + * Mute another user. + * @function Users.mute + * @param {nodeID} nodeID The node or session ID of the user you want to mute. + */ void mute(const QUuid& nodeID); + /**jsdoc + * Returns `true` if the DomainServer will allow this Node/Avatar to make kick + * @function Users.getCanKick + * @return {bool} `true` if the client can kick other users, `false` if not. + */ bool getCanKick(); + /**jsdoc + * Toggle the state of the ignore in radius feature + * @function Users.toggleIgnoreRadius + */ + void toggleIgnoreRadius(); + + /**jsdoc + * Enables the ignore radius feature. + * @function Users.enableIgnoreRadius + */ + void enableIgnoreRadius(); + + /**jsdoc + * Disables the ignore radius feature. + * @function Users.disableIgnoreRadius + */ + void disableIgnoreRadius(); + + /**jsdoc + * sets the parameters for the ignore radius feature. + * @function Users.setIgnoreRadius + * @param {number} radius The radius for the auto ignore in radius feature + * @param {bool} [enabled=true] Whether the ignore in radius feature should be enabled + */ + void setIgnoreRadius(float radius, bool enabled = true); + + /**jsdoc + * Returns the effective radius of the ingore radius feature if it is enabled. + * @function Users.getIgnoreRadius + * @return {number} radius of the ignore feature + */ + float getIgnoreRadius(); + + /**jsdoc + * Returns `true` if the ignore in radius feature is enabled + * @function Users.getIgnoreRadiusEnabled + * @return {bool} `true` if the ignore in radius feature is enabled, `false` if not. + */ + bool getIgnoreRadiusEnabled(); + signals: void canKickChanged(bool canKick); }; diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 4376960ea5..90a77b508d 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -33,7 +33,8 @@ var DEFAULT_SCRIPTS = [ "system/dialTone.js", "system/firstPersonHMD.js", "system/snapshot.js", - "system/help.js" + "system/help.js", + "system/bubble.js" ]; // add a menu item for debugging diff --git a/scripts/system/assets/images/tools/bubble.svg b/scripts/system/assets/images/tools/bubble.svg new file mode 100644 index 0000000000..064b7734a9 --- /dev/null +++ b/scripts/system/assets/images/tools/bubble.svg @@ -0,0 +1,275 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/bubble.js b/scripts/system/bubble.js new file mode 100644 index 0000000000..ba317ecdca --- /dev/null +++ b/scripts/system/bubble.js @@ -0,0 +1,58 @@ +"use strict"; + +// +// bubble.js +// scripts/system/ +// +// Created by Brad Hefta-Gaub on 11/18/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 +// +/* global Toolbars, Script, Users, Overlays, AvatarList, Controller, Camera, getControllerWorldLocation */ + + +(function() { // BEGIN LOCAL_SCOPE + +// grab the toolbar +var toolbar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); + +var ASSETS_PATH = Script.resolvePath("assets"); +var TOOLS_PATH = Script.resolvePath("assets/images/tools/"); + +function buttonImageURL() { + return TOOLS_PATH + 'bubble.svg'; +} + +var bubbleActive = Users.getIgnoreRadiusEnabled(); + +// setup the mod button and add it to the toolbar +var button = toolbar.addButton({ + objectName: 'bubble', + imageURL: buttonImageURL(), + visible: true, + buttonState: bubbleActive ? 0 : 1, + defaultState: bubbleActive ? 0 : 1, + hoverState: bubbleActive ? 2 : 3, + alpha: 0.9 +}); + + +// handle clicks on the toolbar button +function buttonClicked(){ + Users.toggleIgnoreRadius(); + bubbleActive = Users.getIgnoreRadiusEnabled(); + button.writeProperty('buttonState', bubbleActive ? 0 : 1); + button.writeProperty('defaultState', bubbleActive ? 0 : 1); + button.writeProperty('hoverState', bubbleActive ? 2 : 3); +} + +button.clicked.connect(buttonClicked); + +// cleanup the toolbar button and overlays when script is stopped +Script.scriptEnding.connect(function() { + toolbar.removeButton('bubble'); +}); + +}()); // END LOCAL_SCOPE diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 1382c94f9c..0ba630b3ff 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -240,11 +240,8 @@ var toolBar = (function () { hoverState: 3, defaultState: 1 }); - activeButton.clicked.connect(function () { - that.setActive(!isActive); - activeButton.writeProperty("buttonState", isActive ? 0 : 1); - activeButton.writeProperty("defaultState", isActive ? 0 : 1); - activeButton.writeProperty("hoverState", isActive ? 2 : 3); + activeButton.clicked.connect(function() { + that.toggle(); }); toolBar = Toolbars.getToolbar(EDIT_TOOLBAR); @@ -440,6 +437,14 @@ var toolBar = (function () { entityListTool.clearEntityList(); }; + + that.toggle = function () { + that.setActive(!isActive); + activeButton.writeProperty("buttonState", isActive ? 0 : 1); + activeButton.writeProperty("defaultState", isActive ? 0 : 1); + activeButton.writeProperty("hoverState", isActive ? 2 : 3); + }; + that.setActive = function (active) { if (active === isActive) { return; @@ -1093,7 +1098,6 @@ function handeMenuEvent(menuItem) { } } } else if (menuItem === "Import Entities" || menuItem === "Import Entities from URL") { - var importURL = null; if (menuItem === "Import Entities") { var fullPath = Window.browse("Select Model to Import", "", "*.json"); @@ -1105,6 +1109,9 @@ function handeMenuEvent(menuItem) { } if (importURL) { + if (!isActive && (Entities.canRez() && Entities.canRezTmp())) { + toolBar.toggle(); + } importSVO(importURL); } } else if (menuItem === "Entity List...") { @@ -1185,8 +1192,6 @@ function importSVO(importURL) { if (isActive) { selectionManager.setSelections(pastedEntityIDs); } - - Window.raiseMainWindow(); } else { Window.notifyEditError("Can't import objects: objects would be out of bounds."); } diff --git a/scripts/system/html/SnapshotReview.html b/scripts/system/html/SnapshotReview.html index db70a1910b..d37afb180c 100644 --- a/scripts/system/html/SnapshotReview.html +++ b/scripts/system/html/SnapshotReview.html @@ -1,48 +1,48 @@ - + Share - + - +
-
-
- -
-
-
-
-
Would you like to share your pic in the Snapshots feed?
-
- - - - +
+
+ +
+
+
+
+
Would you like to share your pics in the Snapshots feed?
+
+ + + + +
+
+
+ +
+
+
+
+ + + + + + + +
-
-
- -
-
-
- - - - - - - - +
-
-
-
- + diff --git a/scripts/system/html/js/SnapshotReview.js b/scripts/system/html/js/SnapshotReview.js index a6515df825..a1bb350789 100644 --- a/scripts/system/html/js/SnapshotReview.js +++ b/scripts/system/html/js/SnapshotReview.js @@ -12,6 +12,9 @@ var paths = [], idCounter = 0, useCheckboxes; function addImage(data) { + if (!data.localPath) { + return; + } var div = document.createElement("DIV"), input = document.createElement("INPUT"), label = document.createElement("LABEL"), @@ -20,21 +23,22 @@ function addImage(data) { function toggle() { data.share = input.checked; } img.src = data.localPath; div.appendChild(img); - data.share = true; if (useCheckboxes) { // I'd rather use css, but the included stylesheet is quite particular. // Our stylesheet(?) requires input.id to match label.for. Otherwise input doesn't display the check state. label.setAttribute('for', id); // cannot do label.for = input.id = id; input.type = "checkbox"; - input.checked = true; + input.checked = (id === "p0"); + data.share = input.checked; input.addEventListener('change', toggle); div.class = "property checkbox"; div.appendChild(input); div.appendChild(label); + } else { + data.share = true; } document.getElementById("snapshot-images").appendChild(div); paths.push(data); - } function handleShareButtons(shareMsg) { var openFeed = document.getElementById('openFeed'); @@ -49,7 +53,7 @@ function handleShareButtons(shareMsg) { window.onload = function () { // Something like the following will allow testing in a browser. //addImage({localPath: 'c:/Users/howar/OneDrive/Pictures/hifi-snap-by--on-2016-07-27_12-58-43.jpg'}); - //addImage({localPath: 'http://lorempixel.com/1512/1680'}); + //addImage({ localPath: 'http://lorempixel.com/1512/1680' }); openEventBridge(function () { // Set up a handler for receiving the data, and tell the .js we are ready to receive it. EventBridge.scriptEventReceived.connect(function (message) { diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index d89b532f31..d2589cb72f 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -522,13 +522,13 @@ function onEditError(msg) { } -function onSnapshotTaken(path, notify) { +function onSnapshotTaken(pathStillSnapshot, pathAnimatedSnapshot, notify) { if (notify) { var imageProperties = { - path: "file:///" + path, + path: "file:///" + pathStillSnapshot, aspectRatio: Window.innerWidth / Window.innerHeight }; - createNotification(wordWrap("Snapshot saved to " + path), NotificationType.SNAPSHOT, imageProperties); + createNotification(wordWrap("Snapshot saved to " + pathStillSnapshot), NotificationType.SNAPSHOT, imageProperties); } } diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 5eebadd02f..b4ebb99ef0 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -36,7 +36,7 @@ var SNAPSHOT_REVIEW_URL = Script.resolvePath("html/SnapshotReview.html"); var outstanding; function confirmShare(data) { - var dialog = new OverlayWebWindow('Snapshot Review', SNAPSHOT_REVIEW_URL, 800, 320); + var dialog = new OverlayWebWindow('Snapshot Review', SNAPSHOT_REVIEW_URL, 800, 520); function onMessage(message) { // Receives message from the html dialog via the qwebchannel EventBridge. This is complicated by the following: // 1. Although we can send POJOs, we cannot receive a toplevel object. (Arrays of POJOs are fine, though.) @@ -120,11 +120,11 @@ function onClicked() { // take snapshot (with no notification) Script.setTimeout(function () { - Window.takeSnapshot(false, 1.91); + Window.takeSnapshot(false, true, 1.91); }, SNAPSHOT_DELAY); } -function resetButtons(path, notify) { +function resetButtons(pathStillSnapshot, pathAnimatedSnapshot, notify) { // show overlays if they were on if (resetOverlays) { Menu.setIsOptionChecked("Overlays", true); @@ -141,7 +141,8 @@ function resetButtons(path, notify) { // last element in data array tells dialog whether we can share or not confirmShare([ - { localPath: path }, + { localPath: pathAnimatedSnapshot }, + { localPath: pathStillSnapshot }, { canShare: !!location.placename, openFeedAfterShare: shouldOpenFeedAfterShare()