Merge pull request #10146 from AndrewMeadows/avoid-invisible-avatars-002

avoid invisible avatars redux
This commit is contained in:
Seth Alves 2017-04-11 07:07:03 -07:00 committed by GitHub
commit bf16309917
9 changed files with 173 additions and 118 deletions

View file

@ -1802,10 +1802,11 @@ Application::~Application() {
_physicsEngine->setCharacterController(nullptr);
// remove avatars from physics engine
DependencyManager::get<AvatarManager>()->clearAllAvatars();
DependencyManager::get<AvatarManager>()->clearOtherAvatars();
VectorOfMotionStates motionStates;
DependencyManager::get<AvatarManager>()->getObjectsToRemoveFromPhysics(motionStates);
_physicsEngine->removeObjects(motionStates);
DependencyManager::get<AvatarManager>()->deleteAllAvatars();
DependencyManager::destroy<AvatarManager>();
DependencyManager::destroy<AnimationCache>();

View file

@ -193,6 +193,9 @@ void Avatar::animateScaleChanges(float deltaTime) {
}
setScale(glm::vec3(animatedScale)); // avatar scale is uniform
// flag the joints as having changed for force update to RenderItem
_hasNewJointData = true;
// TODO: rebuilding the shape constantly is somehwat expensive.
// We should only rebuild after significant change.
rebuildCollisionShape();
@ -200,8 +203,12 @@ void Avatar::animateScaleChanges(float deltaTime) {
}
void Avatar::setTargetScale(float targetScale) {
AvatarData::setTargetScale(targetScale);
_isAnimatingScale = true;
float newValue = glm::clamp(targetScale, MIN_AVATAR_SCALE, MAX_AVATAR_SCALE);
if (_targetScale != newValue) {
_targetScale = newValue;
_scaleChanged = usecTimestampNow();
_isAnimatingScale = true;
}
}
void Avatar::updateAvatarEntities() {
@ -476,7 +483,7 @@ static TextRenderer3D* textRenderer(TextRendererType type) {
return displayNameRenderer;
}
bool Avatar::addToScene(AvatarSharedPointer self, std::shared_ptr<render::Scene> scene, render::Transaction& transaction) {
void Avatar::addToScene(AvatarSharedPointer self, std::shared_ptr<render::Scene> scene, render::Transaction& transaction) {
auto avatarPayload = new render::Payload<AvatarData>(self);
auto avatarPayloadPointer = Avatar::PayloadPointer(avatarPayload);
_renderItemID = scene->allocateID();
@ -486,9 +493,6 @@ bool Avatar::addToScene(AvatarSharedPointer self, std::shared_ptr<render::Scene>
for (auto& attachmentModel : _attachmentModels) {
attachmentModel->addToScene(scene, transaction);
}
_inScene = true;
return true;
}
void Avatar::removeFromScene(AvatarSharedPointer self, std::shared_ptr<render::Scene> scene, render::Transaction& transaction) {
@ -498,7 +502,6 @@ void Avatar::removeFromScene(AvatarSharedPointer self, std::shared_ptr<render::S
for (auto& attachmentModel : _attachmentModels) {
attachmentModel->removeFromScene(scene, transaction);
}
_inScene = false;
}
void Avatar::updateRenderItem(render::Transaction& transaction) {
@ -1450,7 +1453,7 @@ void Avatar::addToScene(AvatarSharedPointer myHandle) {
}
}
void Avatar::ensureInScene(AvatarSharedPointer self) {
if (!_inScene) {
if (!render::Item::isValidID(_renderItemID)) {
addToScene(self);
}
}

View file

@ -81,7 +81,7 @@ public:
virtual void render(RenderArgs* renderArgs, const glm::vec3& cameraPosition);
bool addToScene(AvatarSharedPointer self, std::shared_ptr<render::Scene> scene,
void addToScene(AvatarSharedPointer self, std::shared_ptr<render::Scene> scene,
render::Transaction& transaction);
void removeFromScene(AvatarSharedPointer self, std::shared_ptr<render::Scene> scene,
@ -305,6 +305,7 @@ protected:
void addToScene(AvatarSharedPointer self);
void ensureInScene(AvatarSharedPointer self);
bool isInScene() const { return render::Item::isValidID(_renderItemID); }
// Some rate tracking support
RateCounter<> _simulationRate;
@ -330,7 +331,6 @@ private:
int _nameRectGeometryID { 0 };
bool _initialized;
bool _isLookAtTarget { false };
bool _inScene { false };
bool _isAnimatingScale { false };
float getBoundingRadius() const;

View file

@ -68,7 +68,7 @@ void AvatarManager::registerMetaTypes(QScriptEngine* engine) {
}
AvatarManager::AvatarManager(QObject* parent) :
_avatarFades(),
_avatarsToFade(),
_myAvatar(std::make_shared<MyAvatar>(std::make_shared<Rig>()))
{
// register a meta type for the weak pointer we'll use for the owning avatar mixer for each avatar
@ -100,15 +100,16 @@ void AvatarManager::init() {
_avatarHash.insert(MY_AVATAR_KEY, _myAvatar);
}
_shouldRender = DependencyManager::get<SceneScriptingInterface>()->shouldRenderAvatars();
connect(DependencyManager::get<SceneScriptingInterface>().data(), &SceneScriptingInterface::shouldRenderAvatarsChanged,
this, &AvatarManager::updateAvatarRenderStatus, Qt::QueuedConnection);
render::ScenePointer scene = qApp->getMain3DScene();
render::Transaction transaction;
if (DependencyManager::get<SceneScriptingInterface>()->shouldRenderAvatars()) {
if (_shouldRender) {
render::ScenePointer scene = qApp->getMain3DScene();
render::Transaction transaction;
_myAvatar->addToScene(_myAvatar, scene, transaction);
scene->enqueueTransaction(transaction);
}
scene->enqueueTransaction(transaction);
}
void AvatarManager::updateMyAvatar(float deltaTime) {
@ -151,7 +152,7 @@ float AvatarManager::getAvatarSimulationRate(const QUuid& sessionID, const QStri
void AvatarManager::updateOtherAvatars(float deltaTime) {
// lock the hash for read to check the size
QReadLocker lock(&_hashLock);
if (_avatarHash.size() < 2 && _avatarFades.isEmpty()) {
if (_avatarHash.size() < 2 && _avatarsToFade.isEmpty()) {
return;
}
lock.unlock();
@ -181,30 +182,24 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
// DO NOT update or fade out uninitialized Avatars
return true; // ignore it
}
if (avatar->shouldDie()) {
removeAvatar(avatar->getID());
return true; // ignore it
}
if (avatar->isDead()) {
return true; // ignore it
}
return false;
});
render::Transaction transaction;
uint64_t startTime = usecTimestampNow();
const uint64_t UPDATE_BUDGET = 2000; // usec
uint64_t updateExpiry = startTime + UPDATE_BUDGET;
int numAvatarsUpdated = 0;
int numAVatarsNotUpdated = 0;
render::Transaction transaction;
while (!sortedAvatars.empty()) {
const AvatarPriority& sortData = sortedAvatars.top();
const auto& avatar = std::static_pointer_cast<Avatar>(sortData.avatar);
// for ALL avatars...
avatar->ensureInScene(avatar);
if (_shouldRender) {
avatar->ensureInScene(avatar);
}
if (!avatar->getMotionState()) {
ShapeInfo shapeInfo;
avatar->computeShapeInfo(shapeInfo);
@ -218,6 +213,10 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
}
}
avatar->animateScaleChanges(deltaTime);
if (avatar->shouldDie()) {
avatar->die();
removeAvatar(avatar->getID());
}
const float OUT_OF_VIEW_THRESHOLD = 0.5f * AvatarData::OUT_OF_VIEW_PENALTY;
uint64_t now = usecTimestampNow();
@ -259,10 +258,24 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
sortedAvatars.pop();
}
if (_shouldRender) {
if (!_avatarsToFade.empty()) {
QReadLocker lock(&_hashLock);
QVector<AvatarSharedPointer>::iterator itr = _avatarsToFade.begin();
while (itr != _avatarsToFade.end() && usecTimestampNow() > updateExpiry) {
auto avatar = std::static_pointer_cast<Avatar>(*itr);
avatar->animateScaleChanges(deltaTime);
avatar->simulate(deltaTime, true);
avatar->updateRenderItem(transaction);
++itr;
}
}
qApp->getMain3DScene()->enqueueTransaction(transaction);
}
_avatarSimulationTime = (float)(usecTimestampNow() - startTime) / (float)USECS_PER_MSEC;
_numAvatarsUpdated = numAvatarsUpdated;
_numAvatarsNotUpdated = numAVatarsNotUpdated;
qApp->getMain3DScene()->enqueueTransaction(transaction);
simulateAvatarFades(deltaTime);
}
@ -277,54 +290,76 @@ void AvatarManager::postUpdate(float deltaTime) {
}
void AvatarManager::simulateAvatarFades(float deltaTime) {
QVector<AvatarSharedPointer>::iterator fadingIterator = _avatarFades.begin();
if (_avatarsToFade.empty()) {
return;
}
const float SHRINK_RATE = 0.15f;
const float MIN_FADE_SCALE = MIN_AVATAR_SCALE;
render::ScenePointer scene = qApp->getMain3DScene();
render::Transaction transaction;
while (fadingIterator != _avatarFades.end()) {
auto avatar = std::static_pointer_cast<Avatar>(*fadingIterator);
QReadLocker locker(&_hashLock);
QVector<AvatarSharedPointer>::iterator itr = _avatarsToFade.begin();
while (itr != _avatarsToFade.end()) {
auto avatar = std::static_pointer_cast<Avatar>(*itr);
avatar->setTargetScale(avatar->getUniformScale() * SHRINK_RATE);
avatar->animateScaleChanges(deltaTime);
if (avatar->getTargetScale() <= MIN_FADE_SCALE) {
avatar->removeFromScene(*fadingIterator, scene, transaction);
// only remove from _avatarFades if we're sure its motionState has been removed from PhysicsEngine
// fading to zero is such a rare event we push unique transaction for each one
if (avatar->isInScene()) {
render::ScenePointer scene = qApp->getMain3DScene();
render::Transaction transaction;
avatar->removeFromScene(*itr, scene, transaction);
if (scene) {
scene->enqueueTransaction(transaction);
}
}
// only remove from _avatarsToFade if we're sure its motionState has been removed from PhysicsEngine
if (_motionStatesToRemoveFromPhysics.empty()) {
fadingIterator = _avatarFades.erase(fadingIterator);
itr = _avatarsToFade.erase(itr);
} else {
++fadingIterator;
++itr;
}
} else {
const bool inView = true; // HACK
avatar->simulate(deltaTime, inView);
++fadingIterator;
++itr;
}
}
scene->enqueueTransaction(transaction);
}
AvatarSharedPointer AvatarManager::newSharedAvatar() {
return std::make_shared<Avatar>(std::make_shared<Rig>());
}
AvatarSharedPointer AvatarManager::addAvatar(const QUuid& sessionUUID, const QWeakPointer<Node>& mixerWeakPointer) {
auto newAvatar = AvatarHashMap::addAvatar(sessionUUID, mixerWeakPointer);
auto rawRenderableAvatar = std::static_pointer_cast<Avatar>(newAvatar);
rawRenderableAvatar->addToScene(rawRenderableAvatar);
return newAvatar;
}
// virtual
void AvatarManager::removeAvatar(const QUuid& sessionUUID, KillAvatarReason removalReason) {
QWriteLocker locker(&_hashLock);
auto removedAvatar = _avatarHash.take(sessionUUID);
if (removedAvatar) {
handleRemovedAvatar(removedAvatar, removalReason);
void AvatarManager::processAvatarDataPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
PerformanceTimer perfTimer("receiveAvatar");
// enumerate over all of the avatars in this packet
// only add them if mixerWeakPointer points to something (meaning that mixer is still around)
while (message->getBytesLeftToRead()) {
AvatarSharedPointer avatarData = parseAvatarData(message, sendingNode);
if (avatarData) {
auto avatar = std::static_pointer_cast<Avatar>(avatarData);
if (avatar->isInScene()) {
if (!_shouldRender) {
// rare transition so we process the transaction immediately
render::ScenePointer scene = qApp->getMain3DScene();
if (scene) {
render::Transaction transaction;
avatar->removeFromScene(avatar, scene, transaction);
scene->enqueueTransaction(transaction);
}
}
} else if (_shouldRender) {
// very rare transition so we process the transaction immediately
render::ScenePointer scene = qApp->getMain3DScene();
if (scene) {
render::Transaction transaction;
avatar->addToScene(avatar, scene, transaction);
scene->enqueueTransaction(transaction);
}
}
}
}
}
@ -353,35 +388,46 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar
DependencyManager::get<NodeList>()->removeFromIgnoreMuteSets(avatar->getSessionUUID());
DependencyManager::get<UsersScriptingInterface>()->avatarDisconnected(avatar->getSessionUUID());
}
_avatarFades.push_back(removedAvatar);
_avatarsToFade.push_back(removedAvatar);
}
void AvatarManager::clearOtherAvatars() {
// clear any avatars that came from an avatar-mixer
QWriteLocker locker(&_hashLock);
// Remove other avatars from the world but don't actually remove them from _avatarHash
// each will either be removed on timeout or will re-added to the world on receipt of update.
render::ScenePointer scene = qApp->getMain3DScene();
render::Transaction transaction;
QReadLocker locker(&_hashLock);
AvatarHash::iterator avatarIterator = _avatarHash.begin();
while (avatarIterator != _avatarHash.end()) {
auto avatar = std::static_pointer_cast<Avatar>(avatarIterator.value());
if (avatar == _myAvatar || !avatar->isInitialized()) {
// don't remove myAvatar or uninitialized avatars from the list
++avatarIterator;
} else {
auto removedAvatar = avatarIterator.value();
avatarIterator = _avatarHash.erase(avatarIterator);
handleRemovedAvatar(removedAvatar);
if (avatar != _myAvatar) {
if (avatar->isInScene()) {
avatar->removeFromScene(avatar, scene, transaction);
}
AvatarMotionState* motionState = avatar->getMotionState();
if (motionState) {
_motionStatesThatMightUpdate.remove(motionState);
_motionStatesToAddToPhysics.remove(motionState);
_motionStatesToRemoveFromPhysics.push_back(motionState);
}
}
++avatarIterator;
}
if (scene) {
scene->enqueueTransaction(transaction);
}
_myAvatar->clearLookAtTargetAvatar();
}
void AvatarManager::clearAllAvatars() {
clearOtherAvatars();
QWriteLocker locker(&_hashLock);
handleRemovedAvatar(_myAvatar);
void AvatarManager::deleteAllAvatars() {
QReadLocker locker(&_hashLock);
AvatarHash::iterator avatarIterator = _avatarHash.begin();
while (avatarIterator != _avatarHash.end()) {
auto avatar = std::static_pointer_cast<Avatar>(avatarIterator.value());
avatarIterator = _avatarHash.erase(avatarIterator);
avatar->die();
}
}
void AvatarManager::setLocalLights(const QVector<AvatarManager::LocalLight>& localLights) {
@ -475,26 +521,25 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents
}
void AvatarManager::updateAvatarRenderStatus(bool shouldRenderAvatars) {
if (DependencyManager::get<SceneScriptingInterface>()->shouldRenderAvatars()) {
_shouldRender = shouldRenderAvatars;
render::ScenePointer scene = qApp->getMain3DScene();
render::Transaction transaction;
if (_shouldRender) {
for (auto avatarData : _avatarHash) {
auto avatar = std::static_pointer_cast<Avatar>(avatarData);
render::ScenePointer scene = qApp->getMain3DScene();
render::Transaction transaction;
avatar->addToScene(avatar, scene, transaction);
scene->enqueueTransaction(transaction);
}
} else {
for (auto avatarData : _avatarHash) {
auto avatar = std::static_pointer_cast<Avatar>(avatarData);
render::ScenePointer scene = qApp->getMain3DScene();
render::Transaction transaction;
avatar->removeFromScene(avatar, scene, transaction);
scene->enqueueTransaction(transaction);
}
}
if (scene) {
scene->enqueueTransaction(transaction);
}
}
AvatarSharedPointer AvatarManager::getAvatarBySessionID(const QUuid& sessionID) const {
if (sessionID == AVATAR_SELF_ID || sessionID == _myAvatar->getSessionUUID()) {
return _myAvatar;

View file

@ -53,7 +53,7 @@ public:
void postUpdate(float deltaTime);
void clearOtherAvatars();
void clearAllAvatars();
void deleteAllAvatars();
bool shouldShowReceiveStats() const { return _shouldShowReceiveStats; }
@ -91,8 +91,8 @@ public slots:
void setShouldShowReceiveStats(bool shouldShowReceiveStats) { _shouldShowReceiveStats = shouldShowReceiveStats; }
void updateAvatarRenderStatus(bool shouldRenderAvatars);
private slots:
virtual void removeAvatar(const QUuid& sessionUUID, KillAvatarReason removalReason = KillAvatarReason::NoReason) override;
protected slots:
void processAvatarDataPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) override;
private:
explicit AvatarManager(QObject* parent = 0);
@ -100,12 +100,15 @@ private:
void simulateAvatarFades(float deltaTime);
// virtual overrides
virtual AvatarSharedPointer newSharedAvatar() override;
virtual AvatarSharedPointer addAvatar(const QUuid& sessionUUID, const QWeakPointer<Node>& mixerWeakPointer) override;
virtual void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason = KillAvatarReason::NoReason) override;
AvatarSharedPointer newSharedAvatar() override;
void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason = KillAvatarReason::NoReason) override;
QVector<AvatarSharedPointer> _avatarsToFade;
SetOfAvatarMotionStates _motionStatesThatMightUpdate;
VectorOfMotionStates _motionStatesToRemoveFromPhysics;
SetOfMotionStates _motionStatesToAddToPhysics;
QVector<AvatarSharedPointer> _avatarFades;
std::shared_ptr<MyAvatar> _myAvatar;
quint64 _lastSendAvatarDataTime = 0; // Controls MyAvatar send data rate.
@ -115,14 +118,11 @@ private:
std::list<QPointer<AudioInjector>> _collisionInjectors;
SetOfAvatarMotionStates _motionStatesThatMightUpdate;
SetOfMotionStates _motionStatesToAddToPhysics;
VectorOfMotionStates _motionStatesToRemoveFromPhysics;
RateCounter<> _myAvatarSendRate;
int _numAvatarsUpdated { 0 };
int _numAvatarsNotUpdated { 0 };
float _avatarSimulationTime { 0.0f };
bool _shouldRender { true };
};
Q_DECLARE_METATYPE(AvatarManager::LocalLight)

View file

@ -54,6 +54,7 @@
#include "DebugDraw.h"
#include "EntityEditPacketSender.h"
#include "MovingEntitiesOperator.h"
#include "SceneScriptingInterface.h"
using namespace std;
@ -1634,7 +1635,7 @@ void MyAvatar::postUpdate(float deltaTime) {
Avatar::postUpdate(deltaTime);
render::ScenePointer scene = qApp->getMain3DScene();
if (_skeletonModel->initWhenReady(scene)) {
if (DependencyManager::get<SceneScriptingInterface>()->shouldRenderAvatars() && _skeletonModel->initWhenReady(scene)) {
initHeadBones();
_skeletonModel->setCauterizeBoneSet(_headBoneSet);
_fstAnimGraphOverrideUrl = _skeletonModel->getGeometry()->getAnimGraphOverrideUrl();

View file

@ -123,6 +123,7 @@ void AvatarData::setTargetScale(float targetScale) {
if (_targetScale != newValue) {
_targetScale = newValue;
_scaleChanged = usecTimestampNow();
_avatarScaleChanged = _scaleChanged;
}
}

View file

@ -80,13 +80,10 @@ AvatarSharedPointer AvatarHashMap::addAvatar(const QUuid& sessionUUID, const QWe
AvatarSharedPointer AvatarHashMap::newOrExistingAvatar(const QUuid& sessionUUID, const QWeakPointer<Node>& mixerWeakPointer) {
QWriteLocker locker(&_hashLock);
auto avatar = _avatarHash.value(sessionUUID);
if (!avatar) {
avatar = addAvatar(sessionUUID, mixerWeakPointer);
}
return avatar;
}
@ -103,27 +100,33 @@ void AvatarHashMap::processAvatarDataPacket(QSharedPointer<ReceivedMessage> mess
// enumerate over all of the avatars in this packet
// only add them if mixerWeakPointer points to something (meaning that mixer is still around)
while (message->getBytesLeftToRead()) {
QUuid sessionUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
parseAvatarData(message, sendingNode);
}
}
int positionBeforeRead = message->getPosition();
AvatarSharedPointer AvatarHashMap::parseAvatarData(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
QUuid sessionUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
QByteArray byteArray = message->readWithoutCopy(message->getBytesLeftToRead());
int positionBeforeRead = message->getPosition();
// make sure this isn't our own avatar data or for a previously ignored node
auto nodeList = DependencyManager::get<NodeList>();
QByteArray byteArray = message->readWithoutCopy(message->getBytesLeftToRead());
if (sessionUUID != _lastOwnerSessionUUID && (!nodeList->isIgnoringNode(sessionUUID) || nodeList->getRequestsDomainListData())) {
auto avatar = newOrExistingAvatar(sessionUUID, sendingNode);
// make sure this isn't our own avatar data or for a previously ignored node
auto nodeList = DependencyManager::get<NodeList>();
// have the matching (or new) avatar parse the data from the packet
int bytesRead = avatar->parseDataFromBuffer(byteArray);
message->seek(positionBeforeRead + bytesRead);
} else {
// create a dummy AvatarData class to throw this data on the ground
AvatarData dummyData;
int bytesRead = dummyData.parseDataFromBuffer(byteArray);
message->seek(positionBeforeRead + bytesRead);
}
if (sessionUUID != _lastOwnerSessionUUID && (!nodeList->isIgnoringNode(sessionUUID) || nodeList->getRequestsDomainListData())) {
auto avatar = newOrExistingAvatar(sessionUUID, sendingNode);
// have the matching (or new) avatar parse the data from the packet
int bytesRead = avatar->parseDataFromBuffer(byteArray);
message->seek(positionBeforeRead + bytesRead);
return avatar;
} else {
// create a dummy AvatarData class to throw this data on the ground
AvatarData dummyData;
int bytesRead = dummyData.parseDataFromBuffer(byteArray);
message->seek(positionBeforeRead + bytesRead);
return std::make_shared<AvatarData>();
}
}

View file

@ -49,11 +49,11 @@ signals:
public slots:
bool isAvatarInRange(const glm::vec3 & position, const float range);
private slots:
protected slots:
void sessionUUIDChanged(const QUuid& sessionUUID, const QUuid& oldUUID);
void processAvatarDataPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
virtual void processAvatarDataPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
void processAvatarIdentityPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
void processKillAvatar(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
void processExitingSpaceBubble(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
@ -61,12 +61,13 @@ private slots:
protected:
AvatarHashMap();
virtual AvatarSharedPointer parseAvatarData(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
virtual AvatarSharedPointer newSharedAvatar();
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) const; // uses a QReadLocker on the hashLock
virtual void removeAvatar(const QUuid& sessionUUID, KillAvatarReason removalReason = KillAvatarReason::NoReason);
virtual void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason = KillAvatarReason::NoReason);
AvatarHash _avatarHash;