diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 3059707313..c66ed1fe18 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -1108,7 +1108,7 @@ Rectangle { function findNearbySessionIndex(sessionId, optionalData) { // no findIndex in .qml var data = optionalData || nearbyUserModelData, length = data.length; for (var i = 0; i < length; i++) { - if (data[i].sessionId === sessionId) { + if (data[i].sessionId === sessionId.toString()) { return i; } } @@ -1120,7 +1120,7 @@ Rectangle { var data = message.params; var index = -1; iAmAdmin = Users.canKick; - index = findNearbySessionIndex('', data); + index = findNearbySessionIndex("", data); if (index !== -1) { myData = data[index]; data.splice(index, 1); @@ -1197,8 +1197,8 @@ Rectangle { for (var userId in message.params) { var audioLevel = message.params[userId][0]; var avgAudioLevel = message.params[userId][1]; - // If the userId is 0, we're updating "myData". - if (userId == 0) { + // If the userId is "", we're updating "myData". + if (userId === "") { myData.audioLevel = audioLevel; myCard.audioLevel = audioLevel; // Defensive programming myData.avgAudioLevel = avgAudioLevel; diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 29ad71ead6..bb9a78d546 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -14,6 +14,7 @@ #include #include +#include #include "AvatarLogging.h" @@ -668,3 +669,49 @@ void AvatarManager::setAvatarSortCoefficient(const QString& name, const QScriptV DependencyManager::get()->broadcastToNodes(std::move(packet), NodeSet() << NodeType::AvatarMixer); } } + + QVariantMap AvatarManager::getPalData(const QList specificAvatarIdentifiers) { + QJsonArray palData; + + auto avatarMap = getHashCopy(); + AvatarHash::iterator itr = avatarMap.begin(); + while (itr != avatarMap.end()) { + std::shared_ptr avatar = std::static_pointer_cast(*itr); + QString currentSessionUUID = avatar->getSessionUUID().toString(); + if (specificAvatarIdentifiers.isEmpty() || specificAvatarIdentifiers.contains(currentSessionUUID)) { + QJsonObject thisAvatarPalData; + + auto myAvatar = DependencyManager::get()->getMyAvatar(); + + if (currentSessionUUID == myAvatar->getSessionUUID().toString()) { + currentSessionUUID = ""; + } + + thisAvatarPalData.insert("sessionUUID", currentSessionUUID); + thisAvatarPalData.insert("sessionDisplayName", avatar->getSessionDisplayName()); + thisAvatarPalData.insert("audioLoudness", avatar->getAudioLoudness()); + thisAvatarPalData.insert("isReplicated", avatar->getIsReplicated()); + + glm::vec3 position = avatar->getWorldPosition(); + QJsonObject jsonPosition; + jsonPosition.insert("x", position.x); + jsonPosition.insert("y", position.y); + jsonPosition.insert("z", position.z); + thisAvatarPalData.insert("position", jsonPosition); + + float palOrbOffset = 0.2f; + int headIndex = avatar->getJointIndex("Head"); + if (headIndex > 0) { + glm::vec3 jointTranslation = avatar->getAbsoluteJointTranslationInObjectFrame(headIndex); + palOrbOffset = jointTranslation.y / 2; + } + thisAvatarPalData.insert("palOrbOffset", palOrbOffset); + + palData.append(thisAvatarPalData); + } + ++itr; + } + QJsonObject doc; + doc.insert("data", palData); + return doc.toVariantMap(); +} diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index ff29c1b381..53461146e5 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -157,6 +157,17 @@ public: */ Q_INVOKABLE void setAvatarSortCoefficient(const QString& name, const QScriptValue& value); + /**jsdoc + * Used in the PAL for getting PAL-related data about avatars nearby. Using this method is faster + * than iterating over each avatar and obtaining data about them in JavaScript, as that method + * locks and unlocks each avatar's data structure potentially hundreds of times per update tick. + * @function AvatarManager.getPalData + * @param {string[]} specificAvatarIdentifiers - A list of specific Avatar Identifiers about + * which you want to get PAL data + * @returns {object} + */ + Q_INVOKABLE QVariantMap getPalData(const QList specificAvatarIdentifiers = QList()); + float getMyAvatarSendRate() const { return _myAvatarSendRate.rate(); } int getIdentityRequestsSent() const { return _identityRequestsSent; } diff --git a/scripts/system/pal.js b/scripts/system/pal.js index 03dfada7ec..b6b11837e1 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -20,7 +20,7 @@ var AppUi = Script.require('appUi'); var populateNearbyUserList, color, textures, removeOverlays, controllerComputePickRay, off, receiveMessage, avatarDisconnected, clearLocalQMLDataAndClosePAL, - createAudioInterval, CHANNEL, getConnectionData, findableByChanged, + CHANNEL, getConnectionData, findableByChanged, avatarAdded, avatarRemoved, avatarSessionChanged; // forward references; // hardcoding these as it appears we cannot traverse the originalTextures in overlays??? Maybe I've missed @@ -448,21 +448,24 @@ function populateNearbyUserList(selectData, oldAudioData) { verticalAngleNormal = filter && Quat.getRight(orientation), horizontalAngleNormal = filter && Quat.getUp(orientation); avatarsOfInterest = {}; - avatars.forEach(function (id) { - var avatar = AvatarList.getAvatar(id); - var name = avatar.sessionDisplayName; + + var avatarData = AvatarList.getPalData().data; + + avatarData.forEach(function (currentAvatarData) { + var id = currentAvatarData.sessionUUID; + var name = currentAvatarData.sessionDisplayName; if (!name) { // Either we got a data packet but no identity yet, or something is really messed up. In any case, // we won't be able to do anything with this user, so don't include them. // In normal circumstances, a refresh will bring in the new user, but if we're very heavily loaded, // we could be losing and gaining people randomly. - print('No avatar identity data for', id); + print('No avatar identity data for', currentAvatarData.sessionUUID); return; } - if (id && myPosition && (Vec3.distance(avatar.position, myPosition) > filter.distance)) { + if (id && myPosition && (Vec3.distance(currentAvatarData.position, myPosition) > filter.distance)) { return; } - var normal = id && filter && Vec3.normalize(Vec3.subtract(avatar.position, myPosition)); + var normal = id && filter && Vec3.normalize(Vec3.subtract(currentAvatarData.position, myPosition)); var horizontal = normal && angleBetweenVectorsInPlane(normal, forward, horizontalAngleNormal); var vertical = normal && angleBetweenVectorsInPlane(normal, forward, verticalAngleNormal); if (id && filter && ((Math.abs(horizontal) > horizontalHalfAngle) || (Math.abs(vertical) > verticalHalfAngle))) { @@ -481,11 +484,11 @@ function populateNearbyUserList(selectData, oldAudioData) { personalMute: !!id && Users.getPersonalMuteStatus(id), // expects proper boolean, not null ignore: !!id && Users.getIgnoreStatus(id), // ditto isPresent: true, - isReplicated: avatar.isReplicated + isReplicated: currentAvatarData.isReplicated }; // Everyone needs to see admin status. Username and fingerprint returns default constructor output if the requesting user isn't an admin. Users.requestUsernameFromID(id); - if (id) { + if (id !== "") { addAvatarNode(id); // No overlay for ourselves avatarsOfInterest[id] = true; } else { @@ -516,30 +519,63 @@ function usernameFromIDReply(id, username, machineFingerprint, isAdmin) { updateUser(data); } +function updateAudioLevel(avatarData) { + // the VU meter should work similarly to the one in AvatarInputs: log scale, exponentially averaged + // But of course it gets the data at a different rate, so we tweak the averaging ratio and frequency + // of updating (the latter for efficiency too). + var audioLevel = 0.0; + var avgAudioLevel = 0.0; + + var data = avatarData.sessionUUID === "" ? myData : ExtendedOverlay.get(avatarData.sessionUUID); + + if (data) { + // we will do exponential moving average by taking some the last loudness and averaging + data.accumulatedLevel = AVERAGING_RATIO * (data.accumulatedLevel || 0) + (1 - AVERAGING_RATIO) * (avatarData.audioLoudness); + + // add 1 to insure we don't go log() and hit -infinity. Math.log is + // natural log, so to get log base 2, just divide by ln(2). + audioLevel = scaleAudio(Math.log(data.accumulatedLevel + 1) / LOG2); + + // decay avgAudioLevel + avgAudioLevel = Math.max((1 - AUDIO_PEAK_DECAY) * (data.avgAudioLevel || 0), audioLevel); + + data.avgAudioLevel = avgAudioLevel; + data.audioLevel = audioLevel; + + // now scale for the gain. Also, asked to boost the low end, so one simple way is + // to take sqrt of the value. Lets try that, see how it feels. + avgAudioLevel = Math.min(1.0, Math.sqrt(avgAudioLevel * (sessionGains[avatarData.sessionUUID] || 0.75))); + } + + var param = {}; + var level = [audioLevel, avgAudioLevel]; + var userId = avatarData.sessionUUID; + param[userId] = level; + sendToQml({ method: 'updateAudioLevel', params: param }); +} + var pingPong = true; function updateOverlays() { var eye = Camera.position; - AvatarList.getAvatarIdentifiers().forEach(function (id) { - if (!id || !avatarsOfInterest[id]) { + + var avatarData = AvatarList.getPalData().data; + + avatarData.forEach(function (currentAvatarData) { + + if (currentAvatarData.sessionUUID === "" || !avatarsOfInterest[currentAvatarData.sessionUUID]) { return; // don't update ourself, or avatars we're not interested in } - var avatar = AvatarList.getAvatar(id); - if (!avatar) { - return; // will be deleted below if there had been an overlay. - } - var overlay = ExtendedOverlay.get(id); + updateAudioLevel(currentAvatarData); + var overlay = ExtendedOverlay.get(currentAvatarData.sessionUUID); if (!overlay) { // For now, we're treating this as a temporary loss, as from the personal space bubble. Add it back. - print('Adding non-PAL avatar node', id); - overlay = addAvatarNode(id); + print('Adding non-PAL avatar node', currentAvatarData.sessionUUID); + overlay = addAvatarNode(currentAvatarData.sessionUUID); } - var target = avatar.position; + + var target = currentAvatarData.position; var distance = Vec3.distance(target, eye); - var offset = 0.2; + var offset = currentAvatarData.palOrbOffset; var diff = Vec3.subtract(target, eye); // get diff between target and eye (a vector pointing to the eye from avatar position) - var headIndex = avatar.getJointIndex("Head"); // base offset on 1/2 distance from hips to head if we can - if (headIndex > 0) { - offset = avatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y / 2; - } // move a bit in front, towards the camera target = Vec3.subtract(target, Vec3.multiply(Vec3.normalize(diff), offset)); @@ -549,7 +585,7 @@ function updateOverlays() { overlay.ping = pingPong; overlay.editOverlay({ - color: color(ExtendedOverlay.isSelected(id), overlay.hovering, overlay.audioLevel), + color: color(ExtendedOverlay.isSelected(currentAvatarData.sessionUUID), overlay.hovering, overlay.audioLevel), position: target, dimensions: 0.032 * distance }); @@ -678,6 +714,14 @@ function tabletVisibilityChanged() { if (!ui.tablet.tabletShown && ui.isOpen) { ui.close(); } + } + +var UPDATE_INTERVAL_MS = 100; +var updateInterval; +function createUpdateInterval() { + return Script.setInterval(function () { + updateOverlays(); + }, UPDATE_INTERVAL_MS); } var previousContextOverlay = ContextOverlay.enabled; @@ -689,10 +733,8 @@ function on() { ContextOverlay.enabled = false; Users.requestsDomainListData = true; - audioTimer = createAudioInterval(AUDIO_LEVEL_UPDATE_INTERVAL_MS); - ui.tablet.tabletShownChanged.connect(tabletVisibilityChanged); - Script.update.connect(updateOverlays); + updateInterval = createUpdateInterval(); Controller.mousePressEvent.connect(handleMouseEvent); Controller.mouseMoveEvent.connect(handleMouseMoveEvent); Users.usernameFromIDReply.connect(usernameFromIDReply); @@ -742,50 +784,6 @@ function scaleAudio(val) { return audioLevel; } -function getAudioLevel(id) { - // the VU meter should work similarly to the one in AvatarInputs: log scale, exponentially averaged - // But of course it gets the data at a different rate, so we tweak the averaging ratio and frequency - // of updating (the latter for efficiency too). - var avatar = AvatarList.getAvatar(id); - var audioLevel = 0.0; - var avgAudioLevel = 0.0; - var data = id ? ExtendedOverlay.get(id) : myData; - if (data) { - - // we will do exponential moving average by taking some the last loudness and averaging - data.accumulatedLevel = AVERAGING_RATIO * (data.accumulatedLevel || 0) + (1 - AVERAGING_RATIO) * (avatar.audioLoudness); - - // add 1 to insure we don't go log() and hit -infinity. Math.log is - // natural log, so to get log base 2, just divide by ln(2). - audioLevel = scaleAudio(Math.log(data.accumulatedLevel + 1) / LOG2); - - // decay avgAudioLevel - avgAudioLevel = Math.max((1 - AUDIO_PEAK_DECAY) * (data.avgAudioLevel || 0), audioLevel); - - data.avgAudioLevel = avgAudioLevel; - data.audioLevel = audioLevel; - - // now scale for the gain. Also, asked to boost the low end, so one simple way is - // to take sqrt of the value. Lets try that, see how it feels. - avgAudioLevel = Math.min(1.0, Math.sqrt(avgAudioLevel * (sessionGains[id] || 0.75))); - } - return [audioLevel, avgAudioLevel]; -} - -function createAudioInterval(interval) { - // we will update the audioLevels periodically - // TODO: tune for efficiency - expecially with large numbers of avatars - return Script.setInterval(function () { - var param = {}; - AvatarList.getAvatarIdentifiers().forEach(function (id) { - var level = getAudioLevel(id), - userId = id || 0; // qml didn't like an object with null/empty string for a key, so... - param[userId] = level; - }); - sendToQml({method: 'updateAudioLevel', params: param}); - }, interval); -} - function avatarDisconnected(nodeID) { // remove from the pal list sendToQml({method: 'avatarDisconnected', params: [nodeID]}); @@ -830,20 +828,17 @@ function startup() { } startup(); -var audioTimer; -var AUDIO_LEVEL_UPDATE_INTERVAL_MS = 100; // 10hz for now (change this and change the AVERAGING_RATIO too) function off() { if (ui.isOpen) { // i.e., only when connected - Script.update.disconnect(updateOverlays); + if (updateInterval) { + Script.clearInterval(updateInterval); + } Controller.mousePressEvent.disconnect(handleMouseEvent); Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); ui.tablet.tabletShownChanged.disconnect(tabletVisibilityChanged); Users.usernameFromIDReply.disconnect(usernameFromIDReply); triggerMapping.disable(); triggerPressMapping.disable(); - if (audioTimer) { - Script.clearInterval(audioTimer); - } } removeOverlays();