mirror of
https://github.com/AleziaKurdis/overte.git
synced 2025-04-07 12:12:39 +02:00
Move all wire frustums to conical frustums
This commit is contained in:
parent
3283c5eaec
commit
27c471ee97
24 changed files with 181 additions and 257 deletions
|
@ -614,13 +614,19 @@ void Agent::sendAvatarViewFrustum() {
|
|||
view.setPosition(scriptedAvatar->getWorldPosition());
|
||||
view.setOrientation(scriptedAvatar->getHeadOrientation());
|
||||
view.calculate();
|
||||
ConicalViewFrustum conicalView { view };
|
||||
|
||||
auto avatarPacket = NLPacket::create(PacketType::AvatarQuery);
|
||||
auto destinationBuffer = reinterpret_cast<unsigned char*>(avatarPacket->getPayload());
|
||||
auto bufferStart = destinationBuffer;
|
||||
|
||||
uint8_t numFrustums = 1;
|
||||
auto viewFrustumByteArray = view.toByteArray();
|
||||
memcpy(destinationBuffer, &numFrustums, sizeof(numFrustums));
|
||||
destinationBuffer += sizeof(numFrustums);
|
||||
|
||||
auto avatarPacket = NLPacket::create(PacketType::AvatarQuery, viewFrustumByteArray.size() + sizeof(numFrustums));
|
||||
avatarPacket->writePrimitive(numFrustums);
|
||||
avatarPacket->write(viewFrustumByteArray);
|
||||
destinationBuffer += conicalView.serialize(destinationBuffer);
|
||||
|
||||
avatarPacket->setPayloadSize(destinationBuffer - bufferStart);
|
||||
|
||||
DependencyManager::get<NodeList>()->broadcastToNodes(std::move(avatarPacket),
|
||||
{ NodeType::AvatarMixer });
|
||||
|
|
|
@ -128,15 +128,16 @@ void AvatarMixerClientData::removeFromRadiusIgnoringSet(SharedNodePointer self,
|
|||
|
||||
void AvatarMixerClientData::readViewFrustumPacket(QByteArray message) {
|
||||
_currentViewFrustums.clear();
|
||||
|
||||
auto sourceBuffer = reinterpret_cast<const unsigned char*>(message.constData());
|
||||
|
||||
uint8_t numFrustums = 0;
|
||||
memcpy(&numFrustums, message.constData(), sizeof(numFrustums));
|
||||
message.remove(0, sizeof(numFrustums));
|
||||
memcpy(&numFrustums, sourceBuffer, sizeof(numFrustums));
|
||||
sourceBuffer += sizeof(numFrustums);
|
||||
|
||||
for (uint8_t i = 0; i < numFrustums; ++i) {
|
||||
ViewFrustum frustum;
|
||||
auto bytesRead = frustum.fromByteArray(message);
|
||||
message.remove(0, bytesRead);
|
||||
ConicalViewFrustum frustum;
|
||||
sourceBuffer += frustum.deserialize(sourceBuffer);
|
||||
|
||||
_currentViewFrustums.push_back(frustum);
|
||||
}
|
||||
|
@ -144,8 +145,8 @@ void AvatarMixerClientData::readViewFrustumPacket(QByteArray message) {
|
|||
|
||||
bool AvatarMixerClientData::otherAvatarInView(const AABox& otherAvatarBox) {
|
||||
return std::any_of(std::begin(_currentViewFrustums), std::end(_currentViewFrustums),
|
||||
[&](const ViewFrustum& viewFrustum) {
|
||||
return viewFrustum.boxIntersectsKeyhole(otherAvatarBox);
|
||||
[&](const ConicalViewFrustum& viewFrustum) {
|
||||
return viewFrustum.intersects(otherAvatarBox);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
#include <PortableHighResolutionClock.h>
|
||||
#include <SimpleMovingAverage.h>
|
||||
#include <UUIDHasher.h>
|
||||
#include <ViewFrustum.h>
|
||||
#include <shared/ConicalViewFrustum.h>
|
||||
|
||||
const QString OUTBOUND_AVATAR_DATA_STATS_KEY = "outbound_av_data_kbps";
|
||||
const QString INBOUND_AVATAR_DATA_STATS_KEY = "inbound_av_data_kbps";
|
||||
|
@ -110,7 +110,7 @@ public:
|
|||
bool getRequestsDomainListData() { return _requestsDomainListData; }
|
||||
void setRequestsDomainListData(bool requesting) { _requestsDomainListData = requesting; }
|
||||
|
||||
const ViewFrustums& getViewFrustums() const { return _currentViewFrustums; }
|
||||
const ConicalViewFrustums& getViewFrustums() const { return _currentViewFrustums; }
|
||||
|
||||
uint64_t getLastOtherAvatarEncodeTime(QUuid otherAvatar) const;
|
||||
void setLastOtherAvatarEncodeTime(const QUuid& otherAvatar, uint64_t time);
|
||||
|
@ -150,7 +150,7 @@ private:
|
|||
|
||||
SimpleMovingAverage _avgOtherAvatarDataRate;
|
||||
std::unordered_set<QUuid> _radiusIgnoredOthers;
|
||||
ViewFrustums _currentViewFrustums;
|
||||
ConicalViewFrustums _currentViewFrustums;
|
||||
|
||||
int _recentOtherAvatarsInView { 0 };
|
||||
int _recentOtherAvatarsOutOfView { 0 };
|
||||
|
|
|
@ -107,16 +107,7 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O
|
|||
|
||||
|
||||
DiffTraversal::View newView;
|
||||
|
||||
ConicalViewFrustum viewFrustum;
|
||||
if (nodeData->hasMainViewFrustum()) {
|
||||
nodeData->copyCurrentMainViewFrustum(viewFrustum);
|
||||
newView.viewFrustums.push_back(viewFrustum);
|
||||
}
|
||||
if (nodeData->hasSecondaryViewFrustum()) {
|
||||
nodeData->copyCurrentSecondaryViewFrustum(viewFrustum);
|
||||
newView.viewFrustums.push_back(viewFrustum);
|
||||
}
|
||||
newView.viewFrustums = nodeData->getCurrentViews();
|
||||
|
||||
int32_t lodLevelOffset = nodeData->getBoundaryLevelAdjust() + (viewFrustumChanged ? LOW_RES_MOVING_ADJUST : NO_BOUNDARY_ADJUST);
|
||||
newView.lodScaleFactor = powf(2.0f, lodLevelOffset);
|
||||
|
|
|
@ -19,7 +19,10 @@ void OctreeHeadlessViewer::queryOctree() {
|
|||
PacketType packetType = getMyQueryMessageType();
|
||||
|
||||
if (_hasViewFrustum) {
|
||||
_octreeQuery.setMainViewFrustum(_viewFrustum);
|
||||
ConicalViewFrustums views { _viewFrustum };
|
||||
_octreeQuery.setConicalViews(views);
|
||||
} else {
|
||||
_octreeQuery.clearConicalViews();
|
||||
}
|
||||
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
|
|
@ -330,7 +330,7 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode*
|
|||
} else {
|
||||
// we aren't forcing a full scene, check if something else suggests we should
|
||||
isFullScene = nodeData->haveJSONParametersChanged() ||
|
||||
(nodeData->hasMainViewFrustum() &&
|
||||
(nodeData->hasConicalViews() &&
|
||||
(nodeData->getViewFrustumJustStoppedChanging() ||
|
||||
nodeData->hasLodChanged()));
|
||||
}
|
||||
|
|
|
@ -5230,10 +5230,10 @@ void Application::updateSecondaryCameraViewFrustum() {
|
|||
assert(camera);
|
||||
|
||||
if (!camera->isEnabled()) {
|
||||
_hasSecondaryViewFrustum = false;
|
||||
return;
|
||||
}
|
||||
|
||||
ViewFrustum secondaryViewFrustum;
|
||||
if (camera->mirrorProjection && !camera->attachedEntityId.isNull()) {
|
||||
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
|
||||
auto entityProperties = entityScriptingInterface->getEntityProperties(camera->attachedEntityId);
|
||||
|
@ -5258,36 +5258,37 @@ void Application::updateSecondaryCameraViewFrustum() {
|
|||
|
||||
// set frustum position to be mirrored camera and set orientation to mirror's adjusted rotation
|
||||
glm::quat mirrorCameraOrientation = glm::quat_cast(worldFromMirrorRotation);
|
||||
_secondaryViewFrustum.setPosition(mirrorCameraPositionWorld);
|
||||
_secondaryViewFrustum.setOrientation(mirrorCameraOrientation);
|
||||
secondaryViewFrustum.setPosition(mirrorCameraPositionWorld);
|
||||
secondaryViewFrustum.setOrientation(mirrorCameraOrientation);
|
||||
|
||||
// build frustum using mirror space translation of mirrored camera
|
||||
float nearClip = mirrorCameraPositionMirror.z + mirrorPropertiesDimensions.z * 2.0f;
|
||||
glm::vec3 upperRight = halfMirrorPropertiesDimensions - mirrorCameraPositionMirror;
|
||||
glm::vec3 bottomLeft = -halfMirrorPropertiesDimensions - mirrorCameraPositionMirror;
|
||||
glm::mat4 frustum = glm::frustum(bottomLeft.x, upperRight.x, bottomLeft.y, upperRight.y, nearClip, camera->farClipPlaneDistance);
|
||||
_secondaryViewFrustum.setProjection(frustum);
|
||||
secondaryViewFrustum.setProjection(frustum);
|
||||
} else {
|
||||
if (!camera->attachedEntityId.isNull()) {
|
||||
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
|
||||
auto entityProperties = entityScriptingInterface->getEntityProperties(camera->attachedEntityId);
|
||||
_secondaryViewFrustum.setPosition(entityProperties.getPosition());
|
||||
_secondaryViewFrustum.setOrientation(entityProperties.getRotation());
|
||||
secondaryViewFrustum.setPosition(entityProperties.getPosition());
|
||||
secondaryViewFrustum.setOrientation(entityProperties.getRotation());
|
||||
} else {
|
||||
_secondaryViewFrustum.setPosition(camera->position);
|
||||
_secondaryViewFrustum.setOrientation(camera->orientation);
|
||||
secondaryViewFrustum.setPosition(camera->position);
|
||||
secondaryViewFrustum.setOrientation(camera->orientation);
|
||||
}
|
||||
|
||||
float aspectRatio = (float)camera->textureWidth / (float)camera->textureHeight;
|
||||
_secondaryViewFrustum.setProjection(camera->vFoV,
|
||||
secondaryViewFrustum.setProjection(camera->vFoV,
|
||||
aspectRatio,
|
||||
camera->nearClipPlaneDistance,
|
||||
camera->farClipPlaneDistance);
|
||||
}
|
||||
// Without calculating the bound planes, the secondary camera will use the same culling frustum as the main camera,
|
||||
// which is not what we want here.
|
||||
_secondaryViewFrustum.calculate();
|
||||
_hasSecondaryViewFrustum = true;
|
||||
secondaryViewFrustum.calculate();
|
||||
|
||||
_conicalViews.push_back(secondaryViewFrustum);
|
||||
}
|
||||
|
||||
static bool domainLoadingInProgress = false;
|
||||
|
@ -5644,6 +5645,8 @@ void Application::update(float deltaTime) {
|
|||
QMutexLocker viewLocker(&_viewMutex);
|
||||
_myCamera.loadViewFrustum(_viewFrustum);
|
||||
|
||||
_conicalViews.clear();
|
||||
_conicalViews.push_back(_viewFrustum);
|
||||
// TODO: Fix this by modeling the way the secondary camera works on how the main camera works
|
||||
// ie. Use a camera object stored in the game logic and informs the Engine on where the secondary
|
||||
// camera should be.
|
||||
|
@ -5660,18 +5663,29 @@ void Application::update(float deltaTime) {
|
|||
quint64 sinceLastQuery = now - _lastQueriedTime;
|
||||
const quint64 TOO_LONG_SINCE_LAST_QUERY = 3 * USECS_PER_SECOND;
|
||||
bool queryIsDue = sinceLastQuery > TOO_LONG_SINCE_LAST_QUERY;
|
||||
bool viewIsDifferentEnough = !_lastQueriedViewFrustum.isVerySimilar(_viewFrustum);
|
||||
viewIsDifferentEnough |= _hasSecondaryViewFrustum && !_lastQueriedSecondaryViewFrustum.isVerySimilar(_secondaryViewFrustum);
|
||||
|
||||
bool viewIsDifferentEnough = false;
|
||||
if (_conicalViews.size() == _lastQueriedViews.size()) {
|
||||
for (size_t i = 0; i < _conicalViews.size(); ++i) {
|
||||
if (!_conicalViews[i].isVerySimilar(_lastQueriedViews[i])) {
|
||||
viewIsDifferentEnough = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
viewIsDifferentEnough = true;
|
||||
}
|
||||
|
||||
|
||||
// if it's been a while since our last query or the view has significantly changed then send a query, otherwise suppress it
|
||||
if (queryIsDue || viewIsDifferentEnough) {
|
||||
_lastQueriedTime = now;
|
||||
if (DependencyManager::get<SceneScriptingInterface>()->shouldRenderEntities()) {
|
||||
queryOctree(NodeType::EntityServer, PacketType::EntityQuery);
|
||||
}
|
||||
sendAvatarViewFrustum();
|
||||
_lastQueriedViewFrustum = _viewFrustum;
|
||||
_lastQueriedSecondaryViewFrustum = _secondaryViewFrustum;
|
||||
|
||||
_lastQueriedViews = _conicalViews;
|
||||
_lastQueriedTime = now;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5844,17 +5858,19 @@ void Application::update(float deltaTime) {
|
|||
}
|
||||
|
||||
void Application::sendAvatarViewFrustum() {
|
||||
uint8_t numFrustums = 1;
|
||||
QByteArray viewFrustumByteArray = _viewFrustum.toByteArray();
|
||||
auto avatarPacket = NLPacket::create(PacketType::AvatarQuery);
|
||||
auto destinationBuffer = reinterpret_cast<unsigned char*>(avatarPacket->getPayload());
|
||||
unsigned char* bufferStart = destinationBuffer;
|
||||
|
||||
if (_hasSecondaryViewFrustum) {
|
||||
++numFrustums;
|
||||
viewFrustumByteArray += _secondaryViewFrustum.toByteArray();
|
||||
uint8_t numFrustums = _conicalViews.size();
|
||||
memcpy(destinationBuffer, &numFrustums, sizeof(numFrustums));
|
||||
destinationBuffer += sizeof(numFrustums);
|
||||
|
||||
for (const auto& view : _conicalViews) {
|
||||
destinationBuffer += view.serialize(destinationBuffer);
|
||||
}
|
||||
|
||||
auto avatarPacket = NLPacket::create(PacketType::AvatarQuery, viewFrustumByteArray.size() + sizeof(numFrustums));
|
||||
avatarPacket->writePrimitive(numFrustums);
|
||||
avatarPacket->write(viewFrustumByteArray);
|
||||
avatarPacket->setPayloadSize(destinationBuffer - bufferStart);
|
||||
|
||||
DependencyManager::get<NodeList>()->broadcastToNodes(std::move(avatarPacket), NodeSet() << NodeType::AvatarMixer);
|
||||
}
|
||||
|
@ -5916,16 +5932,7 @@ void Application::queryOctree(NodeType_t serverType, PacketType packetType) {
|
|||
return; // bail early if settings are not loaded
|
||||
}
|
||||
|
||||
ViewFrustum viewFrustum;
|
||||
copyViewFrustum(viewFrustum);
|
||||
_octreeQuery.setMainViewFrustum(viewFrustum);
|
||||
|
||||
if (hasSecondaryViewFrustum()) {
|
||||
copySecondaryViewFrustum(viewFrustum);
|
||||
_octreeQuery.setSecondaryViewFrustum(viewFrustum);
|
||||
} else {
|
||||
_octreeQuery.clearSecondaryViewFrustum();
|
||||
}
|
||||
_octreeQuery.setConicalViews(_conicalViews);
|
||||
|
||||
auto lodManager = DependencyManager::get<LODManager>();
|
||||
_octreeQuery.setOctreeSizeScale(lodManager->getOctreeSizeScale());
|
||||
|
@ -6010,11 +6017,6 @@ void Application::copyDisplayViewFrustum(ViewFrustum& viewOut) const {
|
|||
viewOut = _displayViewFrustum;
|
||||
}
|
||||
|
||||
void Application::copySecondaryViewFrustum(ViewFrustum& viewOut) const {
|
||||
QMutexLocker viewLocker(&_viewMutex);
|
||||
viewOut = _secondaryViewFrustum;
|
||||
}
|
||||
|
||||
void Application::resetSensors(bool andReload) {
|
||||
DependencyManager::get<DdeFaceTracker>()->reset();
|
||||
DependencyManager::get<EyeTracker>()->reset();
|
||||
|
|
|
@ -47,6 +47,7 @@
|
|||
#include <AbstractUriHandler.h>
|
||||
#include <shared/RateCounter.h>
|
||||
#include <ThreadSafeValueCache.h>
|
||||
#include <shared/ConicalViewFrustum.h>
|
||||
#include <shared/FileLogger.h>
|
||||
|
||||
#include <RunningMarker.h>
|
||||
|
@ -175,14 +176,14 @@ public:
|
|||
Camera& getCamera() { return _myCamera; }
|
||||
const Camera& getCamera() const { return _myCamera; }
|
||||
// Represents the current view frustum of the avatar.
|
||||
void copyViewFrustum(ViewFrustum& viewOut) const override;
|
||||
void copySecondaryViewFrustum(ViewFrustum& viewOut) const override;
|
||||
bool hasSecondaryViewFrustum() const override { return _hasSecondaryViewFrustum; }
|
||||
void copyViewFrustum(ViewFrustum& viewOut) const;
|
||||
// Represents the view frustum of the current rendering pass,
|
||||
// which might be different from the viewFrustum, i.e. shadowmap
|
||||
// passes, mirror window passes, etc
|
||||
void copyDisplayViewFrustum(ViewFrustum& viewOut) const;
|
||||
|
||||
const ConicalViewFrustums& getConicalViews() const override { return _conicalViews; }
|
||||
|
||||
const OctreePacketProcessor& getOctreePacketProcessor() const { return _octreeProcessor; }
|
||||
QSharedPointer<EntityTreeRenderer> getEntities() const { return DependencyManager::get<EntityTreeRenderer>(); }
|
||||
QUndoStack* getUndoStack() { return &_undoStack; }
|
||||
|
@ -574,11 +575,10 @@ private:
|
|||
|
||||
mutable QMutex _viewMutex { QMutex::Recursive };
|
||||
ViewFrustum _viewFrustum; // current state of view frustum, perspective, orientation, etc.
|
||||
ViewFrustum _lastQueriedViewFrustum; // last view frustum used to query octree servers
|
||||
ViewFrustum _displayViewFrustum;
|
||||
ViewFrustum _secondaryViewFrustum;
|
||||
ViewFrustum _lastQueriedSecondaryViewFrustum; // last secondary view frustum used to query octree servers
|
||||
bool _hasSecondaryViewFrustum;
|
||||
|
||||
ConicalViewFrustums _conicalViews;
|
||||
ConicalViewFrustums _lastQueriedViews; // last views used to query servers
|
||||
quint64 _lastQueriedTime;
|
||||
|
||||
OctreeQuery _octreeQuery { true }; // NodeData derived class for querying octee cells from octree servers
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
#include <UsersScriptingInterface.h>
|
||||
#include <UUID.h>
|
||||
#include <avatars-renderer/OtherAvatar.h>
|
||||
#include <shared/ConicalViewFrustum.h>
|
||||
|
||||
#include "Application.h"
|
||||
#include "AvatarManager.h"
|
||||
|
@ -156,17 +157,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
|
|||
};
|
||||
|
||||
|
||||
ViewFrustums views;
|
||||
|
||||
ViewFrustum view;
|
||||
qApp->copyCurrentViewFrustum(view);
|
||||
views.push_back(view);
|
||||
|
||||
if (qApp->hasSecondaryViewFrustum()) {
|
||||
qApp->copySecondaryViewFrustum(view);
|
||||
views.push_back(view);
|
||||
}
|
||||
|
||||
const auto& views = qApp->getConicalViews();
|
||||
PrioritySortUtil::PriorityQueue<SortableAvatar> sortedAvatars(views,
|
||||
AvatarData::_avatarSortCoefficientSize,
|
||||
AvatarData::_avatarSortCoefficientCenter,
|
||||
|
|
|
@ -296,8 +296,7 @@ void EntityTreeRenderer::addPendingEntities(const render::ScenePointer& scene, r
|
|||
}
|
||||
}
|
||||
|
||||
void EntityTreeRenderer::updateChangedEntities(const render::ScenePointer& scene, const ViewFrustums& views,
|
||||
render::Transaction& transaction) {
|
||||
void EntityTreeRenderer::updateChangedEntities(const render::ScenePointer& scene, render::Transaction& transaction) {
|
||||
PROFILE_RANGE_EX(simulation_physics, "ChangeInScene", 0xffff00ff, (uint64_t)_changedEntities.size());
|
||||
PerformanceTimer pt("change");
|
||||
std::unordered_set<EntityItemID> changedEntities;
|
||||
|
@ -358,6 +357,8 @@ void EntityTreeRenderer::updateChangedEntities(const render::ScenePointer& scene
|
|||
|
||||
// prioritize and sort the renderables
|
||||
uint64_t sortStart = usecTimestampNow();
|
||||
|
||||
const auto& views = _viewState->getConicalViews();
|
||||
PrioritySortUtil::PriorityQueue<SortableRenderer> sortedRenderables(views);
|
||||
{
|
||||
PROFILE_RANGE_EX(simulation_physics, "SortRenderables", 0xffff00ff, (uint64_t)_renderablesToUpdate.size());
|
||||
|
@ -417,19 +418,7 @@ void EntityTreeRenderer::update(bool simulate) {
|
|||
render::Transaction transaction;
|
||||
addPendingEntities(scene, transaction);
|
||||
|
||||
ViewFrustums views;
|
||||
|
||||
ViewFrustum view;
|
||||
_viewState->copyViewFrustum(view);
|
||||
views.push_back(view);
|
||||
|
||||
if (_viewState->hasSecondaryViewFrustum()) {
|
||||
_viewState->copySecondaryViewFrustum(view);
|
||||
views.push_back(view);
|
||||
}
|
||||
|
||||
|
||||
updateChangedEntities(scene, views, transaction);
|
||||
updateChangedEntities(scene, transaction);
|
||||
scene->enqueueTransaction(transaction);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -148,8 +148,7 @@ protected:
|
|||
|
||||
private:
|
||||
void addPendingEntities(const render::ScenePointer& scene, render::Transaction& transaction);
|
||||
void updateChangedEntities(const render::ScenePointer& scene, const ViewFrustums& views,
|
||||
render::Transaction& transaction);
|
||||
void updateChangedEntities(const render::ScenePointer& scene, render::Transaction& transaction);
|
||||
EntityRendererPointer renderableForEntity(const EntityItemPointer& entity) const { return renderableForEntityId(entity->getID()); }
|
||||
render::ItemID renderableIdForEntity(const EntityItemPointer& entity) const { return renderableIdForEntityId(entity->getID()); }
|
||||
|
||||
|
|
|
@ -18,10 +18,6 @@
|
|||
#include <GLMHelpers.h>
|
||||
#include <udt/PacketHeaders.h>
|
||||
|
||||
using QueryFlags = uint8_t;
|
||||
const QueryFlags QUERY_HAS_MAIN_FRUSTUM = 1U << 0;
|
||||
const QueryFlags QUERY_HAS_SECONDARY_FRUSTUM = 1U << 1;
|
||||
|
||||
OctreeQuery::OctreeQuery(bool randomizeConnectionID) {
|
||||
if (randomizeConnectionID) {
|
||||
// randomize our initial octree query connection ID using random_device
|
||||
|
@ -38,23 +34,13 @@ int OctreeQuery::getBroadcastData(unsigned char* destinationBuffer) {
|
|||
memcpy(destinationBuffer, &_connectionID, sizeof(_connectionID));
|
||||
destinationBuffer += sizeof(_connectionID);
|
||||
|
||||
// flags for wether the frustums are present
|
||||
QueryFlags frustumFlags = 0;
|
||||
if (_hasMainFrustum) {
|
||||
frustumFlags |= QUERY_HAS_MAIN_FRUSTUM;
|
||||
}
|
||||
if (_hasSecondaryFrustum) {
|
||||
frustumFlags |= QUERY_HAS_SECONDARY_FRUSTUM;
|
||||
}
|
||||
memcpy(destinationBuffer, &frustumFlags, sizeof(frustumFlags));
|
||||
destinationBuffer += sizeof(frustumFlags);
|
||||
// Number of frustums
|
||||
uint8_t numFrustums = _conicalViews.size();
|
||||
memcpy(destinationBuffer, &numFrustums, sizeof(numFrustums));
|
||||
destinationBuffer += sizeof(numFrustums);
|
||||
|
||||
if (_hasMainFrustum) {
|
||||
destinationBuffer += _mainViewFrustum.serialize(destinationBuffer);
|
||||
}
|
||||
|
||||
if (_hasSecondaryFrustum) {
|
||||
destinationBuffer += _secondaryViewFrustum.serialize(destinationBuffer);
|
||||
for (const auto& view : _conicalViews) {
|
||||
destinationBuffer += view.serialize(destinationBuffer);
|
||||
}
|
||||
|
||||
// desired Max Octree PPS
|
||||
|
@ -118,19 +104,15 @@ int OctreeQuery::parseData(ReceivedMessage& message) {
|
|||
}
|
||||
|
||||
// check if this query uses a view frustum
|
||||
QueryFlags frustumFlags { 0 };
|
||||
memcpy(&frustumFlags, sourceBuffer, sizeof(frustumFlags));
|
||||
sourceBuffer += sizeof(frustumFlags);
|
||||
uint8_t numFrustums = 0;
|
||||
memcpy(&numFrustums, sourceBuffer, sizeof(numFrustums));
|
||||
sourceBuffer += sizeof(numFrustums);
|
||||
|
||||
_hasMainFrustum = frustumFlags & QUERY_HAS_MAIN_FRUSTUM;
|
||||
_hasSecondaryFrustum = frustumFlags & QUERY_HAS_SECONDARY_FRUSTUM;
|
||||
|
||||
if (_hasMainFrustum) {
|
||||
sourceBuffer += _mainViewFrustum.deserialize(sourceBuffer);
|
||||
}
|
||||
|
||||
if (_hasSecondaryFrustum) {
|
||||
sourceBuffer += _secondaryViewFrustum.deserialize(sourceBuffer);
|
||||
_conicalViews.clear();
|
||||
for (int i = 0; i < numFrustums; ++i) {
|
||||
ConicalViewFrustum view;
|
||||
sourceBuffer += view.deserialize(sourceBuffer);
|
||||
_conicalViews.push_back(view);
|
||||
}
|
||||
|
||||
// desired Max Octree PPS
|
||||
|
|
|
@ -33,13 +33,9 @@ public:
|
|||
int getBroadcastData(unsigned char* destinationBuffer);
|
||||
int parseData(ReceivedMessage& message) override;
|
||||
|
||||
bool hasMainViewFrustum() const { return _hasMainFrustum; }
|
||||
void setMainViewFrustum(const ViewFrustum& viewFrustum) { _hasMainFrustum = true; _mainViewFrustum = viewFrustum; }
|
||||
void clearMainViewFrustum() { _hasMainFrustum = false; }
|
||||
|
||||
bool hasSecondaryViewFrustum() const { return _hasSecondaryFrustum; }
|
||||
void setSecondaryViewFrustum(const ViewFrustum& viewFrustum) { _hasSecondaryFrustum = true; _secondaryViewFrustum = viewFrustum; }
|
||||
void clearSecondaryViewFrustum() { _hasSecondaryFrustum = false; }
|
||||
bool hasConicalViews() const { return !_conicalViews.empty(); }
|
||||
void setConicalViews(ConicalViewFrustums views) { _conicalViews = views; }
|
||||
void clearConicalViews() { _conicalViews.clear(); }
|
||||
|
||||
// getters/setters for JSON filter
|
||||
QJsonObject getJSONParameters() { QReadLocker locker { &_jsonParametersLock }; return _jsonParameters; }
|
||||
|
@ -64,10 +60,7 @@ public slots:
|
|||
void setBoundaryLevelAdjust(int boundaryLevelAdjust) { _boundaryLevelAdjust = boundaryLevelAdjust; }
|
||||
|
||||
protected:
|
||||
bool _hasMainFrustum { false };
|
||||
ConicalViewFrustum _mainViewFrustum;
|
||||
bool _hasSecondaryFrustum { false };
|
||||
ConicalViewFrustum _secondaryViewFrustum;
|
||||
ConicalViewFrustums _conicalViews;
|
||||
|
||||
// octree server sending items
|
||||
int _maxQueryPPS = DEFAULT_MAX_OCTREE_PPS;
|
||||
|
|
|
@ -139,23 +139,13 @@ void OctreeQueryNode::writeToPacket(const unsigned char* buffer, unsigned int by
|
|||
}
|
||||
}
|
||||
|
||||
void OctreeQueryNode::copyCurrentMainViewFrustum(ConicalViewFrustum& viewOut) const {
|
||||
QMutexLocker viewLocker(&_viewMutex);
|
||||
viewOut = _currentMainViewFrustum;
|
||||
}
|
||||
|
||||
void OctreeQueryNode::copyCurrentSecondaryViewFrustum(ConicalViewFrustum& viewOut) const {
|
||||
QMutexLocker viewLocker(&_viewMutex);
|
||||
viewOut = _currentSecondaryViewFrustum;
|
||||
}
|
||||
|
||||
bool OctreeQueryNode::updateCurrentViewFrustum() {
|
||||
// if shutting down, return immediately
|
||||
if (_isShuttingDown) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_hasMainFrustum && !_hasSecondaryFrustum) {
|
||||
if (!hasConicalViews()) {
|
||||
// this client does not use a view frustum so the view frustum for this query has not changed
|
||||
return false;
|
||||
}
|
||||
|
@ -164,12 +154,17 @@ bool OctreeQueryNode::updateCurrentViewFrustum() {
|
|||
|
||||
{ // if there has been a change, then recalculate
|
||||
QMutexLocker viewLocker(&_viewMutex);
|
||||
if (_hasMainFrustum && !_mainViewFrustum.isVerySimilar(_currentMainViewFrustum)) {
|
||||
_currentMainViewFrustum = _mainViewFrustum;
|
||||
currentViewFrustumChanged = true;
|
||||
}
|
||||
if (_hasSecondaryFrustum && !_secondaryViewFrustum.isVerySimilar(_currentSecondaryViewFrustum)) {
|
||||
_currentSecondaryViewFrustum = _secondaryViewFrustum;
|
||||
|
||||
if (_conicalViews.size() == _currentConicalViews.size()) {
|
||||
for (size_t i = 0; i < _conicalViews.size(); ++i) {
|
||||
if (!_conicalViews[i].isVerySimilar(_currentConicalViews[i])) {
|
||||
_currentConicalViews = _conicalViews;
|
||||
currentViewFrustumChanged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_currentConicalViews = _conicalViews;
|
||||
currentViewFrustumChanged = true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,8 +49,7 @@ public:
|
|||
|
||||
OctreeElementExtraEncodeData extraEncodeData;
|
||||
|
||||
void copyCurrentMainViewFrustum(ConicalViewFrustum& viewOut) const;
|
||||
void copyCurrentSecondaryViewFrustum(ConicalViewFrustum& viewOut) const;
|
||||
const ConicalViewFrustums& getCurrentViews() const { return _currentConicalViews; }
|
||||
|
||||
// These are not classic setters because they are calculating and maintaining state
|
||||
// which is set asynchronously through the network receive
|
||||
|
@ -97,8 +96,7 @@ private:
|
|||
quint64 _firstSuppressedPacket { usecTimestampNow() };
|
||||
|
||||
mutable QMutex _viewMutex { QMutex::Recursive };
|
||||
ConicalViewFrustum _currentMainViewFrustum;
|
||||
ConicalViewFrustum _currentSecondaryViewFrustum;
|
||||
ConicalViewFrustums _currentConicalViews;
|
||||
bool _viewFrustumChanging { false };
|
||||
bool _viewFrustumJustStoppedChanging { true };
|
||||
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
#ifndef hifi_OctreeUtils_h
|
||||
#define hifi_OctreeUtils_h
|
||||
|
||||
#include <NumericalConstants.h>
|
||||
|
||||
#include "OctreeConstants.h"
|
||||
|
||||
class AABox;
|
||||
|
@ -33,7 +35,6 @@ float getOrthographicAccuracySize(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
|
||||
|
||||
|
|
|
@ -24,6 +24,8 @@ class Transform;
|
|||
class QThread;
|
||||
class ViewFrustum;
|
||||
class PickRay;
|
||||
class ConicalViewFrustum;
|
||||
using ConicalViewFrustums = std::vector<ConicalViewFrustum>;
|
||||
|
||||
/// Interface provided by Application to other objects that need access to the current view state details
|
||||
class AbstractViewStateInterface {
|
||||
|
@ -31,9 +33,7 @@ public:
|
|||
/// copies the current view frustum for rendering the view state
|
||||
virtual void copyCurrentViewFrustum(ViewFrustum& viewOut) const = 0;
|
||||
|
||||
virtual void copyViewFrustum(ViewFrustum& viewOut) const = 0;
|
||||
virtual void copySecondaryViewFrustum(ViewFrustum& viewOut) const = 0;
|
||||
virtual bool hasSecondaryViewFrustum() const = 0;
|
||||
virtual const ConicalViewFrustums& getConicalViews() const = 0;
|
||||
|
||||
virtual QThread* getMainThread() = 0;
|
||||
|
||||
|
|
|
@ -48,6 +48,8 @@ const int BYTES_PER_KILOBYTE = 1000;
|
|||
const int BYTES_PER_KILOBIT = BYTES_PER_KILOBYTE / BITS_IN_BYTE;
|
||||
const int KILO_PER_MEGA = 1000;
|
||||
|
||||
const float SQRT_THREE = 1.73205080f;
|
||||
|
||||
#define KB_TO_BYTES_SHIFT 10
|
||||
#define MB_TO_BYTES_SHIFT 20
|
||||
#define GB_TO_BYTES_SHIFT 30
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
#include <queue>
|
||||
|
||||
#include "NumericalConstants.h"
|
||||
#include "ViewFrustum.h"
|
||||
#include "shared/ConicalViewFrustum.h"
|
||||
|
||||
/* PrioritySortUtil is a helper for sorting 3D things relative to a ViewFrustum. To use:
|
||||
|
||||
|
@ -84,12 +84,12 @@ namespace PrioritySortUtil {
|
|||
class PriorityQueue {
|
||||
public:
|
||||
PriorityQueue() = delete;
|
||||
PriorityQueue(const ViewFrustums& views) : _views(views) { }
|
||||
PriorityQueue(const ViewFrustums& views, float angularWeight, float centerWeight, float ageWeight)
|
||||
PriorityQueue(const ConicalViewFrustums& views) : _views(views) { }
|
||||
PriorityQueue(const ConicalViewFrustums& views, float angularWeight, float centerWeight, float ageWeight)
|
||||
: _views(views), _angularWeight(angularWeight), _centerWeight(centerWeight), _ageWeight(ageWeight)
|
||||
{ }
|
||||
|
||||
void setViews(const ViewFrustums& views) { _views = views; }
|
||||
void setViews(const ConicalViewFrustums& views) { _views = views; }
|
||||
|
||||
void setWeights(float angularWeight, float centerWeight, float ageWeight) {
|
||||
_angularWeight = angularWeight;
|
||||
|
@ -118,7 +118,7 @@ namespace PrioritySortUtil {
|
|||
return priority;
|
||||
}
|
||||
|
||||
float computePriority(const ViewFrustum& view, const T& thing) const {
|
||||
float computePriority(const ConicalViewFrustum& view, const T& thing) const {
|
||||
// priority = weighted linear combination of multiple values:
|
||||
// (a) angular size
|
||||
// (b) proximity to center of view
|
||||
|
@ -143,8 +143,8 @@ namespace PrioritySortUtil {
|
|||
+ _ageWeight * cosineAngleFactor * age;
|
||||
|
||||
// decrement priority of things outside keyhole
|
||||
if (distance - radius > view.getCenterRadius()) {
|
||||
if (!view.sphereIntersectsFrustum(position, radius)) {
|
||||
if (distance - radius > view.getRadius()) {
|
||||
if (!view.intersects(offset, distance, radius)) {
|
||||
constexpr float OUT_OF_VIEW_PENALTY = -10.0f;
|
||||
priority += OUT_OF_VIEW_PENALTY;
|
||||
}
|
||||
|
@ -152,7 +152,7 @@ namespace PrioritySortUtil {
|
|||
return priority;
|
||||
}
|
||||
|
||||
ViewFrustums _views;
|
||||
ConicalViewFrustums _views;
|
||||
std::priority_queue<T> _queue;
|
||||
float _angularWeight { DEFAULT_ANGULAR_COEF };
|
||||
float _centerWeight { DEFAULT_CENTER_COEF };
|
||||
|
|
|
@ -138,69 +138,6 @@ const char* ViewFrustum::debugPlaneName (int plane) const {
|
|||
return "Unknown";
|
||||
}
|
||||
|
||||
int ViewFrustum::fromByteArray(const QByteArray& input) {
|
||||
|
||||
// From the wire!
|
||||
glm::vec3 cameraPosition;
|
||||
glm::quat cameraOrientation;
|
||||
float cameraCenterRadius;
|
||||
float cameraFov;
|
||||
float cameraAspectRatio;
|
||||
float cameraNearClip;
|
||||
float cameraFarClip;
|
||||
|
||||
const unsigned char* startPosition = reinterpret_cast<const unsigned char*>(input.constData());
|
||||
const unsigned char* sourceBuffer = startPosition;
|
||||
|
||||
// camera details
|
||||
memcpy(&cameraPosition, sourceBuffer, sizeof(cameraPosition));
|
||||
sourceBuffer += sizeof(cameraPosition);
|
||||
sourceBuffer += unpackOrientationQuatFromBytes(sourceBuffer, cameraOrientation);
|
||||
sourceBuffer += unpackFloatAngleFromTwoByte((uint16_t*)sourceBuffer, &cameraFov);
|
||||
sourceBuffer += unpackFloatRatioFromTwoByte(sourceBuffer, cameraAspectRatio);
|
||||
sourceBuffer += unpackClipValueFromTwoByte(sourceBuffer, cameraNearClip);
|
||||
sourceBuffer += unpackClipValueFromTwoByte(sourceBuffer, cameraFarClip);
|
||||
memcpy(&cameraCenterRadius, sourceBuffer, sizeof(cameraCenterRadius));
|
||||
sourceBuffer += sizeof(cameraCenterRadius);
|
||||
|
||||
setPosition(cameraPosition);
|
||||
setOrientation(cameraOrientation);
|
||||
setCenterRadius(cameraCenterRadius);
|
||||
|
||||
// Also make sure it's got the correct lens details from the camera
|
||||
if (0.0f != cameraAspectRatio &&
|
||||
0.0f != cameraNearClip &&
|
||||
0.0f != cameraFarClip &&
|
||||
cameraNearClip != cameraFarClip) {
|
||||
|
||||
setProjection(cameraFov, cameraAspectRatio, cameraNearClip, cameraFarClip);
|
||||
calculate();
|
||||
}
|
||||
|
||||
return sourceBuffer - startPosition;
|
||||
}
|
||||
|
||||
|
||||
QByteArray ViewFrustum::toByteArray() {
|
||||
static const int LARGE_ENOUGH = 1024;
|
||||
QByteArray viewFrustumDataByteArray(LARGE_ENOUGH, 0);
|
||||
unsigned char* destinationBuffer = reinterpret_cast<unsigned char*>(viewFrustumDataByteArray.data());
|
||||
unsigned char* startPosition = destinationBuffer;
|
||||
|
||||
// camera details
|
||||
memcpy(destinationBuffer, &_position, sizeof(_position));
|
||||
destinationBuffer += sizeof(_position);
|
||||
destinationBuffer += packOrientationQuatToBytes(destinationBuffer, _orientation);
|
||||
destinationBuffer += packFloatAngleToTwoByte(destinationBuffer, _fieldOfView);
|
||||
destinationBuffer += packFloatRatioToTwoByte(destinationBuffer, _aspectRatio);
|
||||
destinationBuffer += packClipValueToTwoByte(destinationBuffer, _nearClip);
|
||||
destinationBuffer += packClipValueToTwoByte(destinationBuffer, _farClip);
|
||||
memcpy(destinationBuffer, &_centerSphereRadius, sizeof(_centerSphereRadius));
|
||||
destinationBuffer += sizeof(_centerSphereRadius);
|
||||
|
||||
return viewFrustumDataByteArray.left(destinationBuffer - startPosition);
|
||||
}
|
||||
|
||||
ViewFrustum::intersection ViewFrustum::calculateCubeFrustumIntersection(const AACube& cube) const {
|
||||
// only check against frustum
|
||||
ViewFrustum::intersection result = INSIDE;
|
||||
|
|
|
@ -147,9 +147,6 @@ public:
|
|||
|
||||
void invalidate(); // causes all reasonable intersection tests to fail
|
||||
|
||||
QByteArray toByteArray();
|
||||
int fromByteArray(const QByteArray& input);
|
||||
|
||||
private:
|
||||
glm::mat4 _view;
|
||||
glm::mat4 _projection;
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
|
||||
#include "ConicalViewFrustum.h"
|
||||
|
||||
|
||||
#include "../NumericalConstants.h"
|
||||
#include "../ViewFrustum.h"
|
||||
|
||||
void ConicalViewFrustum::set(const ViewFrustum& viewFrustum) {
|
||||
|
@ -45,13 +47,46 @@ bool ConicalViewFrustum::isVerySimilar(const ConicalViewFrustum& other) const {
|
|||
const float MIN_RELATIVE_ERROR = 0.01f; // 1%
|
||||
|
||||
return glm::distance2(_position, other._position) < MIN_POSITION_SLOP_SQUARED &&
|
||||
angleBetween(_direction, other._direction) < MIN_ANGLE_BETWEEN &&
|
||||
closeEnough(_angle, other._angle, MIN_RELATIVE_ERROR) &&
|
||||
closeEnough(_farClip, other._farClip, MIN_RELATIVE_ERROR) &&
|
||||
closeEnough(_radius, other._radius, MIN_RELATIVE_ERROR);
|
||||
angleBetween(_direction, other._direction) < MIN_ANGLE_BETWEEN &&
|
||||
closeEnough(_angle, other._angle, MIN_RELATIVE_ERROR) &&
|
||||
closeEnough(_farClip, other._farClip, MIN_RELATIVE_ERROR) &&
|
||||
closeEnough(_radius, other._radius, MIN_RELATIVE_ERROR);
|
||||
}
|
||||
|
||||
bool ConicalViewFrustum::intersects(const glm::vec3& position, float distance, float radius) const {
|
||||
bool ConicalViewFrustum::intersects(const AACube& cube) const {
|
||||
auto radius = 0.5f * SQRT_THREE * cube.getScale(); // radius of bounding sphere
|
||||
auto position = cube.calcCenter() - _position; // position of bounding sphere in view-frame
|
||||
float distance = glm::length(position);
|
||||
|
||||
return intersects(position, distance, radius);
|
||||
}
|
||||
|
||||
bool ConicalViewFrustum::intersects(const AABox& box) const {
|
||||
auto radius = 0.5f * glm::length(box.getScale()); // radius of bounding sphere
|
||||
auto position = box.calcCenter() - _position; // position of bounding sphere in view-frame
|
||||
float distance = glm::length(position);
|
||||
|
||||
return intersects(position, distance, radius);
|
||||
}
|
||||
|
||||
bool ConicalViewFrustum::getAngularSize(const AACube& cube) const {
|
||||
auto radius = 0.5f * SQRT_THREE * cube.getScale(); // radius of bounding sphere
|
||||
auto position = cube.calcCenter() - _position; // position of bounding sphere in view-frame
|
||||
float distance = glm::length(position);
|
||||
|
||||
return getAngularSize(distance, radius);
|
||||
}
|
||||
|
||||
bool ConicalViewFrustum::getAngularSize(const AABox& box) const {
|
||||
auto radius = 0.5f * glm::length(box.getScale()); // radius of bounding sphere
|
||||
auto position = box.calcCenter() - _position; // position of bounding sphere in view-frame
|
||||
float distance = glm::length(position);
|
||||
|
||||
return getAngularSize(distance, radius);
|
||||
}
|
||||
|
||||
|
||||
bool ConicalViewFrustum::intersects(const glm::vec3& relativePosition, float distance, float radius) const {
|
||||
if (distance < _radius + radius) {
|
||||
// Inside keyhole radius
|
||||
return true;
|
||||
|
@ -68,7 +103,7 @@ bool ConicalViewFrustum::intersects(const glm::vec3& position, float distance, f
|
|||
// 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)
|
||||
return glm::dot(position, _direction) >
|
||||
return glm::dot(relativePosition, _direction) >
|
||||
sqrtf(distance * distance - radius * radius) * _cosAngle - radius * _sinAngle;
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
#include <glm/glm.hpp>
|
||||
|
||||
class AACube;
|
||||
class AABox;
|
||||
class ViewFrustum;
|
||||
using ViewFrustums = std::vector<ViewFrustum>;
|
||||
|
||||
|
@ -42,7 +43,12 @@ public:
|
|||
|
||||
bool isVerySimilar(const ConicalViewFrustum& other) const;
|
||||
|
||||
bool intersects(const glm::vec3& position, float distance, float radius) const;
|
||||
bool intersects(const AACube& cube) const;
|
||||
bool intersects(const AABox& box) const;
|
||||
bool getAngularSize(const AACube& cube) const;
|
||||
bool getAngularSize(const AABox& box) const;
|
||||
|
||||
bool intersects(const glm::vec3& relativePosition, float distance, float radius) const;
|
||||
bool getAngularSize(float distance, float radius) const;
|
||||
|
||||
int serialize(unsigned char* destinationBuffer) const;
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
#include <QtWidgets/QMessageBox>
|
||||
#include <QtWidgets/QApplication>
|
||||
|
||||
#include <shared/ConicalViewFrustum.h>
|
||||
#include <shared/RateCounter.h>
|
||||
#include <shared/NetworkUtils.h>
|
||||
#include <shared/FileLogger.h>
|
||||
|
@ -441,14 +442,9 @@ protected:
|
|||
viewOut = _viewFrustum;
|
||||
}
|
||||
|
||||
void copyViewFrustum(ViewFrustum& viewOut) const override {
|
||||
viewOut = _viewFrustum;
|
||||
}
|
||||
|
||||
void copySecondaryViewFrustum(ViewFrustum& viewOut) const override {}
|
||||
|
||||
bool hasSecondaryViewFrustum() const override {
|
||||
return false;
|
||||
const ConicalViewFrustums& getConicalViews() const override {
|
||||
assert(false);
|
||||
return ConicalViewFrustums();
|
||||
}
|
||||
|
||||
QThread* getMainThread() override {
|
||||
|
|
Loading…
Reference in a new issue