diff --git a/assignment-client/src/entities/EntityPriorityQueue.cpp b/assignment-client/src/entities/EntityPriorityQueue.cpp new file mode 100644 index 0000000000..999a05f2e2 --- /dev/null +++ b/assignment-client/src/entities/EntityPriorityQueue.cpp @@ -0,0 +1,53 @@ +// +// EntityPriorityQueue.cpp +// assignment-client/src/entities +// +// Created by Andrew Meadows 2017.08.08 +// Copyright 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 "EntityPriorityQueue.h" + +const float PrioritizedEntity::DO_NOT_SEND = -1.0e-6f; +const float PrioritizedEntity::FORCE_REMOVE = -1.0e-5f; +const float PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY = 1.0f; + +void ConicalView::set(const ViewFrustum& viewFrustum) { + // The ConicalView has two parts: a central sphere (same as ViewFrustum) and a circular cone that bounds the frustum part. + // Why? Because approximate intersection tests are much faster to compute for a cone than for a frustum. + _position = viewFrustum.getPosition(); + _direction = viewFrustum.getDirection(); + + // We cache the sin and cos of the half angle of the cone that bounds the frustum. + // (the math here is left as an exercise for the reader) + float A = viewFrustum.getAspectRatio(); + float t = tanf(0.5f * viewFrustum.getFieldOfView()); + _cosAngle = 1.0f / sqrtf(1.0f + (A * A + 1.0f) * (t * t)); + _sinAngle = sqrtf(1.0f - _cosAngle * _cosAngle); + + _radius = viewFrustum.getCenterRadius(); +} + +float ConicalView::computePriority(const AACube& cube) const { + glm::vec3 p = cube.calcCenter() - _position; // position of bounding sphere in view-frame + float d = glm::length(p); // distance to center of bounding sphere + float r = 0.5f * cube.getScale(); // radius of bounding sphere + if (d < _radius + r) { + return r; + } + // We check the angle between the center of the cube and the _direction of the view. + // If it is less than the sum of the half-angle from center of cone to outer edge plus + // the half apparent angle of the bounding sphere then it is in view. + // + // The math here is left as an exercise for the reader with the following hints: + // (1) We actually check the dot product of the cube's local position rather than the angle and + // (2) we take advantage of this trig identity: cos(A+B) = cos(A)*cos(B) - sin(A)*sin(B) + if (glm::dot(p, _direction) > sqrtf(d * d - r * r) * _cosAngle - r * _sinAngle) { + const float AVOID_DIVIDE_BY_ZERO = 0.001f; + return r / (d + AVOID_DIVIDE_BY_ZERO); + } + return PrioritizedEntity::DO_NOT_SEND; +} diff --git a/assignment-client/src/entities/EntityPriorityQueue.h b/assignment-client/src/entities/EntityPriorityQueue.h new file mode 100644 index 0000000000..e308d9b549 --- /dev/null +++ b/assignment-client/src/entities/EntityPriorityQueue.h @@ -0,0 +1,66 @@ +// +// EntityPriorityQueue.h +// assignment-client/src/entities +// +// Created by Andrew Meadows 2017.08.08 +// Copyright 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 +// + +#ifndef hifi_EntityPriorityQueue_h +#define hifi_EntityPriorityQueue_h + +#include + +#include +#include + +const float SQRT_TWO_OVER_TWO = 0.7071067811865f; +const float DEFAULT_VIEW_RADIUS = 10.0f; + +// ConicalView is an approximation of a ViewFrustum for fast calculation of sort priority. +class ConicalView { +public: + ConicalView() {} + ConicalView(const ViewFrustum& viewFrustum) { set(viewFrustum); } + void set(const ViewFrustum& viewFrustum); + float computePriority(const AACube& cube) const; +private: + glm::vec3 _position { 0.0f, 0.0f, 0.0f }; + glm::vec3 _direction { 0.0f, 0.0f, 1.0f }; + float _sinAngle { SQRT_TWO_OVER_TWO }; + float _cosAngle { SQRT_TWO_OVER_TWO }; + float _radius { DEFAULT_VIEW_RADIUS }; +}; + +// PrioritizedEntity is a placeholder in a sorted queue. +class PrioritizedEntity { +public: + static const float DO_NOT_SEND; + static const float FORCE_REMOVE; + static const float WHEN_IN_DOUBT_PRIORITY; + + PrioritizedEntity(EntityItemPointer entity, float priority, bool forceRemove = false) : _weakEntity(entity), _rawEntityPointer(entity.get()), _priority(priority), _forceRemove(forceRemove) {} + EntityItemPointer getEntity() const { return _weakEntity.lock(); } + EntityItem* getRawEntityPointer() const { return _rawEntityPointer; } + float getPriority() const { return _priority; } + bool shouldForceRemove() const { return _forceRemove; } + + class Compare { + public: + bool operator() (const PrioritizedEntity& A, const PrioritizedEntity& B) { return A._priority < B._priority; } + }; + friend class Compare; + +private: + EntityItemWeakPointer _weakEntity; + EntityItem* _rawEntityPointer; + float _priority; + bool _forceRemove; +}; + +using EntityPriorityQueue = std::priority_queue< PrioritizedEntity, std::vector, PrioritizedEntity::Compare >; + +#endif // hifi_EntityPriorityQueue_h diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 7febdc67e1..03014bae6a 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -13,9 +13,18 @@ #include #include +#include #include "EntityServer.h" + +EntityTreeSendThread::EntityTreeSendThread(OctreeServer* myServer, const SharedNodePointer& node) : + OctreeSendThread(myServer, node) +{ + connect(std::static_pointer_cast(myServer->getOctree()).get(), &EntityTree::editingEntityPointer, this, &EntityTreeSendThread::editingEntityPointer, Qt::QueuedConnection); + connect(std::static_pointer_cast(myServer->getOctree()).get(), &EntityTree::deletingEntityPointer, this, &EntityTreeSendThread::deletingEntityPointer, Qt::QueuedConnection); +} + void EntityTreeSendThread::preDistributionProcessing() { auto node = _node.toStrongRef(); auto nodeData = static_cast(node->getLinkedData()); @@ -80,6 +89,72 @@ void EntityTreeSendThread::preDistributionProcessing() { } } +void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData, + bool viewFrustumChanged, bool isFullScene) { + if (viewFrustumChanged || _traversal.finished()) { + ViewFrustum viewFrustum; + nodeData->copyCurrentViewFrustum(viewFrustum); + EntityTreeElementPointer root = std::dynamic_pointer_cast(_myServer->getOctree()->getRoot()); + int32_t lodLevelOffset = nodeData->getBoundaryLevelAdjust() + (viewFrustumChanged ? LOW_RES_MOVING_ADJUST : NO_BOUNDARY_ADJUST); + startNewTraversal(viewFrustum, root, lodLevelOffset, nodeData->getUsesFrustum()); + + // When the viewFrustum changed the sort order may be incorrect, so we re-sort + // and also use the opportunity to cull anything no longer in view + if (viewFrustumChanged && !_sendQueue.empty()) { + EntityPriorityQueue prevSendQueue; + _sendQueue.swap(prevSendQueue); + _entitiesInQueue.clear(); + // Re-add elements from previous traversal if they still need to be sent + float lodScaleFactor = _traversal.getCurrentLODScaleFactor(); + glm::vec3 viewPosition = _traversal.getCurrentView().getPosition(); + while (!prevSendQueue.empty()) { + EntityItemPointer entity = prevSendQueue.top().getEntity(); + bool forceRemove = prevSendQueue.top().shouldForceRemove(); + prevSendQueue.pop(); + if (entity) { + if (!forceRemove) { + bool success = false; + AACube cube = entity->getQueryAACube(success); + if (success) { + if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { + float priority = _conicalView.computePriority(cube); + if (priority != PrioritizedEntity::DO_NOT_SEND) { + float distance = glm::distance(cube.calcCenter(), viewPosition) + MIN_VISIBLE_DISTANCE; + float angularDiameter = cube.getScale() / distance; + if (angularDiameter > MIN_ENTITY_ANGULAR_DIAMETER * lodScaleFactor) { + _sendQueue.push(PrioritizedEntity(entity, priority)); + _entitiesInQueue.insert(entity.get()); + } + } + } + } else { + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); + _entitiesInQueue.insert(entity.get()); + } + } else { + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::FORCE_REMOVE, true)); + _entitiesInQueue.insert(entity.get()); + } + } + } + } + } + + if (!_traversal.finished()) { + quint64 startTime = usecTimestampNow(); + + #ifdef DEBUG + const uint64_t TIME_BUDGET = 400; // usec + #else + const uint64_t TIME_BUDGET = 200; // usec + #endif + _traversal.traverse(TIME_BUDGET); + OctreeServer::trackTreeTraverseTime((float)(usecTimestampNow() - startTime)); + } + + OctreeSendThread::traverseTreeAndSendContents(node, nodeData, viewFrustumChanged, isFullScene); +} + bool EntityTreeSendThread::addAncestorsToExtraFlaggedEntities(const QUuid& filteredEntityID, EntityItem& entityItem, EntityNodeData& nodeData) { // check if this entity has a parent that is also an entity @@ -129,4 +204,288 @@ bool EntityTreeSendThread::addDescendantsToExtraFlaggedEntities(const QUuid& fil return hasNewChild || hasNewDescendants; } +void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTreeElementPointer root, int32_t lodLevelOffset, bool usesViewFrustum) { + DiffTraversal::Type type = _traversal.prepareNewTraversal(view, root, lodLevelOffset, usesViewFrustum); + // there are three types of traversal: + // + // (1) FirstTime = at login --> find everything in view + // (2) Repeat = view hasn't changed --> find what has changed since last complete traversal + // (3) Differential = view has changed --> find what has changed or in new view but not old + // + // The "scanCallback" we provide to the traversal depends on the type: + // + // The _conicalView is updated here as a cached view approximation used by the lambdas for efficient + // computation of entity sorting priorities. + // + _conicalView.set(_traversal.getCurrentView()); + switch (type) { + case DiffTraversal::First: + // When we get to a First traversal, clear the _knownState + _knownState.clear(); + if (usesViewFrustum) { + float lodScaleFactor = _traversal.getCurrentLODScaleFactor(); + glm::vec3 viewPosition = _traversal.getCurrentView().getPosition(); + _traversal.setScanCallback([=](DiffTraversal::VisibleElement& next) { + next.element->forEachEntity([=](EntityItemPointer entity) { + // Bail early if we've already checked this entity this frame + if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { + return; + } + bool success = false; + AACube cube = entity->getQueryAACube(success); + if (success) { + if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { + // Check the size of the entity, it's possible that a "too small to see" entity is included in a + // larger octree cell because of its position (for example if it crosses the boundary of a cell it + // pops to the next higher cell. So we want to check to see that the entity is large enough to be seen + // before we consider including it. + float distance = glm::distance(cube.calcCenter(), viewPosition) + MIN_VISIBLE_DISTANCE; + float angularDiameter = cube.getScale() / distance; + if (angularDiameter > MIN_ENTITY_ANGULAR_DIAMETER * lodScaleFactor) { + float priority = _conicalView.computePriority(cube); + _sendQueue.push(PrioritizedEntity(entity, priority)); + _entitiesInQueue.insert(entity.get()); + } + } + } else { + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); + _entitiesInQueue.insert(entity.get()); + } + }); + }); + } else { + _traversal.setScanCallback([this](DiffTraversal::VisibleElement& next) { + next.element->forEachEntity([this](EntityItemPointer entity) { + // Bail early if we've already checked this entity this frame + if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { + return; + } + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); + _entitiesInQueue.insert(entity.get()); + }); + }); + } + break; + case DiffTraversal::Repeat: + if (usesViewFrustum) { + float lodScaleFactor = _traversal.getCurrentLODScaleFactor(); + glm::vec3 viewPosition = _traversal.getCurrentView().getPosition(); + _traversal.setScanCallback([=](DiffTraversal::VisibleElement& next) { + uint64_t startOfCompletedTraversal = _traversal.getStartOfCompletedTraversal(); + if (next.element->getLastChangedContent() > startOfCompletedTraversal) { + next.element->forEachEntity([=](EntityItemPointer entity) { + // Bail early if we've already checked this entity this frame + if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { + return; + } + auto knownTimestamp = _knownState.find(entity.get()); + if (knownTimestamp == _knownState.end()) { + bool success = false; + AACube cube = entity->getQueryAACube(success); + if (success) { + if (next.intersection == ViewFrustum::INSIDE || _traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { + // See the DiffTraversal::First case for an explanation of the "entity is too small" check + float distance = glm::distance(cube.calcCenter(), viewPosition) + MIN_VISIBLE_DISTANCE; + float angularDiameter = cube.getScale() / distance; + if (angularDiameter > MIN_ENTITY_ANGULAR_DIAMETER * lodScaleFactor) { + float priority = _conicalView.computePriority(cube); + _sendQueue.push(PrioritizedEntity(entity, priority)); + _entitiesInQueue.insert(entity.get()); + } + } + } else { + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); + _entitiesInQueue.insert(entity.get()); + } + } else if (entity->getLastEdited() > knownTimestamp->second) { + // it is known and it changed --> put it on the queue with any priority + // TODO: sort these correctly + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); + _entitiesInQueue.insert(entity.get()); + } + }); + } + }); + } else { + _traversal.setScanCallback([this](DiffTraversal::VisibleElement& next) { + uint64_t startOfCompletedTraversal = _traversal.getStartOfCompletedTraversal(); + if (next.element->getLastChangedContent() > startOfCompletedTraversal) { + next.element->forEachEntity([this](EntityItemPointer entity) { + // Bail early if we've already checked this entity this frame + if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { + return; + } + auto knownTimestamp = _knownState.find(entity.get()); + if (knownTimestamp == _knownState.end() || entity->getLastEdited() > knownTimestamp->second) { + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); + _entitiesInQueue.insert(entity.get()); + } + }); + } + }); + } + break; + case DiffTraversal::Differential: + assert(usesViewFrustum); + float lodScaleFactor = _traversal.getCurrentLODScaleFactor(); + glm::vec3 viewPosition = _traversal.getCurrentView().getPosition(); + float completedLODScaleFactor = _traversal.getCompletedLODScaleFactor(); + glm::vec3 completedViewPosition = _traversal.getCompletedView().getPosition(); + _traversal.setScanCallback([=] (DiffTraversal::VisibleElement& next) { + next.element->forEachEntity([=](EntityItemPointer entity) { + // Bail early if we've already checked this entity this frame + if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { + return; + } + auto knownTimestamp = _knownState.find(entity.get()); + if (knownTimestamp == _knownState.end()) { + bool success = false; + AACube cube = entity->getQueryAACube(success); + if (success) { + if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { + // See the DiffTraversal::First case for an explanation of the "entity is too small" check + float distance = glm::distance(cube.calcCenter(), viewPosition) + MIN_VISIBLE_DISTANCE; + float angularDiameter = cube.getScale() / distance; + if (angularDiameter > MIN_ENTITY_ANGULAR_DIAMETER * lodScaleFactor) { + if (!_traversal.getCompletedView().cubeIntersectsKeyhole(cube)) { + float priority = _conicalView.computePriority(cube); + _sendQueue.push(PrioritizedEntity(entity, priority)); + _entitiesInQueue.insert(entity.get()); + } else { + // If this entity was skipped last time because it was too small, we still need to send it + distance = glm::distance(cube.calcCenter(), completedViewPosition) + MIN_VISIBLE_DISTANCE; + angularDiameter = cube.getScale() / distance; + if (angularDiameter <= MIN_ENTITY_ANGULAR_DIAMETER * completedLODScaleFactor) { + // this object was skipped in last completed traversal + float priority = _conicalView.computePriority(cube); + _sendQueue.push(PrioritizedEntity(entity, priority)); + _entitiesInQueue.insert(entity.get()); + } + } + } + } + } else { + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); + _entitiesInQueue.insert(entity.get()); + } + } else if (entity->getLastEdited() > knownTimestamp->second) { + // it is known and it changed --> put it on the queue with any priority + // TODO: sort these correctly + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); + _entitiesInQueue.insert(entity.get()); + } + }); + }); + break; + } +} + +bool EntityTreeSendThread::traverseTreeAndBuildNextPacketPayload(EncodeBitstreamParams& params, const QJsonObject& jsonFilters) { + if (_sendQueue.empty()) { + OctreeServer::trackEncodeTime(OctreeServer::SKIP_TIME); + return false; + } + quint64 encodeStart = usecTimestampNow(); + if (!_packetData.hasContent()) { + // This is the beginning of a new packet. + // We pack minimal data for this to be accepted as an OctreeElement payload for the root element. + // The Octree header bytes look like this: + // + // 0x00 octalcode for root + // 0x00 colors (1 bit where recipient should call: child->readElementDataFromBuffer()) + // 0xXX childrenInTreeMask (when params.includeExistsBits is true: 1 bit where child is existant) + // 0x00 childrenInBufferMask (1 bit where recipient should call: child->readElementData() recursively) + const uint8_t zeroByte = 0; + _packetData.appendValue(zeroByte); // octalcode + _packetData.appendValue(zeroByte); // colors + if (params.includeExistsBits) { + uint8_t childrenExistBits = 0; + EntityTreeElementPointer root = std::dynamic_pointer_cast(_myServer->getOctree()->getRoot()); + for (int32_t i = 0; i < NUMBER_OF_CHILDREN; ++i) { + if (root->getChildAtIndex(i)) { + childrenExistBits += (1 << i); + } + } + _packetData.appendValue(childrenExistBits); // childrenInTreeMask + } + _packetData.appendValue(zeroByte); // childrenInBufferMask + + // Pack zero for numEntities. + // But before we do: grab current byteOffset so we can come back later + // and update this with the real number. + _numEntities = 0; + _numEntitiesOffset = _packetData.getUncompressedByteOffset(); + _packetData.appendValue(_numEntities); + } + + LevelDetails entitiesLevel = _packetData.startLevel(); + uint64_t sendTime = usecTimestampNow(); + auto nodeData = static_cast(params.nodeData); + nodeData->stats.encodeStarted(); + while(!_sendQueue.empty()) { + PrioritizedEntity queuedItem = _sendQueue.top(); + EntityItemPointer entity = queuedItem.getEntity(); + if (entity) { + // Only send entities that match the jsonFilters, but keep track of everything we've tried to send so we don't try to send it again + if (entity->matchesJSONFilters(jsonFilters)) { + OctreeElement::AppendState appendEntityState = entity->appendEntityData(&_packetData, params, _extraEncodeData); + + if (appendEntityState != OctreeElement::COMPLETED) { + if (appendEntityState == OctreeElement::PARTIAL) { + ++_numEntities; + } + params.stopReason = EncodeBitstreamParams::DIDNT_FIT; + break; + } + ++_numEntities; + } + if (queuedItem.shouldForceRemove()) { + _knownState.erase(entity.get()); + } else { + _knownState[entity.get()] = sendTime; + } + } + _sendQueue.pop(); + _entitiesInQueue.erase(entity.get()); + } + nodeData->stats.encodeStopped(); + if (_sendQueue.empty()) { + assert(_entitiesInQueue.empty()); + params.stopReason = EncodeBitstreamParams::FINISHED; + _extraEncodeData->entities.clear(); + } + + if (_numEntities == 0) { + _packetData.discardLevel(entitiesLevel); + OctreeServer::trackEncodeTime((float)(usecTimestampNow() - encodeStart)); + return false; + } + _packetData.endLevel(entitiesLevel); + _packetData.updatePriorBytes(_numEntitiesOffset, (const unsigned char*)&_numEntities, sizeof(_numEntities)); + OctreeServer::trackEncodeTime((float)(usecTimestampNow() - encodeStart)); + return true; +} + +void EntityTreeSendThread::editingEntityPointer(const EntityItemPointer& entity) { + if (entity) { + if (_entitiesInQueue.find(entity.get()) == _entitiesInQueue.end() && _knownState.find(entity.get()) != _knownState.end()) { + bool success = false; + AACube cube = entity->getQueryAACube(success); + if (success) { + // We can force a removal from _knownState if the current view is used and entity is out of view + if (_traversal.doesCurrentUseViewFrustum() && !_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::FORCE_REMOVE, true)); + _entitiesInQueue.insert(entity.get()); + } + } else { + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY, true)); + _entitiesInQueue.insert(entity.get()); + } + } + } +} + +void EntityTreeSendThread::deletingEntityPointer(EntityItem* entity) { + _knownState.erase(entity); +} diff --git a/assignment-client/src/entities/EntityTreeSendThread.h b/assignment-client/src/entities/EntityTreeSendThread.h index bfb4c743f1..49901491ff 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.h +++ b/assignment-client/src/entities/EntityTreeSendThread.h @@ -12,24 +12,55 @@ #ifndef hifi_EntityTreeSendThread_h #define hifi_EntityTreeSendThread_h +#include + #include "../octree/OctreeSendThread.h" +#include + +#include "EntityPriorityQueue.h" + class EntityNodeData; class EntityItem; class EntityTreeSendThread : public OctreeSendThread { + Q_OBJECT public: - EntityTreeSendThread(OctreeServer* myServer, const SharedNodePointer& node) : OctreeSendThread(myServer, node) {}; + EntityTreeSendThread(OctreeServer* myServer, const SharedNodePointer& node); protected: - virtual void preDistributionProcessing() override; + void traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData, + bool viewFrustumChanged, bool isFullScene) override; private: // the following two methods return booleans to indicate if any extra flagged entities were new additions to set bool addAncestorsToExtraFlaggedEntities(const QUuid& filteredEntityID, EntityItem& entityItem, EntityNodeData& nodeData); bool addDescendantsToExtraFlaggedEntities(const QUuid& filteredEntityID, EntityItem& entityItem, EntityNodeData& nodeData); + void startNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root, int32_t lodLevelOffset, bool usesViewFrustum); + bool traverseTreeAndBuildNextPacketPayload(EncodeBitstreamParams& params, const QJsonObject& jsonFilters) override; + + void preDistributionProcessing() override; + bool hasSomethingToSend(OctreeQueryNode* nodeData) override { return !_sendQueue.empty(); } + bool shouldStartNewTraversal(OctreeQueryNode* nodeData, bool viewFrustumChanged) override { return viewFrustumChanged || _traversal.finished(); } + void preStartNewScene(OctreeQueryNode* nodeData, bool isFullScene) override {}; + bool shouldTraverseAndSend(OctreeQueryNode* nodeData) override { return true; } + + DiffTraversal _traversal; + EntityPriorityQueue _sendQueue; + std::unordered_set _entitiesInQueue; + std::unordered_map _knownState; + ConicalView _conicalView; // cached optimized view for fast priority calculations + + // packet construction stuff + EntityTreeElementExtraEncodeDataPointer _extraEncodeData { new EntityTreeElementExtraEncodeData() }; + int32_t _numEntitiesOffset { 0 }; + uint16_t _numEntities { 0 }; + +private slots: + void editingEntityPointer(const EntityItemPointer& entity); + void deletingEntityPointer(EntityItem* entity); }; #endif // hifi_EntityTreeSendThread_h diff --git a/assignment-client/src/octree/OctreeSendThread.cpp b/assignment-client/src/octree/OctreeSendThread.cpp index 9345f96b1d..89e3d403fc 100644 --- a/assignment-client/src/octree/OctreeSendThread.cpp +++ b/assignment-client/src/octree/OctreeSendThread.cpp @@ -17,7 +17,6 @@ #include #include -#include "OctreeQueryNode.h" #include "OctreeSendThread.h" #include "OctreeServer.h" #include "OctreeServerConsts.h" @@ -27,8 +26,8 @@ quint64 startSceneSleepTime = 0; quint64 endSceneSleepTime = 0; OctreeSendThread::OctreeSendThread(OctreeServer* myServer, const SharedNodePointer& node) : - _myServer(myServer), _node(node), + _myServer(myServer), _nodeUuid(node->getUUID()) { QString safeServerName("Octree"); @@ -48,7 +47,7 @@ OctreeSendThread::OctreeSendThread(OctreeServer* myServer, const SharedNodePoint OctreeSendThread::~OctreeSendThread() { setIsShuttingDown(); - + QString safeServerName("Octree"); if (_myServer) { safeServerName = _myServer->getMyServerName(); @@ -301,9 +300,25 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* return numPackets; } +void OctreeSendThread::preStartNewScene(OctreeQueryNode* nodeData, bool isFullScene) { + // If we're starting a full scene, then definitely we want to empty the elementBag + if (isFullScene) { + nodeData->elementBag.deleteAll(); + } + + // This is the start of "resending" the scene. + bool dontRestartSceneOnMove = false; // this is experimental + if (dontRestartSceneOnMove) { + if (nodeData->elementBag.isEmpty()) { + nodeData->elementBag.insert(_myServer->getOctree()->getRoot()); + } + } else { + nodeData->elementBag.insert(_myServer->getOctree()->getRoot()); + } +} + /// Version of octree element distributor that sends the deepest LOD level at once int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged) { - OctreeServer::didPacketDistributor(this); // if shutting down, exit early @@ -311,7 +326,7 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* return 0; } - if (nodeData->elementBag.isEmpty()) { + if (shouldStartNewTraversal(nodeData, viewFrustumChanged)) { // if we're about to do a fresh pass, // give our pre-distribution processing a chance to do what it needs preDistributionProcessing(); @@ -345,7 +360,7 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* // If the current view frustum has changed OR we have nothing to send, then search against // the current view frustum for things to send. - if (viewFrustumChanged || nodeData->elementBag.isEmpty()) { + if (shouldStartNewTraversal(nodeData, viewFrustumChanged)) { // if our view has changed, we need to reset these things... if (viewFrustumChanged) { @@ -367,11 +382,6 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* _packetsSentThisInterval += handlePacketSend(node, nodeData, isFullScene); - // If we're starting a full scene, then definitely we want to empty the elementBag - if (isFullScene) { - nodeData->elementBag.deleteAll(); - } - // TODO: add these to stats page //::startSceneSleepTime = _usleepTime; @@ -380,19 +390,11 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* nodeData->stats.sceneStarted(isFullScene, viewFrustumChanged, _myServer->getOctree()->getRoot(), _myServer->getJurisdiction()); - // This is the start of "resending" the scene. - bool dontRestartSceneOnMove = false; // this is experimental - if (dontRestartSceneOnMove) { - if (nodeData->elementBag.isEmpty()) { - nodeData->elementBag.insert(_myServer->getOctree()->getRoot()); - } - } else { - nodeData->elementBag.insert(_myServer->getOctree()->getRoot()); - } + preStartNewScene(nodeData, isFullScene); } // If we have something in our elementBag, then turn them into packets and send them out... - if (!nodeData->elementBag.isEmpty()) { + if (shouldTraverseAndSend(nodeData)) { quint64 start = usecTimestampNow(); traverseTreeAndSendContents(node, nodeData, viewFrustumChanged, isFullScene); @@ -441,7 +443,7 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* // if after sending packets we've emptied our bag, then we want to remember that we've sent all // the octree elements from the current view frustum - if (nodeData->elementBag.isEmpty()) { + if (!hasSomethingToSend(nodeData)) { nodeData->updateLastKnownViewFrustum(); nodeData->setViewSent(true); @@ -458,7 +460,7 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* return _truePacketsSent; } -bool OctreeSendThread::traverseTreeAndBuildNextPacketPayload(EncodeBitstreamParams& params) { +bool OctreeSendThread::traverseTreeAndBuildNextPacketPayload(EncodeBitstreamParams& params, const QJsonObject& jsonFilters) { bool somethingToSend = false; OctreeQueryNode* nodeData = static_cast(params.nodeData); if (!nodeData->elementBag.isEmpty()) { @@ -502,8 +504,7 @@ void OctreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, Octre EncodeBitstreamParams params(INT_MAX, WANT_EXISTS_BITS, DONT_CHOP, viewFrustumChanged, boundaryLevelAdjust, octreeSizeScale, isFullScene, _myServer->getJurisdiction(), nodeData); - // Our trackSend() function is implemented by the server subclass, and will be called back - // during the encodeTreeBitstream() as new entities/data elements are sent + // Our trackSend() function is implemented by the server subclass, and will be called back as new entities/data elements are sent params.trackSend = [this](const QUuid& dataID, quint64 dataEdited) { _myServer->trackSend(dataID, dataEdited, _nodeUuid); }; @@ -513,7 +514,7 @@ void OctreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, Octre } bool somethingToSend = true; // assume we have something - bool bagHadSomething = !nodeData->elementBag.isEmpty(); + bool hadSomething = hasSomethingToSend(nodeData); while (somethingToSend && _packetsSentThisInterval < maxPacketsPerInterval && !nodeData->isShuttingDown()) { float compressAndWriteElapsedUsec = OctreeServer::SKIP_TIME; float packetSendingElapsedUsec = OctreeServer::SKIP_TIME; @@ -523,7 +524,7 @@ void OctreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, Octre bool lastNodeDidntFit = false; // assume each node fits params.stopReason = EncodeBitstreamParams::UNKNOWN; // reset params.stopReason before traversal - somethingToSend = traverseTreeAndBuildNextPacketPayload(params); + somethingToSend = traverseTreeAndBuildNextPacketPayload(params, nodeData->getJSONParameters()); if (params.stopReason == EncodeBitstreamParams::DIDNT_FIT) { lastNodeDidntFit = true; @@ -531,7 +532,7 @@ void OctreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, Octre } // If the bag had contents but is now empty then we know we've sent the entire scene. - bool completedScene = bagHadSomething && nodeData->elementBag.isEmpty(); + bool completedScene = hadSomething && nodeData->elementBag.isEmpty(); if (completedScene || lastNodeDidntFit) { // we probably want to flush what has accumulated in nodeData but: // do we have more data to send? and is there room? diff --git a/assignment-client/src/octree/OctreeSendThread.h b/assignment-client/src/octree/OctreeSendThread.h index 8f75092528..bc7d2c2588 100644 --- a/assignment-client/src/octree/OctreeSendThread.h +++ b/assignment-client/src/octree/OctreeSendThread.h @@ -19,6 +19,7 @@ #include #include #include +#include "OctreeQueryNode.h" class OctreeQueryNode; class OctreeServer; @@ -51,24 +52,27 @@ protected: /// Implements generic processing behavior for this thread. virtual bool process() override; - /// Called before a packetDistributor pass to allow for pre-distribution processing - virtual void preDistributionProcessing() {}; virtual void traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged, bool isFullScene); - virtual bool traverseTreeAndBuildNextPacketPayload(EncodeBitstreamParams& params); + virtual bool traverseTreeAndBuildNextPacketPayload(EncodeBitstreamParams& params, const QJsonObject& jsonFilters); - OctreeServer* _myServer { nullptr }; + OctreePacketData _packetData; QWeakPointer _node; + OctreeServer* _myServer { nullptr }; private: + /// Called before a packetDistributor pass to allow for pre-distribution processing + virtual void preDistributionProcessing() {}; int handlePacketSend(SharedNodePointer node, OctreeQueryNode* nodeData, bool dontSuppressDuplicate = false); int packetDistributor(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged); + virtual bool hasSomethingToSend(OctreeQueryNode* nodeData) { return !nodeData->elementBag.isEmpty(); } + virtual bool shouldStartNewTraversal(OctreeQueryNode* nodeData, bool viewFrustumChanged) { return viewFrustumChanged || !hasSomethingToSend(nodeData); } + virtual void preStartNewScene(OctreeQueryNode* nodeData, bool isFullScene); + virtual bool shouldTraverseAndSend(OctreeQueryNode* nodeData) { return hasSomethingToSend(nodeData); } QUuid _nodeUuid; - OctreePacketData _packetData; - int _truePacketsSent { 0 }; // available for debug stats int _trueBytesSent { 0 }; // available for debug stats int _packetsSentThisInterval { 0 }; // used for bandwidth throttle condition diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index 974f00326b..4a1aade59d 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -60,6 +60,8 @@ int OctreeServer::_longTreeWait = 0; int OctreeServer::_shortTreeWait = 0; int OctreeServer::_noTreeWait = 0; +SimpleMovingAverage OctreeServer::_averageTreeTraverseTime(MOVING_AVERAGE_SAMPLE_COUNTS); + SimpleMovingAverage OctreeServer::_averageNodeWaitTime(MOVING_AVERAGE_SAMPLE_COUNTS); SimpleMovingAverage OctreeServer::_averageCompressAndWriteTime(MOVING_AVERAGE_SAMPLE_COUNTS); @@ -106,6 +108,8 @@ void OctreeServer::resetSendingStats() { _shortTreeWait = 0; _noTreeWait = 0; + _averageTreeTraverseTime.reset(); + _averageNodeWaitTime.reset(); _averageCompressAndWriteTime.reset(); @@ -522,6 +526,10 @@ bool OctreeServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url (double)_averageTreeExtraLongWaitTime.getAverage(), (double)(extraLongVsTotal * AS_PERCENT), _extraLongTreeWait); + // traverse + float averageTreeTraverseTime = getAverageTreeTraverseTime(); + statsString += QString().sprintf(" Average tree traverse time: %9.2f usecs\r\n\r\n", (double)averageTreeTraverseTime); + // encode float averageEncodeTime = getAverageEncodeTime(); statsString += QString().sprintf(" Average encode time: %9.2f usecs\r\n", (double)averageEncodeTime); @@ -883,7 +891,7 @@ OctreeServer::UniqueSendThread OctreeServer::newSendThread(const SharedNodePoint OctreeServer::UniqueSendThread OctreeServer::createSendThread(const SharedNodePointer& node) { auto sendThread = newSendThread(node); - + // we want to be notified when the thread finishes connect(sendThread.get(), &GenericThread::finished, this, &OctreeServer::removeSendThread); sendThread->initialize(true); @@ -905,13 +913,13 @@ void OctreeServer::handleOctreeQueryPacket(QSharedPointer messa // need to make sure we have it in our nodeList. auto nodeList = DependencyManager::get(); nodeList->updateNodeWithDataFromPacket(message, senderNode); - + auto it = _sendThreads.find(senderNode->getUUID()); if (it == _sendThreads.end()) { _sendThreads.emplace(senderNode->getUUID(), createSendThread(senderNode)); } else if (it->second->isShuttingDown()) { _sendThreads.erase(it); // Remove right away and wait on thread to be - + _sendThreads.emplace(senderNode->getUUID(), createSendThread(senderNode)); } } @@ -1085,7 +1093,7 @@ void OctreeServer::readConfiguration() { if (getPayload().size() > 0) { parsePayload(); } - + const QJsonObject& settingsObject = DependencyManager::get()->getDomainHandler().getSettingsObject(); QString settingsKey = getMyDomainSettingsKey(); @@ -1212,9 +1220,9 @@ void OctreeServer::run() { OctreeElement::resetPopulationStatistics(); _tree = createTree(); _tree->setIsServer(true); - + qDebug() << "Waiting for connection to domain to request settings from domain-server."; - + // wait until we have the domain-server settings, otherwise we bail DomainHandler& domainHandler = DependencyManager::get()->getDomainHandler(); connect(&domainHandler, &DomainHandler::settingsReceived, this, &OctreeServer::domainSettingsRequestComplete); @@ -1225,9 +1233,9 @@ void OctreeServer::run() { } void OctreeServer::domainSettingsRequestComplete() { - + auto nodeList = DependencyManager::get(); - + // we need to ask the DS about agents so we can ping/reply with them nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer }); @@ -1237,26 +1245,26 @@ void OctreeServer::domainSettingsRequestComplete() { packetReceiver.registerListener(PacketType::JurisdictionRequest, this, "handleJurisdictionRequestPacket"); packetReceiver.registerListener(PacketType::OctreeFileReplacement, this, "handleOctreeFileReplacement"); packetReceiver.registerListener(PacketType::OctreeFileReplacementFromUrl, this, "handleOctreeFileReplacementFromURL"); - + readConfiguration(); - + beforeRun(); // after payload has been processed - + connect(nodeList.data(), SIGNAL(nodeAdded(SharedNodePointer)), SLOT(nodeAdded(SharedNodePointer))); connect(nodeList.data(), SIGNAL(nodeKilled(SharedNodePointer)), SLOT(nodeKilled(SharedNodePointer))); #ifndef WIN32 setvbuf(stdout, NULL, _IOLBF, 0); #endif - + nodeList->linkedDataCreateCallback = [this](Node* node) { auto queryNodeData = createOctreeQueryNode(); queryNodeData->init(); node->setLinkedData(std::move(queryNodeData)); }; - + srand((unsigned)time(0)); - + // if we want Persistence, set up the local file and persist thread if (_wantPersist) { // If persist filename does not exist, let's see if there is one beside the application binary @@ -1351,24 +1359,24 @@ void OctreeServer::domainSettingsRequestComplete() { } } qDebug() << "Backups will be stored in: " << _backupDirectoryPath; - + // now set up PersistThread _persistThread = new OctreePersistThread(_tree, _persistAbsoluteFilePath, _backupDirectoryPath, _persistInterval, _wantBackup, _settings, _debugTimestampNow, _persistAsFileType); _persistThread->initialize(true); } - + // set up our jurisdiction broadcaster... if (_jurisdiction) { _jurisdiction->setNodeType(getMyNodeType()); } _jurisdictionSender = new JurisdictionSender(_jurisdiction, getMyNodeType()); _jurisdictionSender->initialize(true); - + // set up our OctreeServerPacketProcessor _octreeInboundPacketProcessor = new OctreeInboundPacketProcessor(this); _octreeInboundPacketProcessor->initialize(true); - + // Convert now to tm struct for local timezone tm* localtm = localtime(&_started); const int MAX_TIME_LENGTH = 128; @@ -1380,7 +1388,7 @@ void OctreeServer::domainSettingsRequestComplete() { if (gmtm) { strftime(utcBuffer, MAX_TIME_LENGTH, " [%m/%d/%Y %X UTC]", gmtm); } - + qDebug() << "Now running... started at: " << localBuffer << utcBuffer; } @@ -1391,7 +1399,7 @@ void OctreeServer::nodeAdded(SharedNodePointer node) { void OctreeServer::nodeKilled(SharedNodePointer node) { quint64 start = usecTimestampNow(); - + // Shutdown send thread auto it = _sendThreads.find(node->getUUID()); if (it != _sendThreads.end()) { @@ -1437,13 +1445,13 @@ void OctreeServer::aboutToFinish() { if (_jurisdictionSender) { _jurisdictionSender->terminating(); } - + // Shut down all the send threads for (auto& it : _sendThreads) { auto& sendThread = *it.second; sendThread.setIsShuttingDown(); } - + // Clear will destruct all the unique_ptr to OctreeSendThreads which will call the GenericThread's dtor // which waits on the thread to be done before returning _sendThreads.clear(); // Cleans up all the send threads. @@ -1563,7 +1571,7 @@ void OctreeServer::sendStatsPacket() { threadsStats["2. packetDistributor"] = (double)howManyThreadsDidPacketDistributor(oneSecondAgo); threadsStats["3. handlePacektSend"] = (double)howManyThreadsDidHandlePacketSend(oneSecondAgo); threadsStats["4. writeDatagram"] = (double)howManyThreadsDidCallWriteDatagram(oneSecondAgo); - + QJsonObject statsArray1; statsArray1["1. configuration"] = getConfiguration(); statsArray1["2. detailed_stats_url"] = getStatusLink(); @@ -1571,13 +1579,13 @@ void OctreeServer::sendStatsPacket() { statsArray1["4. persistFileLoadTime"] = getFileLoadTime(); statsArray1["5. clients"] = getCurrentClientCount(); statsArray1["6. threads"] = threadsStats; - + // Octree Stats QJsonObject octreeStats; octreeStats["1. elementCount"] = (double)OctreeElement::getNodeCount(); octreeStats["2. internalElementCount"] = (double)OctreeElement::getInternalNodeCount(); octreeStats["3. leafElementCount"] = (double)OctreeElement::getLeafNodeCount(); - + // Stats Object 2 QJsonObject dataObject1; dataObject1["1. totalPackets"] = (double)OctreeSendThread::_totalPackets; @@ -1590,12 +1598,12 @@ void OctreeServer::sendStatsPacket() { QJsonObject timingArray1; timingArray1["1. avgLoopTime"] = getAverageLoopTime(); timingArray1["2. avgInsideTime"] = getAverageInsideTime(); - timingArray1["3. avgTreeLockTime"] = getAverageTreeWaitTime(); + timingArray1["3. avgTreeTraverseTime"] = getAverageTreeTraverseTime(); timingArray1["4. avgEncodeTime"] = getAverageEncodeTime(); timingArray1["5. avgCompressAndWriteTime"] = getAverageCompressAndWriteTime(); timingArray1["6. avgSendTime"] = getAveragePacketSendingTime(); timingArray1["7. nodeWaitTime"] = getAverageNodeWaitTime(); - + QJsonObject statsObject2; statsObject2["data"] = dataObject1; statsObject2["timing"] = timingArray1; @@ -1615,18 +1623,18 @@ void OctreeServer::sendStatsPacket() { timingArray2["4. avgProcessTimePerElement"] = (double)_octreeInboundPacketProcessor->getAverageProcessTimePerElement(); timingArray2["5. avgLockWaitTimePerElement"] = (double)_octreeInboundPacketProcessor->getAverageLockWaitTimePerElement(); } - + QJsonObject statsObject3; statsObject3["data"] = dataArray2; statsObject3["timing"] = timingArray2; - + // Merge everything QJsonObject jsonArray; jsonArray["1. misc"] = statsArray1; jsonArray["2. octree"] = octreeStats; jsonArray["3. outbound"] = statsObject2; jsonArray["4. inbound"] = statsObject3; - + QJsonObject statsObject; statsObject[QString(getMyServerName()) + "Server"] = jsonArray; addPacketStatsAndSendStatsPacket(statsObject); diff --git a/assignment-client/src/octree/OctreeServer.h b/assignment-client/src/octree/OctreeServer.h index 5043ea681c..f930f299f3 100644 --- a/assignment-client/src/octree/OctreeServer.h +++ b/assignment-client/src/octree/OctreeServer.h @@ -96,6 +96,9 @@ public: static void trackTreeWaitTime(float time); static float getAverageTreeWaitTime() { return _averageTreeWaitTime.getAverage(); } + static void trackTreeTraverseTime(float time) { _averageTreeTraverseTime.updateAverage(time); } + static float getAverageTreeTraverseTime() { return _averageTreeTraverseTime.getAverage(); } + static void trackNodeWaitTime(float time) { _averageNodeWaitTime.updateAverage(time); } static float getAverageNodeWaitTime() { return _averageNodeWaitTime.getAverage(); } @@ -228,6 +231,8 @@ protected: static int _shortTreeWait; static int _noTreeWait; + static SimpleMovingAverage _averageTreeTraverseTime; + static SimpleMovingAverage _averageNodeWaitTime; static SimpleMovingAverage _averageCompressAndWriteTime; diff --git a/interface/resources/qml/Stats.qml b/interface/resources/qml/Stats.qml index 119f24c71f..96e267f67f 100644 --- a/interface/resources/qml/Stats.qml +++ b/interface/resources/qml/Stats.qml @@ -206,6 +206,10 @@ Item { text: "Audio Codec: " + root.audioCodec + " Noise Gate: " + root.audioNoiseGate; } + StatText { + visible: root.expanded; + text: "Entity Servers In: " + root.entityPacketsInKbps + " kbps"; + } StatText { visible: root.expanded; text: "Downloads: " + root.downloads + "/" + root.downloadLimit + diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 85172fc73f..36e5b3d859 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4696,12 +4696,8 @@ void Application::resetPhysicsReadyInformation() { void Application::reloadResourceCaches() { resetPhysicsReadyInformation(); - { - QMutexLocker viewLocker(&_viewMutex); - _viewFrustum.setPosition(glm::vec3(0.0f, 0.0f, TREE_SCALE)); - _viewFrustum.setOrientation(glm::quat()); - } - // Clear entities out of view frustum + // Query the octree to refresh everything in view + _lastQueriedTime = 0; queryOctree(NodeType::EntityServer, PacketType::EntityQuery, _entityServerJurisdictions); DependencyManager::get()->clearCache(); @@ -4859,13 +4855,9 @@ void Application::update(float deltaTime) { // we haven't yet enabled physics. we wait until we think we have all the collision information // for nearby entities before starting bullet up. quint64 now = usecTimestampNow(); - bool timeout = false; const int PHYSICS_CHECK_TIMEOUT = 2 * USECS_PER_SECOND; - if (_lastPhysicsCheckTime > 0 && now - _lastPhysicsCheckTime > PHYSICS_CHECK_TIMEOUT) { - timeout = true; - } - if (timeout || _fullSceneReceivedCounter > _fullSceneCounterAtLastPhysicsCheck) { + if (now - _lastPhysicsCheckTime > PHYSICS_CHECK_TIMEOUT || _fullSceneReceivedCounter > _fullSceneCounterAtLastPhysicsCheck) { // we've received a new full-scene octree stats packet, or it's been long enough to try again anyway _lastPhysicsCheckTime = now; _fullSceneCounterAtLastPhysicsCheck = _fullSceneReceivedCounter; @@ -5303,7 +5295,7 @@ int Application::sendNackPackets() { return packetsSent; } -void Application::queryOctree(NodeType_t serverType, PacketType packetType, NodeToJurisdictionMap& jurisdictions, bool forceResend) { +void Application::queryOctree(NodeType_t serverType, PacketType packetType, NodeToJurisdictionMap& jurisdictions) { if (!_settingsLoaded) { return; // bail early if settings are not loaded @@ -5459,16 +5451,6 @@ void Application::queryOctree(NodeType_t serverType, PacketType packetType, Node _octreeQuery.setMaxQueryPacketsPerSecond(0); } - // if asked to forceResend, then set the query's position/orientation to be degenerate in a manner - // that will cause our next query to be guarenteed to be different and the server will resend to us - if (forceResend) { - _octreeQuery.setCameraPosition(glm::vec3(-0.1, -0.1, -0.1)); - const glm::quat OFF_IN_NEGATIVE_SPACE = glm::quat(-0.5, 0, -0.5, 1.0); - _octreeQuery.setCameraOrientation(OFF_IN_NEGATIVE_SPACE); - _octreeQuery.setCameraNearClip(0.1f); - _octreeQuery.setCameraFarClip(0.1f); - } - // encode the query data int packetSize = _octreeQuery.getBroadcastData(reinterpret_cast(queryPacket->getPayload())); queryPacket->setPayloadSize(packetSize); @@ -5717,8 +5699,6 @@ void Application::clearDomainOctreeDetails() { skyStage->setBackgroundMode(model::SunSkyStage::SKY_DEFAULT); - _recentlyClearedDomain = true; - DependencyManager::get()->clearUnusedResources(); DependencyManager::get()->clearUnusedResources(); DependencyManager::get()->clearUnusedResources(); @@ -5765,14 +5745,10 @@ void Application::nodeActivated(SharedNodePointer node) { } } - // If we get a new EntityServer activated, do a "forceRedraw" query. This will send a degenerate - // query so that the server will think our next non-degenerate query is "different enough" to send - // us a full scene - if (_recentlyClearedDomain && node->getType() == NodeType::EntityServer) { - _recentlyClearedDomain = false; - if (DependencyManager::get()->shouldRenderEntities()) { - queryOctree(NodeType::EntityServer, PacketType::EntityQuery, _entityServerJurisdictions, true); - } + // If we get a new EntityServer activated, reset lastQueried time + // so we will do a proper query during update + if (node->getType() == NodeType::EntityServer) { + _lastQueriedTime = 0; } if (node->getType() == NodeType::AudioMixer) { diff --git a/interface/src/Application.h b/interface/src/Application.h index 74e84ae92c..a706ce2b63 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -467,7 +467,7 @@ private: void updateThreads(float deltaTime); void updateDialogs(float deltaTime) const; - void queryOctree(NodeType_t serverType, PacketType packetType, NodeToJurisdictionMap& jurisdictions, bool forceResend = false); + void queryOctree(NodeType_t serverType, PacketType packetType, NodeToJurisdictionMap& jurisdictions); void renderRearViewMirror(RenderArgs* renderArgs, const QRect& region, bool isZoomed); @@ -659,12 +659,10 @@ private: uint32_t _fullSceneCounterAtLastPhysicsCheck { 0 }; // _fullSceneReceivedCounter last time we checked physics ready uint32_t _nearbyEntitiesCountAtLastPhysicsCheck { 0 }; // how many in-range entities last time we checked physics ready uint32_t _nearbyEntitiesStabilityCount { 0 }; // how many times has _nearbyEntitiesCountAtLastPhysicsCheck been the same - quint64 _lastPhysicsCheckTime { 0 }; // when did we last check to see if physics was ready + quint64 _lastPhysicsCheckTime { usecTimestampNow() }; // when did we last check to see if physics was ready bool _keyboardDeviceHasFocus { true }; - bool _recentlyClearedDomain { false }; - QString _returnFromFullScreenMirrorTo; ConnectionMonitor _connectionMonitor; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 19179d613d..10e2202553 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -584,7 +584,7 @@ void MyAvatar::simulate(float deltaTime) { } auto now = usecTimestampNow(); EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender(); - MovingEntitiesOperator moveOperator(entityTree); + MovingEntitiesOperator moveOperator; forEachDescendant([&](SpatiallyNestablePointer object) { // if the queryBox has changed, tell the entity-server if (object->getNestableType() == NestableType::Entity && object->checkAndMaybeUpdateQueryAACube()) { diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index 8e3636dd7e..767e499503 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -180,10 +180,12 @@ void Stats::updateStats(bool force) { int totalPingOctree = 0; int octreeServerCount = 0; int pingOctreeMax = 0; + int totalEntityKbps = 0; nodeList->eachNode([&](const SharedNodePointer& node) { // TODO: this should also support entities if (node->getType() == NodeType::EntityServer) { totalPingOctree += node->getPingMs(); + totalEntityKbps += node->getInboundBandwidth(); octreeServerCount++; if (pingOctreeMax < node->getPingMs()) { pingOctreeMax = node->getPingMs(); @@ -248,6 +250,7 @@ void Stats::updateStats(bool force) { STAT_UPDATE(audioCodec, audioClient->getSelectedAudioFormat()); STAT_UPDATE(audioNoiseGate, audioClient->getNoiseGateOpen() ? "Open" : "Closed"); + STAT_UPDATE(entityPacketsInKbps, octreeServerCount ? totalEntityKbps / octreeServerCount : -1); auto loadingRequests = ResourceCache::getLoadingRequests(); STAT_UPDATE(downloads, loadingRequests.size()); diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index 74d2589c35..b3c920d4ef 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -85,6 +85,7 @@ class Stats : public QQuickItem { STATS_PROPERTY(int, audioPacketLoss, 0) STATS_PROPERTY(QString, audioCodec, QString()) STATS_PROPERTY(QString, audioNoiseGate, QString()) + STATS_PROPERTY(int, entityPacketsInKbps, 0) STATS_PROPERTY(int, downloads, 0) STATS_PROPERTY(int, downloadLimit, 0) @@ -212,6 +213,7 @@ signals: void audioPacketLossChanged(); void audioCodecChanged(); void audioNoiseGateChanged(); + void entityPacketsInKbpsChanged(); void downloadsChanged(); void downloadLimitChanged(); diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index ca5ca54144..c857ad97ab 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -9,6 +9,9 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include +#include + #include "ModelOverlay.h" #include @@ -60,6 +63,15 @@ void ModelOverlay::update(float deltatime) { _model->simulate(deltatime); } _isLoaded = _model->isActive(); + + + if (isAnimatingSomething()) { + if (!jointsMapped()) { + mapAnimationJoints(_model->getJointNames()); + } + animate(); + } + } bool ModelOverlay::addToScene(Overlay::Pointer overlay, const render::ScenePointer& scene, render::Transaction& transaction) { @@ -172,6 +184,51 @@ void ModelOverlay::setProperties(const QVariantMap& properties) { } _updateModel = true; } + + auto animationSettings = properties["animationSettings"]; + if (animationSettings.canConvert(QVariant::Map)) { + QVariantMap animationSettingsMap = animationSettings.toMap(); + + auto animationURL = animationSettingsMap["url"]; + auto animationFPS = animationSettingsMap["fps"]; + auto animationCurrentFrame = animationSettingsMap["currentFrame"]; + auto animationFirstFrame = animationSettingsMap["firstFrame"]; + auto animationLastFrame = animationSettingsMap["lastFrame"]; + auto animationRunning = animationSettingsMap["running"]; + auto animationLoop = animationSettingsMap["loop"]; + auto animationHold = animationSettingsMap["hold"]; + auto animationAllowTranslation = animationSettingsMap["allowTranslation"]; + + if (animationURL.canConvert(QVariant::Url)) { + _animationURL = animationURL.toUrl(); + } + if (animationFPS.isValid()) { + _animationFPS = animationFPS.toFloat(); + } + if (animationCurrentFrame.isValid()) { + _animationCurrentFrame = animationCurrentFrame.toFloat(); + } + if (animationFirstFrame.isValid()) { + _animationFirstFrame = animationFirstFrame.toFloat(); + } + if (animationLastFrame.isValid()) { + _animationLastFrame = animationLastFrame.toFloat(); + } + + if (animationRunning.canConvert(QVariant::Bool)) { + _animationRunning = animationRunning.toBool(); + } + if (animationLoop.canConvert(QVariant::Bool)) { + _animationLoop = animationLoop.toBool(); + } + if (animationHold.canConvert(QVariant::Bool)) { + _animationHold = animationHold.toBool(); + } + if (animationAllowTranslation.canConvert(QVariant::Bool)) { + _animationAllowTranslation = animationAllowTranslation.toBool(); + } + + } } template @@ -259,6 +316,24 @@ QVariant ModelOverlay::getProperty(const QString& property) { }); } + // animation properties + if (property == "animationSettings") { + QVariantMap animationSettingsMap; + + animationSettingsMap["url"] = _animationURL; + animationSettingsMap["fps"] = _animationFPS; + animationSettingsMap["currentFrame"] = _animationCurrentFrame; + animationSettingsMap["firstFrame"] = _animationFirstFrame; + animationSettingsMap["lastFrame"] = _animationLastFrame; + animationSettingsMap["running"] = _animationRunning; + animationSettingsMap["loop"] = _animationLoop; + animationSettingsMap["hold"]= _animationHold; + animationSettingsMap["allowTranslation"] = _animationAllowTranslation; + + return animationSettingsMap; + } + + return Volume3DOverlay::getProperty(property); } @@ -301,3 +376,134 @@ QString ModelOverlay::getName() const { } return QString("Overlay:") + getType() + ":" + _url.toString(); } + + +void ModelOverlay::animate() { + + if (!_animation || !_animation->isLoaded() || !_model || !_model->isLoaded()) { + return; + } + + + QVector jointsData; + + const QVector& frames = _animation->getFramesReference(); // NOTE: getFrames() is too heavy + int frameCount = frames.size(); + if (frameCount <= 0) { + return; + } + + if (!_lastAnimated) { + _lastAnimated = usecTimestampNow(); + return; + } + + auto now = usecTimestampNow(); + auto interval = now - _lastAnimated; + _lastAnimated = now; + float deltaTime = (float)interval / (float)USECS_PER_SECOND; + _animationCurrentFrame += (deltaTime * _animationFPS); + + int animationCurrentFrame = (int)(glm::floor(_animationCurrentFrame)) % frameCount; + if (animationCurrentFrame < 0 || animationCurrentFrame > frameCount) { + animationCurrentFrame = 0; + } + + if (animationCurrentFrame == _lastKnownCurrentFrame) { + return; + } + _lastKnownCurrentFrame = animationCurrentFrame; + + if (_jointMapping.size() != _model->getJointStateCount()) { + return; + } + + QStringList animationJointNames = _animation->getGeometry().getJointNames(); + auto& fbxJoints = _animation->getGeometry().joints; + + auto& originalFbxJoints = _model->getFBXGeometry().joints; + auto& originalFbxIndices = _model->getFBXGeometry().jointIndices; + + const QVector& rotations = frames[_lastKnownCurrentFrame].rotations; + const QVector& translations = frames[_lastKnownCurrentFrame].translations; + + jointsData.resize(_jointMapping.size()); + for (int j = 0; j < _jointMapping.size(); j++) { + int index = _jointMapping[j]; + + if (index >= 0) { + glm::mat4 translationMat; + + if (_animationAllowTranslation) { + if (index < translations.size()) { + translationMat = glm::translate(translations[index]); + } + } else if (index < animationJointNames.size()) { + QString jointName = fbxJoints[index].name; + + if (originalFbxIndices.contains(jointName)) { + // Making sure the joint names exist in the original model the animation is trying to apply onto. If they do, then remap and get its translation. + int remappedIndex = originalFbxIndices[jointName] - 1; // JointIndeces seem to always start from 1 and the found index is always 1 higher than actual. + translationMat = glm::translate(originalFbxJoints[remappedIndex].translation); + } + } + glm::mat4 rotationMat; + if (index < rotations.size()) { + rotationMat = glm::mat4_cast(fbxJoints[index].preRotation * rotations[index] * fbxJoints[index].postRotation); + } else { + rotationMat = glm::mat4_cast(fbxJoints[index].preRotation * fbxJoints[index].postRotation); + } + + glm::mat4 finalMat = (translationMat * fbxJoints[index].preTransform * + rotationMat * fbxJoints[index].postTransform); + auto& jointData = jointsData[j]; + jointData.translation = extractTranslation(finalMat); + jointData.translationSet = true; + jointData.rotation = glmExtractRotation(finalMat); + jointData.rotationSet = true; + } + } + // Set the data in the model + copyAnimationJointDataToModel(jointsData); +} + + +void ModelOverlay::mapAnimationJoints(const QStringList& modelJointNames) { + + // if we don't have animation, or we're already joint mapped then bail early + if (!hasAnimation() || jointsMapped()) { + return; + } + + if (!_animation || _animation->getURL() != _animationURL) { + _animation = DependencyManager::get()->getAnimation(_animationURL); + } + + if (_animation && _animation->isLoaded()) { + QStringList animationJointNames = _animation->getJointNames(); + + if (modelJointNames.size() > 0 && animationJointNames.size() > 0) { + _jointMapping.resize(modelJointNames.size()); + for (int i = 0; i < modelJointNames.size(); i++) { + _jointMapping[i] = animationJointNames.indexOf(modelJointNames[i]); + } + _jointMappingCompleted = true; + _jointMappingURL = _animationURL; + } + } +} + +void ModelOverlay::copyAnimationJointDataToModel(QVector jointsData) { + if (!_model || !_model->isLoaded()) { + return; + } + + // relay any inbound joint changes from scripts/animation/network to the model/rig + for (int index = 0; index < jointsData.size(); ++index) { + auto& jointData = jointsData[index]; + _model->setJointRotation(index, true, jointData.rotation, 1.0f); + _model->setJointTranslation(index, true, jointData.translation, 1.0f); + } + _updateModel = true; +} + diff --git a/interface/src/ui/overlays/ModelOverlay.h b/interface/src/ui/overlays/ModelOverlay.h index 8d8429b29e..edee4f7ac6 100644 --- a/interface/src/ui/overlays/ModelOverlay.h +++ b/interface/src/ui/overlays/ModelOverlay.h @@ -13,6 +13,7 @@ #define hifi_ModelOverlay_h #include +#include #include "Volume3DOverlay.h" @@ -45,6 +46,9 @@ public: float getLoadPriority() const { return _loadPriority; } + bool hasAnimation() const { return !_animationURL.isEmpty(); } + bool jointsMapped() const { return _jointMappingURL == _animationURL && _jointMappingCompleted; } + protected: Transform evalRenderTransform() override; @@ -53,6 +57,14 @@ protected: template vectorType mapJoints(mapFunction function) const; + void animate(); + void mapAnimationJoints(const QStringList& modelJointNames); + bool isAnimatingSomething() const { + return !_animationURL.isEmpty() && _animationRunning && _animationFPS != 0.0f; + } + void copyAnimationJointDataToModel(QVector jointsData); + + private: ModelPointer _model; @@ -62,6 +74,25 @@ private: bool _updateModel = { false }; bool _scaleToFit = { false }; float _loadPriority { 0.0f }; + + AnimationPointer _animation; + + QUrl _animationURL; + float _animationFPS { 0.0f }; + float _animationCurrentFrame { 0.0f }; + bool _animationRunning { false }; + bool _animationLoop { false }; + float _animationFirstFrame { 0.0f }; + float _animationLastFrame = { 0.0f }; + bool _animationHold { false }; + bool _animationAllowTranslation { false }; + uint64_t _lastAnimated { 0 }; + int _lastKnownCurrentFrame { -1 }; + + QUrl _jointMappingURL; + bool _jointMappingCompleted { false }; + QVector _jointMapping; // domain is index into model-joints, range is index into animation-joints + }; #endif // hifi_ModelOverlay_h diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 86a1e629b4..712c728dcb 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -249,6 +249,7 @@ void Rig::reset(const FBXGeometry& geometry) { _rightShoulderJointIndex = _rightElbowJointIndex >= 0 ? geometry.joints.at(_rightElbowJointIndex).parentIndex : -1; if (!_animGraphURL.isEmpty()) { + _animNode.reset(); initAnimGraph(_animGraphURL); } } @@ -1619,7 +1620,7 @@ void Rig::updateFromControllerParameters(const ControllerParameters& params, flo } void Rig::initAnimGraph(const QUrl& url) { - if (_animGraphURL != url) { + if (_animGraphURL != url || !_animNode) { _animGraphURL = url; _animNode.reset(); diff --git a/libraries/entities/src/AddEntityOperator.cpp b/libraries/entities/src/AddEntityOperator.cpp index 78d986f538..2ff1c6f622 100644 --- a/libraries/entities/src/AddEntityOperator.cpp +++ b/libraries/entities/src/AddEntityOperator.cpp @@ -9,18 +9,17 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "AddEntityOperator.h" + #include "EntityItem.h" #include "EntityTree.h" #include "EntityTreeElement.h" -#include "AddEntityOperator.h" - AddEntityOperator::AddEntityOperator(EntityTreePointer tree, EntityItemPointer newEntity) : _tree(tree), _newEntity(newEntity), - _foundNew(false), - _changeTime(usecTimestampNow()), - _newEntityBox() + _newEntityBox(), + _foundNew(false) { // caller must have verified existence of newEntity assert(_newEntity); diff --git a/libraries/entities/src/AddEntityOperator.h b/libraries/entities/src/AddEntityOperator.h index 48ee49f4d1..0c36797e24 100644 --- a/libraries/entities/src/AddEntityOperator.h +++ b/libraries/entities/src/AddEntityOperator.h @@ -12,20 +12,28 @@ #ifndef hifi_AddEntityOperator_h #define hifi_AddEntityOperator_h +#include + +#include +#include + +#include "EntityTypes.h" + +class EntityTree; +using EntityTreePointer = std::shared_ptr; + class AddEntityOperator : public RecurseOctreeOperator { public: AddEntityOperator(EntityTreePointer tree, EntityItemPointer newEntity); - + virtual bool preRecursion(const OctreeElementPointer& element) override; virtual bool postRecursion(const OctreeElementPointer& element) override; virtual OctreeElementPointer possiblyCreateChildAt(const OctreeElementPointer& element, int childIndex) override; private: EntityTreePointer _tree; EntityItemPointer _newEntity; - bool _foundNew; - quint64 _changeTime; - AABox _newEntityBox; + bool _foundNew; }; diff --git a/libraries/entities/src/DiffTraversal.cpp b/libraries/entities/src/DiffTraversal.cpp new file mode 100644 index 0000000000..2f9423daa3 --- /dev/null +++ b/libraries/entities/src/DiffTraversal.cpp @@ -0,0 +1,252 @@ +// +// DiffTraversal.cpp +// assignment-client/src/entities +// +// Created by Andrew Meadows 2017.08.08 +// Copyright 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 "DiffTraversal.h" + +#include + + +DiffTraversal::Waypoint::Waypoint(EntityTreeElementPointer& element) : _nextIndex(0) { + assert(element); + _weakElement = element; +} + +void DiffTraversal::Waypoint::getNextVisibleElementFirstTime(DiffTraversal::VisibleElement& next, + const DiffTraversal::View& view) { + // NOTE: no need to set next.intersection in the "FirstTime" context + if (_nextIndex == -1) { + // root case is special: + // its intersection is always INTERSECT, + // we never bother checking for LOD culling, and + // we can skip it if the content hasn't changed + ++_nextIndex; + next.element = _weakElement.lock(); + return; + } else if (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer element = _weakElement.lock(); + if (element) { + while (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); + ++_nextIndex; + if (nextElement) { + if (!view.usesViewFrustum) { + // No LOD truncation if we aren't using the view frustum + next.element = nextElement; + return; + } else if (view.viewFrustum.cubeIntersectsKeyhole(nextElement->getAACube())) { + // check for LOD truncation + float distance = glm::distance(view.viewFrustum.getPosition(), nextElement->getAACube().calcCenter()) + MIN_VISIBLE_DISTANCE; + float angularDiameter = nextElement->getAACube().getScale() / distance; + if (angularDiameter > MIN_ELEMENT_ANGULAR_DIAMETER * view.lodScaleFactor) { + next.element = nextElement; + return; + } + } + } + } + } + } + next.element.reset(); +} + +void DiffTraversal::Waypoint::getNextVisibleElementRepeat( + DiffTraversal::VisibleElement& next, const DiffTraversal::View& view, uint64_t lastTime) { + if (_nextIndex == -1) { + // root case is special + ++_nextIndex; + EntityTreeElementPointer element = _weakElement.lock(); + if (element->getLastChangedContent() > lastTime) { + next.element = element; + next.intersection = ViewFrustum::INTERSECT; + return; + } + } + if (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer element = _weakElement.lock(); + if (element) { + while (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); + ++_nextIndex; + if (nextElement && nextElement->getLastChanged() > lastTime) { + if (!view.usesViewFrustum) { + // No LOD truncation if we aren't using the view frustum + next.element = nextElement; + next.intersection = ViewFrustum::INSIDE; + return; + } else { + // check for LOD truncation + float distance = glm::distance(view.viewFrustum.getPosition(), nextElement->getAACube().calcCenter()) + MIN_VISIBLE_DISTANCE; + float angularDiameter = nextElement->getAACube().getScale() / distance; + if (angularDiameter > MIN_ELEMENT_ANGULAR_DIAMETER * view.lodScaleFactor) { + ViewFrustum::intersection intersection = view.viewFrustum.calculateCubeKeyholeIntersection(nextElement->getAACube()); + if (intersection != ViewFrustum::OUTSIDE) { + next.element = nextElement; + next.intersection = intersection; + return; + } + } + } + } + } + } + } + next.element.reset(); + next.intersection = ViewFrustum::OUTSIDE; +} + +void DiffTraversal::Waypoint::getNextVisibleElementDifferential(DiffTraversal::VisibleElement& next, + const DiffTraversal::View& view, const DiffTraversal::View& lastView) { + if (_nextIndex == -1) { + // root case is special + ++_nextIndex; + EntityTreeElementPointer element = _weakElement.lock(); + next.element = element; + next.intersection = ViewFrustum::INTERSECT; + return; + } else if (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer element = _weakElement.lock(); + if (element) { + while (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); + ++_nextIndex; + if (nextElement) { + AACube cube = nextElement->getAACube(); + // check for LOD truncation + float distance = glm::distance(view.viewFrustum.getPosition(), cube.calcCenter()) + MIN_VISIBLE_DISTANCE; + float angularDiameter = cube.getScale() / distance; + if (angularDiameter > MIN_ELEMENT_ANGULAR_DIAMETER * view.lodScaleFactor) { + if (view.viewFrustum.calculateCubeKeyholeIntersection(cube) != ViewFrustum::OUTSIDE) { + next.element = nextElement; + next.intersection = ViewFrustum::OUTSIDE; + return; + } + } + } + } + } + } + next.element.reset(); + next.intersection = ViewFrustum::OUTSIDE; +} + +DiffTraversal::DiffTraversal() { + const int32_t MIN_PATH_DEPTH = 16; + _path.reserve(MIN_PATH_DEPTH); +} + +DiffTraversal::Type DiffTraversal::prepareNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root, int32_t lodLevelOffset, bool usesViewFrustum) { + assert(root); + // there are three types of traversal: + // + // (1) First = fresh view --> find all elements in view + // (2) Repeat = view hasn't changed --> find elements changed since last complete traversal + // (3) Differential = view has changed --> find elements changed or in new view but not old + // + // for each traversal type we assign the appropriate _getNextVisibleElementCallback + // + // _getNextVisibleElementCallback = identifies elements that need to be traversed, + // updates VisibleElement ref argument with pointer-to-element and view-intersection + // (INSIDE, INTERSECT, or OUTSIDE) + // + // external code should update the _scanElementCallback after calling prepareNewTraversal + // + _currentView.usesViewFrustum = usesViewFrustum; + float lodScaleFactor = powf(2.0f, lodLevelOffset); + + Type type; + // If usesViewFrustum changes, treat it as a First traversal + if (_completedView.startTime == 0 || _currentView.usesViewFrustum != _completedView.usesViewFrustum) { + type = Type::First; + _currentView.viewFrustum = viewFrustum; + _currentView.lodScaleFactor = lodScaleFactor; + _getNextVisibleElementCallback = [this](DiffTraversal::VisibleElement& next) { + _path.back().getNextVisibleElementFirstTime(next, _currentView); + }; + } else if (!_currentView.usesViewFrustum || + (_completedView.viewFrustum.isVerySimilar(viewFrustum) && + lodScaleFactor == _completedView.lodScaleFactor)) { + type = Type::Repeat; + _getNextVisibleElementCallback = [this](DiffTraversal::VisibleElement& next) { + _path.back().getNextVisibleElementRepeat(next, _completedView, _completedView.startTime); + }; + } else { + type = Type::Differential; + _currentView.viewFrustum = viewFrustum; + _currentView.lodScaleFactor = lodScaleFactor; + _getNextVisibleElementCallback = [this](DiffTraversal::VisibleElement& next) { + _path.back().getNextVisibleElementDifferential(next, _currentView, _completedView); + }; + } + + _path.clear(); + _path.push_back(DiffTraversal::Waypoint(root)); + // set root fork's index such that root element returned at getNextElement() + _path.back().initRootNextIndex(); + + _currentView.startTime = usecTimestampNow(); + + return type; +} + +void DiffTraversal::getNextVisibleElement(DiffTraversal::VisibleElement& next) { + if (_path.empty()) { + next.element.reset(); + next.intersection = ViewFrustum::OUTSIDE; + return; + } + _getNextVisibleElementCallback(next); + if (next.element) { + int8_t nextIndex = _path.back().getNextIndex(); + if (nextIndex > 0) { + _path.push_back(DiffTraversal::Waypoint(next.element)); + } + } else { + // we're done at this level + while (!next.element) { + // pop one level + _path.pop_back(); + if (_path.empty()) { + // we've traversed the entire tree + _completedView = _currentView; + return; + } + // keep looking for next + _getNextVisibleElementCallback(next); + if (next.element) { + // we've descended one level so add it to the path + _path.push_back(DiffTraversal::Waypoint(next.element)); + } + } + } +} + +void DiffTraversal::setScanCallback(std::function cb) { + if (!cb) { + _scanElementCallback = [](DiffTraversal::VisibleElement& a){}; + } else { + _scanElementCallback = cb; + } +} + +void DiffTraversal::traverse(uint64_t timeBudget) { + uint64_t expiry = usecTimestampNow() + timeBudget; + DiffTraversal::VisibleElement next; + getNextVisibleElement(next); + while (next.element) { + if (next.element->hasContent()) { + _scanElementCallback(next); + } + if (usecTimestampNow() > expiry) { + break; + } + getNextVisibleElement(next); + } +} diff --git a/libraries/entities/src/DiffTraversal.h b/libraries/entities/src/DiffTraversal.h new file mode 100644 index 0000000000..c26e48ae5f --- /dev/null +++ b/libraries/entities/src/DiffTraversal.h @@ -0,0 +1,85 @@ +// +// DiffTraversal.h +// assignment-client/src/entities +// +// Created by Andrew Meadows 2017.08.08 +// Copyright 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 +// + +#ifndef hifi_DiffTraversal_h +#define hifi_DiffTraversal_h + +#include + +#include "EntityTreeElement.h" + +// DiffTraversal traverses the tree and applies _scanElementCallback on elements it finds +class DiffTraversal { +public: + // VisibleElement is a struct identifying an element and how it intersected the view. + // The intersection is used to optimize culling entities from the sendQueue. + class VisibleElement { + public: + EntityTreeElementPointer element; + ViewFrustum::intersection intersection { ViewFrustum::OUTSIDE }; + }; + + // View is a struct with a ViewFrustum and LOD parameters + class View { + public: + ViewFrustum viewFrustum; + uint64_t startTime { 0 }; + float lodScaleFactor { 1.0f }; + bool usesViewFrustum { true }; + }; + + // Waypoint is an bookmark in a "path" of waypoints during a traversal. + class Waypoint { + public: + Waypoint(EntityTreeElementPointer& element); + + void getNextVisibleElementFirstTime(VisibleElement& next, const View& view); + void getNextVisibleElementRepeat(VisibleElement& next, const View& view, uint64_t lastTime); + void getNextVisibleElementDifferential(VisibleElement& next, const View& view, const View& lastView); + + int8_t getNextIndex() const { return _nextIndex; } + void initRootNextIndex() { _nextIndex = -1; } + + protected: + EntityTreeElementWeakPointer _weakElement; + int8_t _nextIndex; + }; + + typedef enum { First, Repeat, Differential } Type; + + DiffTraversal(); + + Type prepareNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root, int32_t lodLevelOffset, bool usesViewFrustum); + + const ViewFrustum& getCurrentView() const { return _currentView.viewFrustum; } + const ViewFrustum& getCompletedView() const { return _completedView.viewFrustum; } + + bool doesCurrentUseViewFrustum() const { return _currentView.usesViewFrustum; } + float getCurrentLODScaleFactor() const { return _currentView.lodScaleFactor; } + float getCompletedLODScaleFactor() const { return _completedView.lodScaleFactor; } + + uint64_t getStartOfCompletedTraversal() const { return _completedView.startTime; } + bool finished() const { return _path.empty(); } + + void setScanCallback(std::function cb); + void traverse(uint64_t timeBudget); + +private: + void getNextVisibleElement(VisibleElement& next); + + View _currentView; + View _completedView; + std::vector _path; + std::function _getNextVisibleElementCallback { nullptr }; + std::function _scanElementCallback { [](VisibleElement& e){} }; +}; + +#endif // hifi_EntityPriorityQueue_h diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 643145942a..6e2e52b380 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -32,7 +32,8 @@ #include "EntitySimulation.h" #include "EntityDynamicFactoryInterface.h" - +Q_DECLARE_METATYPE(EntityItemPointer); +int entityItemPointernMetaTypeId = qRegisterMetaType(); int EntityItem::_maxActionsDataSize = 800; quint64 EntityItem::_rememberDeletedActionTime = 20 * USECS_PER_SECOND; @@ -2024,7 +2025,7 @@ bool EntityItem::removeActionInternal(const QUuid& actionID, EntitySimulationPoi _previouslyDeletedActions.insert(actionID, usecTimestampNow()); if (_objectActions.contains(actionID)) { if (!simulation) { - EntityTreeElementPointer element = _element; // use local copy of _element for logic below + EntityTreeElementPointer element = _element; // use local copy of _element for logic below EntityTreePointer entityTree = element ? element->getTree() : nullptr; simulation = entityTree ? entityTree->getSimulation() : nullptr; } diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 4eac23c867..88750da463 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -62,7 +62,8 @@ class MeshProxyList; /// EntityItem class this is the base class for all entity types. It handles the basic properties and functionality available /// to all other entity types. In particular: postion, size, rotation, age, lifetime, velocity, gravity. You can not instantiate /// one directly, instead you must only construct one of it's derived classes with additional features. -class EntityItem : public SpatiallyNestable, public ReadWriteLockable { +class EntityItem : public QObject, public SpatiallyNestable, public ReadWriteLockable { + Q_OBJECT // These two classes manage lists of EntityItem pointers and must be able to cleanup pointers when an EntityItem is deleted. // To make the cleanup robust each EntityItem has backpointers to its manager classes (which are only ever set/cleared by // the managers themselves, hence they are fiends) whose NULL status can be used to determine which managers still need to diff --git a/libraries/entities/src/EntitySimulation.cpp b/libraries/entities/src/EntitySimulation.cpp index 6a1c359b5a..2e330fdcc5 100644 --- a/libraries/entities/src/EntitySimulation.cpp +++ b/libraries/entities/src/EntitySimulation.cpp @@ -141,7 +141,7 @@ void EntitySimulation::callUpdateOnEntitiesThatNeedIt(const quint64& now) { void EntitySimulation::sortEntitiesThatMoved() { // NOTE: this is only for entities that have been moved by THIS EntitySimulation. // External changes to entity position/shape are expected to be sorted outside of the EntitySimulation. - MovingEntitiesOperator moveOperator(_entityTree); + MovingEntitiesOperator moveOperator; AACube domainBounds(glm::vec3((float)-HALF_TREE_SCALE), (float)TREE_SCALE); SetOfEntities::iterator itemItr = _entitiesToSort.begin(); while (itemItr != _entitiesToSort.end()) { diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index bcb73f352c..bf37a08386 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -22,7 +22,6 @@ #include "VariantMapToScriptValue.h" #include "AddEntityOperator.h" -#include "MovingEntitiesOperator.h" #include "UpdateEntityOperator.h" #include "QVariantGLM.h" #include "EntitiesLogging.h" @@ -107,6 +106,111 @@ void EntityTree::eraseAllOctreeElements(bool createNewRoot) { clearDeletedEntities(); } +void EntityTree::readBitstreamToTree(const unsigned char* bitstream, + uint64_t bufferSizeBytes, ReadBitstreamToTreeParams& args) { + Octree::readBitstreamToTree(bitstream, bufferSizeBytes, args); + + // add entities + QHash::const_iterator itr; + for (itr = _entitiesToAdd.constBegin(); itr != _entitiesToAdd.constEnd(); ++itr) { + const EntityItemPointer& entityItem = itr.value(); + AddEntityOperator theOperator(getThisPointer(), entityItem); + recurseTreeWithOperator(&theOperator); + postAddEntity(entityItem); + } + _entitiesToAdd.clear(); + + // move entities + if (_entityMover.hasMovingEntities()) { + PerformanceTimer perfTimer("recurseTreeWithOperator"); + recurseTreeWithOperator(&_entityMover); + _entityMover.reset(); + } +} + +int EntityTree::readEntityDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args) { + const unsigned char* dataAt = data; + int bytesRead = 0; + uint16_t numberOfEntities = 0; + int expectedBytesPerEntity = EntityItem::expectedBytes(); + + args.elementsPerPacket++; + + if (bytesLeftToRead >= (int)sizeof(numberOfEntities)) { + // read our entities in.... + numberOfEntities = *(uint16_t*)dataAt; + + dataAt += sizeof(numberOfEntities); + bytesLeftToRead -= (int)sizeof(numberOfEntities); + bytesRead += sizeof(numberOfEntities); + + if (bytesLeftToRead >= (int)(numberOfEntities * expectedBytesPerEntity)) { + for (uint16_t i = 0; i < numberOfEntities; i++) { + int bytesForThisEntity = 0; + EntityItemID entityItemID = EntityItemID::readEntityItemIDFromBuffer(dataAt, bytesLeftToRead); + EntityItemPointer entity = findEntityByEntityItemID(entityItemID); + + if (entity) { + QString entityScriptBefore = entity->getScript(); + QUuid parentIDBefore = entity->getParentID(); + QString entityServerScriptsBefore = entity->getServerScripts(); + quint64 entityScriptTimestampBefore = entity->getScriptTimestamp(); + + bytesForThisEntity = entity->readEntityDataFromBuffer(dataAt, bytesLeftToRead, args); + if (entity->getDirtyFlags()) { + entityChanged(entity); + } + _entityMover.addEntityToMoveList(entity, entity->getQueryAACube()); + + QString entityScriptAfter = entity->getScript(); + QString entityServerScriptsAfter = entity->getServerScripts(); + quint64 entityScriptTimestampAfter = entity->getScriptTimestamp(); + bool reload = entityScriptTimestampBefore != entityScriptTimestampAfter; + + // If the script value has changed on us, or it's timestamp has changed to force + // a reload then we want to send out a script changing signal... + if (reload || entityScriptBefore != entityScriptAfter) { + emitEntityScriptChanging(entityItemID, reload); // the entity script has changed + } + if (reload || entityServerScriptsBefore != entityServerScriptsAfter) { + emitEntityServerScriptChanging(entityItemID, reload); // the entity server script has changed + } + + QUuid parentIDAfter = entity->getParentID(); + if (parentIDBefore != parentIDAfter) { + addToNeedsParentFixupList(entity); + } + } else { + entity = EntityTypes::constructEntityItem(dataAt, bytesLeftToRead, args); + if (entity) { + bytesForThisEntity = entity->readEntityDataFromBuffer(dataAt, bytesLeftToRead, args); + + // don't add if we've recently deleted.... + if (!isDeletedEntity(entityItemID)) { + _entitiesToAdd.insert(entityItemID, entity); + + if (entity->getCreated() == UNKNOWN_CREATED_TIME) { + entity->recordCreationTime(); + } + #ifdef WANT_DEBUG + } else { + qCDebug(entities) << "Received packet for previously deleted entity [" << + entityItemID << "] ignoring. (inside " << __FUNCTION__ << ")"; + #endif + } + } + } + // Move the buffer forward to read more entities + dataAt += bytesForThisEntity; + bytesLeftToRead -= bytesForThisEntity; + bytesRead += bytesForThisEntity; + } + } + } + + return bytesRead; +} + bool EntityTree::handlesEditPacketType(PacketType packetType) const { // we handle these types of "edit" packets switch (packetType) { @@ -193,7 +297,9 @@ bool EntityTree::updateEntity(EntityItemPointer entity, const EntityItemProperti } UpdateEntityOperator theOperator(getThisPointer(), containingElement, entity, queryCube); recurseTreeWithOperator(&theOperator); - entity->setProperties(tempProperties); + if (entity->setProperties(tempProperties)) { + emit editingEntityPointer(entity); + } _isDirty = true; } } @@ -268,7 +374,9 @@ bool EntityTree::updateEntity(EntityItemPointer entity, const EntityItemProperti } UpdateEntityOperator theOperator(getThisPointer(), containingElement, entity, newQueryAACube); recurseTreeWithOperator(&theOperator); - entity->setProperties(properties); + if (entity->setProperties(properties)) { + emit editingEntityPointer(entity); + } // if the entity has children, run UpdateEntityOperator on them. If the children have children, recurse QQueue toProcess; @@ -443,6 +551,7 @@ void EntityTree::deleteEntity(const EntityItemID& entityID, bool force, bool ign unhookChildAvatar(entityID); emit deletingEntity(entityID); + emit deletingEntityPointer(existingEntity.get()); // NOTE: callers must lock the tree before using this method DeleteEntityOperator theOperator(getThisPointer(), entityID); @@ -451,6 +560,10 @@ void EntityTree::deleteEntity(const EntityItemID& entityID, bool force, bool ign auto descendantID = descendant->getID(); theOperator.addEntityIDToDeleteList(descendantID); emit deletingEntity(descendantID); + EntityItemPointer descendantEntity = std::dynamic_pointer_cast(descendant); + if (descendantEntity) { + emit deletingEntityPointer(descendantEntity.get()); + } }); recurseTreeWithOperator(&theOperator); @@ -500,6 +613,7 @@ void EntityTree::deleteEntities(QSet entityIDs, bool force, bool i unhookChildAvatar(entityID); theOperator.addEntityIDToDeleteList(entityID); emit deletingEntity(entityID); + emit deletingEntityPointer(existingEntity.get()); } if (theOperator.getEntities().size() > 0) { @@ -1137,7 +1251,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c if (!isPhysics) { properties.setLastEditedBy(senderNode->getUUID()); } - updateEntity(entityItemID, properties, senderNode); + updateEntity(existingEntity, properties, senderNode); existingEntity->markAsChangedOnServer(); endUpdate = usecTimestampNow(); _totalUpdates++; @@ -1250,7 +1364,7 @@ void EntityTree::entityChanged(EntityItemPointer entity) { void EntityTree::fixupNeedsParentFixups() { - MovingEntitiesOperator moveOperator(getThisPointer()); + MovingEntitiesOperator moveOperator; QWriteLocker locker(&_needsParentFixupLock); @@ -1674,7 +1788,7 @@ QVector EntityTree::sendEntities(EntityEditPacketSender* packetSen // add-entity packet to the server. // fix the queryAACubes of any children that were read in before their parents, get them into the correct element - MovingEntitiesOperator moveOperator(localTree); + MovingEntitiesOperator moveOperator; QHash::iterator i = map.begin(); while (i != map.end()) { EntityItemID newID = i.value(); diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index cb16f2fac1..d0448f438a 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -19,11 +19,12 @@ #include class EntityTree; -typedef std::shared_ptr EntityTreePointer; - +using EntityTreePointer = std::shared_ptr; +#include "AddEntityOperator.h" #include "EntityTreeElement.h" #include "DeleteEntityOperator.h" +#include "MovingEntitiesOperator.h" class EntityEditFilters; class Model; @@ -80,6 +81,10 @@ public: virtual void eraseAllOctreeElements(bool createNewRoot = true) override; + virtual void readBitstreamToTree(const unsigned char* bitstream, + uint64_t bufferSizeBytes, ReadBitstreamToTreeParams& args) override; + int readEntityDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args); + // These methods will allow the OctreeServer to send your tree inbound edit packets of your // own definition. Implement these to allow your octree based server to support editing virtual bool getWantSVOfileVersions() const override { return true; } @@ -264,7 +269,9 @@ public: signals: void deletingEntity(const EntityItemID& entityID); + void deletingEntityPointer(EntityItem* entityID); void addingEntity(const EntityItemID& entityID); + void editingEntityPointer(const EntityItemPointer& entityID); void entityScriptChanging(const EntityItemID& entityItemID, const bool reload); void entityServerScriptChanging(const EntityItemID& entityItemID, const bool reload); void newCollisionSoundURL(const QUrl& url, const EntityItemID& entityID); @@ -347,6 +354,9 @@ protected: bool filterProperties(EntityItemPointer& existingEntity, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, FilterType filterType); bool _hasEntityEditFilter{ false }; QStringList _entityScriptSourceWhitelist; + + MovingEntitiesOperator _entityMover; + QHash _entitiesToAdd; }; #endif // hifi_EntityTree_h diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp index 487bf60f61..0c33855a61 100644 --- a/libraries/entities/src/EntityTreeElement.cpp +++ b/libraries/entities/src/EntityTreeElement.cpp @@ -107,7 +107,7 @@ bool EntityTreeElement::shouldIncludeChildData(int childIndex, EncodeBitstreamPa OctreeElementExtraEncodeData* extraEncodeData = &entityNodeData->extraEncodeData; assert(extraEncodeData); // EntityTrees always require extra encode data on their encoding passes - + if (extraEncodeData->contains(this)) { EntityTreeElementExtraEncodeDataPointer entityTreeElementExtraEncodeData = std::static_pointer_cast((*extraEncodeData)[this]); @@ -305,7 +305,7 @@ OctreeElement::AppendState EntityTreeElement::appendElementData(OctreePacketData int numberOfEntitiesOffset = 0; withReadLock([&] { QVector indexesOfEntitiesToInclude; - + // It's possible that our element has been previous completed. In this case we'll simply not include any of our // entities for encoding. This is needed because we encode the element data at the "parent" level, and so we // need to handle the case where our sibling elements need encoding but we don't. @@ -432,11 +432,11 @@ OctreeElement::AppendState EntityTreeElement::appendElementData(OctreePacketData // and include the entity in our final count of entities packetData->endLevel(entityLevel); actualNumberOfEntities++; - } - // If the entity item got completely appended, then we can remove it from the extra encode data - if (appendEntityState == OctreeElement::COMPLETED) { - entityTreeElementExtraEncodeData->entities.remove(entity->getEntityItemID()); + // If the entity item got completely appended, then we can remove it from the extra encode data + if (appendEntityState == OctreeElement::COMPLETED) { + entityTreeElementExtraEncodeData->entities.remove(entity->getEntityItemID()); + } } // If any part of the entity items didn't fit, then the element is considered partial @@ -893,6 +893,7 @@ void EntityTreeElement::cleanupEntities() { } _entityItems.clear(); }); + bumpChangedContent(); } bool EntityTreeElement::removeEntityWithEntityItemID(const EntityItemID& id) { @@ -906,6 +907,7 @@ bool EntityTreeElement::removeEntityWithEntityItemID(const EntityItemID& id) { // NOTE: only EntityTreeElement should ever be changing the value of entity->_element entity->_element = NULL; _entityItems.removeAt(i); + bumpChangedContent(); break; } } @@ -922,144 +924,16 @@ bool EntityTreeElement::removeEntityItem(EntityItemPointer entity) { // NOTE: only EntityTreeElement should ever be changing the value of entity->_element assert(entity->_element.get() == this); entity->_element = NULL; + bumpChangedContent(); return true; } return false; } -// Things we want to accomplish as we read these entities from the data buffer. -// -// 1) correctly update the properties of the entity -// 2) add any new entities that didn't previously exist -// -// TODO: Do we also need to do this? -// 3) mark our tree as dirty down to the path of the previous location of the entity -// 4) mark our tree as dirty down to the path of the new location of the entity -// -// Since we're potentially reading several entities, we'd prefer to do all the moving around -// and dirty path marking in one pass. int EntityTreeElement::readElementDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args) { - // If we're the root, but this bitstream doesn't support root elements with data, then - // return without reading any bytes - if (this == _myTree->getRoot().get() && args.bitstreamVersion < VERSION_ROOT_ELEMENT_HAS_DATA) { - return 0; - } - - const unsigned char* dataAt = data; - int bytesRead = 0; - uint16_t numberOfEntities = 0; - int expectedBytesPerEntity = EntityItem::expectedBytes(); - - args.elementsPerPacket++; - - if (bytesLeftToRead >= (int)sizeof(numberOfEntities)) { - // read our entities in.... - numberOfEntities = *(uint16_t*)dataAt; - - dataAt += sizeof(numberOfEntities); - bytesLeftToRead -= (int)sizeof(numberOfEntities); - bytesRead += sizeof(numberOfEntities); - - if (bytesLeftToRead >= (int)(numberOfEntities * expectedBytesPerEntity)) { - for (uint16_t i = 0; i < numberOfEntities; i++) { - int bytesForThisEntity = 0; - EntityItemID entityItemID; - EntityItemPointer entityItem = NULL; - - // Old model files don't have UUIDs in them. So we don't want to try to read those IDs from the stream. - // Since this can only happen on loading an old file, we can safely treat these as new entity cases, - // which will correctly handle the case of creating models and letting them parse the old format. - if (args.bitstreamVersion >= VERSION_ENTITIES_SUPPORT_SPLIT_MTU) { - entityItemID = EntityItemID::readEntityItemIDFromBuffer(dataAt, bytesLeftToRead); - entityItem = _myTree->findEntityByEntityItemID(entityItemID); - } - - // If the item already exists in our tree, we want do the following... - // 1) allow the existing item to read from the databuffer - // 2) check to see if after reading the item, the containing element is still correct, fix it if needed - // - // TODO: Do we need to also do this? - // 3) remember the old cube for the entity so we can mark it as dirty - if (entityItem) { - QString entityScriptBefore = entityItem->getScript(); - QUuid parentIDBefore = entityItem->getParentID(); - QString entityServerScriptsBefore = entityItem->getServerScripts(); - quint64 entityScriptTimestampBefore = entityItem->getScriptTimestamp(); - bool bestFitBefore = bestFitEntityBounds(entityItem); - EntityTreeElementPointer currentContainingElement = _myTree->getContainingElement(entityItemID); - - bytesForThisEntity = entityItem->readEntityDataFromBuffer(dataAt, bytesLeftToRead, args); - if (entityItem->getDirtyFlags()) { - _myTree->entityChanged(entityItem); - } - bool bestFitAfter = bestFitEntityBounds(entityItem); - - if (bestFitBefore != bestFitAfter) { - // This is the case where the entity existed, and is in some element in our tree... - if (!bestFitBefore && bestFitAfter) { - // This is the case where the entity existed, and is in some element in our tree... - if (currentContainingElement.get() != this) { - // if the currentContainingElement is non-null, remove the entity from it - if (currentContainingElement) { - currentContainingElement->removeEntityItem(entityItem); - } - addEntityItem(entityItem); - } - } - } - - QString entityScriptAfter = entityItem->getScript(); - QString entityServerScriptsAfter = entityItem->getServerScripts(); - quint64 entityScriptTimestampAfter = entityItem->getScriptTimestamp(); - bool reload = entityScriptTimestampBefore != entityScriptTimestampAfter; - - // If the script value has changed on us, or it's timestamp has changed to force - // a reload then we want to send out a script changing signal... - if (entityScriptBefore != entityScriptAfter || reload) { - _myTree->emitEntityScriptChanging(entityItemID, reload); // the entity script has changed - } - if (entityServerScriptsBefore != entityServerScriptsAfter || reload) { - _myTree->emitEntityServerScriptChanging(entityItemID, reload); // the entity server script has changed - } - - QUuid parentIDAfter = entityItem->getParentID(); - if (parentIDBefore != parentIDAfter) { - _myTree->addToNeedsParentFixupList(entityItem); - } - - } else { - entityItem = EntityTypes::constructEntityItem(dataAt, bytesLeftToRead, args); - if (entityItem) { - bytesForThisEntity = entityItem->readEntityDataFromBuffer(dataAt, bytesLeftToRead, args); - - // don't add if we've recently deleted.... - if (!_myTree->isDeletedEntity(entityItem->getID())) { - _myTree->addEntityMapEntry(entityItem); - addEntityItem(entityItem); // add this new entity to this elements entities - entityItemID = entityItem->getEntityItemID(); - _myTree->postAddEntity(entityItem); - if (entityItem->getCreated() == UNKNOWN_CREATED_TIME) { - entityItem->recordCreationTime(); - } - } else { - #ifdef WANT_DEBUG - qCDebug(entities) << "Received packet for previously deleted entity [" << - entityItem->getID() << "] ignoring. (inside " << __FUNCTION__ << ")"; - #endif - } - } - } - // Move the buffer forward to read more entities - dataAt += bytesForThisEntity; - bytesLeftToRead -= bytesForThisEntity; - bytesRead += bytesForThisEntity; - } - } - } - - return bytesRead; + return _myTree->readEntityDataFromBuffer(data, bytesLeftToRead, args); } void EntityTreeElement::addEntityItem(EntityItemPointer entity) { @@ -1068,6 +942,7 @@ void EntityTreeElement::addEntityItem(EntityItemPointer entity) { withWriteLock([&] { _entityItems.push_back(entity); }); + bumpChangedContent(); entity->_element = getThisPointer(); } diff --git a/libraries/entities/src/EntityTreeElement.h b/libraries/entities/src/EntityTreeElement.h index aee8c7cfd6..c7fb80c330 100644 --- a/libraries/entities/src/EntityTreeElement.h +++ b/libraries/entities/src/EntityTreeElement.h @@ -21,11 +21,12 @@ #include "EntityItem.h" #include "EntityTree.h" -typedef QVector EntityItems; - class EntityTree; class EntityTreeElement; -typedef std::shared_ptr EntityTreeElementPointer; + +using EntityItems = QVector; +using EntityTreeElementWeakPointer = std::weak_ptr; +using EntityTreeElementPointer = std::shared_ptr; class EntityTreeUpdateArgs { public: @@ -173,7 +174,6 @@ public: void setTree(EntityTreePointer tree) { _myTree = tree; } EntityTreePointer getTree() const { return _myTree; } - bool updateEntity(const EntityItem& entity); void addEntityItem(EntityItemPointer entity); EntityItemPointer getClosestEntity(glm::vec3 position) const; @@ -238,10 +238,14 @@ public: return std::static_pointer_cast(shared_from_this()); } + void bumpChangedContent() { _lastChangedContent = usecTimestampNow(); } + uint64_t getLastChangedContent() const { return _lastChangedContent; } + protected: virtual void init(unsigned char * octalCode) override; EntityTreePointer _myTree; EntityItems _entityItems; + uint64_t _lastChangedContent { 0 }; }; #endif // hifi_EntityTreeElement_h diff --git a/libraries/entities/src/MovingEntitiesOperator.cpp b/libraries/entities/src/MovingEntitiesOperator.cpp index 42e5a2ece5..cf043dd93e 100644 --- a/libraries/entities/src/MovingEntitiesOperator.cpp +++ b/libraries/entities/src/MovingEntitiesOperator.cpp @@ -16,15 +16,7 @@ #include "MovingEntitiesOperator.h" -MovingEntitiesOperator::MovingEntitiesOperator(EntityTreePointer tree) : - _tree(tree), - _changeTime(usecTimestampNow()), - _foundOldCount(0), - _foundNewCount(0), - _lookingCount(0), - _wantDebug(false) -{ -} +MovingEntitiesOperator::MovingEntitiesOperator() { } MovingEntitiesOperator::~MovingEntitiesOperator() { if (_wantDebug) { @@ -146,7 +138,7 @@ bool MovingEntitiesOperator::preRecursion(const OctreeElementPointer& element) { // In Pre-recursion, we're generally deciding whether or not we want to recurse this // path of the tree. For this operation, we want to recurse the branch of the tree if - // and of the following are true: + // any of the following are true: // * We have not yet found the old entity, and this branch contains our old entity // * We have not yet found the new entity, and this branch contains our new entity // @@ -200,6 +192,8 @@ bool MovingEntitiesOperator::preRecursion(const OctreeElementPointer& element) { oldElement->removeEntityItem(details.entity); } entityTreeElement->addEntityItem(details.entity); + } else { + entityTreeElement->bumpChangedContent(); } _foundNewCount++; //details.newFound = true; // TODO: would be nice to add this optimization @@ -230,8 +224,6 @@ bool MovingEntitiesOperator::postRecursion(const OctreeElementPointer& element) if ((shouldRecurseSubTree(element))) { element->markWithChangedTime(); } - - // It's not OK to prune if we have the potential of deleting the original containing element // because if we prune the containing element then new might end up reallocating the same memory later @@ -286,3 +278,10 @@ OctreeElementPointer MovingEntitiesOperator::possiblyCreateChildAt(const OctreeE } return NULL; } + +void MovingEntitiesOperator::reset() { + _entitiesToMove.clear(); + _foundOldCount = 0; + _foundNewCount = 0; + _lookingCount = 0; +} diff --git a/libraries/entities/src/MovingEntitiesOperator.h b/libraries/entities/src/MovingEntitiesOperator.h index fc6ccf2513..d93efa60f2 100644 --- a/libraries/entities/src/MovingEntitiesOperator.h +++ b/libraries/entities/src/MovingEntitiesOperator.h @@ -12,6 +12,11 @@ #ifndef hifi_MovingEntitiesOperator_h #define hifi_MovingEntitiesOperator_h +#include + +#include "EntityTypes.h" +#include "EntityTreeElement.h" + class EntityToMoveDetails { public: EntityItemPointer entity; @@ -34,7 +39,7 @@ inline bool operator==(const EntityToMoveDetails& a, const EntityToMoveDetails& class MovingEntitiesOperator : public RecurseOctreeOperator { public: - MovingEntitiesOperator(EntityTreePointer tree); + MovingEntitiesOperator(); ~MovingEntitiesOperator(); void addEntityToMoveList(EntityItemPointer entity, const AACube& newCube); @@ -42,16 +47,15 @@ public: virtual bool postRecursion(const OctreeElementPointer& element) override; virtual OctreeElementPointer possiblyCreateChildAt(const OctreeElementPointer& element, int childIndex) override; bool hasMovingEntities() const { return _entitiesToMove.size() > 0; } + void reset(); private: - EntityTreePointer _tree; - QSet _entitiesToMove; - quint64 _changeTime; - int _foundOldCount; - int _foundNewCount; - int _lookingCount; bool shouldRecurseSubTree(const OctreeElementPointer& element); - - bool _wantDebug; + + QSet _entitiesToMove; + int _foundOldCount { 0 }; + int _foundNewCount { 0 }; + int _lookingCount { 0 }; + bool _wantDebug { false }; }; #endif // hifi_MovingEntitiesOperator_h diff --git a/libraries/entities/src/UpdateEntityOperator.cpp b/libraries/entities/src/UpdateEntityOperator.cpp index 7a5c87187a..fa7e5ca38f 100644 --- a/libraries/entities/src/UpdateEntityOperator.cpp +++ b/libraries/entities/src/UpdateEntityOperator.cpp @@ -173,6 +173,8 @@ bool UpdateEntityOperator::preRecursion(const OctreeElementPointer& element) { if (oldElement != _containingElement) { qCDebug(entities) << "WARNING entity moved during UpdateEntityOperator recursion"; _containingElement->removeEntityItem(_existingEntity); + } else { + _containingElement->bumpChangedContent(); } if (_wantDebug) { @@ -211,6 +213,7 @@ bool UpdateEntityOperator::preRecursion(const OctreeElementPointer& element) { if (_wantDebug) { qCDebug(entities) << " *** This is the same OLD ELEMENT ***"; } + _containingElement->bumpChangedContent(); } else { // otherwise, this is an add case. if (oldElement) { diff --git a/libraries/octree/src/Octree.h b/libraries/octree/src/Octree.h index 3b84618a56..a2df5f44e5 100644 --- a/libraries/octree/src/Octree.h +++ b/libraries/octree/src/Octree.h @@ -92,7 +92,8 @@ public: OUT_OF_VIEW, WAS_IN_VIEW, NO_CHANGE, - OCCLUDED + OCCLUDED, + FINISHED } reason; reason stopReason; @@ -232,7 +233,7 @@ public: virtual void eraseAllOctreeElements(bool createNewRoot = true); - void readBitstreamToTree(const unsigned char* bitstream, uint64_t bufferSizeBytes, ReadBitstreamToTreeParams& args); + virtual void readBitstreamToTree(const unsigned char* bitstream, uint64_t bufferSizeBytes, ReadBitstreamToTreeParams& args); void deleteOctalCodeFromTree(const unsigned char* codeBuffer, bool collapseEmptyTrees = DONT_COLLAPSE); void reaverageOctreeElements(OctreeElementPointer startElement = OctreeElementPointer()); diff --git a/libraries/octree/src/OctreeConstants.h b/libraries/octree/src/OctreeConstants.h index 53442f52d6..062d4e1ef2 100644 --- a/libraries/octree/src/OctreeConstants.h +++ b/libraries/octree/src/OctreeConstants.h @@ -21,7 +21,8 @@ const int TREE_SCALE = 32768; // ~20 miles.. This is the number of meters of the const int HALF_TREE_SCALE = TREE_SCALE / 2; // This controls the LOD. Larger number will make smaller voxels visible at greater distance. -const float DEFAULT_OCTREE_SIZE_SCALE = TREE_SCALE * 400.0f; +const float MAX_VISIBILITY_DISTANCE_FOR_UNIT_ELEMENT = 400.0f; // max distance where a 1x1x1 cube is visible for 20:20 vision +const float DEFAULT_OCTREE_SIZE_SCALE = TREE_SCALE * MAX_VISIBILITY_DISTANCE_FOR_UNIT_ELEMENT; // Since entities like models live inside of octree cells, and they themselves can have very small mesh parts, // we want to have some constant that controls have big a mesh part must be to render even if the octree cell itself @@ -36,7 +37,10 @@ const int NUMBER_OF_CHILDREN = 8; const int MAX_TREE_SLICE_BYTES = 26; -const float VIEW_FRUSTUM_FOV_OVERSEND = 60.0f; +// The oversend below is 20 degrees because that is the minimum oversend necessary to prevent missing entities +// near the edge of the view. The value here is determined by hard-coded values in ViewFrsutum::isVerySimilar(). +// TODO: move this parameter to the OctreeQueryNode context. +const float VIEW_FRUSTUM_FOV_OVERSEND = 20.0f; // These are guards to prevent our voxel tree recursive routines from spinning out of control const int UNREASONABLY_DEEP_RECURSION = 29; // use this for something that you want to be shallow, but not spin out diff --git a/libraries/octree/src/OctreePacketData.cpp b/libraries/octree/src/OctreePacketData.cpp index 5fd7e4dba3..493dfdcff5 100644 --- a/libraries/octree/src/OctreePacketData.cpp +++ b/libraries/octree/src/OctreePacketData.cpp @@ -582,9 +582,7 @@ bool OctreePacketData::compressContent() { if (compressedData.size() < (int)MAX_OCTREE_PACKET_DATA_SIZE) { _compressedBytes = compressedData.size(); - for (int i = 0; i < _compressedBytes; i++) { - _compressed[i] = compressedData[i]; - } + memcpy(_compressed, compressedData.constData(), _compressedBytes); _dirty = false; success = true; } @@ -598,25 +596,22 @@ void OctreePacketData::loadFinalizedContent(const unsigned char* data, int lengt if (data && length > 0) { if (_enableCompression) { - QByteArray compressedData; - for (int i = 0; i < length; i++) { - compressedData[i] = data[i]; - _compressed[i] = compressedData[i]; - } _compressedBytes = length; + memcpy(_compressed, data, _compressedBytes); + + QByteArray compressedData; + compressedData.resize(_compressedBytes); + memcpy(compressedData.data(), data, _compressedBytes); + QByteArray uncompressedData = qUncompress(compressedData); if (uncompressedData.size() <= _bytesAvailable) { _bytesInUse = uncompressedData.size(); _bytesAvailable -= uncompressedData.size(); - - for (int i = 0; i < _bytesInUse; i++) { - _uncompressed[i] = uncompressedData[i]; - } + memcpy(_uncompressed, uncompressedData.constData(), _bytesInUse); } } else { - for (int i = 0; i < length; i++) { - _uncompressed[i] = _compressed[i] = data[i]; - } + memcpy(_uncompressed, data, length); + memcpy(_compressed, data, length); _bytesInUse = _compressedBytes = length; } } else { diff --git a/libraries/octree/src/OctreeQuery.cpp b/libraries/octree/src/OctreeQuery.cpp index 7d9fc7d08c..a88f730a50 100644 --- a/libraries/octree/src/OctreeQuery.cpp +++ b/libraries/octree/src/OctreeQuery.cpp @@ -17,7 +17,18 @@ #include "OctreeConstants.h" #include "OctreeQuery.h" -OctreeQuery::OctreeQuery() { +const float DEFAULT_FOV = 45.0f; // degrees +const float DEFAULT_ASPECT_RATIO = 1.0f; +const float DEFAULT_NEAR_CLIP = 0.1f; +const float DEFAULT_FAR_CLIP = 3.0f; + +OctreeQuery::OctreeQuery() : + _cameraFov(DEFAULT_FOV), + _cameraAspectRatio(DEFAULT_ASPECT_RATIO), + _cameraNearClip(DEFAULT_NEAR_CLIP), + _cameraFarClip(DEFAULT_FAR_CLIP), + _cameraCenterRadius(DEFAULT_FAR_CLIP) +{ _maxQueryPPS = DEFAULT_MAX_OCTREE_PPS; } diff --git a/libraries/octree/src/OctreeQuery.h b/libraries/octree/src/OctreeQuery.h index 058c1dc585..81a63a696c 100644 --- a/libraries/octree/src/OctreeQuery.h +++ b/libraries/octree/src/OctreeQuery.h @@ -89,14 +89,14 @@ public slots: protected: // camera details for the avatar - glm::vec3 _cameraPosition = glm::vec3(0.0f); - glm::quat _cameraOrientation = glm::quat(); - float _cameraFov = 0.0f; - float _cameraAspectRatio = 1.0f; - float _cameraNearClip = 0.0f; - float _cameraFarClip = 0.0f; - float _cameraCenterRadius { 0.0f }; - glm::vec3 _cameraEyeOffsetPosition = glm::vec3(0.0f); + glm::vec3 _cameraPosition { glm::vec3(0.0f) }; + glm::quat _cameraOrientation { glm::quat() }; + float _cameraFov; + float _cameraAspectRatio; + float _cameraNearClip; + float _cameraFarClip; + float _cameraCenterRadius; + glm::vec3 _cameraEyeOffsetPosition { glm::vec3(0.0f) }; // octree server sending items int _maxQueryPPS = DEFAULT_MAX_OCTREE_PPS; diff --git a/libraries/octree/src/OctreeQueryNode.cpp b/libraries/octree/src/OctreeQueryNode.cpp index 4ebe650f6a..c26b4ce77b 100644 --- a/libraries/octree/src/OctreeQueryNode.cpp +++ b/libraries/octree/src/OctreeQueryNode.cpp @@ -182,6 +182,7 @@ bool OctreeQueryNode::updateCurrentViewFrustum() { getCameraAspectRatio(), getCameraNearClip(), getCameraFarClip())); + newestViewFrustum.calculate(); } @@ -189,7 +190,6 @@ bool OctreeQueryNode::updateCurrentViewFrustum() { QMutexLocker viewLocker(&_viewMutex); if (!newestViewFrustum.isVerySimilar(_currentViewFrustum)) { _currentViewFrustum = newestViewFrustum; - _currentViewFrustum.calculate(); currentViewFrustumChanged = true; } } diff --git a/libraries/octree/src/OctreeUtils.h b/libraries/octree/src/OctreeUtils.h index 279eb51509..c257bcd5f1 100644 --- a/libraries/octree/src/OctreeUtils.h +++ b/libraries/octree/src/OctreeUtils.h @@ -27,4 +27,11 @@ float boundaryDistanceForRenderLevel(unsigned int renderLevel, float voxelSizeSc float getAccuracyAngle(float octreeSizeScale, int boundaryLevelAdjust); +// MIN_ELEMENT_ANGULAR_DIAMETER = angular diameter of 1x1x1m cube at 400m = sqrt(3) / 400 = 0.0043301 radians ~= 0.25 degrees +const float MIN_ELEMENT_ANGULAR_DIAMETER = 0.0043301f; // radians +// NOTE: the entity bounding cube is larger than the smallest possible containing octree element by sqrt(3) +const float SQRT_THREE = 1.73205080f; +const float MIN_ENTITY_ANGULAR_DIAMETER = MIN_ELEMENT_ANGULAR_DIAMETER * SQRT_THREE; +const float MIN_VISIBLE_DISTANCE = 0.0001f; // helps avoid divide-by-zero check + #endif // hifi_OctreeUtils_h diff --git a/libraries/shared/src/ViewFrustum.cpp b/libraries/shared/src/ViewFrustum.cpp index 7e4f64686b..978221e167 100644 --- a/libraries/shared/src/ViewFrustum.cpp +++ b/libraries/shared/src/ViewFrustum.cpp @@ -56,16 +56,17 @@ void ViewFrustum::setProjection(const glm::mat4& projection) { _projection = projection; glm::mat4 inverseProjection = glm::inverse(projection); - // compute our dimensions the usual way + // compute frustum corners for (int i = 0; i < NUM_FRUSTUM_CORNERS; ++i) { _corners[i] = inverseProjection * NDC_VALUES[i]; _corners[i] /= _corners[i].w; } + + // compute frustum properties _nearClip = -_corners[BOTTOM_LEFT_NEAR].z; _farClip = -_corners[BOTTOM_LEFT_FAR].z; _aspectRatio = (_corners[TOP_RIGHT_NEAR].x - _corners[BOTTOM_LEFT_NEAR].x) / (_corners[TOP_RIGHT_NEAR].y - _corners[BOTTOM_LEFT_NEAR].y); - glm::vec4 top = inverseProjection * vec4(0.0f, 1.0f, -1.0f, 1.0f); top /= top.w; _fieldOfView = abs(glm::degrees(2.0f * abs(glm::angle(vec3(0.0f, 0.0f, -1.0f), glm::normalize(vec3(top)))))); @@ -160,16 +161,12 @@ void ViewFrustum::fromByteArray(const QByteArray& input) { setCenterRadius(cameraCenterRadius); // Also make sure it's got the correct lens details from the camera - const float VIEW_FRUSTUM_FOV_OVERSEND = 60.0f; - float originalFOV = cameraFov; - float wideFOV = originalFOV + VIEW_FRUSTUM_FOV_OVERSEND; - if (0.0f != cameraAspectRatio && 0.0f != cameraNearClip && 0.0f != cameraFarClip && cameraNearClip != cameraFarClip) { setProjection(glm::perspective( - glm::radians(wideFOV), // hack + glm::radians(cameraFov), cameraAspectRatio, cameraNearClip, cameraFarClip)); @@ -324,125 +321,27 @@ bool ViewFrustum::boxIntersectsKeyhole(const AABox& box) const { return true; } -bool testMatches(glm::quat lhs, glm::quat rhs, float epsilon = EPSILON) { - return (fabs(lhs.x - rhs.x) <= epsilon && fabs(lhs.y - rhs.y) <= epsilon && fabs(lhs.z - rhs.z) <= epsilon - && fabs(lhs.w - rhs.w) <= epsilon); +bool closeEnough(float a, float b, float relativeError) { + assert(relativeError >= 0.0f); + // NOTE: we add EPSILON to the denominator so we can avoid checking for division by zero. + // This method works fine when: fabsf(a + b) >> EPSILON + return fabsf(a - b) / (0.5f * fabsf(a + b) + EPSILON) < relativeError; } -bool testMatches(glm::vec3 lhs, glm::vec3 rhs, float epsilon = EPSILON) { - return (fabs(lhs.x - rhs.x) <= epsilon && fabs(lhs.y - rhs.y) <= epsilon && fabs(lhs.z - rhs.z) <= epsilon); -} +// TODO: the slop and relative error should be passed in by argument rather than hard-coded. +bool ViewFrustum::isVerySimilar(const ViewFrustum& other) const { + const float MIN_POSITION_SLOP_SQUARED = 25.0f; // 5 meters squared + const float MIN_ORIENTATION_DOT = 0.9924039f; // dot product of two quaternions 10 degrees apart + const float MIN_RELATIVE_ERROR = 0.01f; // 1% -bool testMatches(float lhs, float rhs, float epsilon = EPSILON) { - return (fabs(lhs - rhs) <= epsilon); -} - -bool ViewFrustum::matches(const ViewFrustum& compareTo, bool debug) const { - bool result = - testMatches(compareTo._position, _position) && - testMatches(compareTo._direction, _direction) && - testMatches(compareTo._up, _up) && - testMatches(compareTo._right, _right) && - testMatches(compareTo._fieldOfView, _fieldOfView) && - testMatches(compareTo._aspectRatio, _aspectRatio) && - testMatches(compareTo._nearClip, _nearClip) && - testMatches(compareTo._farClip, _farClip) && - testMatches(compareTo._focalLength, _focalLength); - - if (!result && debug) { - qCDebug(shared, "ViewFrustum::matches()... result=%s", debug::valueOf(result)); - qCDebug(shared, "%s -- compareTo._position=%f,%f,%f _position=%f,%f,%f", - (testMatches(compareTo._position,_position) ? "MATCHES " : "NO MATCH"), - (double)compareTo._position.x, (double)compareTo._position.y, (double)compareTo._position.z, - (double)_position.x, (double)_position.y, (double)_position.z); - qCDebug(shared, "%s -- compareTo._direction=%f,%f,%f _direction=%f,%f,%f", - (testMatches(compareTo._direction, _direction) ? "MATCHES " : "NO MATCH"), - (double)compareTo._direction.x, (double)compareTo._direction.y, (double)compareTo._direction.z, - (double)_direction.x, (double)_direction.y, (double)_direction.z ); - qCDebug(shared, "%s -- compareTo._up=%f,%f,%f _up=%f,%f,%f", - (testMatches(compareTo._up, _up) ? "MATCHES " : "NO MATCH"), - (double)compareTo._up.x, (double)compareTo._up.y, (double)compareTo._up.z, - (double)_up.x, (double)_up.y, (double)_up.z ); - qCDebug(shared, "%s -- compareTo._right=%f,%f,%f _right=%f,%f,%f", - (testMatches(compareTo._right, _right) ? "MATCHES " : "NO MATCH"), - (double)compareTo._right.x, (double)compareTo._right.y, (double)compareTo._right.z, - (double)_right.x, (double)_right.y, (double)_right.z ); - qCDebug(shared, "%s -- compareTo._fieldOfView=%f _fieldOfView=%f", - (testMatches(compareTo._fieldOfView, _fieldOfView) ? "MATCHES " : "NO MATCH"), - (double)compareTo._fieldOfView, (double)_fieldOfView); - qCDebug(shared, "%s -- compareTo._aspectRatio=%f _aspectRatio=%f", - (testMatches(compareTo._aspectRatio, _aspectRatio) ? "MATCHES " : "NO MATCH"), - (double)compareTo._aspectRatio, (double)_aspectRatio); - qCDebug(shared, "%s -- compareTo._nearClip=%f _nearClip=%f", - (testMatches(compareTo._nearClip, _nearClip) ? "MATCHES " : "NO MATCH"), - (double)compareTo._nearClip, (double)_nearClip); - qCDebug(shared, "%s -- compareTo._farClip=%f _farClip=%f", - (testMatches(compareTo._farClip, _farClip) ? "MATCHES " : "NO MATCH"), - (double)compareTo._farClip, (double)_farClip); - qCDebug(shared, "%s -- compareTo._focalLength=%f _focalLength=%f", - (testMatches(compareTo._focalLength, _focalLength) ? "MATCHES " : "NO MATCH"), - (double)compareTo._focalLength, (double)_focalLength); - } - return result; -} - -bool ViewFrustum::isVerySimilar(const ViewFrustum& compareTo, bool debug) const { - - // Compute distance between the two positions - const float POSITION_SIMILAR_ENOUGH = 5.0f; // 5 meters - float positionDistance = glm::distance(_position, compareTo._position); - - // Compute the angular distance between the two orientations - const float ORIENTATION_SIMILAR_ENOUGH = 10.0f; // 10 degrees in any direction - glm::quat dQOrientation = _orientation * glm::inverse(compareTo._orientation); - float angleOrientation = compareTo._orientation == _orientation ? 0.0f : glm::degrees(glm::angle(dQOrientation)); - if (isNaN(angleOrientation)) { - angleOrientation = 0.0f; - } - - bool result = - testMatches(0, positionDistance, POSITION_SIMILAR_ENOUGH) && - testMatches(0, angleOrientation, ORIENTATION_SIMILAR_ENOUGH) && - testMatches(compareTo._fieldOfView, _fieldOfView) && - testMatches(compareTo._aspectRatio, _aspectRatio) && - testMatches(compareTo._nearClip, _nearClip) && - testMatches(compareTo._farClip, _farClip) && - testMatches(compareTo._focalLength, _focalLength); - - - if (!result && debug) { - qCDebug(shared, "ViewFrustum::isVerySimilar()... result=%s\n", debug::valueOf(result)); - qCDebug(shared, "%s -- compareTo._position=%f,%f,%f _position=%f,%f,%f", - (testMatches(compareTo._position,_position, POSITION_SIMILAR_ENOUGH) ? - "IS SIMILAR ENOUGH " : "IS NOT SIMILAR ENOUGH"), - (double)compareTo._position.x, (double)compareTo._position.y, (double)compareTo._position.z, - (double)_position.x, (double)_position.y, (double)_position.z ); - - qCDebug(shared, "%s -- positionDistance=%f", - (testMatches(0,positionDistance, POSITION_SIMILAR_ENOUGH) ? "IS SIMILAR ENOUGH " : "IS NOT SIMILAR ENOUGH"), - (double)positionDistance); - - qCDebug(shared, "%s -- angleOrientation=%f", - (testMatches(0, angleOrientation, ORIENTATION_SIMILAR_ENOUGH) ? "IS SIMILAR ENOUGH " : "IS NOT SIMILAR ENOUGH"), - (double)angleOrientation); - - qCDebug(shared, "%s -- compareTo._fieldOfView=%f _fieldOfView=%f", - (testMatches(compareTo._fieldOfView, _fieldOfView) ? "MATCHES " : "NO MATCH"), - (double)compareTo._fieldOfView, (double)_fieldOfView); - qCDebug(shared, "%s -- compareTo._aspectRatio=%f _aspectRatio=%f", - (testMatches(compareTo._aspectRatio, _aspectRatio) ? "MATCHES " : "NO MATCH"), - (double)compareTo._aspectRatio, (double)_aspectRatio); - qCDebug(shared, "%s -- compareTo._nearClip=%f _nearClip=%f", - (testMatches(compareTo._nearClip, _nearClip) ? "MATCHES " : "NO MATCH"), - (double)compareTo._nearClip, (double)_nearClip); - qCDebug(shared, "%s -- compareTo._farClip=%f _farClip=%f", - (testMatches(compareTo._farClip, _farClip) ? "MATCHES " : "NO MATCH"), - (double)compareTo._farClip, (double)_farClip); - qCDebug(shared, "%s -- compareTo._focalLength=%f _focalLength=%f", - (testMatches(compareTo._focalLength, _focalLength) ? "MATCHES " : "NO MATCH"), - (double)compareTo._focalLength, (double)_focalLength); - } - return result; + return glm::distance2(_position, other._position) < MIN_POSITION_SLOP_SQUARED && + fabsf(glm::dot(_orientation, other._orientation)) > MIN_ORIENTATION_DOT && + closeEnough(_fieldOfView, other._fieldOfView, MIN_RELATIVE_ERROR) && + closeEnough(_aspectRatio, other._aspectRatio, MIN_RELATIVE_ERROR) && + closeEnough(_nearClip, other._nearClip, MIN_RELATIVE_ERROR) && + closeEnough(_farClip, other._farClip, MIN_RELATIVE_ERROR) && + closeEnough(_focalLength, other._focalLength, MIN_RELATIVE_ERROR) && + closeEnough(_centerSphereRadius, other._centerSphereRadius, MIN_RELATIVE_ERROR); } PickRay ViewFrustum::computePickRay(float x, float y) { diff --git a/libraries/shared/src/ViewFrustum.h b/libraries/shared/src/ViewFrustum.h index 221b0b5a07..d1b88fb2a5 100644 --- a/libraries/shared/src/ViewFrustum.h +++ b/libraries/shared/src/ViewFrustum.h @@ -107,12 +107,7 @@ public: bool cubeIntersectsKeyhole(const AACube& cube) const; bool boxIntersectsKeyhole(const AABox& box) const; - // some frustum comparisons - bool matches(const ViewFrustum& compareTo, bool debug = false) const; - bool matches(const ViewFrustum* compareTo, bool debug = false) const { return matches(*compareTo, debug); } - - bool isVerySimilar(const ViewFrustum& compareTo, bool debug = false) const; - bool isVerySimilar(const ViewFrustum* compareTo, bool debug = false) const { return isVerySimilar(*compareTo, debug); } + bool isVerySimilar(const ViewFrustum& compareTo) const; PickRay computePickRay(float x, float y); void computePickRay(float x, float y, glm::vec3& origin, glm::vec3& direction) const; diff --git a/plugins/oculus/src/OculusControllerManager.cpp b/plugins/oculus/src/OculusControllerManager.cpp index 6f7be26554..d0c717bd20 100644 --- a/plugins/oculus/src/OculusControllerManager.cpp +++ b/plugins/oculus/src/OculusControllerManager.cpp @@ -334,10 +334,8 @@ void OculusControllerManager::TouchDevice::handleHeadPose(float deltaTime, glm::mat4 defaultHeadOffset = glm::inverse(inputCalibrationData.defaultCenterEyeMat) * inputCalibrationData.defaultHeadMat; - controller::Pose hmdHeadPose = pose.transform(sensorToAvatar); - pose.valid = true; - _poseStateMap[controller::HEAD] = hmdHeadPose.postTransform(defaultHeadOffset); + _poseStateMap[controller::HEAD] = pose.postTransform(defaultHeadOffset).transform(sensorToAvatar); } void OculusControllerManager::TouchDevice::handleRotationForUntrackedHand(const controller::InputCalibrationData& inputCalibrationData,