Space bubble improvements (#9142)

* The space bubble around a player's avatar is now visualized. When another avatar enters a player's bubble, The bubble visualization will appear, a soft tone will play, and the "Bubble" HUD button will flash.
* The space bubble radius setting has been removed. Space bubble size now scales based on avatar scale.
* Space bubble collision detection is now more accurate and reliable.
* CTRL + N toggles the bubble.
* The "Bubble" HUD button has been moved to the proper location.
This commit is contained in:
Zach Fox 2016-12-12 14:22:54 -08:00 committed by GitHub
parent 84bf93b0fc
commit 7030c7b0a6
35 changed files with 361 additions and 119 deletions

View file

@ -351,7 +351,9 @@ void Agent::executeScript() {
Transform audioTransform; Transform audioTransform;
audioTransform.setTranslation(scriptedAvatar->getPosition()); audioTransform.setTranslation(scriptedAvatar->getPosition());
audioTransform.setRotation(scriptedAvatar->getOrientation()); audioTransform.setRotation(scriptedAvatar->getOrientation());
AbstractAudioInterface::emitAudioPacket(audio.data(), audio.size(), audioSequenceNumber, audioTransform, PacketType::MicrophoneAudioNoEcho); AbstractAudioInterface::emitAudioPacket(audio.data(), audio.size(), audioSequenceNumber,
audioTransform, scriptedAvatar->getPosition(), glm::vec3(0),
PacketType::MicrophoneAudioNoEcho);
}); });
auto avatarHashMap = DependencyManager::set<AvatarHashMap>(); auto avatarHashMap = DependencyManager::set<AvatarHashMap>();
@ -424,8 +426,9 @@ void Agent::setIsListeningToAudioStream(bool isListeningToAudioStream) {
}, },
[&](const SharedNodePointer& node) { [&](const SharedNodePointer& node) {
qDebug() << "sending KillAvatar message to Audio Mixers"; qDebug() << "sending KillAvatar message to Audio Mixers";
auto packet = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID, true); auto packet = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID + sizeof(KillAvatarReason), true);
packet->write(getSessionUUID().toRfc4122()); packet->write(getSessionUUID().toRfc4122());
packet->writePrimitive(KillAvatarReason::NoReason);
nodeList->sendPacket(std::move(packet), *node); nodeList->sendPacket(std::move(packet), *node);
}); });
@ -475,8 +478,9 @@ void Agent::setIsAvatar(bool isAvatar) {
}, },
[&](const SharedNodePointer& node) { [&](const SharedNodePointer& node) {
qDebug() << "sending KillAvatar message to Avatar and Audio Mixers"; qDebug() << "sending KillAvatar message to Avatar and Audio Mixers";
auto packet = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID, true); auto packet = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID + sizeof(KillAvatarReason), true);
packet->write(getSessionUUID().toRfc4122()); packet->write(getSessionUUID().toRfc4122());
packet->writePrimitive(KillAvatarReason::NoReason);
nodeList->sendPacket(std::move(packet), *node); nodeList->sendPacket(std::move(packet), *node);
}); });
} }
@ -580,6 +584,8 @@ void Agent::processAgentAvatarAudio() {
audioPacket->writePrimitive(scriptedAvatar->getPosition()); audioPacket->writePrimitive(scriptedAvatar->getPosition());
glm::quat headOrientation = scriptedAvatar->getHeadOrientation(); glm::quat headOrientation = scriptedAvatar->getHeadOrientation();
audioPacket->writePrimitive(headOrientation); audioPacket->writePrimitive(headOrientation);
audioPacket->writePrimitive(scriptedAvatar->getPosition());
audioPacket->writePrimitive(glm::vec3(0));
} else if (nextSoundOutput) { } else if (nextSoundOutput) {
// write the codec // write the codec
@ -592,6 +598,8 @@ void Agent::processAgentAvatarAudio() {
audioPacket->writePrimitive(scriptedAvatar->getPosition()); audioPacket->writePrimitive(scriptedAvatar->getPosition());
glm::quat headOrientation = scriptedAvatar->getHeadOrientation(); glm::quat headOrientation = scriptedAvatar->getHeadOrientation();
audioPacket->writePrimitive(headOrientation); audioPacket->writePrimitive(headOrientation);
audioPacket->writePrimitive(scriptedAvatar->getPosition());
audioPacket->writePrimitive(glm::vec3(0));
QByteArray encodedBuffer; QByteArray encodedBuffer;
if (_flushEncoder) { if (_flushEncoder) {

View file

@ -90,6 +90,8 @@ public:
bool shouldMuteClient() { return _shouldMuteClient; } bool shouldMuteClient() { return _shouldMuteClient; }
void setShouldMuteClient(bool shouldMuteClient) { _shouldMuteClient = shouldMuteClient; } void setShouldMuteClient(bool shouldMuteClient) { _shouldMuteClient = shouldMuteClient; }
glm::vec3 getPosition() { return getAvatarAudioStream() ? getAvatarAudioStream()->getPosition() : glm::vec3(0); } glm::vec3 getPosition() { return getAvatarAudioStream() ? getAvatarAudioStream()->getPosition() : glm::vec3(0); }
glm::vec3 getAvatarBoundingBoxCorner() { return getAvatarAudioStream() ? getAvatarAudioStream()->getAvatarBoundingBoxCorner() : glm::vec3(0); }
glm::vec3 getAvatarBoundingBoxScale() { return getAvatarAudioStream() ? getAvatarAudioStream()->getAvatarBoundingBoxScale() : glm::vec3(0); }
signals: signals:
void injectorStreamFinished(const QUuid& streamIdentifier); void injectorStreamFinished(const QUuid& streamIdentifier);

View file

@ -211,20 +211,46 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& node) {
if (otherData if (otherData
&& !node->isIgnoringNodeWithID(otherNode->getUUID()) && !otherNode->isIgnoringNodeWithID(node->getUUID())) { && !node->isIgnoringNodeWithID(otherNode->getUUID()) && !otherNode->isIgnoringNodeWithID(node->getUUID())) {
// check if distance is inside ignore radius // check to see if we're ignoring in radius
if (node->isIgnoreRadiusEnabled() || otherNode->isIgnoreRadiusEnabled()) { bool insideIgnoreRadius = false;
float ignoreRadius = glm::min(node->getIgnoreRadius(), otherNode->getIgnoreRadius()); // If the otherNode equals the node, we're doing a comparison on ourselves
if (glm::distance(nodeData->getPosition(), otherData->getPosition()) < ignoreRadius) { if (*otherNode == *node) {
// skip, distance is inside ignore radius // We'll always be inside the radius in that case.
return; insideIgnoreRadius = true;
// Check to see if the space bubble is enabled
} else if ((node->isIgnoreRadiusEnabled() || otherNode->isIgnoreRadiusEnabled()) && (*otherNode != *node)) {
// Define the minimum bubble size
static const glm::vec3 minBubbleSize = glm::vec3(0.3f, 1.3f, 0.3f);
AudioMixerClientData* nodeData = reinterpret_cast<AudioMixerClientData*>(node->getLinkedData());
// Set up the bounding box for the current node
AABox nodeBox(nodeData->getAvatarBoundingBoxCorner(), nodeData->getAvatarBoundingBoxScale());
// Clamp the size of the bounding box to a minimum scale
if (glm::any(glm::lessThan(nodeData->getAvatarBoundingBoxScale(), minBubbleSize))) {
nodeBox.setScaleStayCentered(minBubbleSize);
}
// Set up the bounding box for the current other node
AABox otherNodeBox(otherData->getAvatarBoundingBoxCorner(), otherData->getAvatarBoundingBoxScale());
// Clamp the size of the bounding box to a minimum scale
if (glm::any(glm::lessThan(otherData->getAvatarBoundingBoxScale(), minBubbleSize))) {
otherNodeBox.setScaleStayCentered(minBubbleSize);
}
// Quadruple the scale of both bounding boxes
nodeBox.embiggen(4.0f);
otherNodeBox.embiggen(4.0f);
// Perform the collision check between the two bounding boxes
if (nodeBox.touches(otherNodeBox)) {
insideIgnoreRadius = true;
} }
} }
// enumerate the ARBs attached to the otherNode and add all that should be added to mix // Enumerate the audio streams attached to the otherNode
auto streamsCopy = otherData->getAudioStreams(); auto streamsCopy = otherData->getAudioStreams();
for (auto& streamPair : streamsCopy) { for (auto& streamPair : streamsCopy) {
auto otherNodeStream = streamPair.second; auto otherNodeStream = streamPair.second;
if (*otherNode != *node || otherNodeStream->shouldLoopbackForNode()) { bool isSelfWithEcho = ((*otherNode == *node) && (otherNodeStream->shouldLoopbackForNode()));
// Add all audio streams that should be added to the mix
if (isSelfWithEcho || (!isSelfWithEcho && !insideIgnoreRadius)) {
addStreamToMix(*nodeData, otherNode->getUUID(), *nodeAudioStream, *otherNodeStream); addStreamToMix(*nodeData, otherNode->getUUID(), *nodeAudioStream, *otherNodeStream);
} }
} }

View file

@ -19,6 +19,7 @@
#include <QtCore/QTimer> #include <QtCore/QTimer>
#include <QtCore/QThread> #include <QtCore/QThread>
#include <AABox.h>
#include <LogHandler.h> #include <LogHandler.h>
#include <NodeList.h> #include <NodeList.h>
#include <udt/PacketHeaders.h> #include <udt/PacketHeaders.h>
@ -240,18 +241,39 @@ void AvatarMixer::broadcastAvatarData() {
} else { } else {
AvatarMixerClientData* otherData = reinterpret_cast<AvatarMixerClientData*>(otherNode->getLinkedData()); AvatarMixerClientData* otherData = reinterpret_cast<AvatarMixerClientData*>(otherNode->getLinkedData());
AvatarMixerClientData* nodeData = reinterpret_cast<AvatarMixerClientData*>(node->getLinkedData()); AvatarMixerClientData* nodeData = reinterpret_cast<AvatarMixerClientData*>(node->getLinkedData());
// check to see if we're ignoring in radius // Check to see if the space bubble is enabled
if (node->isIgnoreRadiusEnabled() || otherNode->isIgnoreRadiusEnabled()) { if (node->isIgnoreRadiusEnabled() || otherNode->isIgnoreRadiusEnabled()) {
float ignoreRadius = glm::min(node->getIgnoreRadius(), otherNode->getIgnoreRadius()); // Define the minimum bubble size
if (glm::distance(nodeData->getPosition(), otherData->getPosition()) < ignoreRadius) { static const glm::vec3 minBubbleSize = glm::vec3(0.3f, 1.3f, 0.3f);
// Define the scale of the box for the current node
glm::vec3 nodeBoxScale = (nodeData->getPosition() - nodeData->getGlobalBoundingBoxCorner()) * 2.0f;
// Define the scale of the box for the current other node
glm::vec3 otherNodeBoxScale = (otherData->getPosition() - otherData->getGlobalBoundingBoxCorner()) * 2.0f;
// Set up the bounding box for the current node
AABox nodeBox(nodeData->getGlobalBoundingBoxCorner(), nodeBoxScale);
// Clamp the size of the bounding box to a minimum scale
if (glm::any(glm::lessThan(nodeBoxScale, minBubbleSize))) {
nodeBox.setScaleStayCentered(minBubbleSize);
}
// Set up the bounding box for the current other node
AABox otherNodeBox(otherData->getGlobalBoundingBoxCorner(), otherNodeBoxScale);
// Clamp the size of the bounding box to a minimum scale
if (glm::any(glm::lessThan(otherNodeBoxScale, minBubbleSize))) {
otherNodeBox.setScaleStayCentered(minBubbleSize);
}
// Quadruple the scale of both bounding boxes
nodeBox.embiggen(4.0f);
otherNodeBox.embiggen(4.0f);
// Perform the collision check between the two bounding boxes
if (nodeBox.touches(otherNodeBox)) {
nodeData->ignoreOther(node, otherNode); nodeData->ignoreOther(node, otherNode);
otherData->ignoreOther(otherNode, node);
return false; return false;
} }
} }
// not close enough to ignore // Not close enough to ignore
nodeData->removeFromRadiusIgnoringSet(otherNode->getUUID()); nodeData->removeFromRadiusIgnoringSet(otherNode->getUUID());
otherData->removeFromRadiusIgnoringSet(node->getUUID());
return true; return true;
} }
}, },
@ -395,8 +417,9 @@ void AvatarMixer::nodeKilled(SharedNodePointer killedNode) {
// this was an avatar we were sending to other people // this was an avatar we were sending to other people
// send a kill packet for it to our other nodes // send a kill packet for it to our other nodes
auto killPacket = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID); auto killPacket = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID + sizeof(KillAvatarReason));
killPacket->write(killedNode->getUUID().toRfc4122()); killPacket->write(killedNode->getUUID().toRfc4122());
killPacket->writePrimitive(KillAvatarReason::AvatarDisconnected);
nodeList->broadcastToNodes(std::move(killPacket), NodeSet() << NodeType::Agent); nodeList->broadcastToNodes(std::move(killPacket), NodeSet() << NodeType::Agent);

View file

@ -45,8 +45,13 @@ uint16_t AvatarMixerClientData::getLastBroadcastSequenceNumber(const QUuid& node
void AvatarMixerClientData::ignoreOther(SharedNodePointer self, SharedNodePointer other) { void AvatarMixerClientData::ignoreOther(SharedNodePointer self, SharedNodePointer other) {
if (!isRadiusIgnoring(other->getUUID())) { if (!isRadiusIgnoring(other->getUUID())) {
addToRadiusIgnoringSet(other->getUUID()); addToRadiusIgnoringSet(other->getUUID());
auto killPacket = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID); auto killPacket = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID + sizeof(KillAvatarReason));
killPacket->write(other->getUUID().toRfc4122()); killPacket->write(other->getUUID().toRfc4122());
if (self->isIgnoreRadiusEnabled()) {
killPacket->writePrimitive(KillAvatarReason::TheirAvatarEnteredYourBubble);
} else {
killPacket->writePrimitive(KillAvatarReason::YourAvatarEnteredTheirBubble);
}
DependencyManager::get<NodeList>()->sendUnreliablePacket(*killPacket, *self); DependencyManager::get<NodeList>()->sendUnreliablePacket(*killPacket, *self);
_hasReceivedFirstPacketsFrom.erase(other->getUUID()); _hasReceivedFirstPacketsFrom.erase(other->getUUID());
} }

View file

@ -81,6 +81,7 @@ public:
void loadJSONStats(QJsonObject& jsonObject) const; void loadJSONStats(QJsonObject& jsonObject) const;
glm::vec3 getPosition() { return _avatar ? _avatar->getPosition() : glm::vec3(0); } glm::vec3 getPosition() { return _avatar ? _avatar->getPosition() : glm::vec3(0); }
glm::vec3 getGlobalBoundingBoxCorner() { return _avatar ? _avatar->getGlobalBoundingBoxCorner() : glm::vec3(0); }
bool isRadiusIgnoring(const QUuid& other) { return _radiusIgnoredOthers.find(other) != _radiusIgnoredOthers.end(); } bool isRadiusIgnoring(const QUuid& other) { return _radiusIgnoredOthers.find(other) != _radiusIgnoredOthers.end(); }
void addToRadiusIgnoringSet(const QUuid& other) { _radiusIgnoredOthers.insert(other); } void addToRadiusIgnoringSet(const QUuid& other) { _radiusIgnoredOthers.insert(other); }
void removeFromRadiusIgnoringSet(const QUuid& other) { _radiusIgnoredOthers.erase(other); } void removeFromRadiusIgnoringSet(const QUuid& other) { _radiusIgnoredOthers.erase(other); }

View file

@ -76,7 +76,7 @@ OriginalDesktop.Desktop {
WebEngine.settings.localContentCanAccessRemoteUrls = true; WebEngine.settings.localContentCanAccessRemoteUrls = true;
[ // Allocate the standard buttons in the correct order. They will get images, etc., via scripts. [ // Allocate the standard buttons in the correct order. They will get images, etc., via scripts.
"hmdToggle", "mute", "mod", "help", "hmdToggle", "mute", "mod", "bubble", "help",
"hudToggle", "hudToggle",
"com.highfidelity.interface.system.editButton", "marketplace", "snapshot", "goto" "com.highfidelity.interface.system.editButton", "marketplace", "snapshot", "goto"
].forEach(function (name) { ].forEach(function (name) {

View file

@ -2527,6 +2527,12 @@ void Application::keyPressEvent(QKeyEvent* event) {
Menu::getInstance()->triggerOption(MenuOption::DefaultSkybox); Menu::getInstance()->triggerOption(MenuOption::DefaultSkybox);
break; break;
case Qt::Key_N:
if (!isOption && !isShifted && isMeta) {
DependencyManager::get<NodeList>()->toggleIgnoreRadius();
}
break;
case Qt::Key_S: case Qt::Key_S:
if (isShifted && isMeta && !isOption) { if (isShifted && isMeta && !isOption) {
Menu::getInstance()->triggerOption(MenuOption::SuppressShortTimings); Menu::getInstance()->triggerOption(MenuOption::SuppressShortTimings);

View file

@ -80,7 +80,7 @@ AvatarManager::AvatarManager(QObject* parent) :
// when we hear that the user has ignored an avatar by session UUID // when we hear that the user has ignored an avatar by session UUID
// immediately remove that avatar instead of waiting for the absence of packets from avatar mixer // immediately remove that avatar instead of waiting for the absence of packets from avatar mixer
connect(nodeList.data(), &NodeList::ignoredNode, this, &AvatarManager::removeAvatar); connect(nodeList.data(), "ignoredNode", this, "removeAvatar");
} }
AvatarManager::~AvatarManager() { AvatarManager::~AvatarManager() {
@ -223,16 +223,16 @@ AvatarSharedPointer AvatarManager::addAvatar(const QUuid& sessionUUID, const QWe
} }
// virtual // virtual
void AvatarManager::removeAvatar(const QUuid& sessionUUID) { void AvatarManager::removeAvatar(const QUuid& sessionUUID, KillAvatarReason removalReason) {
QWriteLocker locker(&_hashLock); QWriteLocker locker(&_hashLock);
auto removedAvatar = _avatarHash.take(sessionUUID); auto removedAvatar = _avatarHash.take(sessionUUID);
if (removedAvatar) { if (removedAvatar) {
handleRemovedAvatar(removedAvatar); handleRemovedAvatar(removedAvatar, removalReason);
} }
} }
void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar) { void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason) {
AvatarHashMap::handleRemovedAvatar(removedAvatar); AvatarHashMap::handleRemovedAvatar(removedAvatar);
// removedAvatar is a shared pointer to an AvatarData but we need to get to the derived Avatar // removedAvatar is a shared pointer to an AvatarData but we need to get to the derived Avatar
@ -247,6 +247,9 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar
_motionStatesToRemoveFromPhysics.push_back(motionState); _motionStatesToRemoveFromPhysics.push_back(motionState);
} }
if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble) {
emit DependencyManager::get<UsersScriptingInterface>()->enteredIgnoreRadius();
}
_avatarFades.push_back(removedAvatar); _avatarFades.push_back(removedAvatar);
} }

View file

@ -79,7 +79,7 @@ public slots:
void updateAvatarRenderStatus(bool shouldRenderAvatars); void updateAvatarRenderStatus(bool shouldRenderAvatars);
private slots: private slots:
virtual void removeAvatar(const QUuid& sessionUUID) override; virtual void removeAvatar(const QUuid& sessionUUID, KillAvatarReason removalReason = KillAvatarReason::NoReason) override;
private: private:
explicit AvatarManager(QObject* parent = 0); explicit AvatarManager(QObject* parent = 0);
@ -91,7 +91,7 @@ private:
virtual AvatarSharedPointer newSharedAvatar() override; virtual AvatarSharedPointer newSharedAvatar() override;
virtual AvatarSharedPointer addAvatar(const QUuid& sessionUUID, const QWeakPointer<Node>& mixerWeakPointer) override; virtual AvatarSharedPointer addAvatar(const QUuid& sessionUUID, const QWeakPointer<Node>& mixerWeakPointer) override;
virtual void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar) override; virtual void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason = KillAvatarReason::NoReason) override;
QVector<AvatarSharedPointer> _avatarFades; QVector<AvatarSharedPointer> _avatarFades;
std::shared_ptr<MyAvatar> _myAvatar; std::shared_ptr<MyAvatar> _myAvatar;

View file

@ -230,6 +230,10 @@ void MyAvatar::simulateAttachments(float deltaTime) {
QByteArray MyAvatar::toByteArray(bool cullSmallChanges, bool sendAll) { QByteArray MyAvatar::toByteArray(bool cullSmallChanges, bool sendAll) {
CameraMode mode = qApp->getCamera()->getMode(); CameraMode mode = qApp->getCamera()->getMode();
_globalPosition = getPosition(); _globalPosition = getPosition();
_globalBoundingBoxCorner.x = _characterController.getCapsuleRadius();
_globalBoundingBoxCorner.y = _characterController.getCapsuleHalfHeight();
_globalBoundingBoxCorner.z = _characterController.getCapsuleRadius();
_globalBoundingBoxCorner += _characterController.getCapsuleLocalOffset();
if (mode == CAMERA_MODE_THIRD_PERSON || mode == CAMERA_MODE_INDEPENDENT) { if (mode == CAMERA_MODE_THIRD_PERSON || mode == CAMERA_MODE_INDEPENDENT) {
// fake the avatar position that is sent up to the AvatarMixer // fake the avatar position that is sent up to the AvatarMixer
glm::vec3 oldPosition = getPosition(); glm::vec3 oldPosition = getPosition();
@ -360,10 +364,18 @@ void MyAvatar::update(float deltaTime) {
updateFromTrackers(deltaTime); updateFromTrackers(deltaTime);
// Get audio loudness data from audio input device // Get audio loudness data from audio input device
// Also get the AudioClient so we can update the avatar bounding box data
// on the AudioClient side.
auto audio = DependencyManager::get<AudioClient>(); auto audio = DependencyManager::get<AudioClient>();
head->setAudioLoudness(audio->getLastInputLoudness()); head->setAudioLoudness(audio->getLastInputLoudness());
head->setAudioAverageLoudness(audio->getAudioAverageInputLoudness()); head->setAudioAverageLoudness(audio->getAudioAverageInputLoudness());
glm::vec3 halfBoundingBoxDimensions(_characterController.getCapsuleRadius(), _characterController.getCapsuleHalfHeight(), _characterController.getCapsuleRadius());
halfBoundingBoxDimensions += _characterController.getCapsuleLocalOffset();
QMetaObject::invokeMethod(audio.data(), "setAvatarBoundingBoxParameters",
Q_ARG(glm::vec3, (getPosition() - halfBoundingBoxDimensions)),
Q_ARG(glm::vec3, (halfBoundingBoxDimensions*2.0f)));
if (_avatarEntityDataLocallyEdited) { if (_avatarEntityDataLocallyEdited) {
sendIdentityPacket(); sendIdentityPacket();
} }

View file

@ -68,18 +68,6 @@ void setupPreferences() {
auto setter = [=](bool value) { myAvatar->setClearOverlayWhenMoving(value); }; auto setter = [=](bool value) { myAvatar->setClearOverlayWhenMoving(value); };
preferences->addPreference(new CheckPreference(AVATAR_BASICS, "Clear overlays when moving", getter, setter)); 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 // UI
{ {

View file

@ -1063,7 +1063,9 @@ void AudioClient::handleAudioInput() {
encodedBuffer = decodedBuffer; encodedBuffer = decodedBuffer;
} }
emitAudioPacket(encodedBuffer.constData(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, audioTransform, packetType, _selectedCodecName); emitAudioPacket(encodedBuffer.constData(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber,
audioTransform, avatarBoundingBoxCorner, avatarBoundingBoxScale,
packetType, _selectedCodecName);
_stats.sentPacket(); _stats.sentPacket();
int bytesInInputRingBuffer = _inputRingBuffer.samplesAvailable() * AudioConstants::SAMPLE_SIZE; int bytesInInputRingBuffer = _inputRingBuffer.samplesAvailable() * AudioConstants::SAMPLE_SIZE;
@ -1085,7 +1087,9 @@ void AudioClient::handleRecordedAudioInput(const QByteArray& audio) {
} }
// FIXME check a flag to see if we should echo audio? // FIXME check a flag to see if we should echo audio?
emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, audioTransform, PacketType::MicrophoneAudioWithEcho, _selectedCodecName); emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber,
audioTransform, avatarBoundingBoxCorner, avatarBoundingBoxScale,
PacketType::MicrophoneAudioWithEcho, _selectedCodecName);
} }
void AudioClient::mixLocalAudioInjectors(float* mixBuffer) { void AudioClient::mixLocalAudioInjectors(float* mixBuffer) {
@ -1619,3 +1623,8 @@ void AudioClient::saveSettings() {
dynamicJitterBufferEnabled.set(_receivedAudioStream.dynamicJitterBufferEnabled()); dynamicJitterBufferEnabled.set(_receivedAudioStream.dynamicJitterBufferEnabled());
staticJitterBufferFrames.set(_receivedAudioStream.getStaticJitterBufferFrames()); staticJitterBufferFrames.set(_receivedAudioStream.getStaticJitterBufferFrames());
} }
void AudioClient::setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 scale) {
avatarBoundingBoxCorner = corner;
avatarBoundingBoxScale = scale;
}

View file

@ -127,6 +127,8 @@ public:
void setPositionGetter(AudioPositionGetter positionGetter) { _positionGetter = positionGetter; } void setPositionGetter(AudioPositionGetter positionGetter) { _positionGetter = positionGetter; }
void setOrientationGetter(AudioOrientationGetter orientationGetter) { _orientationGetter = orientationGetter; } void setOrientationGetter(AudioOrientationGetter orientationGetter) { _orientationGetter = orientationGetter; }
Q_INVOKABLE void setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 scale);
QVector<AudioInjector*>& getActiveLocalAudioInjectors() { return _activeLocalAudioInjectors; } QVector<AudioInjector*>& getActiveLocalAudioInjectors() { return _activeLocalAudioInjectors; }
void checkDevices(); void checkDevices();
@ -324,6 +326,9 @@ private:
AudioPositionGetter _positionGetter; AudioPositionGetter _positionGetter;
AudioOrientationGetter _orientationGetter; AudioOrientationGetter _orientationGetter;
glm::vec3 avatarBoundingBoxCorner;
glm::vec3 avatarBoundingBoxScale;
QVector<QString> _inputDevices; QVector<QString> _inputDevices;
QVector<QString> _outputDevices; QVector<QString> _outputDevices;

View file

@ -19,8 +19,9 @@
#include "AudioConstants.h" #include "AudioConstants.h"
void AbstractAudioInterface::emitAudioPacket(const void* audioData, size_t bytes, quint16& sequenceNumber, void AbstractAudioInterface::emitAudioPacket(const void* audioData, size_t bytes, quint16& sequenceNumber,
const Transform& transform, PacketType packetType, QString codecName) { const Transform& transform, glm::vec3 avatarBoundingBoxCorner, glm::vec3 avatarBoundingBoxScale,
PacketType packetType, QString codecName) {
static std::mutex _mutex; static std::mutex _mutex;
using Locker = std::unique_lock<std::mutex>; using Locker = std::unique_lock<std::mutex>;
auto nodeList = DependencyManager::get<NodeList>(); auto nodeList = DependencyManager::get<NodeList>();
@ -55,6 +56,10 @@ void AbstractAudioInterface::emitAudioPacket(const void* audioData, size_t bytes
// pack the orientation // pack the orientation
audioPacket->writePrimitive(transform.getRotation()); audioPacket->writePrimitive(transform.getRotation());
audioPacket->writePrimitive(avatarBoundingBoxCorner);
audioPacket->writePrimitive(avatarBoundingBoxScale);
if (audioPacket->getType() != PacketType::SilentAudioFrame) { if (audioPacket->getType() != PacketType::SilentAudioFrame) {
// audio samples have already been packed (written to networkAudioSamples) // audio samples have already been packed (written to networkAudioSamples)
int leadingBytes = audioPacket->getPayloadSize(); int leadingBytes = audioPacket->getPayloadSize();

View file

@ -28,7 +28,8 @@ class AbstractAudioInterface : public QObject {
public: public:
AbstractAudioInterface(QObject* parent = 0) : QObject(parent) {}; AbstractAudioInterface(QObject* parent = 0) : QObject(parent) {};
static void emitAudioPacket(const void* audioData, size_t bytes, quint16& sequenceNumber, const Transform& transform, static void emitAudioPacket(const void* audioData, size_t bytes, quint16& sequenceNumber,
const Transform& transform, glm::vec3 avatarBoundingBoxCorner, glm::vec3 avatarBoundingBoxScale,
PacketType packetType, QString codecName = QString("")); PacketType packetType, QString codecName = QString(""));
public slots: public slots:

View file

@ -77,6 +77,8 @@ int PositionalAudioStream::parsePositionalData(const QByteArray& positionalByteA
packetStream.readRawData(reinterpret_cast<char*>(&_position), sizeof(_position)); packetStream.readRawData(reinterpret_cast<char*>(&_position), sizeof(_position));
packetStream.readRawData(reinterpret_cast<char*>(&_orientation), sizeof(_orientation)); packetStream.readRawData(reinterpret_cast<char*>(&_orientation), sizeof(_orientation));
packetStream.readRawData(reinterpret_cast<char*>(&_avatarBoundingBoxCorner), sizeof(_avatarBoundingBoxCorner));
packetStream.readRawData(reinterpret_cast<char*>(&_avatarBoundingBoxScale), sizeof(_avatarBoundingBoxScale));
// if this node sent us a NaN for first float in orientation then don't consider this good audio and bail // if this node sent us a NaN for first float in orientation then don't consider this good audio and bail
if (glm::isnan(_orientation.x)) { if (glm::isnan(_orientation.x)) {

View file

@ -46,6 +46,8 @@ public:
PositionalAudioStream::Type getType() const { return _type; } PositionalAudioStream::Type getType() const { return _type; }
const glm::vec3& getPosition() const { return _position; } const glm::vec3& getPosition() const { return _position; }
const glm::quat& getOrientation() const { return _orientation; } const glm::quat& getOrientation() const { return _orientation; }
const glm::vec3& getAvatarBoundingBoxCorner() const { return _avatarBoundingBoxCorner; }
const glm::vec3& getAvatarBoundingBoxScale() const { return _avatarBoundingBoxScale; }
protected: protected:
@ -60,6 +62,9 @@ protected:
glm::vec3 _position; glm::vec3 _position;
glm::quat _orientation; glm::quat _orientation;
glm::vec3 _avatarBoundingBoxCorner;
glm::vec3 _avatarBoundingBoxScale;
bool _shouldLoopbackForNode; bool _shouldLoopbackForNode;
bool _isStereo; bool _isStereo;
// Ignore penumbra filter // Ignore penumbra filter

View file

@ -55,6 +55,7 @@ namespace AvatarDataPacket {
PACKED_BEGIN struct Header { PACKED_BEGIN struct Header {
float position[3]; // skeletal model's position float position[3]; // skeletal model's position
float globalPosition[3]; // avatar's position float globalPosition[3]; // avatar's position
float globalBoundingBoxCorner[3]; // global position of the lowest corner of the avatar's bounding box
uint16_t localOrientation[3]; // avatar's local euler angles (degrees, compressed) relative to the thing it's attached to uint16_t localOrientation[3]; // avatar's local euler angles (degrees, compressed) relative to the thing it's attached to
uint16_t scale; // (compressed) 'ratio' encoding uses sign bit as flag. uint16_t scale; // (compressed) 'ratio' encoding uses sign bit as flag.
float lookAtPosition[3]; // world space position that eyes are focusing on. float lookAtPosition[3]; // world space position that eyes are focusing on.
@ -64,7 +65,7 @@ namespace AvatarDataPacket {
float sensorToWorldTrans[3]; // fourth column of sensor to world matrix float sensorToWorldTrans[3]; // fourth column of sensor to world matrix
uint8_t flags; uint8_t flags;
} PACKED_END; } PACKED_END;
const size_t HEADER_SIZE = 69; const size_t HEADER_SIZE = 81;
// only present if HAS_REFERENTIAL flag is set in header.flags // only present if HAS_REFERENTIAL flag is set in header.flags
PACKED_BEGIN struct ParentInfo { PACKED_BEGIN struct ParentInfo {
@ -205,6 +206,9 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) {
header->globalPosition[0] = _globalPosition.x; header->globalPosition[0] = _globalPosition.x;
header->globalPosition[1] = _globalPosition.y; header->globalPosition[1] = _globalPosition.y;
header->globalPosition[2] = _globalPosition.z; header->globalPosition[2] = _globalPosition.z;
header->globalBoundingBoxCorner[0] = getPosition().x - _globalBoundingBoxCorner.x;
header->globalBoundingBoxCorner[1] = getPosition().y - _globalBoundingBoxCorner.y;
header->globalBoundingBoxCorner[2] = getPosition().z - _globalBoundingBoxCorner.z;
glm::vec3 bodyEulerAngles = glm::degrees(safeEulerAngles(getLocalOrientation())); glm::vec3 bodyEulerAngles = glm::degrees(safeEulerAngles(getLocalOrientation()));
packFloatAngleToTwoByte((uint8_t*)(header->localOrientation + 0), bodyEulerAngles.y); packFloatAngleToTwoByte((uint8_t*)(header->localOrientation + 0), bodyEulerAngles.y);
@ -481,6 +485,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
glm::vec3 position = glm::vec3(header->position[0], header->position[1], header->position[2]); glm::vec3 position = glm::vec3(header->position[0], header->position[1], header->position[2]);
_globalPosition = glm::vec3(header->globalPosition[0], header->globalPosition[1], header->globalPosition[2]); _globalPosition = glm::vec3(header->globalPosition[0], header->globalPosition[1], header->globalPosition[2]);
_globalBoundingBoxCorner = glm::vec3(header->globalBoundingBoxCorner[0], header->globalBoundingBoxCorner[1], header->globalBoundingBoxCorner[2]);
if (isNaN(position)) { if (isNaN(position)) {
if (shouldLogError(now)) { if (shouldLogError(now)) {
qCWarning(avatars) << "Discard AvatarData packet: position NaN, uuid " << getSessionUUID(); qCWarning(avatars) << "Discard AvatarData packet: position NaN, uuid " << getSessionUUID();

View file

@ -133,6 +133,15 @@ enum KeyState {
DELETE_KEY_DOWN DELETE_KEY_DOWN
}; };
enum KillAvatarReason : uint8_t {
NoReason = 0,
AvatarDisconnected,
AvatarIgnored,
TheirAvatarEnteredYourBubble,
YourAvatarEnteredTheirBubble
};
Q_DECLARE_METATYPE(KillAvatarReason);
class QDataStream; class QDataStream;
class AttachmentData; class AttachmentData;
@ -353,6 +362,7 @@ public:
void fromJson(const QJsonObject& json); void fromJson(const QJsonObject& json);
glm::vec3 getClientGlobalPosition() { return _globalPosition; } glm::vec3 getClientGlobalPosition() { return _globalPosition; }
glm::vec3 getGlobalBoundingBoxCorner() { return _globalBoundingBoxCorner; }
Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const; Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const;
Q_INVOKABLE void setAvatarEntityData(const AvatarEntityMap& avatarEntityData); Q_INVOKABLE void setAvatarEntityData(const AvatarEntityMap& avatarEntityData);
@ -436,6 +446,7 @@ protected:
// where Entities are located. This is currently only used by the mixer to decide how often to send // where Entities are located. This is currently only used by the mixer to decide how often to send
// updates about one avatar to another. // updates about one avatar to another.
glm::vec3 _globalPosition; glm::vec3 _globalPosition;
glm::vec3 _globalBoundingBoxCorner;
mutable ReadWriteLockable _avatarEntitiesLock; mutable ReadWriteLockable _avatarEntitiesLock;
AvatarEntityIDs _avatarEntityDetached; // recently detached from this avatar AvatarEntityIDs _avatarEntityDetached; // recently detached from this avatar

View file

@ -141,20 +141,23 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer<ReceivedMessage>
void AvatarHashMap::processKillAvatar(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) { void AvatarHashMap::processKillAvatar(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
// read the node id // read the node id
QUuid sessionUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); QUuid sessionUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
removeAvatar(sessionUUID);
KillAvatarReason reason;
message->readPrimitive(&reason);
removeAvatar(sessionUUID, reason);
} }
void AvatarHashMap::removeAvatar(const QUuid& sessionUUID) { void AvatarHashMap::removeAvatar(const QUuid& sessionUUID, KillAvatarReason removalReason) {
QWriteLocker locker(&_hashLock); QWriteLocker locker(&_hashLock);
auto removedAvatar = _avatarHash.take(sessionUUID); auto removedAvatar = _avatarHash.take(sessionUUID);
if (removedAvatar) { if (removedAvatar) {
handleRemovedAvatar(removedAvatar); handleRemovedAvatar(removedAvatar, removalReason);
} }
} }
void AvatarHashMap::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar) { void AvatarHashMap::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason) {
qDebug() << "Removed avatar with UUID" << uuidStringWithoutCurlyBraces(removedAvatar->getSessionUUID()) qDebug() << "Removed avatar with UUID" << uuidStringWithoutCurlyBraces(removedAvatar->getSessionUUID())
<< "from AvatarHashMap"; << "from AvatarHashMap";
emit avatarRemovedEvent(removedAvatar->getSessionUUID()); emit avatarRemovedEvent(removedAvatar->getSessionUUID());

View file

@ -65,9 +65,9 @@ protected:
virtual AvatarSharedPointer addAvatar(const QUuid& sessionUUID, const QWeakPointer<Node>& mixerWeakPointer); virtual AvatarSharedPointer addAvatar(const QUuid& sessionUUID, const QWeakPointer<Node>& mixerWeakPointer);
AvatarSharedPointer newOrExistingAvatar(const QUuid& sessionUUID, const QWeakPointer<Node>& mixerWeakPointer); AvatarSharedPointer newOrExistingAvatar(const QUuid& sessionUUID, const QWeakPointer<Node>& mixerWeakPointer);
virtual AvatarSharedPointer findAvatar(const QUuid& sessionUUID); // uses a QReadLocker on the hashLock virtual AvatarSharedPointer findAvatar(const QUuid& sessionUUID); // uses a QReadLocker on the hashLock
virtual void removeAvatar(const QUuid& sessionUUID); virtual void removeAvatar(const QUuid& sessionUUID, KillAvatarReason removalReason = KillAvatarReason::NoReason);
virtual void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar); virtual void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason = KillAvatarReason::NoReason);
AvatarHash _avatarHash; AvatarHash _avatarHash;
// "Case-based safety": Most access to the _avatarHash is on the same thread. Write access is protected by a write-lock. // "Case-based safety": Most access to the _avatarHash is on the same thread. Write access is protected by a write-lock.

View file

@ -104,11 +104,8 @@ void Node::addIgnoredNode(const QUuid& otherNodeID) {
void Node::parseIgnoreRadiusRequestMessage(QSharedPointer<ReceivedMessage> message) { void Node::parseIgnoreRadiusRequestMessage(QSharedPointer<ReceivedMessage> message) {
bool enabled; bool enabled;
float radius;
message->readPrimitive(&enabled); message->readPrimitive(&enabled);
message->readPrimitive(&radius);
_ignoreRadiusEnabled = enabled; _ignoreRadiusEnabled = enabled;
_ignoreRadius = radius;
} }
QDataStream& operator<<(QDataStream& out, const Node& node) { QDataStream& operator<<(QDataStream& out, const Node& node) {

View file

@ -80,7 +80,6 @@ public:
friend QDataStream& operator>>(QDataStream& in, Node& node); friend QDataStream& operator>>(QDataStream& in, Node& node);
bool isIgnoreRadiusEnabled() const { return _ignoreRadiusEnabled; } bool isIgnoreRadiusEnabled() const { return _ignoreRadiusEnabled; }
float getIgnoreRadius() { return _ignoreRadiusEnabled ? _ignoreRadius.load() : std::numeric_limits<float>::max(); }
private: private:
// privatize copy and assignment operator to disallow Node copying // privatize copy and assignment operator to disallow Node copying
@ -100,7 +99,6 @@ private:
tbb::concurrent_unordered_set<QUuid, UUIDHasher> _ignoredNodeIDSet; tbb::concurrent_unordered_set<QUuid, UUIDHasher> _ignoredNodeIDSet;
std::atomic_bool _ignoreRadiusEnabled; std::atomic_bool _ignoreRadiusEnabled;
std::atomic<float> _ignoreRadius { 0.0f };
}; };
Q_DECLARE_METATYPE(Node*) Q_DECLARE_METATYPE(Node*)

View file

@ -750,10 +750,9 @@ bool NodeList::sockAddrBelongsToDomainOrNode(const HifiSockAddr& sockAddr) {
return _domainHandler.getSockAddr() == sockAddr || LimitedNodeList::sockAddrBelongsToNode(sockAddr); return _domainHandler.getSockAddr() == sockAddr || LimitedNodeList::sockAddrBelongsToNode(sockAddr);
} }
void NodeList::ignoreNodesInRadius(float radiusToIgnore, bool enabled) { void NodeList::ignoreNodesInRadius(bool enabled) {
bool isEnabledChange = _ignoreRadiusEnabled.get() != enabled; bool isEnabledChange = _ignoreRadiusEnabled.get() != enabled;
_ignoreRadiusEnabled.set(enabled); _ignoreRadiusEnabled.set(enabled);
_ignoreRadius.set(radiusToIgnore);
eachMatchingNode([](const SharedNodePointer& node)->bool { eachMatchingNode([](const SharedNodePointer& node)->bool {
return (node->getType() == NodeType::AudioMixer || node->getType() == NodeType::AvatarMixer); return (node->getType() == NodeType::AudioMixer || node->getType() == NodeType::AvatarMixer);
@ -768,7 +767,6 @@ void NodeList::ignoreNodesInRadius(float radiusToIgnore, bool enabled) {
void NodeList::sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode) { void NodeList::sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode) {
auto ignorePacket = NLPacket::create(PacketType::RadiusIgnoreRequest, sizeof(bool) + sizeof(float), true); auto ignorePacket = NLPacket::create(PacketType::RadiusIgnoreRequest, sizeof(bool) + sizeof(float), true);
ignorePacket->writePrimitive(_ignoreRadiusEnabled.get()); ignorePacket->writePrimitive(_ignoreRadiusEnabled.get());
ignorePacket->writePrimitive(_ignoreRadius.get());
sendPacket(std::move(ignorePacket), *destinationNode); sendPacket(std::move(ignorePacket), *destinationNode);
} }

View file

@ -71,12 +71,11 @@ public:
void setIsShuttingDown(bool isShuttingDown) { _isShuttingDown = isShuttingDown; } void setIsShuttingDown(bool isShuttingDown) { _isShuttingDown = isShuttingDown; }
void ignoreNodesInRadius(float radiusToIgnore, bool enabled = true); void ignoreNodesInRadius(bool enabled = true);
float getIgnoreRadius() const { return _ignoreRadius.get(); }
bool getIgnoreRadiusEnabled() const { return _ignoreRadiusEnabled.get(); } bool getIgnoreRadiusEnabled() const { return _ignoreRadiusEnabled.get(); }
void toggleIgnoreRadius() { ignoreNodesInRadius(getIgnoreRadius(), !getIgnoreRadiusEnabled()); } void toggleIgnoreRadius() { ignoreNodesInRadius(!getIgnoreRadiusEnabled()); }
void enableIgnoreRadius() { ignoreNodesInRadius(getIgnoreRadius(), true); } void enableIgnoreRadius() { ignoreNodesInRadius(true); }
void disableIgnoreRadius() { ignoreNodesInRadius(getIgnoreRadius(), false); } void disableIgnoreRadius() { ignoreNodesInRadius(false); }
void ignoreNodeBySessionID(const QUuid& nodeID); void ignoreNodeBySessionID(const QUuid& nodeID);
bool isIgnoringNode(const QUuid& nodeID) const; bool isIgnoringNode(const QUuid& nodeID) const;
@ -156,7 +155,6 @@ private:
void sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode); void sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode);
Setting::Handle<bool> _ignoreRadiusEnabled { "IgnoreRadiusEnabled", true }; Setting::Handle<bool> _ignoreRadiusEnabled { "IgnoreRadiusEnabled", true };
Setting::Handle<float> _ignoreRadius { "IgnoreRadius", 1.0f };
#if (PR_BUILD || DEV_BUILD) #if (PR_BUILD || DEV_BUILD)
bool _shouldSendNewerVersion { false }; bool _shouldSendNewerVersion { false };

View file

@ -53,7 +53,7 @@ PacketVersion versionForPacketType(PacketType packetType) {
case PacketType::AvatarData: case PacketType::AvatarData:
case PacketType::BulkAvatarData: case PacketType::BulkAvatarData:
case PacketType::KillAvatar: case PacketType::KillAvatar:
return static_cast<PacketVersion>(AvatarMixerPacketVersion::HandControllerJoints); return static_cast<PacketVersion>(AvatarMixerPacketVersion::HasKillAvatarReason);
case PacketType::ICEServerHeartbeat: case PacketType::ICEServerHeartbeat:
return 18; // ICE Server Heartbeat signing return 18; // ICE Server Heartbeat signing
case PacketType::AssetGetInfo: case PacketType::AssetGetInfo:

View file

@ -202,7 +202,8 @@ enum class AvatarMixerPacketVersion : PacketVersion {
AvatarEntities, AvatarEntities,
AbsoluteSixByteRotations, AbsoluteSixByteRotations,
SensorToWorldMat, SensorToWorldMat,
HandControllerJoints HandControllerJoints,
HasKillAvatarReason
}; };
enum class DomainConnectRequestVersion : PacketVersion { enum class DomainConnectRequestVersion : PacketVersion {

View file

@ -52,14 +52,6 @@ void UsersScriptingInterface::disableIgnoreRadius() {
DependencyManager::get<NodeList>()->disableIgnoreRadius(); DependencyManager::get<NodeList>()->disableIgnoreRadius();
} }
void UsersScriptingInterface::setIgnoreRadius(float radius, bool enabled) {
DependencyManager::get<NodeList>()->ignoreNodesInRadius(radius, enabled);
}
float UsersScriptingInterface::getIgnoreRadius() {
return DependencyManager::get<NodeList>()->getIgnoreRadius();
}
bool UsersScriptingInterface::getIgnoreRadiusEnabled() { bool UsersScriptingInterface::getIgnoreRadiusEnabled() {
return DependencyManager::get<NodeList>()->getIgnoreRadiusEnabled(); return DependencyManager::get<NodeList>()->getIgnoreRadiusEnabled();
} }

View file

@ -76,21 +76,6 @@ public slots:
*/ */
void 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 /**jsdoc
* Returns `true` if the ignore in radius feature is enabled * Returns `true` if the ignore in radius feature is enabled
* @function Users.getIgnoreRadiusEnabled * @function Users.getIgnoreRadiusEnabled
@ -101,6 +86,12 @@ public slots:
signals: signals:
void canKickChanged(bool canKick); void canKickChanged(bool canKick);
void ignoreRadiusEnabledChanged(bool isEnabled); void ignoreRadiusEnabledChanged(bool isEnabled);
/**jsdoc
* Notifies scripts that another user has entered the ignore radius
* @function Users.enteredIgnoreRadius
*/
void enteredIgnoreRadius();
}; };

View file

@ -508,6 +508,11 @@ void AABox::embiggen(const glm::vec3& scale) {
_scale *= scale; _scale *= scale;
} }
void AABox::setScaleStayCentered(const glm::vec3& scale) {
_corner += -0.5f * scale;
_scale = scale;
}
void AABox::scale(float scale) { void AABox::scale(float scale) {
_corner *= scale; _corner *= scale;
_scale *= scale; _scale *= scale;

View file

@ -96,6 +96,9 @@ public:
void embiggen(float scale); void embiggen(float scale);
void embiggen(const glm::vec3& scale); void embiggen(const glm::vec3& scale);
// Set a new scale for the Box, but keep it centered at its current location
void setScaleStayCentered(const glm::vec3& scale);
// Transform the extents with transform // Transform the extents with transform
void transform(const Transform& transform); void transform(const Transform& transform);

Binary file not shown.

Binary file not shown.

View file

@ -13,42 +13,181 @@
/* global Toolbars, Script, Users, Overlays, AvatarList, Controller, Camera, getControllerWorldLocation */ /* global Toolbars, Script, Users, Overlays, AvatarList, Controller, Camera, getControllerWorldLocation */
(function() { // BEGIN LOCAL_SCOPE (function () { // BEGIN LOCAL_SCOPE
// grab the toolbar // grab the toolbar
var toolbar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); var toolbar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system");
// Used for animating and disappearing the bubble
var bubbleOverlayTimestamp;
// Used for flashing the HUD button upon activation
var bubbleButtonFlashState = false;
// Used for flashing the HUD button upon activation
var bubbleButtonTimestamp;
// The bubble model itself
var bubbleOverlay = Overlays.addOverlay("model", {
url: Script.resolvePath("assets/models/bubble-v12.fbx"), // If you'd like to change the model, modify this line (and the dimensions below)
dimensions: { x: 1.0, y: 0.75, z: 1.0 },
position: { x: MyAvatar.position.x, y: -MyAvatar.scale * 2 + MyAvatar.position.y + MyAvatar.scale * 0.28, z: MyAvatar.position.z },
rotation: Quat.fromPitchYawRollDegrees(MyAvatar.bodyPitch, 0, MyAvatar.bodyRoll),
scale: { x: 2, y: MyAvatar.scale * 0.5 + 0.5, z: 2 },
visible: false,
ignoreRayIntersection: true
});
// The bubble activation sound
var bubbleActivateSound = SoundCache.getSound(Script.resolvePath("assets/sounds/bubble.wav"));
// Is the update() function connected?
var updateConnected = false;
var ASSETS_PATH = Script.resolvePath("assets"); const BUBBLE_VISIBLE_DURATION_MS = 3000;
var TOOLS_PATH = Script.resolvePath("assets/images/tools/"); const BUBBLE_RAISE_ANIMATION_DURATION_MS = 750;
const BUBBLE_HUD_ICON_FLASH_INTERVAL_MS = 500;
function buttonImageURL() { var ASSETS_PATH = Script.resolvePath("assets");
return TOOLS_PATH + 'bubble.svg'; var TOOLS_PATH = Script.resolvePath("assets/images/tools/");
}
function onBubbleToggled() { function buttonImageURL() {
var bubbleActive = Users.getIgnoreRadiusEnabled(); return TOOLS_PATH + 'bubble.svg';
button.writeProperty('buttonState', bubbleActive ? 0 : 1); }
button.writeProperty('defaultState', bubbleActive ? 0 : 1);
button.writeProperty('hoverState', bubbleActive ? 2 : 3);
}
// setup the mod button and add it to the toolbar // Hides the bubble model overlay and resets the button flash state
var button = toolbar.addButton({ function hideOverlays() {
objectName: 'bubble', Overlays.editOverlay(bubbleOverlay, {
imageURL: buttonImageURL(), visible: false
visible: true, });
alpha: 0.9 bubbleButtonFlashState = false;
}); }
onBubbleToggled();
button.clicked.connect(Users.toggleIgnoreRadius); // Make the bubble overlay visible, set its position, and play the sound
Users.ignoreRadiusEnabledChanged.connect(onBubbleToggled); function createOverlays() {
Audio.playSound(bubbleActivateSound, {
position: { x: MyAvatar.position.x, y: MyAvatar.position.y, z: MyAvatar.position.z },
localOnly: true,
volume: 0.4
});
hideOverlays();
if (updateConnected === true) {
updateConnected = false;
Script.update.disconnect(update);
}
// cleanup the toolbar button and overlays when script is stopped Overlays.editOverlay(bubbleOverlay, {
Script.scriptEnding.connect(function() { position: { x: MyAvatar.position.x, y: -MyAvatar.scale * 2 + MyAvatar.position.y + MyAvatar.scale * 0.28, z: MyAvatar.position.z },
toolbar.removeButton('bubble'); rotation: Quat.fromPitchYawRollDegrees(MyAvatar.bodyPitch, 0, MyAvatar.bodyRoll),
button.clicked.disconnect(Users.toggleIgnoreRadius); scale: { x: 2, y: MyAvatar.scale * 0.5 + 0.5, z: 2 },
Users.ignoreRadiusEnabledChanged.disconnect(onBubbleToggled); visible: true
}); });
bubbleOverlayTimestamp = Date.now();
bubbleButtonTimestamp = bubbleOverlayTimestamp;
Script.update.connect(update);
updateConnected = true;
}
// Called from the C++ scripting interface to show the bubble overlay
function enteredIgnoreRadius() {
createOverlays();
}
// Used to set the state of the bubble HUD button
function writeButtonProperties(parameter) {
button.writeProperty('buttonState', parameter ? 0 : 1);
button.writeProperty('defaultState', parameter ? 0 : 1);
button.writeProperty('hoverState', parameter ? 2 : 3);
}
// The bubble script's update function
update = function () {
var timestamp = Date.now();
var delay = (timestamp - bubbleOverlayTimestamp);
var overlayAlpha = 1.0 - (delay / BUBBLE_VISIBLE_DURATION_MS);
if (overlayAlpha > 0) {
// Flash button
if ((timestamp - bubbleButtonTimestamp) >= BUBBLE_VISIBLE_DURATION_MS) {
writeButtonProperties(bubbleButtonFlashState);
bubbleButtonTimestamp = timestamp;
bubbleButtonFlashState = !bubbleButtonFlashState;
}
if (delay < BUBBLE_RAISE_ANIMATION_DURATION_MS) {
Overlays.editOverlay(bubbleOverlay, {
// Quickly raise the bubble from the ground up
position: {
x: MyAvatar.position.x,
y: (-((BUBBLE_RAISE_ANIMATION_DURATION_MS - delay) / BUBBLE_RAISE_ANIMATION_DURATION_MS)) * MyAvatar.scale * 2 + MyAvatar.position.y + MyAvatar.scale * 0.28,
z: MyAvatar.position.z
},
rotation: Quat.fromPitchYawRollDegrees(MyAvatar.bodyPitch, 0, MyAvatar.bodyRoll),
scale: {
x: 2,
y: ((1 - ((BUBBLE_RAISE_ANIMATION_DURATION_MS - delay) / BUBBLE_RAISE_ANIMATION_DURATION_MS)) * MyAvatar.scale * 0.5 + 0.5),
z: 2
}
});
} else {
// Keep the bubble in place for a couple seconds
Overlays.editOverlay(bubbleOverlay, {
position: {
x: MyAvatar.position.x,
y: MyAvatar.position.y + MyAvatar.scale * 0.28,
z: MyAvatar.position.z
},
rotation: Quat.fromPitchYawRollDegrees(MyAvatar.bodyPitch, 0, MyAvatar.bodyRoll),
scale: {
x: 2,
y: MyAvatar.scale * 0.5 + 0.5,
z: 2
}
});
}
} else {
hideOverlays();
if (updateConnected === true) {
Script.update.disconnect(update);
updateConnected = false;
}
var bubbleActive = Users.getIgnoreRadiusEnabled();
writeButtonProperties(bubbleActive);
}
};
// When the space bubble is toggled...
function onBubbleToggled() {
var bubbleActive = Users.getIgnoreRadiusEnabled();
writeButtonProperties(bubbleActive);
if (bubbleActive) {
createOverlays();
} else {
hideOverlays();
if (updateConnected === true) {
Script.update.disconnect(update);
updateConnected = false;
}
}
}
// Setup the bubble button and add it to the toolbar
var button = toolbar.addButton({
objectName: 'bubble',
imageURL: buttonImageURL(),
visible: true,
alpha: 0.9
});
onBubbleToggled();
button.clicked.connect(Users.toggleIgnoreRadius);
Users.ignoreRadiusEnabledChanged.connect(onBubbleToggled);
Users.enteredIgnoreRadius.connect(enteredIgnoreRadius);
// Cleanup the toolbar button and overlays when script is stopped
Script.scriptEnding.connect(function () {
toolbar.removeButton('bubble');
button.clicked.disconnect(Users.toggleIgnoreRadius);
Users.ignoreRadiusEnabledChanged.disconnect(onBubbleToggled);
Users.enteredIgnoreRadius.disconnect(enteredIgnoreRadius);
Overlays.deleteOverlay(bubbleOverlay);
bubbleButtonFlashState = false;
if (updateConnected === true) {
Script.update.disconnect(update);
}
});
}()); // END LOCAL_SCOPE }()); // END LOCAL_SCOPE