// // Created by Bradley Austin Davis on 2017/04/27 // Copyright 2013-2017 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "OtherAvatar.h" #include #include #include #include "Application.h" #include "AvatarMotionState.h" #include "DetailedMotionState.h" #include "DebugDraw.h" const float DISPLAYNAME_FADE_TIME = 0.5f; const float DISPLAYNAME_FADE_FACTOR = pow(0.01f, 1.0f / DISPLAYNAME_FADE_TIME); /// Maximum length of joint history storage before it gets reallocated. /// Only small number of past entries in history are used, but larger number is stored to avoid frequent reallocations. constexpr size_t OTHER_AVATAR_JOINT_HISTORY_SIZE = 2000; /// Number of previous samples used to determine latency. /// Must be less than OTHER_AVATAR_JOINT_HISTORY_LOOK_BACK. constexpr size_t OTHER_AVATAR_JOINT_LATENCY_WINDOW = 20; /// How many entries backwards in history are searched before interpolation. constexpr size_t OTHER_AVATAR_JOINT_HISTORY_LOOK_BACK = 45; static_assert(OTHER_AVATAR_JOINT_LATENCY_WINDOW < OTHER_AVATAR_JOINT_HISTORY_LOOK_BACK); /// Maximum allowed latency for the avatar motion interpolation in microseconds. /// Joint positions are only transmitted when the joint moves, so the latency needs to be determined from previous data. constexpr quint64 OTHER_AVATAR_JOINT_HISTORY_MAX_LATENCY = 600000; /// For smooth motion assumed latency needs to be slightly higher than estimated one. /// Estimated latency will be multiplied by this coefficient. constexpr double OTHER_AVATAR_JOINT_LATENCY_COEFFICIENT = 1.3; static glm::u8vec3 getLoadingOrbColor(Avatar::LoadingStatus loadingStatus) { const glm::u8vec3 NO_MODEL_COLOR(0xe3, 0xe3, 0xe3); const glm::u8vec3 LOAD_MODEL_COLOR(0xef, 0x93, 0xd1); const glm::u8vec3 LOAD_SUCCESS_COLOR(0x1f, 0xc6, 0xa6); const glm::u8vec3 LOAD_FAILURE_COLOR(0xc6, 0x21, 0x47); switch (loadingStatus) { case Avatar::LoadingStatus::NoModel: return NO_MODEL_COLOR; case Avatar::LoadingStatus::LoadModel: return LOAD_MODEL_COLOR; case Avatar::LoadingStatus::LoadSuccess: return LOAD_SUCCESS_COLOR; case Avatar::LoadingStatus::LoadFailure: default: return LOAD_FAILURE_COLOR; } } OtherAvatar::OtherAvatar(QThread* thread) : Avatar(thread) { // give the pointer to our head to inherited _headData variable from AvatarData _headData = new Head(this); _skeletonModel = std::make_shared(this, nullptr); _skeletonModel->setLoadingPriorityOperator([]() { return OTHERAVATAR_LOADING_PRIORITY; }); connect(_skeletonModel.get(), &Model::setURLFinished, this, &Avatar::setModelURLFinished); connect(_skeletonModel.get(), &Model::rigReady, this, &Avatar::rigReady); connect(_skeletonModel.get(), &Model::rigReset, this, &Avatar::rigReset); } OtherAvatar::~OtherAvatar() { removeOrb(); } void OtherAvatar::removeOrb() { if (!_otherAvatarOrbMeshPlaceholderID.isNull()) { DependencyManager::get()->deleteEntity(_otherAvatarOrbMeshPlaceholderID); _otherAvatarOrbMeshPlaceholderID = UNKNOWN_ENTITY_ID; } } void OtherAvatar::updateOrbPosition() { if (!_otherAvatarOrbMeshPlaceholderID.isNull()) { EntityItemProperties properties; glm::vec3 headPosition; if (!_skeletonModel->getHeadPosition(headPosition)) { headPosition = getWorldPosition(); } properties.setPosition(headPosition); DependencyManager::get()->editEntity(_otherAvatarOrbMeshPlaceholderID, properties); } } void OtherAvatar::createOrb() { if (_otherAvatarOrbMeshPlaceholderID.isNull()) { EntityItemProperties properties; properties.setType(EntityTypes::Sphere); properties.setAlpha(1.0f); properties.setColor(getLoadingOrbColor(_loadingStatus)); properties.setName("Loading Avatar " + getID().toString()); properties.setPrimitiveMode(PrimitiveMode::LINES); properties.getPulse().setMin(0.5f); properties.getPulse().setMax(1.0f); properties.getPulse().setColorMode(PulseMode::IN_PHASE); properties.setIgnorePickIntersection(true); properties.getGrab().setGrabbable(false); properties.setFadeInMode(COMPONENT_MODE_DISABLED); properties.setFadeOutMode(COMPONENT_MODE_DISABLED); properties.setPosition(getHead()->getPosition()); properties.setRotation(glm::quat(0.0f, 0.0f, 0.0f, 1.0)); properties.setDimensions(glm::vec3(0.5f, 0.5f, 0.5f)); properties.setVisible(true); _otherAvatarOrbMeshPlaceholderID = DependencyManager::get()->addEntityInternal(properties, entity::HostType::LOCAL); } } void OtherAvatar::indicateLoadingStatus(LoadingStatus loadingStatus) { Avatar::indicateLoadingStatus(loadingStatus); if (_otherAvatarOrbMeshPlaceholderID != UNKNOWN_ENTITY_ID) { EntityItemProperties properties; properties.setColor(getLoadingOrbColor(_loadingStatus)); DependencyManager::get()->editEntity(_otherAvatarOrbMeshPlaceholderID, properties); } } void OtherAvatar::setSpaceIndex(int32_t index) { assert(_spaceIndex == -1); _spaceIndex = index; } void OtherAvatar::updateSpaceProxy(workload::Transaction& transaction) const { if (_spaceIndex > -1) { float approximateBoundingRadius = glm::length(getTargetScale()); workload::Sphere sphere(getWorldPosition(), approximateBoundingRadius); transaction.update(_spaceIndex, sphere); } } int OtherAvatar::parseDataFromBuffer(const QByteArray& buffer) { int32_t bytesRead = Avatar::parseDataFromBuffer(buffer); for (auto& detailedMotionState : _detailedMotionStates) { // NOTE: we activate _detailedMotionStates is because they are KINEMATIC // and Bullet will automagically call DetailedMotionState::getWorldTransform() when active. detailedMotionState->forceActive(); } if (_moving && _motionState) { _motionState->addDirtyFlags(Simulation::DIRTY_POSITION); } return bytesRead; } const btCollisionShape* OtherAvatar::createCollisionShape(int32_t jointIndex, bool& isBound, std::vector& boundJoints) { ShapeInfo shapeInfo; isBound = false; QString jointName = ""; if (jointIndex > -1 && jointIndex < (int32_t)_multiSphereShapes.size()) { jointName = _multiSphereShapes[jointIndex].getJointName(); } switch (_bodyLOD) { case BodyLOD::Sphere: shapeInfo.setSphere(0.5f * getFitBounds().getDimensions().y); boundJoints.clear(); for (auto &spheres : _multiSphereShapes) { if (spheres.isValid()) { boundJoints.push_back(spheres.getJointIndex()); } } isBound = true; break; case BodyLOD::MultiSphereLow: if (jointName.contains("RightHand", Qt::CaseInsensitive) || jointName.contains("LeftHand", Qt::CaseInsensitive)) { if (jointName.size() <= QString("RightHand").size()) { AABox handBound; for (auto &spheres : _multiSphereShapes) { if (spheres.isValid() && spheres.getJointName().contains(jointName, Qt::CaseInsensitive)) { boundJoints.push_back(spheres.getJointIndex()); handBound += spheres.getBoundingBox(); } } shapeInfo.setSphere(0.5f * handBound.getLargestDimension()); glm::vec3 jointPosition; glm::quat jointRotation; _skeletonModel->getJointPositionInWorldFrame(jointIndex, jointPosition); _skeletonModel->getJointRotationInWorldFrame(jointIndex, jointRotation); glm::vec3 positionOffset = glm::inverse(jointRotation) * (handBound.calcCenter() - jointPosition); shapeInfo.setOffset(positionOffset); isBound = true; } break; } // Note: MultiSphereLow case really means: "skip fingers and use spheres for hands, // else fall through to MultiSphereHigh case" /* fall-thru */ case BodyLOD::MultiSphereHigh: computeDetailedShapeInfo(shapeInfo, jointIndex); break; default: assert(false); // should never reach here break; } return ObjectMotionState::getShapeManager()->getShape(shapeInfo); } void OtherAvatar::forgetDetailedMotionStates() { // NOTE: the DetailedMotionStates are deleted after being added to PhysicsEngine::Transaction::_objectsToRemove // See AvatarManager::handleProcessedPhysicsTransaction() _detailedMotionStates.clear(); } void OtherAvatar::setWorkloadRegion(uint8_t region) { _workloadRegion = region; computeShapeLOD(); } void OtherAvatar::computeShapeLOD() { // auto newBodyLOD = _workloadRegion < workload::Region::R3 ? BodyLOD::MultiSphereShapes : BodyLOD::CapsuleShape; // auto newBodyLOD = BodyLOD::CapsuleShape; BodyLOD newLOD; switch (_workloadRegion) { case workload::Region::R1: newLOD = BodyLOD::MultiSphereHigh; break; case workload::Region::R2: newLOD = BodyLOD::MultiSphereLow; break; case workload::Region::UNKNOWN: case workload::Region::INVALID: case workload::Region::R4: case workload::Region::R3: default: newLOD = BodyLOD::Sphere; break; } if (newLOD != _bodyLOD) { _bodyLOD = newLOD; if (isInPhysicsSimulation()) { _needsDetailedRebuild = true; } } } bool OtherAvatar::isInPhysicsSimulation() const { return _motionState && _motionState->getRigidBody(); } bool OtherAvatar::shouldBeInPhysicsSimulation() const { return !isDead() && _workloadRegion <= workload::Region::R3; } bool OtherAvatar::needsPhysicsUpdate() const { constexpr uint32_t FLAGS_OF_INTEREST = Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS | Simulation::DIRTY_POSITION | Simulation::DIRTY_COLLISION_GROUP; return (_needsDetailedRebuild || (_motionState && (bool)(_motionState->getIncomingDirtyFlags() & FLAGS_OF_INTEREST))); } void OtherAvatar::rebuildCollisionShape() { if (_motionState) { // do not actually rebuild here, instead flag for later _motionState->addDirtyFlags(Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS); _needsDetailedRebuild = true; } } void OtherAvatar::setCollisionWithOtherAvatarsFlags() { if (_motionState) { _motionState->addDirtyFlags(Simulation::DIRTY_COLLISION_GROUP); } } void OtherAvatar::interpolateJoints() { auto now = usecTimestampNow(); // there's no history to interpolate from, // just set the rig poses to whatever we have if ((size_t)_jointData.size() != _jointHistory.size()) { goto finishRigSetup; } for (int i = 0; i < _jointData.size(); i++) { const auto &history{_jointHistory[i]}; // We don't process very old samples for performance reasons, but we remove them only sporadically to avoid excessive reallocations. const size_t historyStart = history.size() > OTHER_AVATAR_JOINT_HISTORY_LOOK_BACK ? history.size() - OTHER_AVATAR_JOINT_HISTORY_LOOK_BACK : 0; // Calculate latency in microseconds and new time point. quint64 latency = 0; // We cannot calculate latency from just one sample. if (history.size() >= 2) { size_t latencySamples = 0; for (size_t historyIndex = historyStart; historyIndex < history.size() - 1; historyIndex++) { latency += std::min(history[historyIndex + 1].first - history[historyIndex].first, OTHER_AVATAR_JOINT_HISTORY_MAX_LATENCY); latencySamples++; } latency /= latencySamples; // Real latency may differ from estimated one, so we need some safety margin. latency *= OTHER_AVATAR_JOINT_LATENCY_COEFFICIENT; } else { // Latency cannot be determined yet since we have only one entry. latency = 0; } // Limit latency to a defined value. latency = std::min(OTHER_AVATAR_JOINT_HISTORY_MAX_LATENCY, latency); // We don't know the future, so avatar motions need to be delayed to be interpolated. auto timePoint = now - latency; // Retrieve previous and current joint data. Q_ASSERT(history.size() > 0); if (history.size() == 0) continue; size_t oldKeyframeIndex = history.size() - 1; size_t newKeyframeIndex = history.size() - 1; // Find the first keyframe that's in the "future". // Searching backwards is more efficient here. for (size_t historyIndex = history.size() - 1; historyIndex >= historyStart; historyIndex--) { if (timePoint > history[historyIndex].first) { if (historyIndex < history.size() - 1) { oldKeyframeIndex = historyIndex; newKeyframeIndex = historyIndex + 1; break; } else { // There are no past entries yet. oldKeyframeIndex = historyIndex; newKeyframeIndex = historyIndex; break; } } } const std::pair &oldKeyframe{ history[oldKeyframeIndex] }; const std::pair &newKeyframe{ history[newKeyframeIndex] }; quint64 timeDiff = timePoint >= oldKeyframe.first ? timePoint - oldKeyframe.first : 0; // We need to avoid division by zero: const float theta = std::min(1.0f, static_cast(timeDiff) / static_cast(std::max(newKeyframe.first - oldKeyframe.first, 1ULL))); _jointData[i].rotation = glm::slerp(oldKeyframe.second.rotation, newKeyframe.second.rotation, theta); _jointData[i].translation = glm::mix(oldKeyframe.second.translation, newKeyframe.second.translation, theta); } finishRigSetup: glm::mat4 rootTransform = glm::scale(_skeletonModel->getScale()) * glm::translate(_skeletonModel->getOffset()); _skeletonModel->getRig().copyJointsFromJointData(_jointData); _skeletonModel->getRig().computeExternalPoses(rootTransform); locationChanged(); // joints changed, so if there are any children, update them. relayJointDataToChildren(); } void OtherAvatar::simulate(float deltaTime, bool inView) { PROFILE_RANGE(simulation, "simulate"); if (!_lerpServerPosition) { _lerpServerPosition = _serverPosition; } // TODO: This is wrong (exponential ease-out rather than linear), // but looks nicer than low-FPS jittering. The correct solution // would be to have a "position changed" field and use a similar // snapshot-interpolation mechanism to what joint interpolations use. _lerpServerPosition = glm::mix(*_lerpServerPosition, _serverPosition, std::min(deltaTime * 10.0f, 1.0f)); _globalPosition = *_lerpServerPosition; if (!hasParent()) { setLocalPosition(_globalPosition); } _simulationRate.increment(); if (inView) { _simulationInViewRate.increment(); } PerformanceTimer perfTimer("simulate"); { PROFILE_RANGE(simulation, "updateJoints"); if (inView) { Head* head = getHead(); if (_hasNewJointData) { quint64 currentTime = usecTimestampNow(); QReadLocker readLock(&_jointDataLock); Q_ASSERT(_hasNewJointDataVec.size() == static_cast(_jointData.size())); // Reset joint history if joint count changed. if (_jointHistory.size() != static_cast(_jointData.size())) { _jointHistory.clear(); _jointHistory.resize(_jointData.size()); for (size_t i = 0; i < _jointHistory.size(); i++) { _jointHistory[i].push_back({currentTime, _jointData[i]}); } qDebug() << "clearing joint history"; } for (size_t i = 0; i < _hasNewJointDataVec.size(); i++) { if (_hasNewJointDataVec[i]) { _hasNewJointDataVec[i] = false; _jointHistory[i].push_back({currentTime, _jointData[i]}); // Cleanup old data and shorten the vector. if (_jointHistory[i].size() > OTHER_AVATAR_JOINT_HISTORY_SIZE) { for (size_t historyIndex = 0; historyIndex < OTHER_AVATAR_JOINT_HISTORY_LOOK_BACK; historyIndex++) { _jointHistory[i][historyIndex] = _jointHistory[i][static_cast(historyIndex + _jointHistory[i].size() - OTHER_AVATAR_JOINT_HISTORY_LOOK_BACK)]; } _jointHistory[i].resize(OTHER_AVATAR_JOINT_HISTORY_LOOK_BACK); } } } _jointDataSimulationRate.increment(); _hasNewJointData = false; } glm::vec3 headPosition = getWorldPosition(); if (!_skeletonModel->getHeadPosition(headPosition)) { headPosition = getWorldPosition(); } head->setPosition(headPosition); head->setScale(getModelScale()); head->simulate(deltaTime); _skeletonModel->simulate(deltaTime, true); } else { // a non-full update is still required so that the position, rotation, scale and bounds of the skeletonModel are updated. _skeletonModel->simulate(deltaTime, false); } _skeletonModelSimulationRate.increment(); } interpolateJoints(); // update animation for display name fade in/out if ( _displayNameTargetAlpha != _displayNameAlpha) { // the alpha function is // Fade out => alpha(t) = factor ^ t => alpha(t+dt) = alpha(t) * factor^(dt) // Fade in => alpha(t) = 1 - factor^t => alpha(t+dt) = 1-(1-alpha(t))*coef^(dt) // factor^(dt) = coef float coef = pow(DISPLAYNAME_FADE_FACTOR, deltaTime); if (_displayNameTargetAlpha < _displayNameAlpha) { // Fading out _displayNameAlpha *= coef; } else { // Fading in _displayNameAlpha = 1.0f - (1.0f - _displayNameAlpha) * coef; } _displayNameAlpha = glm::abs(_displayNameAlpha - _displayNameTargetAlpha) < 0.01f ? _displayNameTargetAlpha : _displayNameAlpha; } { PROFILE_RANGE(simulation, "misc"); measureMotionDerivatives(deltaTime); updatePalms(); } { PROFILE_RANGE(simulation, "entities"); handleChangedAvatarEntityData(); updateAttachedAvatarEntities(); } { PROFILE_RANGE(simulation, "grabs"); applyGrabChanges(); } } void OtherAvatar::debugJointData() const { // Get a copy of the joint data auto jointData = getJointData(); auto skeletonData = getSkeletonData(); if ((int)skeletonData.size() == jointData.size() && jointData.size() != 0) { const vec4 RED(1.0f, 0.0f, 0.0f, 1.0f); const vec4 GREEN(0.0f, 1.0f, 0.0f, 1.0f); const vec4 BLUE(0.0f, 0.0f, 1.0f, 1.0f); const vec4 LIGHT_RED(1.0f, 0.5f, 0.5f, 1.0f); const vec4 LIGHT_GREEN(0.5f, 1.0f, 0.5f, 1.0f); const vec4 LIGHT_BLUE(0.5f, 0.5f, 1.0f, 1.0f); const vec4 GREY(0.3f, 0.3f, 0.3f, 1.0f); const vec4 WHITE(1.0f, 1.0f, 1.0f, 1.0f); const float AXIS_LENGTH = 0.1f; AnimPoseVec absoluteJointPoses; AnimPose rigToAvatar = AnimPose(Quaternions::Y_180 * getWorldOrientation(), getWorldPosition()); bool drawBones = false; for (int i = 0; i < jointData.size(); i++) { float jointScale = skeletonData[i].defaultScale * getTargetScale() * METERS_PER_CENTIMETER; auto absoluteRotation = jointData[i].rotationIsDefaultPose ? skeletonData[i].defaultRotation : jointData[i].rotation; auto localJointTranslation = jointScale * (jointData[i].translationIsDefaultPose ? skeletonData[i].defaultTranslation : jointData[i].translation); bool isHips = skeletonData[i].jointName == "Hips"; if (isHips) { localJointTranslation = glm::vec3(0.0f); drawBones = true; } AnimPose absoluteParentPose; int parentIndex = skeletonData[i].parentIndex; if (parentIndex != -1 && parentIndex < (int)absoluteJointPoses.size()) { absoluteParentPose = absoluteJointPoses[parentIndex]; } AnimPose absoluteJointPose = AnimPose(absoluteRotation, absoluteParentPose.trans() + absoluteParentPose.rot() * localJointTranslation); auto jointPose = rigToAvatar * absoluteJointPose; auto parentPose = rigToAvatar * absoluteParentPose; if (drawBones) { glm::vec3 xAxis = jointPose.rot() * Vectors::UNIT_X; glm::vec3 yAxis = jointPose.rot() * Vectors::UNIT_Y; glm::vec3 zAxis = jointPose.rot() * Vectors::UNIT_Z; DebugDraw::getInstance().drawRay(jointPose.trans(), jointPose.trans() + AXIS_LENGTH * xAxis, jointData[i].rotationIsDefaultPose ? LIGHT_RED : RED); DebugDraw::getInstance().drawRay(jointPose.trans(), jointPose.trans() + AXIS_LENGTH * yAxis, jointData[i].rotationIsDefaultPose ? LIGHT_GREEN : GREEN); DebugDraw::getInstance().drawRay(jointPose.trans(), jointPose.trans() + AXIS_LENGTH * zAxis, jointData[i].rotationIsDefaultPose ? LIGHT_BLUE : BLUE); if (!isHips) { DebugDraw::getInstance().drawRay(jointPose.trans(), parentPose.trans(), jointData[i].translationIsDefaultPose ? WHITE : GREY); } } absoluteJointPoses.push_back(absoluteJointPose); } } } void OtherAvatar::handleChangedAvatarEntityData() { PerformanceTimer perfTimer("avatarEntities"); // AVATAR ENTITY UPDATE FLOW // - if queueEditEntityMessage() sees "AvatarEntity" HostType it calls _myAvatar->storeAvatarEntityDataPayload() // - storeAvatarEntityDataPayload() saves the payload and flags the trait instance for the entity as updated, // - ClientTraitsHandler::sendChangedTraitsToMixer() sends the entity bytes to the mixer which relays them to other interfaces // - AvatarHashMap::processBulkAvatarTraits() on other interfaces calls avatar->processTraitInstance() // - AvatarData::processTraitInstance() calls storeAvatarEntityDataPayload(), which sets _avatarEntityDataChanged = true // - (My)Avatar::simulate() calls handleChangedAvatarEntityData() every frame which checks _avatarEntityDataChanged // and here we are... // AVATAR ENTITY DELETE FLOW // - EntityScriptingInterface::deleteEntity() calls _myAvatar->clearAvatarEntity() for deleted avatar entities // - clearAvatarEntity() removes the avatar entity and flags the trait instance for the entity as deleted // - ClientTraitsHandler::sendChangedTraitsToMixer() sends a deletion to the mixer which relays to other interfaces // - AvatarHashMap::processBulkAvatarTraits() on other interfaces calls avatar->processDeletedTraitInstace() // - AvatarData::processDeletedTraitInstance() calls clearAvatarEntity() // - AvatarData::clearAvatarEntity() sets _avatarEntityDataChanged = true and adds the ID to the detached list // - (My)Avatar::simulate() calls handleChangedAvatarEntityData() every frame which checks _avatarEntityDataChanged // and here we are... if (!_avatarEntityDataChanged) { return; } auto treeRenderer = DependencyManager::get(); EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; if (!entityTree) { return; } PackedAvatarEntityMap packedAvatarEntityData; _avatarEntitiesLock.withReadLock([&] { packedAvatarEntityData = _packedAvatarEntityData; }); entityTree->withWriteLock([&] { AvatarEntityMap::const_iterator dataItr = packedAvatarEntityData.begin(); while (dataItr != packedAvatarEntityData.end()) { // compute hash of data. TODO? cache this? QByteArray data = dataItr.value(); uint32_t newHash = qHash(data); // check to see if we recognize this hash and whether it was already successfully processed QUuid entityID = dataItr.key(); MapOfAvatarEntityDataHashes::iterator stateItr = _avatarEntityDataHashes.find(entityID); if (stateItr != _avatarEntityDataHashes.end()) { if (stateItr.value().success) { if (newHash == stateItr.value().hash) { // data hasn't changed --> nothing to do ++dataItr; continue; } } else { // NOTE: if the data was unsuccessful in producing an entity in the past // we will try again just in case something changed (unlikely). // Unfortunately constantly trying to build the entity for this data costs // CPU cycles that we'd rather not spend. // TODO? put a maximum number of tries on this? } } else { // sanity check data QUuid id; EntityTypes::EntityType type; EntityTypes::extractEntityTypeAndID((unsigned char*)(data.data()), data.size(), type, id); if (id != entityID || !EntityTypes::typeIsValid(type)) { // skip for corrupt ++dataItr; continue; } // remember this hash for the future stateItr = _avatarEntityDataHashes.insert(entityID, AvatarEntityDataHash(newHash)); } ++dataItr; EntityItemProperties properties; int32_t bytesLeftToRead = data.size(); unsigned char* dataAt = (unsigned char*)(data.data()); // FIXME: This function will cause unintented changes in SpaillyNestable // E.g overriding the ID index of an exisiting entity to temporary entity // in the following map QHash _children; // Andrew Meadows will address this issue if (!properties.constructFromBuffer(dataAt, bytesLeftToRead)) { // properties are corrupt continue; } properties.setEntityHostType(entity::HostType::AVATAR); properties.setOwningAvatarID(getID()); // there's no entity-server to tell us we're the simulation owner, so always set the // simulationOwner to the owningAvatarID and a high priority. properties.setSimulationOwner(getID(), AVATAR_ENTITY_SIMULATION_PRIORITY); if (properties.getParentID() == AVATAR_SELF_ID) { properties.setParentID(getID()); } // NOTE: if this avatar entity is not attached to us, strip its entity script completely... auto attachedScript = properties.getScript(); if (!isMyAvatar() && !attachedScript.isEmpty()) { QString noScript; properties.setScript(noScript); } auto specifiedHref = properties.getHref(); if (!isMyAvatar() && !specifiedHref.isEmpty()) { qCDebug(avatars) << "removing entity href from avatar attached entity:" << entityID << "old href:" << specifiedHref; QString noHref; properties.setHref(noHref); } // When grabbing avatar entities, they are parented to the joint moving them, then when un-grabbed // they go back to the default parent (null uuid). When un-gripped, others saw the entity disappear. // The thinking here is the local position was noticed as changing, but not the parentID (since it is now // back to the default), and the entity flew off somewhere. Marking all changed definitely fixes this, // and seems safe (per Seth). properties.markAllChanged(); // try to build the entity EntityItemPointer entity = entityTree->findEntityByEntityItemID(EntityItemID(entityID)); bool success = true; if (entity) { QUuid oldParentID = entity->getParentID(); // Since has overwrtiiten the back pointer // from the parent children map (see comment for function call above), // we need to for reset the back pointer in the map correctly by setting the parentID, but // since the parentID of the entity has not changed we first need to set it some ither ID, // then set the the original ID for the changes to take effect // TODO: This is a horrible hack and once properties.constructFromBuffer no longer causes // side effects...remove the following three lines const QUuid NULL_ID = QUuid("{00000000-0000-0000-0000-000000000005}"); entity->setParentID(NULL_ID); entity->setParentID(oldParentID); if (entity->stillHasMyGrab()) { // For this case: we want to ignore transform+velocities coming from authoritative OtherAvatar // because the MyAvatar is grabbing and we expect the local grab state // to have enough information to prevent simulation drift. // // Clever readers might realize this could cause problems. For example, // if an ignored OtherAvatar were to simultanously grab the object then there would be // a noticeable discrepancy between participants in the distributed physics simulation, // however the difference would be stable and would not drift. properties.clearTransformOrVelocityChanges(); } if (entityTree->updateEntity(entityID, properties)) { entity->updateLastEditedFromRemote(); } else { success = false; } if (oldParentID != entity->getParentID()) { if (entity->getParentID() == getID()) { onAddAttachedAvatarEntity(entityID); } else if (oldParentID == getID()) { onRemoveAttachedAvatarEntity(entityID); } } } else { entity = entityTree->addEntity(entityID, properties); if (!entity) { success = false; } else if (entity->getParentID() == getID()) { onAddAttachedAvatarEntity(entityID); } } stateItr.value().success = success; if (success) { stateItr.value().hash = newHash; } else { stateItr.value().hash = 0; } } AvatarEntityIDs recentlyRemovedAvatarEntities = getAndClearRecentlyRemovedIDs(); if (!recentlyRemovedAvatarEntities.empty()) { // only lock this thread when absolutely necessary AvatarEntityMap packedAvatarEntityData; _avatarEntitiesLock.withReadLock([&] { packedAvatarEntityData = _packedAvatarEntityData; }); if (!recentlyRemovedAvatarEntities.empty()) { std::vector idsToDelete; idsToDelete.reserve(recentlyRemovedAvatarEntities.size()); foreach (auto entityID, recentlyRemovedAvatarEntities) { if (!packedAvatarEntityData.contains(entityID)) { idsToDelete.push_back(entityID); } } if (!idsToDelete.empty()) { bool force = true; bool ignoreWarnings = true; entityTree->deleteEntitiesByID(idsToDelete, force, ignoreWarnings); } } // TODO: move this outside of tree lock // remove stale data hashes foreach (auto entityID, recentlyRemovedAvatarEntities) { MapOfAvatarEntityDataHashes::iterator stateItr = _avatarEntityDataHashes.find(entityID); if (stateItr != _avatarEntityDataHashes.end()) { _avatarEntityDataHashes.erase(stateItr); } onRemoveAttachedAvatarEntity(entityID); } } if (packedAvatarEntityData.size() != _avatarEntityForRecording.size()) { createRecordingIDs(); } }); _avatarEntityDataChanged = false; _hasCheckedForAvatarEntities = true; } void OtherAvatar::onAddAttachedAvatarEntity(const QUuid& id) { for (uint32_t i = 0; i < _attachedAvatarEntities.size(); ++i) { if (_attachedAvatarEntities[i] == id) { return; } } _attachedAvatarEntities.push_back(id); } void OtherAvatar::onRemoveAttachedAvatarEntity(const QUuid& id) { for (uint32_t i = 0; i < _attachedAvatarEntities.size(); ++i) { if (_attachedAvatarEntities[i] == id) { if (i != _attachedAvatarEntities.size() - 1) { _attachedAvatarEntities[i] = _attachedAvatarEntities.back(); } _attachedAvatarEntities.pop_back(); break; } } } void OtherAvatar::updateAttachedAvatarEntities() { if (!_attachedAvatarEntities.empty()) { auto treeRenderer = DependencyManager::get(); if (!treeRenderer) { return; } for (const QUuid& id : _attachedAvatarEntities) { treeRenderer->onEntityChanged(id); } } } void OtherAvatar::onIdentityRecieved() { if (_avatarEntityIdentityCountdown > 0) { _avatarEntityIdentityCountdown--; } else { _hasCheckedForAvatarEntities = true; } }