//// 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 <glm/gtx/norm.hpp>#include <glm/gtx/vector_angle.hpp> #include <AvatarLogging.h> #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<SkeletonModel>(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<EntityScriptingInterface>()->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<EntityScriptingInterface>()->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<EntityScriptingInterface>()->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<EntityScriptingInterface>()->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<int32_t>& 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<quint64, JointData> &oldKeyframe{ history[oldKeyframeIndex] }; const std::pair<quint64, JointData> &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<float>(timeDiff) / static_cast<float>(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<size_t>(_jointData.size())); // Reset joint history if joint count changed. if (_jointHistory.size() != static_cast<size_t>(_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<qsizetype>(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);Lines 1–500 of 768. Load more
// // 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 <glm/gtx/norm.hpp> #include <glm/gtx/vector_angle.hpp> #include <AvatarLogging.h> #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<SkeletonModel>(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<EntityScriptingInterface>()->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<EntityScriptingInterface>()->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<EntityScriptingInterface>()->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<EntityScriptingInterface>()->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<int32_t>& 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<quint64, JointData> &oldKeyframe{ history[oldKeyframeIndex] }; const std::pair<quint64, JointData> &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<float>(timeDiff) / static_cast<float>(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<size_t>(_jointData.size())); // Reset joint history if joint count changed. if (_jointHistory.size() != static_cast<size_t>(_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<qsizetype>(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);