mirror of
https://github.com/AleziaKurdis/overte.git
synced 2025-04-07 04:53:28 +02:00
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:
parent
84bf93b0fc
commit
7030c7b0a6
35 changed files with 361 additions and 119 deletions
|
@ -351,7 +351,9 @@ void Agent::executeScript() {
|
|||
Transform audioTransform;
|
||||
audioTransform.setTranslation(scriptedAvatar->getPosition());
|
||||
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>();
|
||||
|
@ -424,8 +426,9 @@ void Agent::setIsListeningToAudioStream(bool isListeningToAudioStream) {
|
|||
},
|
||||
[&](const SharedNodePointer& node) {
|
||||
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->writePrimitive(KillAvatarReason::NoReason);
|
||||
nodeList->sendPacket(std::move(packet), *node);
|
||||
});
|
||||
|
||||
|
@ -475,8 +478,9 @@ void Agent::setIsAvatar(bool isAvatar) {
|
|||
},
|
||||
[&](const SharedNodePointer& node) {
|
||||
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->writePrimitive(KillAvatarReason::NoReason);
|
||||
nodeList->sendPacket(std::move(packet), *node);
|
||||
});
|
||||
}
|
||||
|
@ -580,6 +584,8 @@ void Agent::processAgentAvatarAudio() {
|
|||
audioPacket->writePrimitive(scriptedAvatar->getPosition());
|
||||
glm::quat headOrientation = scriptedAvatar->getHeadOrientation();
|
||||
audioPacket->writePrimitive(headOrientation);
|
||||
audioPacket->writePrimitive(scriptedAvatar->getPosition());
|
||||
audioPacket->writePrimitive(glm::vec3(0));
|
||||
} else if (nextSoundOutput) {
|
||||
|
||||
// write the codec
|
||||
|
@ -592,6 +598,8 @@ void Agent::processAgentAvatarAudio() {
|
|||
audioPacket->writePrimitive(scriptedAvatar->getPosition());
|
||||
glm::quat headOrientation = scriptedAvatar->getHeadOrientation();
|
||||
audioPacket->writePrimitive(headOrientation);
|
||||
audioPacket->writePrimitive(scriptedAvatar->getPosition());
|
||||
audioPacket->writePrimitive(glm::vec3(0));
|
||||
|
||||
QByteArray encodedBuffer;
|
||||
if (_flushEncoder) {
|
||||
|
|
|
@ -90,6 +90,8 @@ public:
|
|||
bool shouldMuteClient() { return _shouldMuteClient; }
|
||||
void setShouldMuteClient(bool shouldMuteClient) { _shouldMuteClient = shouldMuteClient; }
|
||||
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:
|
||||
void injectorStreamFinished(const QUuid& streamIdentifier);
|
||||
|
|
|
@ -211,20 +211,46 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& node) {
|
|||
if (otherData
|
||||
&& !node->isIgnoringNodeWithID(otherNode->getUUID()) && !otherNode->isIgnoringNodeWithID(node->getUUID())) {
|
||||
|
||||
// check if distance is inside ignore radius
|
||||
if (node->isIgnoreRadiusEnabled() || otherNode->isIgnoreRadiusEnabled()) {
|
||||
float ignoreRadius = glm::min(node->getIgnoreRadius(), otherNode->getIgnoreRadius());
|
||||
if (glm::distance(nodeData->getPosition(), otherData->getPosition()) < ignoreRadius) {
|
||||
// skip, distance is inside ignore radius
|
||||
return;
|
||||
// check to see if we're ignoring in radius
|
||||
bool insideIgnoreRadius = false;
|
||||
// If the otherNode equals the node, we're doing a comparison on ourselves
|
||||
if (*otherNode == *node) {
|
||||
// We'll always be inside the radius in that case.
|
||||
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();
|
||||
for (auto& streamPair : streamsCopy) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
#include <QtCore/QTimer>
|
||||
#include <QtCore/QThread>
|
||||
|
||||
#include <AABox.h>
|
||||
#include <LogHandler.h>
|
||||
#include <NodeList.h>
|
||||
#include <udt/PacketHeaders.h>
|
||||
|
@ -240,18 +241,39 @@ void AvatarMixer::broadcastAvatarData() {
|
|||
} else {
|
||||
AvatarMixerClientData* otherData = reinterpret_cast<AvatarMixerClientData*>(otherNode->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()) {
|
||||
float ignoreRadius = glm::min(node->getIgnoreRadius(), otherNode->getIgnoreRadius());
|
||||
if (glm::distance(nodeData->getPosition(), otherData->getPosition()) < ignoreRadius) {
|
||||
// Define the minimum bubble size
|
||||
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);
|
||||
otherData->ignoreOther(otherNode, node);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// not close enough to ignore
|
||||
// Not close enough to ignore
|
||||
nodeData->removeFromRadiusIgnoringSet(otherNode->getUUID());
|
||||
otherData->removeFromRadiusIgnoringSet(node->getUUID());
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
@ -395,8 +417,9 @@ void AvatarMixer::nodeKilled(SharedNodePointer killedNode) {
|
|||
|
||||
// this was an avatar we were sending to other people
|
||||
// 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->writePrimitive(KillAvatarReason::AvatarDisconnected);
|
||||
|
||||
nodeList->broadcastToNodes(std::move(killPacket), NodeSet() << NodeType::Agent);
|
||||
|
||||
|
|
|
@ -45,8 +45,13 @@ 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);
|
||||
auto killPacket = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID + sizeof(KillAvatarReason));
|
||||
killPacket->write(other->getUUID().toRfc4122());
|
||||
if (self->isIgnoreRadiusEnabled()) {
|
||||
killPacket->writePrimitive(KillAvatarReason::TheirAvatarEnteredYourBubble);
|
||||
} else {
|
||||
killPacket->writePrimitive(KillAvatarReason::YourAvatarEnteredTheirBubble);
|
||||
}
|
||||
DependencyManager::get<NodeList>()->sendUnreliablePacket(*killPacket, *self);
|
||||
_hasReceivedFirstPacketsFrom.erase(other->getUUID());
|
||||
}
|
||||
|
|
|
@ -81,6 +81,7 @@ public:
|
|||
void loadJSONStats(QJsonObject& jsonObject) const;
|
||||
|
||||
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(); }
|
||||
void addToRadiusIgnoringSet(const QUuid& other) { _radiusIgnoredOthers.insert(other); }
|
||||
void removeFromRadiusIgnoringSet(const QUuid& other) { _radiusIgnoredOthers.erase(other); }
|
||||
|
|
|
@ -76,7 +76,7 @@ OriginalDesktop.Desktop {
|
|||
WebEngine.settings.localContentCanAccessRemoteUrls = true;
|
||||
|
||||
[ // 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",
|
||||
"com.highfidelity.interface.system.editButton", "marketplace", "snapshot", "goto"
|
||||
].forEach(function (name) {
|
||||
|
|
|
@ -2527,6 +2527,12 @@ void Application::keyPressEvent(QKeyEvent* event) {
|
|||
Menu::getInstance()->triggerOption(MenuOption::DefaultSkybox);
|
||||
break;
|
||||
|
||||
case Qt::Key_N:
|
||||
if (!isOption && !isShifted && isMeta) {
|
||||
DependencyManager::get<NodeList>()->toggleIgnoreRadius();
|
||||
}
|
||||
break;
|
||||
|
||||
case Qt::Key_S:
|
||||
if (isShifted && isMeta && !isOption) {
|
||||
Menu::getInstance()->triggerOption(MenuOption::SuppressShortTimings);
|
||||
|
|
|
@ -80,7 +80,7 @@ AvatarManager::AvatarManager(QObject* parent) :
|
|||
|
||||
// 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
|
||||
connect(nodeList.data(), &NodeList::ignoredNode, this, &AvatarManager::removeAvatar);
|
||||
connect(nodeList.data(), "ignoredNode", this, "removeAvatar");
|
||||
}
|
||||
|
||||
AvatarManager::~AvatarManager() {
|
||||
|
@ -223,16 +223,16 @@ AvatarSharedPointer AvatarManager::addAvatar(const QUuid& sessionUUID, const QWe
|
|||
}
|
||||
|
||||
// virtual
|
||||
void AvatarManager::removeAvatar(const QUuid& sessionUUID) {
|
||||
void AvatarManager::removeAvatar(const QUuid& sessionUUID, KillAvatarReason removalReason) {
|
||||
QWriteLocker locker(&_hashLock);
|
||||
|
||||
auto removedAvatar = _avatarHash.take(sessionUUID);
|
||||
if (removedAvatar) {
|
||||
handleRemovedAvatar(removedAvatar);
|
||||
handleRemovedAvatar(removedAvatar, removalReason);
|
||||
}
|
||||
}
|
||||
|
||||
void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar) {
|
||||
void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason) {
|
||||
AvatarHashMap::handleRemovedAvatar(removedAvatar);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble) {
|
||||
emit DependencyManager::get<UsersScriptingInterface>()->enteredIgnoreRadius();
|
||||
}
|
||||
_avatarFades.push_back(removedAvatar);
|
||||
}
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@ public slots:
|
|||
void updateAvatarRenderStatus(bool shouldRenderAvatars);
|
||||
|
||||
private slots:
|
||||
virtual void removeAvatar(const QUuid& sessionUUID) override;
|
||||
virtual void removeAvatar(const QUuid& sessionUUID, KillAvatarReason removalReason = KillAvatarReason::NoReason) override;
|
||||
|
||||
private:
|
||||
explicit AvatarManager(QObject* parent = 0);
|
||||
|
@ -91,7 +91,7 @@ private:
|
|||
virtual AvatarSharedPointer newSharedAvatar() 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;
|
||||
std::shared_ptr<MyAvatar> _myAvatar;
|
||||
|
|
|
@ -230,6 +230,10 @@ void MyAvatar::simulateAttachments(float deltaTime) {
|
|||
QByteArray MyAvatar::toByteArray(bool cullSmallChanges, bool sendAll) {
|
||||
CameraMode mode = qApp->getCamera()->getMode();
|
||||
_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) {
|
||||
// fake the avatar position that is sent up to the AvatarMixer
|
||||
glm::vec3 oldPosition = getPosition();
|
||||
|
@ -360,10 +364,18 @@ void MyAvatar::update(float deltaTime) {
|
|||
updateFromTrackers(deltaTime);
|
||||
|
||||
// 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>();
|
||||
head->setAudioLoudness(audio->getLastInputLoudness());
|
||||
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) {
|
||||
sendIdentityPacket();
|
||||
}
|
||||
|
|
|
@ -68,18 +68,6 @@ 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
|
||||
{
|
||||
|
|
|
@ -1063,7 +1063,9 @@ void AudioClient::handleAudioInput() {
|
|||
encodedBuffer = decodedBuffer;
|
||||
}
|
||||
|
||||
emitAudioPacket(encodedBuffer.constData(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, audioTransform, packetType, _selectedCodecName);
|
||||
emitAudioPacket(encodedBuffer.constData(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber,
|
||||
audioTransform, avatarBoundingBoxCorner, avatarBoundingBoxScale,
|
||||
packetType, _selectedCodecName);
|
||||
_stats.sentPacket();
|
||||
|
||||
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?
|
||||
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) {
|
||||
|
@ -1619,3 +1623,8 @@ void AudioClient::saveSettings() {
|
|||
dynamicJitterBufferEnabled.set(_receivedAudioStream.dynamicJitterBufferEnabled());
|
||||
staticJitterBufferFrames.set(_receivedAudioStream.getStaticJitterBufferFrames());
|
||||
}
|
||||
|
||||
void AudioClient::setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 scale) {
|
||||
avatarBoundingBoxCorner = corner;
|
||||
avatarBoundingBoxScale = scale;
|
||||
}
|
||||
|
|
|
@ -127,6 +127,8 @@ public:
|
|||
void setPositionGetter(AudioPositionGetter positionGetter) { _positionGetter = positionGetter; }
|
||||
void setOrientationGetter(AudioOrientationGetter orientationGetter) { _orientationGetter = orientationGetter; }
|
||||
|
||||
Q_INVOKABLE void setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 scale);
|
||||
|
||||
QVector<AudioInjector*>& getActiveLocalAudioInjectors() { return _activeLocalAudioInjectors; }
|
||||
|
||||
void checkDevices();
|
||||
|
@ -324,6 +326,9 @@ private:
|
|||
AudioPositionGetter _positionGetter;
|
||||
AudioOrientationGetter _orientationGetter;
|
||||
|
||||
glm::vec3 avatarBoundingBoxCorner;
|
||||
glm::vec3 avatarBoundingBoxScale;
|
||||
|
||||
QVector<QString> _inputDevices;
|
||||
QVector<QString> _outputDevices;
|
||||
|
||||
|
|
|
@ -19,8 +19,9 @@
|
|||
|
||||
#include "AudioConstants.h"
|
||||
|
||||
void AbstractAudioInterface::emitAudioPacket(const void* audioData, size_t bytes, quint16& sequenceNumber,
|
||||
const Transform& transform, PacketType packetType, QString codecName) {
|
||||
void AbstractAudioInterface::emitAudioPacket(const void* audioData, size_t bytes, quint16& sequenceNumber,
|
||||
const Transform& transform, glm::vec3 avatarBoundingBoxCorner, glm::vec3 avatarBoundingBoxScale,
|
||||
PacketType packetType, QString codecName) {
|
||||
static std::mutex _mutex;
|
||||
using Locker = std::unique_lock<std::mutex>;
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
@ -55,6 +56,10 @@ void AbstractAudioInterface::emitAudioPacket(const void* audioData, size_t bytes
|
|||
// pack the orientation
|
||||
audioPacket->writePrimitive(transform.getRotation());
|
||||
|
||||
audioPacket->writePrimitive(avatarBoundingBoxCorner);
|
||||
audioPacket->writePrimitive(avatarBoundingBoxScale);
|
||||
|
||||
|
||||
if (audioPacket->getType() != PacketType::SilentAudioFrame) {
|
||||
// audio samples have already been packed (written to networkAudioSamples)
|
||||
int leadingBytes = audioPacket->getPayloadSize();
|
||||
|
|
|
@ -28,7 +28,8 @@ class AbstractAudioInterface : public QObject {
|
|||
public:
|
||||
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(""));
|
||||
|
||||
public slots:
|
||||
|
|
|
@ -77,6 +77,8 @@ int PositionalAudioStream::parsePositionalData(const QByteArray& positionalByteA
|
|||
|
||||
packetStream.readRawData(reinterpret_cast<char*>(&_position), sizeof(_position));
|
||||
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 (glm::isnan(_orientation.x)) {
|
||||
|
|
|
@ -46,6 +46,8 @@ public:
|
|||
PositionalAudioStream::Type getType() const { return _type; }
|
||||
const glm::vec3& getPosition() const { return _position; }
|
||||
const glm::quat& getOrientation() const { return _orientation; }
|
||||
const glm::vec3& getAvatarBoundingBoxCorner() const { return _avatarBoundingBoxCorner; }
|
||||
const glm::vec3& getAvatarBoundingBoxScale() const { return _avatarBoundingBoxScale; }
|
||||
|
||||
|
||||
protected:
|
||||
|
@ -60,6 +62,9 @@ protected:
|
|||
glm::vec3 _position;
|
||||
glm::quat _orientation;
|
||||
|
||||
glm::vec3 _avatarBoundingBoxCorner;
|
||||
glm::vec3 _avatarBoundingBoxScale;
|
||||
|
||||
bool _shouldLoopbackForNode;
|
||||
bool _isStereo;
|
||||
// Ignore penumbra filter
|
||||
|
|
|
@ -55,6 +55,7 @@ namespace AvatarDataPacket {
|
|||
PACKED_BEGIN struct Header {
|
||||
float position[3]; // skeletal model'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 scale; // (compressed) 'ratio' encoding uses sign bit as flag.
|
||||
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
|
||||
uint8_t flags;
|
||||
} 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
|
||||
PACKED_BEGIN struct ParentInfo {
|
||||
|
@ -205,6 +206,9 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) {
|
|||
header->globalPosition[0] = _globalPosition.x;
|
||||
header->globalPosition[1] = _globalPosition.y;
|
||||
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()));
|
||||
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]);
|
||||
_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 (shouldLogError(now)) {
|
||||
qCWarning(avatars) << "Discard AvatarData packet: position NaN, uuid " << getSessionUUID();
|
||||
|
|
|
@ -133,6 +133,15 @@ enum KeyState {
|
|||
DELETE_KEY_DOWN
|
||||
};
|
||||
|
||||
enum KillAvatarReason : uint8_t {
|
||||
NoReason = 0,
|
||||
AvatarDisconnected,
|
||||
AvatarIgnored,
|
||||
TheirAvatarEnteredYourBubble,
|
||||
YourAvatarEnteredTheirBubble
|
||||
};
|
||||
Q_DECLARE_METATYPE(KillAvatarReason);
|
||||
|
||||
class QDataStream;
|
||||
|
||||
class AttachmentData;
|
||||
|
@ -353,6 +362,7 @@ public:
|
|||
void fromJson(const QJsonObject& json);
|
||||
|
||||
glm::vec3 getClientGlobalPosition() { return _globalPosition; }
|
||||
glm::vec3 getGlobalBoundingBoxCorner() { return _globalBoundingBoxCorner; }
|
||||
|
||||
Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const;
|
||||
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
|
||||
// updates about one avatar to another.
|
||||
glm::vec3 _globalPosition;
|
||||
glm::vec3 _globalBoundingBoxCorner;
|
||||
|
||||
mutable ReadWriteLockable _avatarEntitiesLock;
|
||||
AvatarEntityIDs _avatarEntityDetached; // recently detached from this avatar
|
||||
|
|
|
@ -141,20 +141,23 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer<ReceivedMessage>
|
|||
void AvatarHashMap::processKillAvatar(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
|
||||
// read the node id
|
||||
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);
|
||||
|
||||
auto removedAvatar = _avatarHash.take(sessionUUID);
|
||||
|
||||
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())
|
||||
<< "from AvatarHashMap";
|
||||
emit avatarRemovedEvent(removedAvatar->getSessionUUID());
|
||||
|
|
|
@ -65,9 +65,9 @@ protected:
|
|||
virtual AvatarSharedPointer addAvatar(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 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;
|
||||
// "Case-based safety": Most access to the _avatarHash is on the same thread. Write access is protected by a write-lock.
|
||||
|
|
|
@ -104,11 +104,8 @@ void Node::addIgnoredNode(const QUuid& otherNodeID) {
|
|||
|
||||
void Node::parseIgnoreRadiusRequestMessage(QSharedPointer<ReceivedMessage> message) {
|
||||
bool enabled;
|
||||
float radius;
|
||||
message->readPrimitive(&enabled);
|
||||
message->readPrimitive(&radius);
|
||||
_ignoreRadiusEnabled = enabled;
|
||||
_ignoreRadius = radius;
|
||||
}
|
||||
|
||||
QDataStream& operator<<(QDataStream& out, const Node& node) {
|
||||
|
|
|
@ -80,7 +80,6 @@ public:
|
|||
friend QDataStream& operator>>(QDataStream& in, Node& node);
|
||||
|
||||
bool isIgnoreRadiusEnabled() const { return _ignoreRadiusEnabled; }
|
||||
float getIgnoreRadius() { return _ignoreRadiusEnabled ? _ignoreRadius.load() : std::numeric_limits<float>::max(); }
|
||||
|
||||
private:
|
||||
// privatize copy and assignment operator to disallow Node copying
|
||||
|
@ -100,7 +99,6 @@ private:
|
|||
tbb::concurrent_unordered_set<QUuid, UUIDHasher> _ignoredNodeIDSet;
|
||||
|
||||
std::atomic_bool _ignoreRadiusEnabled;
|
||||
std::atomic<float> _ignoreRadius { 0.0f };
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(Node*)
|
||||
|
|
|
@ -750,10 +750,9 @@ bool NodeList::sockAddrBelongsToDomainOrNode(const HifiSockAddr& 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;
|
||||
_ignoreRadiusEnabled.set(enabled);
|
||||
_ignoreRadius.set(radiusToIgnore);
|
||||
|
||||
eachMatchingNode([](const SharedNodePointer& node)->bool {
|
||||
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) {
|
||||
auto ignorePacket = NLPacket::create(PacketType::RadiusIgnoreRequest, sizeof(bool) + sizeof(float), true);
|
||||
ignorePacket->writePrimitive(_ignoreRadiusEnabled.get());
|
||||
ignorePacket->writePrimitive(_ignoreRadius.get());
|
||||
sendPacket(std::move(ignorePacket), *destinationNode);
|
||||
}
|
||||
|
||||
|
|
|
@ -71,12 +71,11 @@ public:
|
|||
|
||||
void setIsShuttingDown(bool isShuttingDown) { _isShuttingDown = isShuttingDown; }
|
||||
|
||||
void ignoreNodesInRadius(float radiusToIgnore, bool enabled = true);
|
||||
float getIgnoreRadius() const { return _ignoreRadius.get(); }
|
||||
void ignoreNodesInRadius(bool enabled = true);
|
||||
bool getIgnoreRadiusEnabled() const { return _ignoreRadiusEnabled.get(); }
|
||||
void toggleIgnoreRadius() { ignoreNodesInRadius(getIgnoreRadius(), !getIgnoreRadiusEnabled()); }
|
||||
void enableIgnoreRadius() { ignoreNodesInRadius(getIgnoreRadius(), true); }
|
||||
void disableIgnoreRadius() { ignoreNodesInRadius(getIgnoreRadius(), false); }
|
||||
void toggleIgnoreRadius() { ignoreNodesInRadius(!getIgnoreRadiusEnabled()); }
|
||||
void enableIgnoreRadius() { ignoreNodesInRadius(true); }
|
||||
void disableIgnoreRadius() { ignoreNodesInRadius(false); }
|
||||
void ignoreNodeBySessionID(const QUuid& nodeID);
|
||||
bool isIgnoringNode(const QUuid& nodeID) const;
|
||||
|
||||
|
@ -156,7 +155,6 @@ private:
|
|||
|
||||
void sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode);
|
||||
Setting::Handle<bool> _ignoreRadiusEnabled { "IgnoreRadiusEnabled", true };
|
||||
Setting::Handle<float> _ignoreRadius { "IgnoreRadius", 1.0f };
|
||||
|
||||
#if (PR_BUILD || DEV_BUILD)
|
||||
bool _shouldSendNewerVersion { false };
|
||||
|
|
|
@ -53,7 +53,7 @@ PacketVersion versionForPacketType(PacketType packetType) {
|
|||
case PacketType::AvatarData:
|
||||
case PacketType::BulkAvatarData:
|
||||
case PacketType::KillAvatar:
|
||||
return static_cast<PacketVersion>(AvatarMixerPacketVersion::HandControllerJoints);
|
||||
return static_cast<PacketVersion>(AvatarMixerPacketVersion::HasKillAvatarReason);
|
||||
case PacketType::ICEServerHeartbeat:
|
||||
return 18; // ICE Server Heartbeat signing
|
||||
case PacketType::AssetGetInfo:
|
||||
|
|
|
@ -202,7 +202,8 @@ enum class AvatarMixerPacketVersion : PacketVersion {
|
|||
AvatarEntities,
|
||||
AbsoluteSixByteRotations,
|
||||
SensorToWorldMat,
|
||||
HandControllerJoints
|
||||
HandControllerJoints,
|
||||
HasKillAvatarReason
|
||||
};
|
||||
|
||||
enum class DomainConnectRequestVersion : PacketVersion {
|
||||
|
|
|
@ -52,14 +52,6 @@ void UsersScriptingInterface::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() {
|
||||
return DependencyManager::get<NodeList>()->getIgnoreRadiusEnabled();
|
||||
}
|
||||
|
|
|
@ -76,21 +76,6 @@ public slots:
|
|||
*/
|
||||
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
|
||||
|
@ -101,6 +86,12 @@ public slots:
|
|||
signals:
|
||||
void canKickChanged(bool canKick);
|
||||
void ignoreRadiusEnabledChanged(bool isEnabled);
|
||||
|
||||
/**jsdoc
|
||||
* Notifies scripts that another user has entered the ignore radius
|
||||
* @function Users.enteredIgnoreRadius
|
||||
*/
|
||||
void enteredIgnoreRadius();
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -508,6 +508,11 @@ void AABox::embiggen(const glm::vec3& scale) {
|
|||
_scale *= scale;
|
||||
}
|
||||
|
||||
void AABox::setScaleStayCentered(const glm::vec3& scale) {
|
||||
_corner += -0.5f * scale;
|
||||
_scale = scale;
|
||||
}
|
||||
|
||||
void AABox::scale(float scale) {
|
||||
_corner *= scale;
|
||||
_scale *= scale;
|
||||
|
|
|
@ -96,6 +96,9 @@ public:
|
|||
void embiggen(float 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
|
||||
void transform(const Transform& transform);
|
||||
|
||||
|
|
BIN
scripts/system/assets/models/bubble-v12.fbx
Normal file
BIN
scripts/system/assets/models/bubble-v12.fbx
Normal file
Binary file not shown.
BIN
scripts/system/assets/sounds/bubble.wav
Normal file
BIN
scripts/system/assets/sounds/bubble.wav
Normal file
Binary file not shown.
|
@ -13,42 +13,181 @@
|
|||
/* global Toolbars, Script, Users, Overlays, AvatarList, Controller, Camera, getControllerWorldLocation */
|
||||
|
||||
|
||||
(function() { // BEGIN LOCAL_SCOPE
|
||||
(function () { // BEGIN LOCAL_SCOPE
|
||||
|
||||
// grab the toolbar
|
||||
var toolbar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system");
|
||||
// grab the toolbar
|
||||
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");
|
||||
var TOOLS_PATH = Script.resolvePath("assets/images/tools/");
|
||||
const BUBBLE_VISIBLE_DURATION_MS = 3000;
|
||||
const BUBBLE_RAISE_ANIMATION_DURATION_MS = 750;
|
||||
const BUBBLE_HUD_ICON_FLASH_INTERVAL_MS = 500;
|
||||
|
||||
function buttonImageURL() {
|
||||
return TOOLS_PATH + 'bubble.svg';
|
||||
}
|
||||
var ASSETS_PATH = Script.resolvePath("assets");
|
||||
var TOOLS_PATH = Script.resolvePath("assets/images/tools/");
|
||||
|
||||
function onBubbleToggled() {
|
||||
var bubbleActive = Users.getIgnoreRadiusEnabled();
|
||||
button.writeProperty('buttonState', bubbleActive ? 0 : 1);
|
||||
button.writeProperty('defaultState', bubbleActive ? 0 : 1);
|
||||
button.writeProperty('hoverState', bubbleActive ? 2 : 3);
|
||||
}
|
||||
function buttonImageURL() {
|
||||
return TOOLS_PATH + 'bubble.svg';
|
||||
}
|
||||
|
||||
// setup the mod button and add it to the toolbar
|
||||
var button = toolbar.addButton({
|
||||
objectName: 'bubble',
|
||||
imageURL: buttonImageURL(),
|
||||
visible: true,
|
||||
alpha: 0.9
|
||||
});
|
||||
onBubbleToggled();
|
||||
// Hides the bubble model overlay and resets the button flash state
|
||||
function hideOverlays() {
|
||||
Overlays.editOverlay(bubbleOverlay, {
|
||||
visible: false
|
||||
});
|
||||
bubbleButtonFlashState = false;
|
||||
}
|
||||
|
||||
button.clicked.connect(Users.toggleIgnoreRadius);
|
||||
Users.ignoreRadiusEnabledChanged.connect(onBubbleToggled);
|
||||
// Make the bubble overlay visible, set its position, and play the sound
|
||||
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
|
||||
Script.scriptEnding.connect(function() {
|
||||
toolbar.removeButton('bubble');
|
||||
button.clicked.disconnect(Users.toggleIgnoreRadius);
|
||||
Users.ignoreRadiusEnabledChanged.disconnect(onBubbleToggled);
|
||||
});
|
||||
Overlays.editOverlay(bubbleOverlay, {
|
||||
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: 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
|
||||
|
|
Loading…
Reference in a new issue