diff --git a/assignment-client/CMakeLists.txt b/assignment-client/CMakeLists.txt index b78d4f81f9..e783001228 100644 --- a/assignment-client/CMakeLists.txt +++ b/assignment-client/CMakeLists.txt @@ -30,6 +30,7 @@ link_hifi_library(octree ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(voxels ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(fbx ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(particles ${TARGET_NAME} "${ROOT_DIR}") +link_hifi_library(models ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(metavoxels ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(script-engine ${TARGET_NAME} "${ROOT_DIR}") diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index e39cb39307..9b4f22679a 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -25,7 +25,9 @@ #include #include #include -#include + +#include // TODO: consider moving to scriptengine.h +#include // TODO: consider moving to scriptengine.h #include "Agent.h" @@ -68,6 +70,10 @@ void Agent::readPendingDatagrams() { _scriptEngine.getParticlesScriptingInterface()->getJurisdictionListener()-> queueReceivedPacket(matchedNode, receivedPacket); break; + case NodeType::ModelServer: + _scriptEngine.getModelsScriptingInterface()->getJurisdictionListener()-> + queueReceivedPacket(matchedNode, receivedPacket); + break; } } @@ -86,6 +92,8 @@ void Agent::readPendingDatagrams() { || datagramPacketType == PacketTypeParticleErase || datagramPacketType == PacketTypeOctreeStats || datagramPacketType == PacketTypeVoxelData + || datagramPacketType == PacketTypeModelData + || datagramPacketType == PacketTypeModelErase ) { // Make sure our Node and NodeList knows we've heard from this node. SharedNodePointer sourceNode = nodeList->sendingNodeForPacket(receivedPacket); @@ -117,6 +125,10 @@ void Agent::readPendingDatagrams() { _particleViewer.processDatagram(mutablePacket, sourceNode); } + if (datagramPacketType == PacketTypeModelData || datagramPacketType == PacketTypeModelErase) { + _modelViewer.processDatagram(mutablePacket, sourceNode); + } + if (datagramPacketType == PacketTypeVoxelData) { _voxelViewer.processDatagram(mutablePacket, sourceNode); } @@ -159,7 +171,9 @@ void Agent::run() { << NodeType::AudioMixer << NodeType::AvatarMixer << NodeType::VoxelServer - << NodeType::ParticleServer); + << NodeType::ParticleServer + << NodeType::ModelServer + ); // figure out the URL for the script for this agent assignment QUrl scriptURL; diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index 9f6a8089cf..6b874a03a8 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -20,6 +20,9 @@ #include #include +#include +#include +#include #include #include #include @@ -64,6 +67,7 @@ private: ParticleTreeHeadlessViewer _particleViewer; VoxelTreeHeadlessViewer _voxelViewer; + ModelTreeHeadlessViewer _modelViewer; MixedAudioRingBuffer _receivedAudioBuffer; AvatarHashMap _avatarHashMap; diff --git a/assignment-client/src/AssignmentFactory.cpp b/assignment-client/src/AssignmentFactory.cpp index cdf5c591e1..d2c2016328 100644 --- a/assignment-client/src/AssignmentFactory.cpp +++ b/assignment-client/src/AssignmentFactory.cpp @@ -16,6 +16,7 @@ #include "audio/AudioMixer.h" #include "avatars/AvatarMixer.h" #include "metavoxels/MetavoxelServer.h" +#include "models/ModelServer.h" #include "particles/ParticleServer.h" #include "voxels/VoxelServer.h" @@ -41,6 +42,8 @@ ThreadedAssignment* AssignmentFactory::unpackAssignment(const QByteArray& packet return new ParticleServer(packet); case Assignment::MetavoxelServerType: return new MetavoxelServer(packet); + case Assignment::ModelServerType: + return new ModelServer(packet); default: return NULL; } diff --git a/assignment-client/src/models/ModelNodeData.h b/assignment-client/src/models/ModelNodeData.h new file mode 100644 index 0000000000..dda83e0225 --- /dev/null +++ b/assignment-client/src/models/ModelNodeData.h @@ -0,0 +1,34 @@ +// +// ModelNodeData.h +// assignment-client/src/models +// +// Created by Brad Hefta-Gaub on 4/29/14 +// Copyright 2014 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_ModelNodeData_h +#define hifi_ModelNodeData_h + +#include + +#include "../octree/OctreeQueryNode.h" + +class ModelNodeData : public OctreeQueryNode { +public: + ModelNodeData() : + OctreeQueryNode(), + _lastDeletedModelsSentAt(0) { }; + + virtual PacketType getMyPacketType() const { return PacketTypeModelData; } + + quint64 getLastDeletedModelsSentAt() const { return _lastDeletedModelsSentAt; } + void setLastDeletedModelsSentAt(quint64 sentAt) { _lastDeletedModelsSentAt = sentAt; } + +private: + quint64 _lastDeletedModelsSentAt; +}; + +#endif // hifi_ModelNodeData_h diff --git a/assignment-client/src/models/ModelServer.cpp b/assignment-client/src/models/ModelServer.cpp new file mode 100644 index 0000000000..a71c98ed83 --- /dev/null +++ b/assignment-client/src/models/ModelServer.cpp @@ -0,0 +1,137 @@ +// +// ModelServer.cpp +// assignment-client/src/models +// +// Created by Brad Hefta-Gaub on 4/29/14 +// Copyright 2014 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 +#include + +#include "ModelServer.h" +#include "ModelServerConsts.h" +#include "ModelNodeData.h" + +const char* MODEL_SERVER_NAME = "Model"; +const char* MODEL_SERVER_LOGGING_TARGET_NAME = "model-server"; +const char* LOCAL_MODELS_PERSIST_FILE = "resources/models.svo"; + +ModelServer::ModelServer(const QByteArray& packet) : OctreeServer(packet) { + // nothing special to do here... +} + +ModelServer::~ModelServer() { + ModelTree* tree = (ModelTree*)_tree; + tree->removeNewlyCreatedHook(this); +} + +OctreeQueryNode* ModelServer::createOctreeQueryNode() { + return new ModelNodeData(); +} + +Octree* ModelServer::createTree() { + ModelTree* tree = new ModelTree(true); + tree->addNewlyCreatedHook(this); + return tree; +} + +void ModelServer::beforeRun() { + QTimer* pruneDeletedModelsTimer = new QTimer(this); + connect(pruneDeletedModelsTimer, SIGNAL(timeout()), this, SLOT(pruneDeletedModels())); + const int PRUNE_DELETED_MODELS_INTERVAL_MSECS = 1 * 1000; // once every second + pruneDeletedModelsTimer->start(PRUNE_DELETED_MODELS_INTERVAL_MSECS); +} + +void ModelServer::modelCreated(const ModelItem& newModel, const SharedNodePointer& senderNode) { + unsigned char outputBuffer[MAX_PACKET_SIZE]; + unsigned char* copyAt = outputBuffer; + + int numBytesPacketHeader = populatePacketHeader(reinterpret_cast(outputBuffer), PacketTypeModelAddResponse); + int packetLength = numBytesPacketHeader; + copyAt += numBytesPacketHeader; + + // encode the creatorTokenID + uint32_t creatorTokenID = newModel.getCreatorTokenID(); + memcpy(copyAt, &creatorTokenID, sizeof(creatorTokenID)); + copyAt += sizeof(creatorTokenID); + packetLength += sizeof(creatorTokenID); + + // encode the model ID + uint32_t modelID = newModel.getID(); + memcpy(copyAt, &modelID, sizeof(modelID)); + copyAt += sizeof(modelID); + packetLength += sizeof(modelID); + + NodeList::getInstance()->writeDatagram((char*) outputBuffer, packetLength, senderNode); +} + + +// ModelServer will use the "special packets" to send list of recently deleted models +bool ModelServer::hasSpecialPacketToSend(const SharedNodePointer& node) { + bool shouldSendDeletedModels = false; + + // check to see if any new models have been added since we last sent to this node... + ModelNodeData* nodeData = static_cast(node->getLinkedData()); + if (nodeData) { + quint64 deletedModelsSentAt = nodeData->getLastDeletedModelsSentAt(); + + ModelTree* tree = static_cast(_tree); + shouldSendDeletedModels = tree->hasModelsDeletedSince(deletedModelsSentAt); + } + + return shouldSendDeletedModels; +} + +int ModelServer::sendSpecialPacket(const SharedNodePointer& node) { + unsigned char outputBuffer[MAX_PACKET_SIZE]; + size_t packetLength = 0; + + ModelNodeData* nodeData = static_cast(node->getLinkedData()); + if (nodeData) { + quint64 deletedModelsSentAt = nodeData->getLastDeletedModelsSentAt(); + quint64 deletePacketSentAt = usecTimestampNow(); + + ModelTree* tree = static_cast(_tree); + bool hasMoreToSend = true; + + // TODO: is it possible to send too many of these packets? what if you deleted 1,000,000 models? + while (hasMoreToSend) { + hasMoreToSend = tree->encodeModelsDeletedSince(deletedModelsSentAt, + outputBuffer, MAX_PACKET_SIZE, packetLength); + + //qDebug() << "sending PacketType_MODEL_ERASE packetLength:" << packetLength; + + NodeList::getInstance()->writeDatagram((char*) outputBuffer, packetLength, SharedNodePointer(node)); + } + + nodeData->setLastDeletedModelsSentAt(deletePacketSentAt); + } + + // TODO: caller is expecting a packetLength, what if we send more than one packet?? + return packetLength; +} + +void ModelServer::pruneDeletedModels() { + ModelTree* tree = static_cast(_tree); + if (tree->hasAnyDeletedModels()) { + + //qDebug() << "there are some deleted models to consider..."; + quint64 earliestLastDeletedModelsSent = usecTimestampNow() + 1; // in the future + foreach (const SharedNodePointer& otherNode, NodeList::getInstance()->getNodeHash()) { + if (otherNode->getLinkedData()) { + ModelNodeData* nodeData = static_cast(otherNode->getLinkedData()); + quint64 nodeLastDeletedModelsSentAt = nodeData->getLastDeletedModelsSentAt(); + if (nodeLastDeletedModelsSentAt < earliestLastDeletedModelsSent) { + earliestLastDeletedModelsSent = nodeLastDeletedModelsSentAt; + } + } + } + //qDebug() << "earliestLastDeletedModelsSent=" << earliestLastDeletedModelsSent; + tree->forgetModelsDeletedBefore(earliestLastDeletedModelsSent); + } +} + diff --git a/assignment-client/src/models/ModelServer.h b/assignment-client/src/models/ModelServer.h new file mode 100644 index 0000000000..da51137385 --- /dev/null +++ b/assignment-client/src/models/ModelServer.h @@ -0,0 +1,50 @@ +// +// ModelServer.h +// assignment-client/src/models +// +// Created by Brad Hefta-Gaub on 4/29/14 +// Copyright 2014 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_ModelServer_h +#define hifi_ModelServer_h + +#include "../octree/OctreeServer.h" + +#include "ModelItem.h" +#include "ModelServerConsts.h" +#include "ModelTree.h" + +/// Handles assignments of type ModelServer - sending models to various clients. +class ModelServer : public OctreeServer, public NewlyCreatedModelHook { + Q_OBJECT +public: + ModelServer(const QByteArray& packet); + ~ModelServer(); + + // Subclasses must implement these methods + virtual OctreeQueryNode* createOctreeQueryNode(); + virtual Octree* createTree(); + virtual unsigned char getMyNodeType() const { return NodeType::ModelServer; } + virtual PacketType getMyQueryMessageType() const { return PacketTypeModelQuery; } + virtual const char* getMyServerName() const { return MODEL_SERVER_NAME; } + virtual const char* getMyLoggingServerTargetName() const { return MODEL_SERVER_LOGGING_TARGET_NAME; } + virtual const char* getMyDefaultPersistFilename() const { return LOCAL_MODELS_PERSIST_FILE; } + + // subclass may implement these method + virtual void beforeRun(); + virtual bool hasSpecialPacketToSend(const SharedNodePointer& node); + virtual int sendSpecialPacket(const SharedNodePointer& node); + + virtual void modelCreated(const ModelItem& newModel, const SharedNodePointer& senderNode); + +public slots: + void pruneDeletedModels(); + +private: +}; + +#endif // hifi_ModelServer_h diff --git a/assignment-client/src/models/ModelServerConsts.h b/assignment-client/src/models/ModelServerConsts.h new file mode 100644 index 0000000000..7d480caaac --- /dev/null +++ b/assignment-client/src/models/ModelServerConsts.h @@ -0,0 +1,19 @@ +// +// ModelServerConsts.h +// assignment-client/src/models +// +// Created by Brad Hefta-Gaub on 4/29/14 +// Copyright 2014 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_ModelServerConsts_h +#define hifi_ModelServerConsts_h + +extern const char* MODEL_SERVER_NAME; +extern const char* MODEL_SERVER_LOGGING_TARGET_NAME; +extern const char* LOCAL_MODELS_PERSIST_FILE; + +#endif // hifi_ModelServerConsts_h diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index bd04dd85d7..5769c15ef1 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -833,7 +833,7 @@ void OctreeServer::readPendingDatagrams() { SharedNodePointer matchingNode = nodeList->sendingNodeForPacket(receivedPacket); if (packetType == getMyQueryMessageType()) { - // If we got a PacketType_VOXEL_QUERY, then we're talking to an NodeType_t_AVATAR, and we + // If we got a query packet, then we're talking to an agent, and we // need to make sure we have it in our nodeList. if (matchingNode) { nodeList->updateNodeWithDataFromPacket(matchingNode, receivedPacket); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index e65f3968e0..77d5afffd7 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -341,7 +341,7 @@ void DomainServer::populateDefaultStaticAssignmentsExcludingTypes(const QSet= moveUntil) { + + // delete it... + if (count == moveUntil) { + print("calling Models.deleteModel()"); + Models.deleteModel(modelID); + } + + // stop it... + if (count >= stopAfter) { + print("calling Script.stop()"); + Script.stop(); + } + + count++; + return; // break early + } + + print("count =" + count); + count++; + + print("modelID.creatorTokenID = " + modelID.creatorTokenID); + + var newProperties = { + position: { + x: originalProperties.position.x + (count * positionDelta.x), + y: originalProperties.position.y + (count * positionDelta.y), + z: originalProperties.position.z + (count * positionDelta.z) + }, + radius : 0.25, + + }; + + + //print("modelID = " + modelID); + print("newProperties.position = " + newProperties.position.x + "," + newProperties.position.y+ "," + newProperties.position.z); + + Models.editModel(modelID, newProperties); +} + + +// register the call back so it fires before each data send +Script.update.connect(moveModel); + diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 557af35e12..0a56109260 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -49,7 +49,7 @@ configure_file(InterfaceVersion.h.in "${PROJECT_BINARY_DIR}/includes/InterfaceVe # grab the implementation and header files from src dirs file(GLOB INTERFACE_SRCS src/*.cpp src/*.h) -foreach(SUBDIR avatar devices renderer ui starfield location scripting voxels) +foreach(SUBDIR avatar devices renderer ui starfield location scripting voxels particles models) file(GLOB_RECURSE SUBDIR_SRCS src/${SUBDIR}/*.cpp src/${SUBDIR}/*.h) set(INTERFACE_SRCS ${INTERFACE_SRCS} "${SUBDIR_SRCS}") endforeach(SUBDIR) @@ -124,6 +124,7 @@ link_hifi_library(fbx ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(metavoxels ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(particles ${TARGET_NAME} "${ROOT_DIR}") +link_hifi_library(models ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(avatars ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(audio ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(script-engine ${TARGET_NAME} "${ROOT_DIR}") diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 09984c43b7..e47d1c11ef 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -54,6 +54,7 @@ #include #include #include +#include #include #include #include @@ -266,7 +267,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : // tell the NodeList instance who to tell the domain server we care about nodeList->addSetOfNodeTypesToNodeInterestSet(NodeSet() << NodeType::AudioMixer << NodeType::AvatarMixer - << NodeType::VoxelServer << NodeType::ParticleServer + << NodeType::VoxelServer << NodeType::ParticleServer << NodeType::ModelServer << NodeType::MetavoxelServer); // connect to the packet sent signal of the _voxelEditSender and the _particleEditSender @@ -766,6 +767,8 @@ void Application::controlledBroadcastToNodes(const QByteArray& packet, const Nod channel = BandwidthMeter::AVATARS; break; case NodeType::VoxelServer: + case NodeType::ParticleServer: + case NodeType::ModelServer: channel = BandwidthMeter::VOXELS; break; default: @@ -1272,8 +1275,8 @@ void Application::dropEvent(QDropEvent *event) { void Application::sendPingPackets() { QByteArray pingPacket = NodeList::getInstance()->constructPingPacket(); - controlledBroadcastToNodes(pingPacket, NodeSet() << NodeType::VoxelServer - << NodeType::ParticleServer + controlledBroadcastToNodes(pingPacket, NodeSet() + << NodeType::VoxelServer << NodeType::ParticleServer << NodeType::ModelServer << NodeType::AudioMixer << NodeType::AvatarMixer << NodeType::MetavoxelServer); } @@ -1665,6 +1668,9 @@ void Application::init() { _particles.init(); _particles.setViewFrustum(getViewFrustum()); + _models.init(); + _models.setViewFrustum(getViewFrustum()); + _metavoxels.init(); _particleCollisionSystem.init(&_particleEditSender, _particles.getTree(), _voxels.getTree(), &_audio, &_avatarManager); @@ -1995,6 +2001,8 @@ void Application::update(float deltaTime) { _particles.update(); // update the particles... _particleCollisionSystem.update(); // collide the particles... + _models.update(); // update the models... + _overlays.update(deltaTime); // let external parties know we're updating @@ -2033,6 +2041,7 @@ void Application::updateMyAvatar(float deltaTime) { _lastQueriedTime = now; queryOctree(NodeType::VoxelServer, PacketTypeVoxelQuery, _voxelServerJurisdictions); queryOctree(NodeType::ParticleServer, PacketTypeParticleQuery, _particleServerJurisdictions); + queryOctree(NodeType::ModelServer, PacketTypeModelQuery, _modelServerJurisdictions); _lastQueriedViewFrustum = _viewFrustum; } } @@ -2334,6 +2343,7 @@ void Application::updateShadowMap() { _avatarManager.renderAvatars(Avatar::SHADOW_RENDER_MODE); _particles.render(); + _models.render(); glPopMatrix(); @@ -2500,6 +2510,13 @@ void Application::displaySide(Camera& whichCamera, bool selfAvatarOnly) { _particles.render(); } + // render models... + if (Menu::getInstance()->isOptionChecked(MenuOption::Models)) { + PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), + "Application::displaySide() ... models..."); + _models.render(); + } + // render the ambient occlusion effect if enabled if (Menu::getInstance()->isOptionChecked(MenuOption::AmbientOcclusion)) { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), @@ -3094,6 +3111,9 @@ void Application::domainChanged(const QString& domainHostname) { // reset the particle renderer _particles.clear(); + // reset the model renderer + _models.clear(); + // reset the voxels renderer _voxels.killLocalVoxels(); } @@ -3170,7 +3190,7 @@ void Application::nodeKilled(SharedNodePointer node) { _voxelFades.push_back(fade); } - // If the voxel server is going away, remove it from our jurisdiction map so we don't send voxels to a dead server + // If the particle server is going away, remove it from our jurisdiction map so we don't send voxels to a dead server _particleServerJurisdictions.erase(_particleServerJurisdictions.find(nodeUUID)); } @@ -3181,6 +3201,37 @@ void Application::nodeKilled(SharedNodePointer node) { } _octreeSceneStatsLock.unlock(); + } else if (node->getType() == NodeType::ModelServer) { + QUuid nodeUUID = node->getUUID(); + // see if this is the first we've heard of this node... + if (_modelServerJurisdictions.find(nodeUUID) != _modelServerJurisdictions.end()) { + unsigned char* rootCode = _modelServerJurisdictions[nodeUUID].getRootOctalCode(); + VoxelPositionSize rootDetails; + voxelDetailsForCode(rootCode, rootDetails); + + qDebug("model server going away...... v[%f, %f, %f, %f]", + rootDetails.x, rootDetails.y, rootDetails.z, rootDetails.s); + + // Add the jurisditionDetails object to the list of "fade outs" + if (!Menu::getInstance()->isOptionChecked(MenuOption::DontFadeOnVoxelServerChanges)) { + VoxelFade fade(VoxelFade::FADE_OUT, NODE_KILLED_RED, NODE_KILLED_GREEN, NODE_KILLED_BLUE); + fade.voxelDetails = rootDetails; + const float slightly_smaller = 0.99f; + fade.voxelDetails.s = fade.voxelDetails.s * slightly_smaller; + _voxelFades.push_back(fade); + } + + // If the model server is going away, remove it from our jurisdiction map so we don't send voxels to a dead server + _modelServerJurisdictions.erase(_modelServerJurisdictions.find(nodeUUID)); + } + + // also clean up scene stats for that server + _octreeSceneStatsLock.lockForWrite(); + if (_octreeServerSceneStats.find(nodeUUID) != _octreeServerSceneStats.end()) { + _octreeServerSceneStats.erase(nodeUUID); + } + _octreeSceneStatsLock.unlock(); + } else if (node->getType() == NodeType::AvatarMixer) { // our avatar mixer has gone away - clear the hash of avatars _avatarManager.clearOtherAvatars(); @@ -3233,8 +3284,10 @@ int Application::parseOctreeStats(const QByteArray& packet, const SharedNodePoin NodeToJurisdictionMap* jurisdiction = NULL; if (sendingNode->getType() == NodeType::VoxelServer) { jurisdiction = &_voxelServerJurisdictions; - } else { + } else if (sendingNode->getType() == NodeType::ParticleServer) { jurisdiction = &_particleServerJurisdictions; + } else { + jurisdiction = &_modelServerJurisdictions; } @@ -3394,6 +3447,9 @@ ScriptEngine* Application::loadScript(const QString& scriptName, bool loadScript scriptEngine->getParticlesScriptingInterface()->setPacketSender(&_particleEditSender); scriptEngine->getParticlesScriptingInterface()->setParticleTree(_particles.getTree()); + scriptEngine->getModelsScriptingInterface()->setPacketSender(&_modelEditSender); + scriptEngine->getModelsScriptingInterface()->setModelTree(_models.getTree()); + // hook our avatar object into this script engine scriptEngine->setAvatarData(_myAvatar, "MyAvatar"); // leave it as a MyAvatar class to expose thrust features diff --git a/interface/src/Application.h b/interface/src/Application.h index 9b613f7ed7..6859db80f5 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -29,6 +29,7 @@ #include #include +#include #include #include #include @@ -51,7 +52,6 @@ #include "Menu.h" #include "MetavoxelSystem.h" #include "PacketHeaders.h" -#include "ParticleTreeRenderer.h" #include "Stars.h" #include "avatar/Avatar.h" #include "avatar/AvatarManager.h" @@ -60,6 +60,8 @@ #include "devices/Faceshift.h" #include "devices/SixenseManager.h" #include "devices/Visage.h" +#include "models/ModelTreeRenderer.h" +#include "particles/ParticleTreeRenderer.h" #include "renderer/AmbientOcclusionEffect.h" #include "renderer/GeometryCache.h" #include "renderer/GlowEffect.h" @@ -247,6 +249,7 @@ public: glm::vec2 getViewportDimensions() const{ return glm::vec2(_glWidget->width(),_glWidget->height()); } NodeToJurisdictionMap& getVoxelServerJurisdictions() { return _voxelServerJurisdictions; } NodeToJurisdictionMap& getParticleServerJurisdictions() { return _particleServerJurisdictions; } + NodeToJurisdictionMap& getModelServerJurisdictions() { return _modelServerJurisdictions; } void pasteVoxelsToOctalCode(const unsigned char* octalCodeDestination); void skipVersion(QString latestVersion); @@ -417,6 +420,8 @@ private: ParticleTreeRenderer _particles; ParticleCollisionSystem _particleCollisionSystem; + ModelTreeRenderer _models; + QByteArray _voxelsFilename; bool _wantToKillLocalVoxels; @@ -494,6 +499,7 @@ private: VoxelHideShowThread _voxelHideShowThread; VoxelEditPacketSender _voxelEditSender; ParticleEditPacketSender _particleEditSender; + ModelEditPacketSender _modelEditSender; int _packetsPerSecond; int _bytesPerSecond; @@ -506,6 +512,7 @@ private: NodeToJurisdictionMap _voxelServerJurisdictions; NodeToJurisdictionMap _particleServerJurisdictions; + NodeToJurisdictionMap _modelServerJurisdictions; NodeToOctreeSceneStats _octreeServerSceneStats; QReadWriteLock _octreeSceneStatsLock; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 7ab2639065..3049d71e40 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -280,8 +280,9 @@ Menu::Menu() : addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Shadows, 0, false); addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Metavoxels, 0, true); - addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::BuckyBalls, 0, true); + addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::BuckyBalls, 0, false); addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Particles, 0, true); + addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Models, 0, true); addActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::LodTools, Qt::SHIFT | Qt::Key_L, this, SLOT(lodTools())); QMenu* voxelOptionsMenu = developerMenu->addMenu("Voxel Options"); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index ef3e7bd6ce..da2585f81a 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -277,9 +277,6 @@ namespace MenuOption { const QString AudioSpatialProcessingWithDiffusions = "With Diffusions"; const QString AudioSpatialProcessingDontDistanceAttenuate = "Don't calculate distance attenuation"; const QString AudioSpatialProcessingAlternateDistanceAttenuate = "Alternate distance attenuation"; - - - const QString Avatars = "Avatars"; const QString Bandwidth = "Bandwidth Display"; const QString BandwidthDetails = "Bandwidth Details"; @@ -331,6 +328,7 @@ namespace MenuOption { const QString MetavoxelEditor = "Metavoxel Editor..."; const QString Metavoxels = "Metavoxels"; const QString Mirror = "Mirror"; + const QString Models = "Models"; const QString MoveWithLean = "Move with Lean"; const QString MuteAudio = "Mute Microphone"; const QString NameLocation = "Name this location"; diff --git a/interface/src/models/ModelTreeRenderer.cpp b/interface/src/models/ModelTreeRenderer.cpp new file mode 100644 index 0000000000..0fa434622e --- /dev/null +++ b/interface/src/models/ModelTreeRenderer.cpp @@ -0,0 +1,123 @@ +// +// ModelTreeRenderer.cpp +// interface/src +// +// Created by Brad Hefta-Gaub on 12/6/13. +// Copyright 2013 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 + +#include "InterfaceConfig.h" + +#include "ModelTreeRenderer.h" + +ModelTreeRenderer::ModelTreeRenderer() : + OctreeRenderer() { +} + +ModelTreeRenderer::~ModelTreeRenderer() { + // delete the models in _modelsItemModels + foreach(Model* model, _modelsItemModels) { + delete model; + } + _modelsItemModels.clear(); +} + +void ModelTreeRenderer::init() { + OctreeRenderer::init(); +} + + +void ModelTreeRenderer::update() { + if (_tree) { + ModelTree* tree = static_cast(_tree); + tree->update(); + } +} + +void ModelTreeRenderer::render() { + OctreeRenderer::render(); +} + +Model* ModelTreeRenderer::getModel(const QString& url) { + Model* model = NULL; + + // if we don't already have this model then create it and initialize it + if (_modelsItemModels.find(url) == _modelsItemModels.end()) { + model = new Model(); + model->init(); + model->setURL(QUrl(url)); + _modelsItemModels[url] = model; + } else { + model = _modelsItemModels[url]; + } + return model; +} + +void ModelTreeRenderer::renderElement(OctreeElement* element, RenderArgs* args) { + // actually render it here... + // we need to iterate the actual modelItems of the element + ModelTreeElement* modelTreeElement = (ModelTreeElement*)element; + + const QList& modelItems = modelTreeElement->getModels(); + + uint16_t numberOfModels = modelItems.size(); + + for (uint16_t i = 0; i < numberOfModels; i++) { + const ModelItem& modelItem = modelItems[i]; + // render modelItem aspoints + glm::vec3 position = modelItem.getPosition() * (float)TREE_SCALE; + glColor3ub(modelItem.getColor()[RED_INDEX],modelItem.getColor()[GREEN_INDEX],modelItem.getColor()[BLUE_INDEX]); + float radius = modelItem.getRadius() * (float)TREE_SCALE; + //glm::vec3 center = position + glm::vec3(radius, radius, radius); // center it around the position + + bool drawAsModel = modelItem.hasModel(); + + args->_renderedItems++; + + if (drawAsModel) { + glPushMatrix(); + const float alpha = 1.0f; + + Model* model = getModel(modelItem.getModelURL()); + + model->setScaleToFit(true, radius * 2.0f); + model->setSnapModelToCenter(true); + + // set the rotation + glm::quat rotation = modelItem.getModelRotation(); + model->setRotation(rotation); + + // set the position + model->setTranslation(position); + + model->simulate(0.0f); + + + model->render(alpha); // TODO: should we allow modelItems to have alpha on their models? + + const bool wantDebugSphere = false; + if (wantDebugSphere) { + glPushMatrix(); + glTranslatef(position.x, position.y, position.z); + glutWireSphere(radius, 15, 15); + glPopMatrix(); + } + + glPopMatrix(); + } else { + glPushMatrix(); + glTranslatef(position.x, position.y, position.z); + glutSolidSphere(radius, 15, 15); + glPopMatrix(); + } + } +} + +void ModelTreeRenderer::processEraseMessage(const QByteArray& dataByteArray, const SharedNodePointer& sourceNode) { + static_cast(_tree)->processEraseMessage(dataByteArray, sourceNode); +} diff --git a/interface/src/models/ModelTreeRenderer.h b/interface/src/models/ModelTreeRenderer.h new file mode 100644 index 0000000000..6c66350d8f --- /dev/null +++ b/interface/src/models/ModelTreeRenderer.h @@ -0,0 +1,55 @@ +// +// ModelTreeRenderer.h +// interface/src +// +// Created by Brad Hefta-Gaub on 12/6/13. +// Copyright 2013 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_ModelTreeRenderer_h +#define hifi_ModelTreeRenderer_h + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "renderer/Model.h" + +// Generic client side Octree renderer class. +class ModelTreeRenderer : public OctreeRenderer { +public: + ModelTreeRenderer(); + virtual ~ModelTreeRenderer(); + + virtual Octree* createTree() { return new ModelTree(true); } + virtual NodeType_t getMyNodeType() const { return NodeType::ModelServer; } + virtual PacketType getMyQueryMessageType() const { return PacketTypeModelQuery; } + virtual PacketType getExpectedPacketType() const { return PacketTypeModelData; } + virtual void renderElement(OctreeElement* element, RenderArgs* args); + + void update(); + + ModelTree* getTree() { return (ModelTree*)_tree; } + + void processEraseMessage(const QByteArray& dataByteArray, const SharedNodePointer& sourceNode); + + virtual void init(); + virtual void render(); + +protected: + Model* getModel(const QString& url); + + QMap _modelsItemModels; +}; + +#endif // hifi_ModelTreeRenderer_h diff --git a/interface/src/ParticleTreeRenderer.cpp b/interface/src/particles/ParticleTreeRenderer.cpp similarity index 100% rename from interface/src/ParticleTreeRenderer.cpp rename to interface/src/particles/ParticleTreeRenderer.cpp diff --git a/interface/src/ParticleTreeRenderer.h b/interface/src/particles/ParticleTreeRenderer.h similarity index 100% rename from interface/src/ParticleTreeRenderer.h rename to interface/src/particles/ParticleTreeRenderer.h diff --git a/interface/src/renderer/Model.cpp b/interface/src/renderer/Model.cpp index 3484ac5fc8..6bc5afd4fe 100644 --- a/interface/src/renderer/Model.cpp +++ b/interface/src/renderer/Model.cpp @@ -34,6 +34,11 @@ static int vec3VectorTypeId = qRegisterMetaType >(); Model::Model(QObject* parent) : QObject(parent), _scale(1.0f, 1.0f, 1.0f), + _scaleToFit(false), + _scaleToFitLargestDimension(0.0f), + _scaledToFit(false), + _snapModelToCenter(false), + _snappedToCenter(false), _shapesAreDirty(true), _boundingRadius(0.f), _boundingShape(), @@ -60,6 +65,13 @@ Model::SkinLocations Model::_skinNormalMapLocations; Model::SkinLocations Model::_skinShadowLocations; void Model::setScale(const glm::vec3& scale) { + setScaleInternal(scale); + // if anyone sets scale manually, then we are no longer scaled to fit + _scaleToFit = false; + _scaledToFit = false; +} + +void Model::setScaleInternal(const glm::vec3& scale) { float scaleLength = glm::length(_scale); float relativeDeltaScale = glm::length(_scale - scale) / scaleLength; @@ -70,6 +82,15 @@ void Model::setScale(const glm::vec3& scale) { } } +void Model::setOffset(const glm::vec3& offset) { + _offset = offset; + + // if someone manually sets our offset, then we are no longer snapped to center + _snapModelToCenter = false; + _snappedToCenter = false; +} + + void Model::initSkinProgram(ProgramObject& program, Model::SkinLocations& locations) { program.bind(); locations.clusterMatrices = program.uniformLocation("clusterMatrices"); @@ -357,6 +378,23 @@ Extents Model::getBindExtents() const { return scaledExtents; } +Extents Model::getMeshExtents() const { + if (!isActive()) { + return Extents(); + } + const Extents& extents = _geometry->getFBXGeometry().meshExtents; + Extents scaledExtents = { extents.minimum * _scale, extents.maximum * _scale }; + return scaledExtents; +} + +Extents Model::getUnscaledMeshExtents() const { + if (!isActive()) { + return Extents(); + } + const Extents& extents = _geometry->getFBXGeometry().meshExtents; + return extents; +} + bool Model::getJointState(int index, glm::quat& rotation) const { if (index == -1 || index >= _jointStates.size()) { return false; @@ -770,9 +808,52 @@ void Blender::run() { Q_ARG(const QVector&, vertices), Q_ARG(const QVector&, normals)); } +void Model::setScaleToFit(bool scaleToFit, float largestDimension) { + if (_scaleToFit != scaleToFit || _scaleToFitLargestDimension != largestDimension) { + _scaleToFit = scaleToFit; + _scaleToFitLargestDimension = largestDimension; + _scaledToFit = false; // force rescaling + } +} + +void Model::scaleToFit() { + Extents modelMeshExtents = getMeshExtents(); + + // size is our "target size in world space" + // we need to set our model scale so that the extents of the mesh, fit in a cube that size... + glm::vec3 dimensions = modelMeshExtents.maximum - modelMeshExtents.minimum; + float maxDimension = glm::max(glm::max(dimensions.x, dimensions.y), dimensions.z); + float maxScale = _scaleToFitLargestDimension / maxDimension; + glm::vec3 scale(maxScale, maxScale, maxScale); + setScaleInternal(scale); + _scaledToFit = true; +} + +void Model::setSnapModelToCenter(bool snapModelToCenter) { + if (_snapModelToCenter != snapModelToCenter) { + _snapModelToCenter = snapModelToCenter; + _snappedToCenter = false; // force re-centering + } +} + +void Model::snapToCenter() { + Extents modelMeshExtents = getUnscaledMeshExtents(); + glm::vec3 halfDimensions = (modelMeshExtents.maximum - modelMeshExtents.minimum) * 0.5f; + glm::vec3 offset = -modelMeshExtents.minimum - halfDimensions; + _offset = offset; + _snappedToCenter = true; +} + void Model::simulate(float deltaTime, bool fullUpdate) { - fullUpdate = updateGeometry() || fullUpdate; + fullUpdate = updateGeometry() || fullUpdate || (_scaleToFit && !_scaledToFit) || (_snapModelToCenter && !_snappedToCenter); if (isActive() && fullUpdate) { + // check for scale to fit + if (_scaleToFit && !_scaledToFit) { + scaleToFit(); + } + if (_snapModelToCenter && !_snappedToCenter) { + snapToCenter(); + } simulateInternal(deltaTime); } } diff --git a/interface/src/renderer/Model.h b/interface/src/renderer/Model.h index 64295cb915..4105b229b3 100644 --- a/interface/src/renderer/Model.h +++ b/interface/src/renderer/Model.h @@ -39,10 +39,19 @@ public: void setRotation(const glm::quat& rotation) { _rotation = rotation; } const glm::quat& getRotation() const { return _rotation; } + /// enables/disables scale to fit behavior, the model will be automatically scaled to the specified largest dimension + void setScaleToFit(bool scaleToFit, float largestDimension = 0.0f); + bool getScaleToFit() const { return _scaleToFit; } /// is scale to fit enabled + bool getIsScaledToFit() const { return _scaledToFit; } /// is model scaled to fit + bool getScaleToFitDimension() const { return _scaleToFitLargestDimension; } /// the dimension model is scaled to + + void setSnapModelToCenter(bool snapModelToCenter); + bool getSnapModelToCenter() { return _snapModelToCenter; } + void setScale(const glm::vec3& scale); const glm::vec3& getScale() const { return _scale; } - void setOffset(const glm::vec3& offset) { _offset = offset; } + void setOffset(const glm::vec3& offset); const glm::vec3& getOffset() const { return _offset; } void setPupilDilation(float dilation) { _pupilDilation = dilation; } @@ -80,6 +89,12 @@ public: /// Returns the extents of the model in its bind pose. Extents getBindExtents() const; + /// Returns the extents of the model's mesh + Extents getMeshExtents() const; + + /// Returns the unscaled extents of the model's mesh + Extents getUnscaledMeshExtents() const; + /// Returns a reference to the shared geometry. const QSharedPointer& getGeometry() const { return _geometry; } @@ -203,6 +218,13 @@ protected: glm::quat _rotation; glm::vec3 _scale; glm::vec3 _offset; + + bool _scaleToFit; /// If you set scaleToFit, we will calculate scale based on MeshExtents + float _scaleToFitLargestDimension; /// this is the dimension that scale to fit will use + bool _scaledToFit; /// have we scaled to fit + + bool _snapModelToCenter; /// is the model's offset automatically adjusted to center around 0,0,0 in model space + bool _snappedToCenter; /// are we currently snapped to center class JointState { public: @@ -229,6 +251,10 @@ protected: // returns 'true' if needs fullUpdate after geometry change bool updateGeometry(); + + void setScaleInternal(const glm::vec3& scale); + void scaleToFit(); + void snapToCenter(); void simulateInternal(float deltaTime); diff --git a/interface/src/ui/OctreeStatsDialog.cpp b/interface/src/ui/OctreeStatsDialog.cpp index c56aa0b6ab..ceeb2cf2d7 100644 --- a/interface/src/ui/OctreeStatsDialog.cpp +++ b/interface/src/ui/OctreeStatsDialog.cpp @@ -232,6 +232,9 @@ void OctreeStatsDialog::showAllOctreeServers() { showOctreeServersOfType(serverCount, NodeType::ParticleServer, "Particle", Application::getInstance()->getParticleServerJurisdictions()); + showOctreeServersOfType(serverCount, NodeType::ModelServer, "Model", + Application::getInstance()->getModelServerJurisdictions()); + if (_voxelServerLabelsCount > serverCount) { for (int i = serverCount; i < _voxelServerLabelsCount; i++) { int serverLabel = _voxelServerLables[i]; diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index 2f1c055540..a391ed239c 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -237,6 +237,7 @@ void Stats::display( int voxelServerCount = 0; foreach (const SharedNodePointer& node, nodeList->getNodeHash()) { + // TODO: this should also support particles and models if (node->getType() == NodeType::VoxelServer) { totalPingVoxel += node->getPingMs(); voxelServerCount++; diff --git a/libraries/models/CMakeLists.txt b/libraries/models/CMakeLists.txt new file mode 100644 index 0000000000..062352e50c --- /dev/null +++ b/libraries/models/CMakeLists.txt @@ -0,0 +1,45 @@ +cmake_minimum_required(VERSION 2.8) + +if (WIN32) + cmake_policy (SET CMP0020 NEW) +endif (WIN32) + +set(ROOT_DIR ../..) +set(MACRO_DIR "${ROOT_DIR}/cmake/macros") + +# setup for find modules +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/modules/") + +set(TARGET_NAME models) + +find_package(Qt5Widgets REQUIRED) + +include(${MACRO_DIR}/AutoMTC.cmake) +auto_mtc(${TARGET_NAME} "${ROOT_DIR}") + +include(${MACRO_DIR}/SetupHifiLibrary.cmake) +setup_hifi_library(${TARGET_NAME} "${AUTOMTC_SRC}") + +include(${MACRO_DIR}/IncludeGLM.cmake) +include_glm(${TARGET_NAME} "${ROOT_DIR}") + +include(${MACRO_DIR}/LinkHifiLibrary.cmake) +link_hifi_library(shared ${TARGET_NAME} "${ROOT_DIR}") +link_hifi_library(octree ${TARGET_NAME} "${ROOT_DIR}") +link_hifi_library(fbx ${TARGET_NAME} "${ROOT_DIR}") +link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}") + +# for streamable +link_hifi_library(metavoxels ${TARGET_NAME} "${ROOT_DIR}") + +# link ZLIB and GnuTLS +find_package(ZLIB) +find_package(GnuTLS REQUIRED) + +# add a definition for ssize_t so that windows doesn't bail on gnutls.h +if (WIN32) + add_definitions(-Dssize_t=long) +endif () + +include_directories(SYSTEM "${ZLIB_INCLUDE_DIRS}" "${GNUTLS_INCLUDE_DIR}") +target_link_libraries(${TARGET_NAME} "${ZLIB_LIBRARIES}" Qt5::Widgets "${GNUTLS_LIBRARY}") diff --git a/libraries/models/src/ModelEditPacketSender.cpp b/libraries/models/src/ModelEditPacketSender.cpp new file mode 100644 index 0000000000..5059b8891b --- /dev/null +++ b/libraries/models/src/ModelEditPacketSender.cpp @@ -0,0 +1,60 @@ +// +// ModelEditPacketSender.cpp +// libraries/models/src +// +// Created by Brad Hefta-Gaub on 8/12/13. +// Copyright 2013 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 +#include +#include +#include +#include "ModelEditPacketSender.h" +#include "ModelItem.h" + + +void ModelEditPacketSender::sendEditModelMessage(PacketType type, ModelItemID modelID, const ModelItemProperties& properties) { + // allows app to disable sending if for example voxels have been disabled + if (!_shouldSend) { + return; // bail early + } + + static unsigned char bufferOut[MAX_PACKET_SIZE]; + int sizeOut = 0; + + // This encodes the voxel edit message into a buffer... + if (ModelItem::encodeModelEditMessageDetails(type, modelID, properties, &bufferOut[0], _maxPacketSize, sizeOut)){ + // If we don't have voxel jurisdictions, then we will simply queue up these packets and wait till we have + // jurisdictions for processing + if (!serversExist()) { + queuePendingPacketToNodes(type, bufferOut, sizeOut); + } else { + queuePacketToNodes(bufferOut, sizeOut); + } + } +} + +void ModelEditPacketSender::adjustEditPacketForClockSkew(unsigned char* codeColorBuffer, ssize_t length, int clockSkew) { + ModelItem::adjustEditPacketForClockSkew(codeColorBuffer, length, clockSkew); +} + + +void ModelEditPacketSender::queueModelEditMessage(PacketType type, ModelItemID modelID, + const ModelItemProperties& properties) { + if (!_shouldSend) { + return; // bail early + } + + // use MAX_PACKET_SIZE since it's static and guaranteed to be larger than _maxPacketSize + static unsigned char bufferOut[MAX_PACKET_SIZE]; + int sizeOut = 0; + + if (ModelItem::encodeModelEditMessageDetails(type, modelID, properties, &bufferOut[0], _maxPacketSize, sizeOut)) { + queueOctreeEditMessage(type, bufferOut, sizeOut); + } +} + diff --git a/libraries/models/src/ModelEditPacketSender.h b/libraries/models/src/ModelEditPacketSender.h new file mode 100644 index 0000000000..287d6982a0 --- /dev/null +++ b/libraries/models/src/ModelEditPacketSender.h @@ -0,0 +1,37 @@ +// +// ModelEditPacketSender.h +// libraries/models/src +// +// Created by Brad Hefta-Gaub on 8/12/13. +// Copyright 2013 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_ModelEditPacketSender_h +#define hifi_ModelEditPacketSender_h + +#include + +#include "ModelItem.h" + +/// Utility for processing, packing, queueing and sending of outbound edit voxel messages. +class ModelEditPacketSender : public OctreeEditPacketSender { + Q_OBJECT +public: + /// Send model add message immediately + /// NOTE: ModelItemProperties assumes that all distances are in meter units + void sendEditModelMessage(PacketType type, ModelItemID modelID, const ModelItemProperties& properties); + + /// Queues an array of several voxel edit messages. Will potentially send a pending multi-command packet. Determines + /// which voxel-server node or nodes the packet should be sent to. Can be called even before voxel servers are known, in + /// which case up to MaxPendingMessages will be buffered and processed when voxel servers are known. + /// NOTE: ModelItemProperties assumes that all distances are in meter units + void queueModelEditMessage(PacketType type, ModelItemID modelID, const ModelItemProperties& properties); + + // My server type is the model server + virtual unsigned char getMyNodeType() const { return NodeType::ModelServer; } + virtual void adjustEditPacketForClockSkew(unsigned char* codeColorBuffer, ssize_t length, int clockSkew); +}; +#endif // hifi_ModelEditPacketSender_h diff --git a/libraries/models/src/ModelItem.cpp b/libraries/models/src/ModelItem.cpp new file mode 100644 index 0000000000..af8500bf25 --- /dev/null +++ b/libraries/models/src/ModelItem.cpp @@ -0,0 +1,802 @@ +// +// ModelItem.cpp +// libraries/models/src +// +// Created by Brad Hefta-Gaub on 12/4/13. +// Copyright 2013 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 + +#include +#include +#include // usecTimestampNow() +#include +#include + + +// This is not ideal, but adding script-engine as a linked library, will cause a circular reference +// I'm open to other potential solutions. Could we change cmake to allow libraries to reference each others +// headers, but not link to each other, this is essentially what this construct is doing, but would be +// better to add includes to the include path, but not link +#include "../../script-engine/src/ScriptEngine.h" + +#include "ModelsScriptingInterface.h" +#include "ModelItem.h" +#include "ModelTree.h" + +uint32_t ModelItem::_nextID = 0; + +// for locally created models +std::map ModelItem::_tokenIDsToIDs; +uint32_t ModelItem::_nextCreatorTokenID = 0; + +uint32_t ModelItem::getIDfromCreatorTokenID(uint32_t creatorTokenID) { + if (_tokenIDsToIDs.find(creatorTokenID) != _tokenIDsToIDs.end()) { + return _tokenIDsToIDs[creatorTokenID]; + } + return UNKNOWN_MODEL_ID; +} + +uint32_t ModelItem::getNextCreatorTokenID() { + uint32_t creatorTokenID = _nextCreatorTokenID; + _nextCreatorTokenID++; + return creatorTokenID; +} + +void ModelItem::handleAddModelResponse(const QByteArray& packet) { + const unsigned char* dataAt = reinterpret_cast(packet.data()); + int numBytesPacketHeader = numBytesForPacketHeader(packet); + dataAt += numBytesPacketHeader; + + uint32_t creatorTokenID; + memcpy(&creatorTokenID, dataAt, sizeof(creatorTokenID)); + dataAt += sizeof(creatorTokenID); + + uint32_t modelItemID; + memcpy(&modelItemID, dataAt, sizeof(modelItemID)); + dataAt += sizeof(modelItemID); + + // add our token to id mapping + _tokenIDsToIDs[creatorTokenID] = modelItemID; +} + +ModelItem::ModelItem() { + rgbColor noColor = { 0, 0, 0 }; + init(glm::vec3(0,0,0), 0, noColor, NEW_MODEL); +} + +ModelItem::ModelItem(const ModelItemID& modelItemID, const ModelItemProperties& properties) { + _id = modelItemID.id; + _creatorTokenID = modelItemID.creatorTokenID; + + // init values with defaults before calling setProperties + uint64_t now = usecTimestampNow(); + _lastEdited = now; + _lastUpdated = now; + + _position = glm::vec3(0,0,0); + _radius = 0; + rgbColor noColor = { 0, 0, 0 }; + memcpy(_color, noColor, sizeof(_color)); + _shouldDie = false; + _modelURL = MODEL_DEFAULT_MODEL_URL; + _modelRotation = MODEL_DEFAULT_MODEL_ROTATION; + + setProperties(properties); +} + + +ModelItem::~ModelItem() { +} + +void ModelItem::init(glm::vec3 position, float radius, rgbColor color, uint32_t id) { + if (id == NEW_MODEL) { + _id = _nextID; + _nextID++; + } else { + _id = id; + } + quint64 now = usecTimestampNow(); + _lastEdited = now; + _lastUpdated = now; + + _position = position; + _radius = radius; + memcpy(_color, color, sizeof(_color)); + _shouldDie = false; + _modelURL = MODEL_DEFAULT_MODEL_URL; + _modelRotation = MODEL_DEFAULT_MODEL_ROTATION; +} + +bool ModelItem::appendModelData(OctreePacketData* packetData) const { + + bool success = packetData->appendValue(getID()); + + //qDebug("ModelItem::appendModelData()... getID()=%d", getID()); + + if (success) { + success = packetData->appendValue(getLastUpdated()); + } + if (success) { + success = packetData->appendValue(getLastEdited()); + } + if (success) { + success = packetData->appendValue(getRadius()); + } + if (success) { + success = packetData->appendPosition(getPosition()); + } + if (success) { + success = packetData->appendColor(getColor()); + } + if (success) { + success = packetData->appendValue(getShouldDie()); + } + + // modelURL + if (success) { + uint16_t modelURLLength = _modelURL.size() + 1; // include NULL + success = packetData->appendValue(modelURLLength); + if (success) { + success = packetData->appendRawData((const unsigned char*)qPrintable(_modelURL), modelURLLength); + } + } + + // modelRotation + if (success) { + success = packetData->appendValue(getModelRotation()); + } + return success; +} + +int ModelItem::expectedBytes() { + int expectedBytes = sizeof(uint32_t) // id + + sizeof(float) // age + + sizeof(quint64) // last updated + + sizeof(quint64) // lasted edited + + sizeof(float) // radius + + sizeof(glm::vec3) // position + + sizeof(rgbColor); // color + // potentially more... + return expectedBytes; +} + +int ModelItem::readModelDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args) { + int bytesRead = 0; + if (bytesLeftToRead >= expectedBytes()) { + int clockSkew = args.sourceNode ? args.sourceNode->getClockSkewUsec() : 0; + + const unsigned char* dataAt = data; + + // id + memcpy(&_id, dataAt, sizeof(_id)); + dataAt += sizeof(_id); + bytesRead += sizeof(_id); + + // _lastUpdated + memcpy(&_lastUpdated, dataAt, sizeof(_lastUpdated)); + dataAt += sizeof(_lastUpdated); + bytesRead += sizeof(_lastUpdated); + _lastUpdated -= clockSkew; + + // _lastEdited + memcpy(&_lastEdited, dataAt, sizeof(_lastEdited)); + dataAt += sizeof(_lastEdited); + bytesRead += sizeof(_lastEdited); + _lastEdited -= clockSkew; + + // radius + memcpy(&_radius, dataAt, sizeof(_radius)); + dataAt += sizeof(_radius); + bytesRead += sizeof(_radius); + + // position + memcpy(&_position, dataAt, sizeof(_position)); + dataAt += sizeof(_position); + bytesRead += sizeof(_position); + + // color + memcpy(_color, dataAt, sizeof(_color)); + dataAt += sizeof(_color); + bytesRead += sizeof(_color); + + // shouldDie + memcpy(&_shouldDie, dataAt, sizeof(_shouldDie)); + dataAt += sizeof(_shouldDie); + bytesRead += sizeof(_shouldDie); + + // modelURL + uint16_t modelURLLength; + memcpy(&modelURLLength, dataAt, sizeof(modelURLLength)); + dataAt += sizeof(modelURLLength); + bytesRead += sizeof(modelURLLength); + QString modelURLString((const char*)dataAt); + _modelURL = modelURLString; + dataAt += modelURLLength; + bytesRead += modelURLLength; + + // modelRotation + int bytes = unpackOrientationQuatFromBytes(dataAt, _modelRotation); + dataAt += bytes; + bytesRead += bytes; + + //printf("ModelItem::readModelDataFromBuffer()... "); debugDump(); + } + return bytesRead; +} + +ModelItem ModelItem::fromEditPacket(const unsigned char* data, int length, int& processedBytes, ModelTree* tree, bool& valid) { + + ModelItem newModelItem; // id and _lastUpdated will get set here... + const unsigned char* dataAt = data; + processedBytes = 0; + + // the first part of the data is our octcode... + int octets = numberOfThreeBitSectionsInCode(data); + int lengthOfOctcode = bytesRequiredForCodeLength(octets); + + // we don't actually do anything with this octcode... + dataAt += lengthOfOctcode; + processedBytes += lengthOfOctcode; + + // id + uint32_t editID; + memcpy(&editID, dataAt, sizeof(editID)); + dataAt += sizeof(editID); + processedBytes += sizeof(editID); + + bool isNewModelItem = (editID == NEW_MODEL); + + // special case for handling "new" modelItems + if (isNewModelItem) { + // If this is a NEW_MODEL, then we assume that there's an additional uint32_t creatorToken, that + // we want to send back to the creator as an map to the actual id + uint32_t creatorTokenID; + memcpy(&creatorTokenID, dataAt, sizeof(creatorTokenID)); + dataAt += sizeof(creatorTokenID); + processedBytes += sizeof(creatorTokenID); + + newModelItem.setCreatorTokenID(creatorTokenID); + newModelItem._newlyCreated = true; + + } else { + // look up the existing modelItem + const ModelItem* existingModelItem = tree->findModelByID(editID, true); + + // copy existing properties before over-writing with new properties + if (existingModelItem) { + newModelItem = *existingModelItem; + } else { + // the user attempted to edit a modelItem that doesn't exist + qDebug() << "user attempted to edit a modelItem that doesn't exist..."; + valid = false; + return newModelItem; + } + newModelItem._id = editID; + newModelItem._newlyCreated = false; + } + + // if we got this far, then our result will be valid + valid = true; + + + // lastEdited + memcpy(&newModelItem._lastEdited, dataAt, sizeof(newModelItem._lastEdited)); + dataAt += sizeof(newModelItem._lastEdited); + processedBytes += sizeof(newModelItem._lastEdited); + + // All of the remaining items are optional, and may or may not be included based on their included values in the + // properties included bits + uint16_t packetContainsBits = 0; + if (!isNewModelItem) { + memcpy(&packetContainsBits, dataAt, sizeof(packetContainsBits)); + dataAt += sizeof(packetContainsBits); + processedBytes += sizeof(packetContainsBits); + } + + + // radius + if (isNewModelItem || ((packetContainsBits & MODEL_PACKET_CONTAINS_RADIUS) == MODEL_PACKET_CONTAINS_RADIUS)) { + memcpy(&newModelItem._radius, dataAt, sizeof(newModelItem._radius)); + dataAt += sizeof(newModelItem._radius); + processedBytes += sizeof(newModelItem._radius); + } + + // position + if (isNewModelItem || ((packetContainsBits & MODEL_PACKET_CONTAINS_POSITION) == MODEL_PACKET_CONTAINS_POSITION)) { + memcpy(&newModelItem._position, dataAt, sizeof(newModelItem._position)); + dataAt += sizeof(newModelItem._position); + processedBytes += sizeof(newModelItem._position); + } + + // color + if (isNewModelItem || ((packetContainsBits & MODEL_PACKET_CONTAINS_COLOR) == MODEL_PACKET_CONTAINS_COLOR)) { + memcpy(newModelItem._color, dataAt, sizeof(newModelItem._color)); + dataAt += sizeof(newModelItem._color); + processedBytes += sizeof(newModelItem._color); + } + + // shouldDie + if (isNewModelItem || ((packetContainsBits & MODEL_PACKET_CONTAINS_SHOULDDIE) == MODEL_PACKET_CONTAINS_SHOULDDIE)) { + memcpy(&newModelItem._shouldDie, dataAt, sizeof(newModelItem._shouldDie)); + dataAt += sizeof(newModelItem._shouldDie); + processedBytes += sizeof(newModelItem._shouldDie); + } + + // modelURL + if (isNewModelItem || ((packetContainsBits & MODEL_PACKET_CONTAINS_MODEL_URL) == MODEL_PACKET_CONTAINS_MODEL_URL)) { + uint16_t modelURLLength; + memcpy(&modelURLLength, dataAt, sizeof(modelURLLength)); + dataAt += sizeof(modelURLLength); + processedBytes += sizeof(modelURLLength); + QString tempString((const char*)dataAt); + newModelItem._modelURL = tempString; + dataAt += modelURLLength; + processedBytes += modelURLLength; + } + + // modelRotation + if (isNewModelItem || ((packetContainsBits & MODEL_PACKET_CONTAINS_MODEL_ROTATION) == MODEL_PACKET_CONTAINS_MODEL_ROTATION)) { + int bytes = unpackOrientationQuatFromBytes(dataAt, newModelItem._modelRotation); + dataAt += bytes; + processedBytes += bytes; + } + + const bool wantDebugging = false; + if (wantDebugging) { + qDebug("ModelItem::fromEditPacket()..."); + qDebug() << " ModelItem id in packet:" << editID; + newModelItem.debugDump(); + } + + return newModelItem; +} + +void ModelItem::debugDump() const { + qDebug("ModelItem id :%u", _id); + qDebug(" edited ago:%f", getEditedAgo()); + qDebug(" should die:%s", debug::valueOf(getShouldDie())); + qDebug(" position:%f,%f,%f", _position.x, _position.y, _position.z); + qDebug(" radius:%f", getRadius()); + qDebug(" color:%d,%d,%d", _color[0], _color[1], _color[2]); +} + +bool ModelItem::encodeModelEditMessageDetails(PacketType command, ModelItemID id, const ModelItemProperties& properties, + unsigned char* bufferOut, int sizeIn, int& sizeOut) { + + bool success = true; // assume the best + unsigned char* copyAt = bufferOut; + sizeOut = 0; + + // get the octal code for the modelItem + + // this could be a problem if the caller doesn't include position.... + glm::vec3 rootPosition(0); + float rootScale = 0.5f; + unsigned char* octcode = pointToOctalCode(rootPosition.x, rootPosition.y, rootPosition.z, rootScale); + + // TODO: Consider this old code... including the correct octree for where the modelItem will go matters for + // modelItem servers with different jurisdictions, but for now, we'll send everything to the root, since the + // tree does the right thing... + // + //unsigned char* octcode = pointToOctalCode(details[i].position.x, details[i].position.y, + // details[i].position.z, details[i].radius); + + int octets = numberOfThreeBitSectionsInCode(octcode); + int lengthOfOctcode = bytesRequiredForCodeLength(octets); + + // add it to our message + memcpy(copyAt, octcode, lengthOfOctcode); + copyAt += lengthOfOctcode; + sizeOut += lengthOfOctcode; + + // Now add our edit content details... + bool isNewModelItem = (id.id == NEW_MODEL); + + // id + memcpy(copyAt, &id.id, sizeof(id.id)); + copyAt += sizeof(id.id); + sizeOut += sizeof(id.id); + + // special case for handling "new" modelItems + if (isNewModelItem) { + // If this is a NEW_MODEL, then we assume that there's an additional uint32_t creatorToken, that + // we want to send back to the creator as an map to the actual id + memcpy(copyAt, &id.creatorTokenID, sizeof(id.creatorTokenID)); + copyAt += sizeof(id.creatorTokenID); + sizeOut += sizeof(id.creatorTokenID); + } + + // lastEdited + quint64 lastEdited = properties.getLastEdited(); + memcpy(copyAt, &lastEdited, sizeof(lastEdited)); + copyAt += sizeof(lastEdited); + sizeOut += sizeof(lastEdited); + + // For new modelItems, all remaining items are mandatory, for an edited modelItem, All of the remaining items are + // optional, and may or may not be included based on their included values in the properties included bits + uint16_t packetContainsBits = properties.getChangedBits(); + if (!isNewModelItem) { + memcpy(copyAt, &packetContainsBits, sizeof(packetContainsBits)); + copyAt += sizeof(packetContainsBits); + sizeOut += sizeof(packetContainsBits); + } + + // radius + if (isNewModelItem || ((packetContainsBits & MODEL_PACKET_CONTAINS_RADIUS) == MODEL_PACKET_CONTAINS_RADIUS)) { + float radius = properties.getRadius() / (float) TREE_SCALE; + memcpy(copyAt, &radius, sizeof(radius)); + copyAt += sizeof(radius); + sizeOut += sizeof(radius); + } + + // position + if (isNewModelItem || ((packetContainsBits & MODEL_PACKET_CONTAINS_POSITION) == MODEL_PACKET_CONTAINS_POSITION)) { + glm::vec3 position = properties.getPosition() / (float)TREE_SCALE; + memcpy(copyAt, &position, sizeof(position)); + copyAt += sizeof(position); + sizeOut += sizeof(position); + } + + // color + if (isNewModelItem || ((packetContainsBits & MODEL_PACKET_CONTAINS_COLOR) == MODEL_PACKET_CONTAINS_COLOR)) { + rgbColor color = { properties.getColor().red, properties.getColor().green, properties.getColor().blue }; + memcpy(copyAt, color, sizeof(color)); + copyAt += sizeof(color); + sizeOut += sizeof(color); + } + + // shoulDie + if (isNewModelItem || ((packetContainsBits & MODEL_PACKET_CONTAINS_SHOULDDIE) == MODEL_PACKET_CONTAINS_SHOULDDIE)) { + bool shouldDie = properties.getShouldDie(); + memcpy(copyAt, &shouldDie, sizeof(shouldDie)); + copyAt += sizeof(shouldDie); + sizeOut += sizeof(shouldDie); + } + + // modelURL + if (isNewModelItem || ((packetContainsBits & MODEL_PACKET_CONTAINS_MODEL_URL) == MODEL_PACKET_CONTAINS_MODEL_URL)) { + uint16_t urlLength = properties.getModelURL().size() + 1; + memcpy(copyAt, &urlLength, sizeof(urlLength)); + copyAt += sizeof(urlLength); + sizeOut += sizeof(urlLength); + memcpy(copyAt, qPrintable(properties.getModelURL()), urlLength); + copyAt += urlLength; + sizeOut += urlLength; + } + + // modelRotation + if (isNewModelItem || ((packetContainsBits & MODEL_PACKET_CONTAINS_MODEL_ROTATION) == MODEL_PACKET_CONTAINS_MODEL_ROTATION)) { + int bytes = packOrientationQuatToBytes(copyAt, properties.getModelRotation()); + copyAt += bytes; + sizeOut += bytes; + } + + bool wantDebugging = false; + if (wantDebugging) { + qDebug("encodeModelItemEditMessageDetails()...."); + qDebug("ModelItem id :%u", id.id); + qDebug(" nextID:%u", _nextID); + } + + // cleanup + delete[] octcode; + + return success; +} + +// adjust any internal timestamps to fix clock skew for this server +void ModelItem::adjustEditPacketForClockSkew(unsigned char* codeColorBuffer, ssize_t length, int clockSkew) { + unsigned char* dataAt = codeColorBuffer; + int octets = numberOfThreeBitSectionsInCode(dataAt); + int lengthOfOctcode = bytesRequiredForCodeLength(octets); + dataAt += lengthOfOctcode; + + // id + uint32_t id; + memcpy(&id, dataAt, sizeof(id)); + dataAt += sizeof(id); + // special case for handling "new" modelItems + if (id == NEW_MODEL) { + // If this is a NEW_MODEL, then we assume that there's an additional uint32_t creatorToken, that + // we want to send back to the creator as an map to the actual id + dataAt += sizeof(uint32_t); + } + + // lastEdited + quint64 lastEditedInLocalTime; + memcpy(&lastEditedInLocalTime, dataAt, sizeof(lastEditedInLocalTime)); + quint64 lastEditedInServerTime = lastEditedInLocalTime + clockSkew; + memcpy(dataAt, &lastEditedInServerTime, sizeof(lastEditedInServerTime)); + const bool wantDebug = false; + if (wantDebug) { + qDebug("ModelItem::adjustEditPacketForClockSkew()..."); + qDebug() << " lastEditedInLocalTime: " << lastEditedInLocalTime; + qDebug() << " clockSkew: " << clockSkew; + qDebug() << " lastEditedInServerTime: " << lastEditedInServerTime; + } +} + +void ModelItem::update(const quint64& now) { + _lastUpdated = now; + setShouldDie(getShouldDie()); +} + +void ModelItem::copyChangedProperties(const ModelItem& other) { + *this = other; +} + +ModelItemProperties ModelItem::getProperties() const { + ModelItemProperties properties; + properties.copyFromModelItem(*this); + return properties; +} + +void ModelItem::setProperties(const ModelItemProperties& properties) { + properties.copyToModelItem(*this); +} + +ModelItemProperties::ModelItemProperties() : + _position(0), + _color(), + _radius(MODEL_DEFAULT_RADIUS), + _shouldDie(false), + _modelURL(""), + _modelRotation(MODEL_DEFAULT_MODEL_ROTATION), + + _id(UNKNOWN_MODEL_ID), + _idSet(false), + _lastEdited(usecTimestampNow()), + + _positionChanged(false), + _colorChanged(false), + _radiusChanged(false), + _shouldDieChanged(false), + _modelURLChanged(false), + _modelRotationChanged(false), + _defaultSettings(true) +{ +} + + +uint16_t ModelItemProperties::getChangedBits() const { + uint16_t changedBits = 0; + if (_radiusChanged) { + changedBits += MODEL_PACKET_CONTAINS_RADIUS; + } + + if (_positionChanged) { + changedBits += MODEL_PACKET_CONTAINS_POSITION; + } + + if (_colorChanged) { + changedBits += MODEL_PACKET_CONTAINS_COLOR; + } + + if (_shouldDieChanged) { + changedBits += MODEL_PACKET_CONTAINS_SHOULDDIE; + } + + if (_modelURLChanged) { + changedBits += MODEL_PACKET_CONTAINS_MODEL_URL; + } + + if (_modelRotationChanged) { + changedBits += MODEL_PACKET_CONTAINS_MODEL_ROTATION; + } + + return changedBits; +} + + +QScriptValue ModelItemProperties::copyToScriptValue(QScriptEngine* engine) const { + QScriptValue properties = engine->newObject(); + + QScriptValue position = vec3toScriptValue(engine, _position); + properties.setProperty("position", position); + + QScriptValue color = xColorToScriptValue(engine, _color); + properties.setProperty("color", color); + + properties.setProperty("radius", _radius); + + properties.setProperty("shouldDie", _shouldDie); + + properties.setProperty("modelURL", _modelURL); + + QScriptValue modelRotation = quatToScriptValue(engine, _modelRotation); + properties.setProperty("modelRotation", modelRotation); + + + if (_idSet) { + properties.setProperty("id", _id); + properties.setProperty("isKnownID", (_id != UNKNOWN_MODEL_ID)); + } + + return properties; +} + +void ModelItemProperties::copyFromScriptValue(const QScriptValue &object) { + + QScriptValue position = object.property("position"); + if (position.isValid()) { + QScriptValue x = position.property("x"); + QScriptValue y = position.property("y"); + QScriptValue z = position.property("z"); + if (x.isValid() && y.isValid() && z.isValid()) { + glm::vec3 newPosition; + newPosition.x = x.toVariant().toFloat(); + newPosition.y = y.toVariant().toFloat(); + newPosition.z = z.toVariant().toFloat(); + if (_defaultSettings || newPosition != _position) { + _position = newPosition; + _positionChanged = true; + } + } + } + + QScriptValue color = object.property("color"); + if (color.isValid()) { + QScriptValue red = color.property("red"); + QScriptValue green = color.property("green"); + QScriptValue blue = color.property("blue"); + if (red.isValid() && green.isValid() && blue.isValid()) { + xColor newColor; + newColor.red = red.toVariant().toInt(); + newColor.green = green.toVariant().toInt(); + newColor.blue = blue.toVariant().toInt(); + if (_defaultSettings || (newColor.red != _color.red || + newColor.green != _color.green || + newColor.blue != _color.blue)) { + _color = newColor; + _colorChanged = true; + } + } + } + + QScriptValue radius = object.property("radius"); + if (radius.isValid()) { + float newRadius; + newRadius = radius.toVariant().toFloat(); + if (_defaultSettings || newRadius != _radius) { + _radius = newRadius; + _radiusChanged = true; + } + } + + QScriptValue shouldDie = object.property("shouldDie"); + if (shouldDie.isValid()) { + bool newShouldDie; + newShouldDie = shouldDie.toVariant().toBool(); + if (_defaultSettings || newShouldDie != _shouldDie) { + _shouldDie = newShouldDie; + _shouldDieChanged = true; + } + } + + QScriptValue modelURL = object.property("modelURL"); + if (modelURL.isValid()) { + QString newModelURL; + newModelURL = modelURL.toVariant().toString(); + if (_defaultSettings || newModelURL != _modelURL) { + _modelURL = newModelURL; + _modelURLChanged = true; + } + } + + QScriptValue modelRotation = object.property("modelRotation"); + if (modelRotation.isValid()) { + QScriptValue x = modelRotation.property("x"); + QScriptValue y = modelRotation.property("y"); + QScriptValue z = modelRotation.property("z"); + QScriptValue w = modelRotation.property("w"); + if (x.isValid() && y.isValid() && z.isValid() && w.isValid()) { + glm::quat newModelRotation; + newModelRotation.x = x.toVariant().toFloat(); + newModelRotation.y = y.toVariant().toFloat(); + newModelRotation.z = z.toVariant().toFloat(); + newModelRotation.w = w.toVariant().toFloat(); + if (_defaultSettings || newModelRotation != _modelRotation) { + _modelRotation = newModelRotation; + _modelRotationChanged = true; + } + } + } + + _lastEdited = usecTimestampNow(); +} + +void ModelItemProperties::copyToModelItem(ModelItem& modelItem) const { + bool somethingChanged = false; + if (_positionChanged) { + modelItem.setPosition(_position / (float) TREE_SCALE); + somethingChanged = true; + } + + if (_colorChanged) { + modelItem.setColor(_color); + somethingChanged = true; + } + + if (_radiusChanged) { + modelItem.setRadius(_radius / (float) TREE_SCALE); + somethingChanged = true; + } + + if (_shouldDieChanged) { + modelItem.setShouldDie(_shouldDie); + somethingChanged = true; + } + + if (_modelURLChanged) { + modelItem.setModelURL(_modelURL); + somethingChanged = true; + } + + if (_modelRotationChanged) { + modelItem.setModelRotation(_modelRotation); + somethingChanged = true; + } + + if (somethingChanged) { + bool wantDebug = false; + if (wantDebug) { + uint64_t now = usecTimestampNow(); + int elapsed = now - _lastEdited; + qDebug() << "ModelItemProperties::copyToModelItem() AFTER update... edited AGO=" << elapsed << + "now=" << now << " _lastEdited=" << _lastEdited; + } + modelItem.setLastEdited(_lastEdited); + } +} + +void ModelItemProperties::copyFromModelItem(const ModelItem& modelItem) { + _position = modelItem.getPosition() * (float) TREE_SCALE; + _color = modelItem.getXColor(); + _radius = modelItem.getRadius() * (float) TREE_SCALE; + _shouldDie = modelItem.getShouldDie(); + _modelURL = modelItem.getModelURL(); + _modelRotation = modelItem.getModelRotation(); + + _id = modelItem.getID(); + _idSet = true; + + _positionChanged = false; + _colorChanged = false; + _radiusChanged = false; + + _shouldDieChanged = false; + _modelURLChanged = false; + _modelRotationChanged = false; + _defaultSettings = false; +} + +QScriptValue ModelItemPropertiesToScriptValue(QScriptEngine* engine, const ModelItemProperties& properties) { + return properties.copyToScriptValue(engine); +} + +void ModelItemPropertiesFromScriptValue(const QScriptValue &object, ModelItemProperties& properties) { + properties.copyFromScriptValue(object); +} + + +QScriptValue ModelItemIDtoScriptValue(QScriptEngine* engine, const ModelItemID& id) { + QScriptValue obj = engine->newObject(); + obj.setProperty("id", id.id); + obj.setProperty("creatorTokenID", id.creatorTokenID); + obj.setProperty("isKnownID", id.isKnownID); + return obj; +} + +void ModelItemIDfromScriptValue(const QScriptValue &object, ModelItemID& id) { + id.id = object.property("id").toVariant().toUInt(); + id.creatorTokenID = object.property("creatorTokenID").toVariant().toUInt(); + id.isKnownID = object.property("isKnownID").toVariant().toBool(); +} + + + diff --git a/libraries/models/src/ModelItem.h b/libraries/models/src/ModelItem.h new file mode 100644 index 0000000000..317299be2d --- /dev/null +++ b/libraries/models/src/ModelItem.h @@ -0,0 +1,253 @@ +// +// ModelItem.h +// libraries/models/src +// +// Created by Brad Hefta-Gaub on 12/4/13. +// Copyright 2013 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_ModelItem_h +#define hifi_ModelItem_h + +#include +#include + +#include +#include + +#include +#include +#include + +class ModelItem; +class ModelEditPacketSender; +class ModelItemProperties; +class ModelsScriptingInterface; +class ModelTree; +class ScriptEngine; +class VoxelEditPacketSender; +class VoxelsScriptingInterface; +struct VoxelDetail; + +const uint32_t NEW_MODEL = 0xFFFFFFFF; +const uint32_t UNKNOWN_MODEL_TOKEN = 0xFFFFFFFF; +const uint32_t UNKNOWN_MODEL_ID = 0xFFFFFFFF; + +const uint16_t MODEL_PACKET_CONTAINS_RADIUS = 1; +const uint16_t MODEL_PACKET_CONTAINS_POSITION = 2; +const uint16_t MODEL_PACKET_CONTAINS_COLOR = 4; +const uint16_t MODEL_PACKET_CONTAINS_SHOULDDIE = 512; +const uint16_t MODEL_PACKET_CONTAINS_MODEL_URL = 1024; +const uint16_t MODEL_PACKET_CONTAINS_MODEL_ROTATION = 2048; + +const float MODEL_DEFAULT_RADIUS = 0.1f / TREE_SCALE; +const float MINIMUM_MODEL_ELEMENT_SIZE = (1.0f / 100000.0f) / TREE_SCALE; // smallest size container +const QString MODEL_DEFAULT_MODEL_URL(""); +const glm::quat MODEL_DEFAULT_MODEL_ROTATION(0, 0, 0, 0); + +/// A collection of properties of a model item used in the scripting API. Translates between the actual properties of a model +/// and a JavaScript style hash/QScriptValue storing a set of properties. Used in scripting to set/get the complete set of +/// model item properties via JavaScript hashes/QScriptValues +/// all units for position, radius, etc are in meter units +class ModelItemProperties { +public: + ModelItemProperties(); + + QScriptValue copyToScriptValue(QScriptEngine* engine) const; + void copyFromScriptValue(const QScriptValue& object); + + void copyToModelItem(ModelItem& modelItem) const; + void copyFromModelItem(const ModelItem& modelItem); + + const glm::vec3& getPosition() const { return _position; } + xColor getColor() const { return _color; } + float getRadius() const { return _radius; } + bool getShouldDie() const { return _shouldDie; } + + const QString& getModelURL() const { return _modelURL; } + const glm::quat& getModelRotation() const { return _modelRotation; } + + quint64 getLastEdited() const { return _lastEdited; } + uint16_t getChangedBits() const; + + /// set position in meter units + void setPosition(const glm::vec3& value) { _position = value; _positionChanged = true; } + void setColor(const xColor& value) { _color = value; _colorChanged = true; } + void setRadius(float value) { _radius = value; _radiusChanged = true; } + void setShouldDie(bool shouldDie) { _shouldDie = shouldDie; _shouldDieChanged = true; } + + // model related properties + void setModelURL(const QString& url) { _modelURL = url; _modelURLChanged = true; } + void setModelRotation(const glm::quat& rotation) { _modelRotation = rotation; _modelRotationChanged = true; } + + /// used by ModelScriptingInterface to return ModelItemProperties for unknown models + void setIsUnknownID() { _id = UNKNOWN_MODEL_ID; _idSet = true; } + +private: + glm::vec3 _position; + xColor _color; + float _radius; + bool _shouldDie; /// to delete it + + QString _modelURL; + glm::quat _modelRotation; + + uint32_t _id; + bool _idSet; + quint64 _lastEdited; + + bool _positionChanged; + bool _colorChanged; + bool _radiusChanged; + bool _shouldDieChanged; + + bool _modelURLChanged; + bool _modelRotationChanged; + bool _defaultSettings; +}; +Q_DECLARE_METATYPE(ModelItemProperties); +QScriptValue ModelItemPropertiesToScriptValue(QScriptEngine* engine, const ModelItemProperties& properties); +void ModelItemPropertiesFromScriptValue(const QScriptValue &object, ModelItemProperties& properties); + + +/// Abstract ID for editing model items. Used in ModelItem JS API - When models are created in the JS api, they are given a +/// local creatorTokenID, the actual id for the model is not known until the server responds to the creator with the +/// correct mapping. This class works with the scripting API an allows the developer to edit models they created. +class ModelItemID { +public: + ModelItemID() : + id(NEW_MODEL), creatorTokenID(UNKNOWN_MODEL_TOKEN), isKnownID(false) { }; + + ModelItemID(uint32_t id, uint32_t creatorTokenID, bool isKnownID) : + id(id), creatorTokenID(creatorTokenID), isKnownID(isKnownID) { }; + + ModelItemID(uint32_t id) : + id(id), creatorTokenID(UNKNOWN_MODEL_TOKEN), isKnownID(true) { }; + + uint32_t id; + uint32_t creatorTokenID; + bool isKnownID; +}; + +Q_DECLARE_METATYPE(ModelItemID); +Q_DECLARE_METATYPE(QVector); +QScriptValue ModelItemIDtoScriptValue(QScriptEngine* engine, const ModelItemID& properties); +void ModelItemIDfromScriptValue(const QScriptValue &object, ModelItemID& properties); + + + +/// ModelItem class - this is the actual model item class. +class ModelItem { + +public: + ModelItem(); + + ModelItem(const ModelItemID& modelItemID, const ModelItemProperties& properties); + + /// creates an NEW model from an model add or edit message data buffer + static ModelItem fromEditPacket(const unsigned char* data, int length, int& processedBytes, ModelTree* tree, bool& valid); + + virtual ~ModelItem(); + virtual void init(glm::vec3 position, float radius, rgbColor color, uint32_t id = NEW_MODEL); + + /// get position in domain scale units (0.0 - 1.0) + const glm::vec3& getPosition() const { return _position; } + + const rgbColor& getColor() const { return _color; } + xColor getXColor() const { xColor color = { _color[RED_INDEX], _color[GREEN_INDEX], _color[BLUE_INDEX] }; return color; } + + /// get radius in domain scale units (0.0 - 1.0) + float getRadius() const { return _radius; } + + // model related properties + bool hasModel() const { return !_modelURL.isEmpty(); } + const QString& getModelURL() const { return _modelURL; } + const glm::quat& getModelRotation() const { return _modelRotation; } + + ModelItemID getModelItemID() const { return ModelItemID(getID(), getCreatorTokenID(), getID() != UNKNOWN_MODEL_ID); } + ModelItemProperties getProperties() const; + + /// The last updated/simulated time of this model from the time perspective of the authoritative server/source + quint64 getLastUpdated() const { return _lastUpdated; } + + /// The last edited time of this model from the time perspective of the authoritative server/source + quint64 getLastEdited() const { return _lastEdited; } + void setLastEdited(quint64 lastEdited) { _lastEdited = lastEdited; } + + /// how long ago was this model edited in seconds + float getEditedAgo() const { return static_cast(usecTimestampNow() - _lastEdited) / static_cast(USECS_PER_SECOND); } + uint32_t getID() const { return _id; } + void setID(uint32_t id) { _id = id; } + bool getShouldDie() const { return _shouldDie; } + uint32_t getCreatorTokenID() const { return _creatorTokenID; } + bool isNewlyCreated() const { return _newlyCreated; } + + /// set position in domain scale units (0.0 - 1.0) + void setPosition(const glm::vec3& value) { _position = value; } + + void setColor(const rgbColor& value) { memcpy(_color, value, sizeof(_color)); } + void setColor(const xColor& value) { + _color[RED_INDEX] = value.red; + _color[GREEN_INDEX] = value.green; + _color[BLUE_INDEX] = value.blue; + } + /// set radius in domain scale units (0.0 - 1.0) + void setRadius(float value) { _radius = value; } + + void setShouldDie(bool shouldDie) { _shouldDie = shouldDie; } + void setCreatorTokenID(uint32_t creatorTokenID) { _creatorTokenID = creatorTokenID; } + + // model related properties + void setModelURL(const QString& url) { _modelURL = url; } + void setModelRotation(const glm::quat& rotation) { _modelRotation = rotation; } + + void setProperties(const ModelItemProperties& properties); + + bool appendModelData(OctreePacketData* packetData) const; + int readModelDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args); + static int expectedBytes(); + + static bool encodeModelEditMessageDetails(PacketType command, ModelItemID id, const ModelItemProperties& details, + unsigned char* bufferOut, int sizeIn, int& sizeOut); + + static void adjustEditPacketForClockSkew(unsigned char* codeColorBuffer, ssize_t length, int clockSkew); + + void update(const quint64& now); + + void debugDump() const; + + // similar to assignment/copy, but it handles keeping lifetime accurate + void copyChangedProperties(const ModelItem& other); + + // these methods allow you to create models, and later edit them. + static uint32_t getIDfromCreatorTokenID(uint32_t creatorTokenID); + static uint32_t getNextCreatorTokenID(); + static void handleAddModelResponse(const QByteArray& packet); + +protected: + glm::vec3 _position; + rgbColor _color; + float _radius; + uint32_t _id; + static uint32_t _nextID; + bool _shouldDie; + + // model related items + QString _modelURL; + glm::quat _modelRotation; + + uint32_t _creatorTokenID; + bool _newlyCreated; + + quint64 _lastUpdated; + quint64 _lastEdited; + + // used by the static interfaces for creator token ids + static uint32_t _nextCreatorTokenID; + static std::map _tokenIDsToIDs; +}; + +#endif // hifi_ModelItem_h diff --git a/libraries/models/src/ModelTree.cpp b/libraries/models/src/ModelTree.cpp new file mode 100644 index 0000000000..236138e2a8 --- /dev/null +++ b/libraries/models/src/ModelTree.cpp @@ -0,0 +1,636 @@ +// +// ModelTree.cpp +// libraries/models/src +// +// Created by Brad Hefta-Gaub on 12/4/13. +// Copyright 2013 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 "ModelTree.h" + +ModelTree::ModelTree(bool shouldReaverage) : Octree(shouldReaverage) { + _rootNode = createNewElement(); +} + +ModelTreeElement* ModelTree::createNewElement(unsigned char * octalCode) { + ModelTreeElement* newElement = new ModelTreeElement(octalCode); + newElement->setTree(this); + return newElement; +} + +bool ModelTree::handlesEditPacketType(PacketType packetType) const { + // we handle these types of "edit" packets + switch (packetType) { + case PacketTypeModelAddOrEdit: + case PacketTypeModelErase: + return true; + default: + return false; + } +} + +class FindAndDeleteModelsArgs { +public: + QList _idsToDelete; +}; + +bool ModelTree::findAndDeleteOperation(OctreeElement* element, void* extraData) { + //qDebug() << "findAndDeleteOperation()"; + + FindAndDeleteModelsArgs* args = static_cast< FindAndDeleteModelsArgs*>(extraData); + + // if we've found and deleted all our target models, then we can stop looking + if (args->_idsToDelete.size() <= 0) { + return false; + } + + ModelTreeElement* modelTreeElement = static_cast(element); + + //qDebug() << "findAndDeleteOperation() args->_idsToDelete.size():" << args->_idsToDelete.size(); + + for (QList::iterator it = args->_idsToDelete.begin(); it != args->_idsToDelete.end(); it++) { + uint32_t modelID = *it; + //qDebug() << "findAndDeleteOperation() modelID:" << modelID; + + if (modelTreeElement->removeModelWithID(modelID)) { + // if the model was in this element, then remove it from our search list. + //qDebug() << "findAndDeleteOperation() it = args->_idsToDelete.erase(it)"; + it = args->_idsToDelete.erase(it); + } + + if (it == args->_idsToDelete.end()) { + //qDebug() << "findAndDeleteOperation() breaking"; + break; + } + } + + // if we've found and deleted all our target models, then we can stop looking + if (args->_idsToDelete.size() <= 0) { + return false; + } + return true; +} + + +class FindAndUpdateModelArgs { +public: + const ModelItem& searchModel; + bool found; +}; + +bool ModelTree::findAndUpdateOperation(OctreeElement* element, void* extraData) { + FindAndUpdateModelArgs* args = static_cast(extraData); + ModelTreeElement* modelTreeElement = static_cast(element); + // Note: updateModel() will only operate on correctly found models + if (modelTreeElement->updateModel(args->searchModel)) { + args->found = true; + return false; // stop searching + } + return true; +} + +void ModelTree::storeModel(const ModelItem& model, const SharedNodePointer& senderNode) { + // First, look for the existing model in the tree.. + FindAndUpdateModelArgs args = { model, false }; + recurseTreeWithOperation(findAndUpdateOperation, &args); + + // if we didn't find it in the tree, then store it... + if (!args.found) { + glm::vec3 position = model.getPosition(); + float size = std::max(MINIMUM_MODEL_ELEMENT_SIZE, model.getRadius()); + + ModelTreeElement* element = (ModelTreeElement*)getOrCreateChildElementAt(position.x, position.y, position.z, size); + element->storeModel(model); + } + // what else do we need to do here to get reaveraging to work + _isDirty = true; +} + +class FindAndUpdateModelWithIDandPropertiesArgs { +public: + const ModelItemID& modelID; + const ModelItemProperties& properties; + bool found; +}; + +bool ModelTree::findAndUpdateWithIDandPropertiesOperation(OctreeElement* element, void* extraData) { + FindAndUpdateModelWithIDandPropertiesArgs* args = static_cast(extraData); + ModelTreeElement* modelTreeElement = static_cast(element); + // Note: updateModel() will only operate on correctly found models + if (modelTreeElement->updateModel(args->modelID, args->properties)) { + args->found = true; + return false; // stop searching + } + + // if we've found our model stop searching + if (args->found) { + return false; + } + + return true; +} + +void ModelTree::updateModel(const ModelItemID& modelID, const ModelItemProperties& properties) { + // First, look for the existing model in the tree.. + FindAndUpdateModelWithIDandPropertiesArgs args = { modelID, properties, false }; + recurseTreeWithOperation(findAndUpdateWithIDandPropertiesOperation, &args); + // if we found it in the tree, then mark the tree as dirty + if (args.found) { + _isDirty = true; + } +} + +void ModelTree::addModel(const ModelItemID& modelID, const ModelItemProperties& properties) { + // This only operates on locally created models + if (modelID.isKnownID) { + return; // not allowed + } + ModelItem model(modelID, properties); + glm::vec3 position = model.getPosition(); + float size = std::max(MINIMUM_MODEL_ELEMENT_SIZE, model.getRadius()); + + ModelTreeElement* element = (ModelTreeElement*)getOrCreateChildElementAt(position.x, position.y, position.z, size); + element->storeModel(model); + + _isDirty = true; +} + +void ModelTree::deleteModel(const ModelItemID& modelID) { + if (modelID.isKnownID) { + FindAndDeleteModelsArgs args; + args._idsToDelete.push_back(modelID.id); + recurseTreeWithOperation(findAndDeleteOperation, &args); + } +} + +// scans the tree and handles mapping locally created models to know IDs. +// in the event that this tree is also viewing the scene, then we need to also +// search the tree to make sure we don't have a duplicate model from the viewing +// operation. +bool ModelTree::findAndUpdateModelItemIDOperation(OctreeElement* element, void* extraData) { + bool keepSearching = true; + + FindAndUpdateModelItemIDArgs* args = static_cast(extraData); + ModelTreeElement* modelTreeElement = static_cast(element); + + // Note: updateModelItemID() will only operate on correctly found models + modelTreeElement->updateModelItemID(args); + + // if we've found and replaced both the creatorTokenID and the viewedModel, then we + // can stop looking, otherwise we will keep looking + if (args->creatorTokenFound && args->viewedModelFound) { + keepSearching = false; + } + + return keepSearching; +} + +void ModelTree::handleAddModelResponse(const QByteArray& packet) { + int numBytesPacketHeader = numBytesForPacketHeader(packet); + + const unsigned char* dataAt = reinterpret_cast(packet.data()) + numBytesPacketHeader; + + uint32_t creatorTokenID; + memcpy(&creatorTokenID, dataAt, sizeof(creatorTokenID)); + dataAt += sizeof(creatorTokenID); + + uint32_t modelID; + memcpy(&modelID, dataAt, sizeof(modelID)); + dataAt += sizeof(modelID); + + // update models in our tree + bool assumeModelFound = !getIsViewing(); // if we're not a viewing tree, then we don't have to find the actual model + FindAndUpdateModelItemIDArgs args = { + modelID, + creatorTokenID, + false, + assumeModelFound, + getIsViewing() + }; + + const bool wantDebug = false; + if (wantDebug) { + qDebug() << "looking for creatorTokenID=" << creatorTokenID << " modelID=" << modelID + << " getIsViewing()=" << getIsViewing(); + } + lockForWrite(); + recurseTreeWithOperation(findAndUpdateModelItemIDOperation, &args); + unlock(); +} + + +class FindNearPointArgs { +public: + glm::vec3 position; + float targetRadius; + bool found; + const ModelItem* closestModel; + float closestModelDistance; +}; + + +bool ModelTree::findNearPointOperation(OctreeElement* element, void* extraData) { + FindNearPointArgs* args = static_cast(extraData); + ModelTreeElement* modelTreeElement = static_cast(element); + + glm::vec3 penetration; + bool sphereIntersection = modelTreeElement->getAABox().findSpherePenetration(args->position, + args->targetRadius, penetration); + + // If this modelTreeElement contains the point, then search it... + if (sphereIntersection) { + const ModelItem* thisClosestModel = modelTreeElement->getClosestModel(args->position); + + // we may have gotten NULL back, meaning no model was available + if (thisClosestModel) { + glm::vec3 modelPosition = thisClosestModel->getPosition(); + float distanceFromPointToModel = glm::distance(modelPosition, args->position); + + // If we're within our target radius + if (distanceFromPointToModel <= args->targetRadius) { + // we are closer than anything else we've found + if (distanceFromPointToModel < args->closestModelDistance) { + args->closestModel = thisClosestModel; + args->closestModelDistance = distanceFromPointToModel; + args->found = true; + } + } + } + + // we should be able to optimize this... + return true; // keep searching in case children have closer models + } + + // if this element doesn't contain the point, then none of it's children can contain the point, so stop searching + return false; +} + +const ModelItem* ModelTree::findClosestModel(glm::vec3 position, float targetRadius) { + FindNearPointArgs args = { position, targetRadius, false, NULL, FLT_MAX }; + lockForRead(); + recurseTreeWithOperation(findNearPointOperation, &args); + unlock(); + return args.closestModel; +} + +class FindAllNearPointArgs { +public: + glm::vec3 position; + float targetRadius; + QVector models; +}; + + +bool ModelTree::findInSphereOperation(OctreeElement* element, void* extraData) { + FindAllNearPointArgs* args = static_cast(extraData); + glm::vec3 penetration; + bool sphereIntersection = element->getAABox().findSpherePenetration(args->position, + args->targetRadius, penetration); + + // If this element contains the point, then search it... + if (sphereIntersection) { + ModelTreeElement* modelTreeElement = static_cast(element); + modelTreeElement->getModels(args->position, args->targetRadius, args->models); + return true; // keep searching in case children have closer models + } + + // if this element doesn't contain the point, then none of it's children can contain the point, so stop searching + return false; +} + +void ModelTree::findModels(const glm::vec3& center, float radius, QVector& foundModels) { + FindAllNearPointArgs args = { center, radius }; + lockForRead(); + recurseTreeWithOperation(findInSphereOperation, &args); + unlock(); + // swap the two lists of model pointers instead of copy + foundModels.swap(args.models); +} + +class FindModelsInBoxArgs { +public: + FindModelsInBoxArgs(const AABox& box) + : _box(box), _foundModels() { + } + + AABox _box; + QVector _foundModels; +}; + +bool ModelTree::findInBoxForUpdateOperation(OctreeElement* element, void* extraData) { + FindModelsInBoxArgs* args = static_cast< FindModelsInBoxArgs*>(extraData); + const AABox& elementBox = element->getAABox(); + if (elementBox.touches(args->_box)) { + ModelTreeElement* modelTreeElement = static_cast(element); + modelTreeElement->getModelsForUpdate(args->_box, args->_foundModels); + return true; + } + return false; +} + +void ModelTree::findModelsForUpdate(const AABox& box, QVector foundModels) { + FindModelsInBoxArgs args(box); + lockForRead(); + recurseTreeWithOperation(findInBoxForUpdateOperation, &args); + unlock(); + // swap the two lists of model pointers instead of copy + foundModels.swap(args._foundModels); +} + +class FindByIDArgs { +public: + uint32_t id; + bool found; + const ModelItem* foundModel; +}; + + +bool ModelTree::findByIDOperation(OctreeElement* element, void* extraData) { +//qDebug() << "ModelTree::findByIDOperation()...."; + + FindByIDArgs* args = static_cast(extraData); + ModelTreeElement* modelTreeElement = static_cast(element); + + // if already found, stop looking + if (args->found) { + return false; + } + + // as the tree element if it has this model + const ModelItem* foundModel = modelTreeElement->getModelWithID(args->id); + if (foundModel) { + args->foundModel = foundModel; + args->found = true; + return false; + } + + // keep looking + return true; +} + + +const ModelItem* ModelTree::findModelByID(uint32_t id, bool alreadyLocked) { + FindByIDArgs args = { id, false, NULL }; + + if (!alreadyLocked) { + lockForRead(); + } + recurseTreeWithOperation(findByIDOperation, &args); + if (!alreadyLocked) { + unlock(); + } + return args.foundModel; +} + + +int ModelTree::processEditPacketData(PacketType packetType, const unsigned char* packetData, int packetLength, + const unsigned char* editData, int maxLength, const SharedNodePointer& senderNode) { + + int processedBytes = 0; + // we handle these types of "edit" packets + switch (packetType) { + case PacketTypeModelAddOrEdit: { + bool isValid; + ModelItem newModel = ModelItem::fromEditPacket(editData, maxLength, processedBytes, this, isValid); + if (isValid) { + storeModel(newModel, senderNode); + if (newModel.isNewlyCreated()) { + notifyNewlyCreatedModel(newModel, senderNode); + } + } + } break; + + // TODO: wire in support here for server to get PacketTypeModelErase messages + // instead of using PacketTypeModelAddOrEdit messages to delete models + case PacketTypeModelErase: + processedBytes = 0; + break; + default: + processedBytes = 0; + break; + } + + return processedBytes; +} + +void ModelTree::notifyNewlyCreatedModel(const ModelItem& newModel, const SharedNodePointer& senderNode) { + _newlyCreatedHooksLock.lockForRead(); + for (size_t i = 0; i < _newlyCreatedHooks.size(); i++) { + _newlyCreatedHooks[i]->modelCreated(newModel, senderNode); + } + _newlyCreatedHooksLock.unlock(); +} + +void ModelTree::addNewlyCreatedHook(NewlyCreatedModelHook* hook) { + _newlyCreatedHooksLock.lockForWrite(); + _newlyCreatedHooks.push_back(hook); + _newlyCreatedHooksLock.unlock(); +} + +void ModelTree::removeNewlyCreatedHook(NewlyCreatedModelHook* hook) { + _newlyCreatedHooksLock.lockForWrite(); + for (size_t i = 0; i < _newlyCreatedHooks.size(); i++) { + if (_newlyCreatedHooks[i] == hook) { + _newlyCreatedHooks.erase(_newlyCreatedHooks.begin() + i); + break; + } + } + _newlyCreatedHooksLock.unlock(); +} + + +bool ModelTree::updateOperation(OctreeElement* element, void* extraData) { + ModelTreeUpdateArgs* args = static_cast(extraData); + ModelTreeElement* modelTreeElement = static_cast(element); + modelTreeElement->update(*args); + return true; +} + +bool ModelTree::pruneOperation(OctreeElement* element, void* extraData) { + ModelTreeElement* modelTreeElement = static_cast(element); + for (int i = 0; i < NUMBER_OF_CHILDREN; i++) { + ModelTreeElement* childAt = modelTreeElement->getChildAtIndex(i); + if (childAt && childAt->isLeaf() && !childAt->hasModels()) { + modelTreeElement->deleteChildAtIndex(i); + } + } + return true; +} + +void ModelTree::update() { + lockForWrite(); + _isDirty = true; + + ModelTreeUpdateArgs args = { }; + recurseTreeWithOperation(updateOperation, &args); + + // now add back any of the models that moved elements.... + int movingModels = args._movingModels.size(); + for (int i = 0; i < movingModels; i++) { + bool shouldDie = args._movingModels[i].getShouldDie(); + + // if the model is still inside our total bounds, then re-add it + AABox treeBounds = getRoot()->getAABox(); + + if (!shouldDie && treeBounds.contains(args._movingModels[i].getPosition())) { + storeModel(args._movingModels[i]); + } else { + uint32_t modelID = args._movingModels[i].getID(); + quint64 deletedAt = usecTimestampNow(); + _recentlyDeletedModelsLock.lockForWrite(); + _recentlyDeletedModelItemIDs.insert(deletedAt, modelID); + _recentlyDeletedModelsLock.unlock(); + } + } + + // prune the tree... + recurseTreeWithOperation(pruneOperation, NULL); + unlock(); +} + + +bool ModelTree::hasModelsDeletedSince(quint64 sinceTime) { + // we can probably leverage the ordered nature of QMultiMap to do this quickly... + bool hasSomethingNewer = false; + + _recentlyDeletedModelsLock.lockForRead(); + QMultiMap::const_iterator iterator = _recentlyDeletedModelItemIDs.constBegin(); + while (iterator != _recentlyDeletedModelItemIDs.constEnd()) { + //qDebug() << "considering... time/key:" << iterator.key(); + if (iterator.key() > sinceTime) { + //qDebug() << "YES newer... time/key:" << iterator.key(); + hasSomethingNewer = true; + } + ++iterator; + } + _recentlyDeletedModelsLock.unlock(); + return hasSomethingNewer; +} + +// sinceTime is an in/out parameter - it will be side effected with the last time sent out +bool ModelTree::encodeModelsDeletedSince(quint64& sinceTime, unsigned char* outputBuffer, size_t maxLength, + size_t& outputLength) { + + bool hasMoreToSend = true; + + unsigned char* copyAt = outputBuffer; + size_t numBytesPacketHeader = populatePacketHeader(reinterpret_cast(outputBuffer), PacketTypeModelErase); + copyAt += numBytesPacketHeader; + outputLength = numBytesPacketHeader; + + uint16_t numberOfIds = 0; // placeholder for now + unsigned char* numberOfIDsAt = copyAt; + memcpy(copyAt, &numberOfIds, sizeof(numberOfIds)); + copyAt += sizeof(numberOfIds); + outputLength += sizeof(numberOfIds); + + // we keep a multi map of model IDs to timestamps, we only want to include the model IDs that have been + // deleted since we last sent to this node + _recentlyDeletedModelsLock.lockForRead(); + QMultiMap::const_iterator iterator = _recentlyDeletedModelItemIDs.constBegin(); + while (iterator != _recentlyDeletedModelItemIDs.constEnd()) { + QList values = _recentlyDeletedModelItemIDs.values(iterator.key()); + for (int valueItem = 0; valueItem < values.size(); ++valueItem) { + + // if the timestamp is more recent then out last sent time, include it + if (iterator.key() > sinceTime) { + uint32_t modelID = values.at(valueItem); + memcpy(copyAt, &modelID, sizeof(modelID)); + copyAt += sizeof(modelID); + outputLength += sizeof(modelID); + numberOfIds++; + + // check to make sure we have room for one more id... + if (outputLength + sizeof(uint32_t) > maxLength) { + break; + } + } + } + + // check to make sure we have room for one more id... + if (outputLength + sizeof(uint32_t) > maxLength) { + + // let our caller know how far we got + sinceTime = iterator.key(); + break; + } + ++iterator; + } + + // if we got to the end, then we're done sending + if (iterator == _recentlyDeletedModelItemIDs.constEnd()) { + hasMoreToSend = false; + } + _recentlyDeletedModelsLock.unlock(); + + // replace the correct count for ids included + memcpy(numberOfIDsAt, &numberOfIds, sizeof(numberOfIds)); + + return hasMoreToSend; +} + +// called by the server when it knows all nodes have been sent deleted packets + +void ModelTree::forgetModelsDeletedBefore(quint64 sinceTime) { + //qDebug() << "forgetModelsDeletedBefore()"; + QSet keysToRemove; + + _recentlyDeletedModelsLock.lockForWrite(); + QMultiMap::iterator iterator = _recentlyDeletedModelItemIDs.begin(); + + // First find all the keys in the map that are older and need to be deleted + while (iterator != _recentlyDeletedModelItemIDs.end()) { + if (iterator.key() <= sinceTime) { + keysToRemove << iterator.key(); + } + ++iterator; + } + + // Now run through the keysToRemove and remove them + foreach (quint64 value, keysToRemove) { + //qDebug() << "removing the key, _recentlyDeletedModelItemIDs.remove(value); time/key:" << value; + _recentlyDeletedModelItemIDs.remove(value); + } + + _recentlyDeletedModelsLock.unlock(); +} + + +void ModelTree::processEraseMessage(const QByteArray& dataByteArray, const SharedNodePointer& sourceNode) { + + const unsigned char* packetData = (const unsigned char*)dataByteArray.constData(); + const unsigned char* dataAt = packetData; + size_t packetLength = dataByteArray.size(); + + size_t numBytesPacketHeader = numBytesForPacketHeader(dataByteArray); + size_t processedBytes = numBytesPacketHeader; + dataAt += numBytesPacketHeader; + + uint16_t numberOfIds = 0; // placeholder for now + memcpy(&numberOfIds, dataAt, sizeof(numberOfIds)); + dataAt += sizeof(numberOfIds); + processedBytes += sizeof(numberOfIds); + + if (numberOfIds > 0) { + FindAndDeleteModelsArgs args; + + for (size_t i = 0; i < numberOfIds; i++) { + if (processedBytes + sizeof(uint32_t) > packetLength) { + break; // bail to prevent buffer overflow + } + + uint32_t modelID = 0; // placeholder for now + memcpy(&modelID, dataAt, sizeof(modelID)); + dataAt += sizeof(modelID); + processedBytes += sizeof(modelID); + + args._idsToDelete.push_back(modelID); + } + + // calling recurse to actually delete the models + recurseTreeWithOperation(findAndDeleteOperation, &args); + } +} diff --git a/libraries/models/src/ModelTree.h b/libraries/models/src/ModelTree.h new file mode 100644 index 0000000000..02086ecd89 --- /dev/null +++ b/libraries/models/src/ModelTree.h @@ -0,0 +1,99 @@ +// +// ModelTree.h +// libraries/models/src +// +// Created by Brad Hefta-Gaub on 12/4/13. +// Copyright 2013 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_ModelTree_h +#define hifi_ModelTree_h + +#include +#include "ModelTreeElement.h" + +class NewlyCreatedModelHook { +public: + virtual void modelCreated(const ModelItem& newModel, const SharedNodePointer& senderNode) = 0; +}; + +class ModelTree : public Octree { + Q_OBJECT +public: + ModelTree(bool shouldReaverage = false); + + /// Implements our type specific root element factory + virtual ModelTreeElement* createNewElement(unsigned char * octalCode = NULL); + + /// Type safe version of getRoot() + ModelTreeElement* getRoot() { return (ModelTreeElement*)_rootNode; } + + + // 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 { return true; } + virtual PacketType expectedDataPacketType() const { return PacketTypeModelData; } + virtual bool handlesEditPacketType(PacketType packetType) const; + virtual int processEditPacketData(PacketType packetType, const unsigned char* packetData, int packetLength, + const unsigned char* editData, int maxLength, const SharedNodePointer& senderNode); + + virtual void update(); + + void storeModel(const ModelItem& model, const SharedNodePointer& senderNode = SharedNodePointer()); + void updateModel(const ModelItemID& modelID, const ModelItemProperties& properties); + void addModel(const ModelItemID& modelID, const ModelItemProperties& properties); + void deleteModel(const ModelItemID& modelID); + const ModelItem* findClosestModel(glm::vec3 position, float targetRadius); + const ModelItem* findModelByID(uint32_t id, bool alreadyLocked = false); + + /// finds all models that touch a sphere + /// \param center the center of the sphere + /// \param radius the radius of the sphere + /// \param foundModels[out] vector of const ModelItem* + /// \remark Side effect: any initial contents in foundModels will be lost + void findModels(const glm::vec3& center, float radius, QVector& foundModels); + + /// finds all models that touch a box + /// \param box the query box + /// \param foundModels[out] vector of non-const ModelItem* + /// \remark Side effect: any initial contents in models will be lost + void findModelsForUpdate(const AABox& box, QVector foundModels); + + void addNewlyCreatedHook(NewlyCreatedModelHook* hook); + void removeNewlyCreatedHook(NewlyCreatedModelHook* hook); + + bool hasAnyDeletedModels() const { return _recentlyDeletedModelItemIDs.size() > 0; } + bool hasModelsDeletedSince(quint64 sinceTime); + bool encodeModelsDeletedSince(quint64& sinceTime, unsigned char* packetData, size_t maxLength, size_t& outputLength); + void forgetModelsDeletedBefore(quint64 sinceTime); + + void processEraseMessage(const QByteArray& dataByteArray, const SharedNodePointer& sourceNode); + void handleAddModelResponse(const QByteArray& packet); + +private: + + static bool updateOperation(OctreeElement* element, void* extraData); + static bool findAndUpdateOperation(OctreeElement* element, void* extraData); + static bool findAndUpdateWithIDandPropertiesOperation(OctreeElement* element, void* extraData); + static bool findNearPointOperation(OctreeElement* element, void* extraData); + static bool findInSphereOperation(OctreeElement* element, void* extraData); + static bool pruneOperation(OctreeElement* element, void* extraData); + static bool findByIDOperation(OctreeElement* element, void* extraData); + static bool findAndDeleteOperation(OctreeElement* element, void* extraData); + static bool findAndUpdateModelItemIDOperation(OctreeElement* element, void* extraData); + static bool findInBoxForUpdateOperation(OctreeElement* element, void* extraData); + + void notifyNewlyCreatedModel(const ModelItem& newModel, const SharedNodePointer& senderNode); + + QReadWriteLock _newlyCreatedHooksLock; + std::vector _newlyCreatedHooks; + + + QReadWriteLock _recentlyDeletedModelsLock; + QMultiMap _recentlyDeletedModelItemIDs; +}; + +#endif // hifi_ModelTree_h diff --git a/libraries/models/src/ModelTreeElement.cpp b/libraries/models/src/ModelTreeElement.cpp new file mode 100644 index 0000000000..687199827c --- /dev/null +++ b/libraries/models/src/ModelTreeElement.cpp @@ -0,0 +1,329 @@ +// +// ModelTreeElement.cpp +// libraries/models/src +// +// Created by Brad Hefta-Gaub on 12/4/13. +// Copyright 2013 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 + +#include "ModelTree.h" +#include "ModelTreeElement.h" + +ModelTreeElement::ModelTreeElement(unsigned char* octalCode) : OctreeElement(), _modelItems(NULL) { + init(octalCode); +}; + +ModelTreeElement::~ModelTreeElement() { + _voxelMemoryUsage -= sizeof(ModelTreeElement); + delete _modelItems; + _modelItems = NULL; +} + +// This will be called primarily on addChildAt(), which means we're adding a child of our +// own type to our own tree. This means we should initialize that child with any tree and type +// specific settings that our children must have. One example is out VoxelSystem, which +// we know must match ours. +OctreeElement* ModelTreeElement::createNewElement(unsigned char* octalCode) { + ModelTreeElement* newChild = new ModelTreeElement(octalCode); + newChild->setTree(_myTree); + return newChild; +} + +void ModelTreeElement::init(unsigned char* octalCode) { + OctreeElement::init(octalCode); + _modelItems = new QList; + _voxelMemoryUsage += sizeof(ModelTreeElement); +} + +ModelTreeElement* ModelTreeElement::addChildAtIndex(int index) { + ModelTreeElement* newElement = (ModelTreeElement*)OctreeElement::addChildAtIndex(index); + newElement->setTree(_myTree); + return newElement; +} + + +bool ModelTreeElement::appendElementData(OctreePacketData* packetData) const { + bool success = true; // assume the best... + + // write our models out... + uint16_t numberOfModels = _modelItems->size(); + success = packetData->appendValue(numberOfModels); + + if (success) { + for (uint16_t i = 0; i < numberOfModels; i++) { + const ModelItem& model = (*_modelItems)[i]; + success = model.appendModelData(packetData); + if (!success) { + break; + } + } + } + return success; +} + +void ModelTreeElement::update(ModelTreeUpdateArgs& args) { + markWithChangedTime(); + // TODO: early exit when _modelItems is empty + + // update our contained models + QList::iterator modelItr = _modelItems->begin(); + while(modelItr != _modelItems->end()) { + ModelItem& model = (*modelItr); + model.update(_lastChanged); + + // If the model wants to die, or if it's left our bounding box, then move it + // into the arguments moving models. These will be added back or deleted completely + if (model.getShouldDie() || !_box.contains(model.getPosition())) { + args._movingModels.push_back(model); + + // erase this model + modelItr = _modelItems->erase(modelItr); + } else { + ++modelItr; + } + } + // TODO: if _modelItems is empty after while loop consider freeing memory in _modelItems if + // internal array is too big (QList internal array does not decrease size except in dtor and + // assignment operator). Otherwise _modelItems could become a "resource leak" for large + // roaming piles of models. +} + +bool ModelTreeElement::findSpherePenetration(const glm::vec3& center, float radius, + glm::vec3& penetration, void** penetratedObject) const { + QList::iterator modelItr = _modelItems->begin(); + QList::const_iterator modelEnd = _modelItems->end(); + while(modelItr != modelEnd) { + ModelItem& model = (*modelItr); + glm::vec3 modelCenter = model.getPosition(); + float modelRadius = model.getRadius(); + + // don't penetrate yourself + if (modelCenter == center && modelRadius == radius) { + return false; + } + + if (findSphereSpherePenetration(center, radius, modelCenter, modelRadius, penetration)) { + // return true on first valid model penetration + *penetratedObject = (void*)(&model); + return true; + } + ++modelItr; + } + return false; +} + +bool ModelTreeElement::updateModel(const ModelItem& model) { + // NOTE: this method must first lookup the model by ID, hence it is O(N) + // and "model is not found" is worst-case (full N) but maybe we don't care? + // (guaranteed that num models per elemen is small?) + const bool wantDebug = false; + uint16_t numberOfModels = _modelItems->size(); + for (uint16_t i = 0; i < numberOfModels; i++) { + ModelItem& thisModel = (*_modelItems)[i]; + if (thisModel.getID() == model.getID()) { + int difference = thisModel.getLastUpdated() - model.getLastUpdated(); + bool changedOnServer = thisModel.getLastEdited() < model.getLastEdited(); + bool localOlder = thisModel.getLastUpdated() < model.getLastUpdated(); + if (changedOnServer || localOlder) { + if (wantDebug) { + qDebug("local model [id:%d] %s and %s than server model by %d, model.isNewlyCreated()=%s", + model.getID(), (changedOnServer ? "CHANGED" : "same"), + (localOlder ? "OLDER" : "NEWER"), + difference, debug::valueOf(model.isNewlyCreated()) ); + } + thisModel.copyChangedProperties(model); + } else { + if (wantDebug) { + qDebug(">>> IGNORING SERVER!!! Would've caused jutter! <<< " + "local model [id:%d] %s and %s than server model by %d, model.isNewlyCreated()=%s", + model.getID(), (changedOnServer ? "CHANGED" : "same"), + (localOlder ? "OLDER" : "NEWER"), + difference, debug::valueOf(model.isNewlyCreated()) ); + } + } + return true; + } + } + return false; +} + +bool ModelTreeElement::updateModel(const ModelItemID& modelID, const ModelItemProperties& properties) { + uint16_t numberOfModels = _modelItems->size(); + for (uint16_t i = 0; i < numberOfModels; i++) { + // note: unlike storeModel() which is called from inbound packets, this is only called by local editors + // and therefore we can be confident that this change is higher priority and should be honored + ModelItem& thisModel = (*_modelItems)[i]; + + bool found = false; + if (modelID.isKnownID) { + found = thisModel.getID() == modelID.id; + } else { + found = thisModel.getCreatorTokenID() == modelID.creatorTokenID; + } + if (found) { + thisModel.setProperties(properties); + + const bool wantDebug = false; + if (wantDebug) { + uint64_t now = usecTimestampNow(); + int elapsed = now - thisModel.getLastEdited(); + + qDebug() << "ModelTreeElement::updateModel() AFTER update... edited AGO=" << elapsed << + "now=" << now << " thisModel.getLastEdited()=" << thisModel.getLastEdited(); + } + return true; + } + } + return false; +} + +void ModelTreeElement::updateModelItemID(FindAndUpdateModelItemIDArgs* args) { + uint16_t numberOfModels = _modelItems->size(); + for (uint16_t i = 0; i < numberOfModels; i++) { + ModelItem& thisModel = (*_modelItems)[i]; + + if (!args->creatorTokenFound) { + // first, we're looking for matching creatorTokenIDs, if we find that, then we fix it to know the actual ID + if (thisModel.getCreatorTokenID() == args->creatorTokenID) { + thisModel.setID(args->modelID); + args->creatorTokenFound = true; + } + } + + // if we're in an isViewing tree, we also need to look for an kill any viewed models + if (!args->viewedModelFound && args->isViewing) { + if (thisModel.getCreatorTokenID() == UNKNOWN_MODEL_TOKEN && thisModel.getID() == args->modelID) { + _modelItems->removeAt(i); // remove the model at this index + numberOfModels--; // this means we have 1 fewer model in this list + i--; // and we actually want to back up i as well. + args->viewedModelFound = true; + } + } + } +} + + + +const ModelItem* ModelTreeElement::getClosestModel(glm::vec3 position) const { + const ModelItem* closestModel = NULL; + float closestModelDistance = FLT_MAX; + uint16_t numberOfModels = _modelItems->size(); + for (uint16_t i = 0; i < numberOfModels; i++) { + float distanceToModel = glm::distance(position, (*_modelItems)[i].getPosition()); + if (distanceToModel < closestModelDistance) { + closestModel = &(*_modelItems)[i]; + } + } + return closestModel; +} + +void ModelTreeElement::getModels(const glm::vec3& searchPosition, float searchRadius, QVector& foundModels) const { + uint16_t numberOfModels = _modelItems->size(); + for (uint16_t i = 0; i < numberOfModels; i++) { + const ModelItem* model = &(*_modelItems)[i]; + float distance = glm::length(model->getPosition() - searchPosition); + if (distance < searchRadius + model->getRadius()) { + foundModels.push_back(model); + } + } +} + +void ModelTreeElement::getModelsForUpdate(const AABox& box, QVector& foundModels) { + QList::iterator modelItr = _modelItems->begin(); + QList::iterator modelEnd = _modelItems->end(); + AABox modelBox; + while(modelItr != modelEnd) { + ModelItem* model = &(*modelItr); + float radius = model->getRadius(); + // NOTE: we actually do box-box collision queries here, which is sloppy but good enough for now + // TODO: decide whether to replace modelBox-box query with sphere-box (requires a square root + // but will be slightly more accurate). + modelBox.setBox(model->getPosition() - glm::vec3(radius), 2.f * radius); + if (modelBox.touches(_box)) { + foundModels.push_back(model); + } + ++modelItr; + } +} + +const ModelItem* ModelTreeElement::getModelWithID(uint32_t id) const { + // NOTE: this lookup is O(N) but maybe we don't care? (guaranteed that num models per elemen is small?) + const ModelItem* foundModel = NULL; + uint16_t numberOfModels = _modelItems->size(); + for (uint16_t i = 0; i < numberOfModels; i++) { + if ((*_modelItems)[i].getID() == id) { + foundModel = &(*_modelItems)[i]; + break; + } + } + return foundModel; +} + +bool ModelTreeElement::removeModelWithID(uint32_t id) { + bool foundModel = false; + uint16_t numberOfModels = _modelItems->size(); + for (uint16_t i = 0; i < numberOfModels; i++) { + if ((*_modelItems)[i].getID() == id) { + foundModel = true; + _modelItems->removeAt(i); + break; + } + } + return foundModel; +} + +int ModelTreeElement::readElementDataFromBuffer(const unsigned char* data, int bytesLeftToRead, + ReadBitstreamToTreeParams& args) { + + const unsigned char* dataAt = data; + int bytesRead = 0; + uint16_t numberOfModels = 0; + int expectedBytesPerModel = ModelItem::expectedBytes(); + + if (bytesLeftToRead >= (int)sizeof(numberOfModels)) { + // read our models in.... + numberOfModels = *(uint16_t*)dataAt; + dataAt += sizeof(numberOfModels); + bytesLeftToRead -= (int)sizeof(numberOfModels); + bytesRead += sizeof(numberOfModels); + + if (bytesLeftToRead >= (int)(numberOfModels * expectedBytesPerModel)) { + for (uint16_t i = 0; i < numberOfModels; i++) { + ModelItem tempModel; + int bytesForThisModel = tempModel.readModelDataFromBuffer(dataAt, bytesLeftToRead, args); + _myTree->storeModel(tempModel); + dataAt += bytesForThisModel; + bytesLeftToRead -= bytesForThisModel; + bytesRead += bytesForThisModel; + } + } + } + + return bytesRead; +} + +// will average a "common reduced LOD view" from the the child elements... +void ModelTreeElement::calculateAverageFromChildren() { + // nothing to do here yet... +} + +// will detect if children are leaves AND collapsable into the parent node +// and in that case will collapse children and make this node +// a leaf, returns TRUE if all the leaves are collapsed into a +// single node +bool ModelTreeElement::collapseChildren() { + // nothing to do here yet... + return false; +} + + +void ModelTreeElement::storeModel(const ModelItem& model) { + _modelItems->push_back(model); + markWithChangedTime(); +} + diff --git a/libraries/models/src/ModelTreeElement.h b/libraries/models/src/ModelTreeElement.h new file mode 100644 index 0000000000..ce03d50065 --- /dev/null +++ b/libraries/models/src/ModelTreeElement.h @@ -0,0 +1,130 @@ +// +// ModelTreeElement.h +// libraries/models/src +// +// Created by Brad Hefta-Gaub on 12/4/13. +// Copyright 2013 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_ModelTreeElement_h +#define hifi_ModelTreeElement_h + +#include +#include + +#include "ModelItem.h" +#include "ModelTree.h" + +class ModelTree; +class ModelTreeElement; + +class ModelTreeUpdateArgs { +public: + QList _movingModels; +}; + +class FindAndUpdateModelItemIDArgs { +public: + uint32_t modelID; + uint32_t creatorTokenID; + bool creatorTokenFound; + bool viewedModelFound; + bool isViewing; +}; + + + +class ModelTreeElement : public OctreeElement { + friend class ModelTree; // to allow createElement to new us... + + ModelTreeElement(unsigned char* octalCode = NULL); + + virtual OctreeElement* createNewElement(unsigned char* octalCode = NULL); + +public: + virtual ~ModelTreeElement(); + + // type safe versions of OctreeElement methods + ModelTreeElement* getChildAtIndex(int index) { return (ModelTreeElement*)OctreeElement::getChildAtIndex(index); } + + // methods you can and should override to implement your tree functionality + + /// Adds a child to the current element. Override this if there is additional child initialization your class needs. + virtual ModelTreeElement* addChildAtIndex(int index); + + /// Override this to implement LOD averaging on changes to the tree. + virtual void calculateAverageFromChildren(); + + /// Override this to implement LOD collapsing and identical child pruning on changes to the tree. + virtual bool collapseChildren(); + + /// Should this element be considered to have content in it. This will be used in collision and ray casting methods. + /// By default we assume that only leaves are actual content, but some octrees may have different semantics. + virtual bool hasContent() const { return isLeaf(); } + + /// Override this to break up large octree elements when an edit operation is performed on a smaller octree element. + /// For example, if the octrees represent solid cubes and a delete of a smaller octree element is done then the + /// meaningful split would be to break the larger cube into smaller cubes of the same color/texture. + virtual void splitChildren() { } + + /// Override to indicate that this element requires a split before editing lower elements in the octree + virtual bool requiresSplit() const { return false; } + + /// Override to serialize the state of this element. This is used for persistance and for transmission across the network. + virtual bool appendElementData(OctreePacketData* packetData) const; + + /// Override to deserialize the state of this element. This is used for loading from a persisted file or from reading + /// from the network. + virtual int readElementDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args); + + /// Override to indicate that the item is currently rendered in the rendering engine. By default we assume that if + /// the element should be rendered, then your rendering engine is rendering. But some rendering engines my have cases + /// where an element is not actually rendering all should render elements. If the isRendered() state doesn't match the + /// shouldRender() state, the tree will remark elements as changed even in cases there the elements have not changed. + virtual bool isRendered() const { return getShouldRender(); } + virtual bool deleteApproved() const { return !hasModels(); } + + virtual bool findSpherePenetration(const glm::vec3& center, float radius, + glm::vec3& penetration, void** penetratedObject) const; + + const QList& getModels() const { return *_modelItems; } + QList& getModels() { return *_modelItems; } + bool hasModels() const { return _modelItems->size() > 0; } + + void update(ModelTreeUpdateArgs& args); + void setTree(ModelTree* tree) { _myTree = tree; } + + bool updateModel(const ModelItem& model); + bool updateModel(const ModelItemID& modelID, const ModelItemProperties& properties); + void updateModelItemID(FindAndUpdateModelItemIDArgs* args); + + const ModelItem* getClosestModel(glm::vec3 position) const; + + /// finds all models that touch a sphere + /// \param position the center of the query sphere + /// \param radius the radius of the query sphere + /// \param models[out] vector of const ModelItem* + void getModels(const glm::vec3& position, float radius, QVector& foundModels) const; + + /// finds all models that touch a box + /// \param box the query box + /// \param models[out] vector of non-const ModelItem* + void getModelsForUpdate(const AABox& box, QVector& foundModels); + + const ModelItem* getModelWithID(uint32_t id) const; + + bool removeModelWithID(uint32_t id); + +protected: + virtual void init(unsigned char * octalCode); + + void storeModel(const ModelItem& model); + + ModelTree* _myTree; + QList* _modelItems; +}; + +#endif // hifi_ModelTreeElement_h diff --git a/libraries/models/src/ModelTreeHeadlessViewer.cpp b/libraries/models/src/ModelTreeHeadlessViewer.cpp new file mode 100644 index 0000000000..5a0907b201 --- /dev/null +++ b/libraries/models/src/ModelTreeHeadlessViewer.cpp @@ -0,0 +1,38 @@ +// +// ModelTreeHeadlessViewer.cpp +// libraries/models/src +// +// Created by Brad Hefta-Gaub on 2/26/14. +// Copyright 2014 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 "ModelTreeHeadlessViewer.h" + +ModelTreeHeadlessViewer::ModelTreeHeadlessViewer() : + OctreeHeadlessViewer() { +} + +ModelTreeHeadlessViewer::~ModelTreeHeadlessViewer() { +} + +void ModelTreeHeadlessViewer::init() { + OctreeHeadlessViewer::init(); +} + + +void ModelTreeHeadlessViewer::update() { + if (_tree) { + ModelTree* tree = static_cast(_tree); + if (tree->tryLockForWrite()) { + tree->update(); + tree->unlock(); + } + } +} + +void ModelTreeHeadlessViewer::processEraseMessage(const QByteArray& dataByteArray, const SharedNodePointer& sourceNode) { + static_cast(_tree)->processEraseMessage(dataByteArray, sourceNode); +} diff --git a/libraries/models/src/ModelTreeHeadlessViewer.h b/libraries/models/src/ModelTreeHeadlessViewer.h new file mode 100644 index 0000000000..0b5cde473d --- /dev/null +++ b/libraries/models/src/ModelTreeHeadlessViewer.h @@ -0,0 +1,45 @@ +// +// ModelTreeHeadlessViewer.h +// libraries/models/src +// +// Created by Brad Hefta-Gaub on 2/26/14. +// Copyright 2014 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_ModelTreeHeadlessViewer_h +#define hifi_ModelTreeHeadlessViewer_h + +#include +#include +#include +#include +#include +#include + +#include "ModelTree.h" + +// Generic client side Octree renderer class. +class ModelTreeHeadlessViewer : public OctreeHeadlessViewer { + Q_OBJECT +public: + ModelTreeHeadlessViewer(); + virtual ~ModelTreeHeadlessViewer(); + + virtual Octree* createTree() { return new ModelTree(true); } + virtual NodeType_t getMyNodeType() const { return NodeType::ModelServer; } + virtual PacketType getMyQueryMessageType() const { return PacketTypeModelQuery; } + virtual PacketType getExpectedPacketType() const { return PacketTypeModelData; } + + void update(); + + ModelTree* getTree() { return (ModelTree*)_tree; } + + void processEraseMessage(const QByteArray& dataByteArray, const SharedNodePointer& sourceNode); + + virtual void init(); +}; + +#endif // hifi_ModelTreeHeadlessViewer_h diff --git a/libraries/models/src/ModelsScriptingInterface.cpp b/libraries/models/src/ModelsScriptingInterface.cpp new file mode 100644 index 0000000000..446b0280a4 --- /dev/null +++ b/libraries/models/src/ModelsScriptingInterface.cpp @@ -0,0 +1,176 @@ +// +// ModelsScriptingInterface.cpp +// libraries/models/src +// +// Created by Brad Hefta-Gaub on 12/6/13. +// Copyright 2013 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 "ModelsScriptingInterface.h" +#include "ModelTree.h" + +ModelsScriptingInterface::ModelsScriptingInterface() : + _nextCreatorTokenID(0), + _modelTree(NULL) +{ +} + + +void ModelsScriptingInterface::queueModelMessage(PacketType packetType, + ModelItemID modelID, const ModelItemProperties& properties) { + getModelPacketSender()->queueModelEditMessage(packetType, modelID, properties); +} + +ModelItemID ModelsScriptingInterface::addModel(const ModelItemProperties& properties) { + + // The application will keep track of creatorTokenID + uint32_t creatorTokenID = ModelItem::getNextCreatorTokenID(); + + ModelItemID id(NEW_MODEL, creatorTokenID, false ); + + // queue the packet + queueModelMessage(PacketTypeModelAddOrEdit, id, properties); + + // If we have a local model tree set, then also update it. + if (_modelTree) { + _modelTree->lockForWrite(); + _modelTree->addModel(id, properties); + _modelTree->unlock(); + } + + return id; +} + +ModelItemID ModelsScriptingInterface::identifyModel(ModelItemID modelID) { + uint32_t actualID = modelID.id; + + if (!modelID.isKnownID) { + actualID = ModelItem::getIDfromCreatorTokenID(modelID.creatorTokenID); + if (actualID == UNKNOWN_MODEL_ID) { + return modelID; // bailing early + } + + // found it! + modelID.id = actualID; + modelID.isKnownID = true; + } + return modelID; +} + +ModelItemProperties ModelsScriptingInterface::getModelProperties(ModelItemID modelID) { + ModelItemProperties results; + ModelItemID identity = identifyModel(modelID); + if (!identity.isKnownID) { + results.setIsUnknownID(); + return results; + } + if (_modelTree) { + _modelTree->lockForRead(); + const ModelItem* model = _modelTree->findModelByID(identity.id, true); + if (model) { + results.copyFromModelItem(*model); + } else { + results.setIsUnknownID(); + } + _modelTree->unlock(); + } + + return results; +} + + + +ModelItemID ModelsScriptingInterface::editModel(ModelItemID modelID, const ModelItemProperties& properties) { + uint32_t actualID = modelID.id; + + // if the model is unknown, attempt to look it up + if (!modelID.isKnownID) { + actualID = ModelItem::getIDfromCreatorTokenID(modelID.creatorTokenID); + } + + // if at this point, we know the id, send the update to the model server + if (actualID != UNKNOWN_MODEL_ID) { + modelID.id = actualID; + modelID.isKnownID = true; + queueModelMessage(PacketTypeModelAddOrEdit, modelID, properties); + } + + // If we have a local model tree set, then also update it. We can do this even if we don't know + // the actual id, because we can edit out local models just with creatorTokenID + if (_modelTree) { + _modelTree->lockForWrite(); + _modelTree->updateModel(modelID, properties); + _modelTree->unlock(); + } + + return modelID; +} + + +// TODO: This deleteModel() method uses the PacketType_MODEL_ADD_OR_EDIT message to send +// a changed model with a shouldDie() property set to true. This works and is currently the only +// way to tell the model server to delete a model. But we should change this to use the PacketType_MODEL_ERASE +// message which takes a list of model id's to delete. +void ModelsScriptingInterface::deleteModel(ModelItemID modelID) { + + // setup properties to kill the model + ModelItemProperties properties; + properties.setShouldDie(true); + + uint32_t actualID = modelID.id; + + // if the model is unknown, attempt to look it up + if (!modelID.isKnownID) { + actualID = ModelItem::getIDfromCreatorTokenID(modelID.creatorTokenID); + } + + // if at this point, we know the id, send the update to the model server + if (actualID != UNKNOWN_MODEL_ID) { + modelID.id = actualID; + modelID.isKnownID = true; + queueModelMessage(PacketTypeModelAddOrEdit, modelID, properties); + } + + // If we have a local model tree set, then also update it. + if (_modelTree) { + _modelTree->lockForWrite(); + _modelTree->deleteModel(modelID); + _modelTree->unlock(); + } +} + +ModelItemID ModelsScriptingInterface::findClosestModel(const glm::vec3& center, float radius) const { + ModelItemID result(UNKNOWN_MODEL_ID, UNKNOWN_MODEL_TOKEN, false); + if (_modelTree) { + _modelTree->lockForRead(); + const ModelItem* closestModel = _modelTree->findClosestModel(center/(float)TREE_SCALE, + radius/(float)TREE_SCALE); + _modelTree->unlock(); + if (closestModel) { + result.id = closestModel->getID(); + result.isKnownID = true; + } + } + return result; +} + + +QVector ModelsScriptingInterface::findModels(const glm::vec3& center, float radius) const { + QVector result; + if (_modelTree) { + _modelTree->lockForRead(); + QVector models; + _modelTree->findModels(center/(float)TREE_SCALE, radius/(float)TREE_SCALE, models); + _modelTree->unlock(); + + foreach (const ModelItem* model, models) { + ModelItemID thisModelItemID(model->getID(), UNKNOWN_MODEL_TOKEN, true); + result << thisModelItemID; + } + } + return result; +} + diff --git a/libraries/models/src/ModelsScriptingInterface.h b/libraries/models/src/ModelsScriptingInterface.h new file mode 100644 index 0000000000..bf8e193f25 --- /dev/null +++ b/libraries/models/src/ModelsScriptingInterface.h @@ -0,0 +1,73 @@ +// +// ModelsScriptingInterface.h +// libraries/models/src +// +// Created by Brad Hefta-Gaub on 12/6/13. +// Copyright 2013 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_ModelsScriptingInterface_h +#define hifi_ModelsScriptingInterface_h + +#include + +#include + +#include +#include "ModelEditPacketSender.h" + +/// handles scripting of Model commands from JS passed to assigned clients +class ModelsScriptingInterface : public OctreeScriptingInterface { + Q_OBJECT +public: + ModelsScriptingInterface(); + + ModelEditPacketSender* getModelPacketSender() const { return (ModelEditPacketSender*)getPacketSender(); } + virtual NodeType_t getServerNodeType() const { return NodeType::ModelServer; } + virtual OctreeEditPacketSender* createPacketSender() { return new ModelEditPacketSender(); } + + void setModelTree(ModelTree* modelTree) { _modelTree = modelTree; } + ModelTree* getModelTree(ModelTree*) { return _modelTree; } + +public slots: + /// adds a model with the specific properties + ModelItemID addModel(const ModelItemProperties& properties); + + /// identify a recently created model to determine its true ID + ModelItemID identifyModel(ModelItemID modelID); + + /// gets the current model properties for a specific model + /// this function will not find return results in script engine contexts which don't have access to models + ModelItemProperties getModelProperties(ModelItemID modelID); + + /// edits a model updating only the included properties, will return the identified ModelItemID in case of + /// successful edit, if the input modelID is for an unknown model this function will have no effect + ModelItemID editModel(ModelItemID modelID, const ModelItemProperties& properties); + + /// deletes a model + void deleteModel(ModelItemID modelID); + + /// finds the closest model to the center point, within the radius + /// will return a ModelItemID.isKnownID = false if no models are in the radius + /// this function will not find any models in script engine contexts which don't have access to models + ModelItemID findClosestModel(const glm::vec3& center, float radius) const; + + /// finds models within the search sphere specified by the center point and radius + /// this function will not find any models in script engine contexts which don't have access to models + QVector findModels(const glm::vec3& center, float radius) const; + +signals: + void modelCollisionWithVoxel(const ModelItemID& modelID, const VoxelDetail& voxel, const CollisionInfo& collision); + void modelCollisionWithModel(const ModelItemID& idA, const ModelItemID& idB, const CollisionInfo& collision); + +private: + void queueModelMessage(PacketType packetType, ModelItemID modelID, const ModelItemProperties& properties); + + uint32_t _nextCreatorTokenID; + ModelTree* _modelTree; +}; + +#endif // hifi_ModelsScriptingInterface_h diff --git a/libraries/networking/src/Assignment.cpp b/libraries/networking/src/Assignment.cpp index dd318aad8e..620c826846 100644 --- a/libraries/networking/src/Assignment.cpp +++ b/libraries/networking/src/Assignment.cpp @@ -29,6 +29,8 @@ Assignment::Type Assignment::typeForNodeType(NodeType_t nodeType) { return Assignment::VoxelServerType; case NodeType::ParticleServer: return Assignment::ParticleServerType; + case NodeType::ModelServer: + return Assignment::ModelServerType; case NodeType::MetavoxelServer: return Assignment::MetavoxelServerType; default: diff --git a/libraries/networking/src/Assignment.h b/libraries/networking/src/Assignment.h index f0f7e8db1a..54aebf9257 100644 --- a/libraries/networking/src/Assignment.h +++ b/libraries/networking/src/Assignment.h @@ -32,6 +32,7 @@ public: VoxelServerType, ParticleServerType, MetavoxelServerType, + ModelServerType, AllTypes }; diff --git a/libraries/networking/src/Node.cpp b/libraries/networking/src/Node.cpp index 15ee443e1f..05f425374b 100644 --- a/libraries/networking/src/Node.cpp +++ b/libraries/networking/src/Node.cpp @@ -29,6 +29,7 @@ void NodeType::init() { TypeNameHash.insert(NodeType::DomainServer, "Domain Server"); TypeNameHash.insert(NodeType::VoxelServer, "Voxel Server"); TypeNameHash.insert(NodeType::ParticleServer, "Particle Server"); + TypeNameHash.insert(NodeType::ModelServer, "Model Server"); TypeNameHash.insert(NodeType::MetavoxelServer, "Metavoxel Server"); TypeNameHash.insert(NodeType::Agent, "Agent"); TypeNameHash.insert(NodeType::AudioMixer, "Audio Mixer"); diff --git a/libraries/networking/src/Node.h b/libraries/networking/src/Node.h index 0f63d01525..f52cda0d0d 100644 --- a/libraries/networking/src/Node.h +++ b/libraries/networking/src/Node.h @@ -30,6 +30,7 @@ namespace NodeType { const NodeType_t DomainServer = 'D'; const NodeType_t VoxelServer = 'V'; const NodeType_t ParticleServer = 'P'; + const NodeType_t ModelServer = 'o'; const NodeType_t MetavoxelServer = 'm'; const NodeType_t EnvironmentServer = 'E'; const NodeType_t Agent = 'I'; diff --git a/libraries/networking/src/PacketHeaders.h b/libraries/networking/src/PacketHeaders.h index b7535e5064..c6b4e3b9e5 100644 --- a/libraries/networking/src/PacketHeaders.h +++ b/libraries/networking/src/PacketHeaders.h @@ -61,6 +61,11 @@ enum PacketType { PacketTypeDomainConnectRequest, PacketTypeDomainServerRequireDTLS, PacketTypeNodeJsonStats, + PacketTypeModelQuery, + PacketTypeModelData, + PacketTypeModelAddOrEdit, + PacketTypeModelErase, + PacketTypeModelAddResponse, }; typedef char PacketVersion; @@ -69,7 +74,7 @@ const QSet NON_VERIFIED_PACKETS = QSet() << PacketTypeDomainServerRequireDTLS << PacketTypeDomainConnectRequest << PacketTypeDomainList << PacketTypeDomainListRequest << PacketTypeCreateAssignment << PacketTypeRequestAssignment << PacketTypeStunResponse - << PacketTypeNodeJsonStats << PacketTypeVoxelQuery << PacketTypeParticleQuery; + << PacketTypeNodeJsonStats << PacketTypeVoxelQuery << PacketTypeParticleQuery << PacketTypeModelQuery; const int NUM_BYTES_MD5_HASH = 16; const int NUM_STATIC_HEADER_BYTES = sizeof(PacketVersion) + NUM_BYTES_RFC4122_UUID; diff --git a/libraries/particles/src/ParticleTree.cpp b/libraries/particles/src/ParticleTree.cpp index aeaf25e23c..09e034ccd1 100644 --- a/libraries/particles/src/ParticleTree.cpp +++ b/libraries/particles/src/ParticleTree.cpp @@ -320,7 +320,7 @@ public: QVector _foundParticles; }; -bool findInBoxForUpdateOperation(OctreeElement* element, void* extraData) { +bool ParticleTree::findInBoxForUpdateOperation(OctreeElement* element, void* extraData) { FindParticlesInBoxArgs* args = static_cast< FindParticlesInBoxArgs*>(extraData); const AABox& elementBox = element->getAABox(); if (elementBox.touches(args->_box)) { diff --git a/libraries/particles/src/ParticleTree.h b/libraries/particles/src/ParticleTree.h index a31c2d38aa..76b9926bdf 100644 --- a/libraries/particles/src/ParticleTree.h +++ b/libraries/particles/src/ParticleTree.h @@ -84,6 +84,7 @@ private: static bool findByIDOperation(OctreeElement* element, void* extraData); static bool findAndDeleteOperation(OctreeElement* element, void* extraData); static bool findAndUpdateParticleIDOperation(OctreeElement* element, void* extraData); + static bool findInBoxForUpdateOperation(OctreeElement* element, void* extraData); void notifyNewlyCreatedParticle(const Particle& newParticle, const SharedNodePointer& senderNode); diff --git a/libraries/script-engine/CMakeLists.txt b/libraries/script-engine/CMakeLists.txt index 48d13e7742..ee918ff864 100644 --- a/libraries/script-engine/CMakeLists.txt +++ b/libraries/script-engine/CMakeLists.txt @@ -26,6 +26,7 @@ link_hifi_library(octree ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(voxels ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(fbx ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(particles ${TARGET_NAME} "${ROOT_DIR}") +link_hifi_library(models ${TARGET_NAME} "${ROOT_DIR}") # link ZLIB find_package(ZLIB) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 82209b1f5b..68233985c5 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -21,14 +21,14 @@ #include #include #include +#include #include #include +#include +#include #include #include #include -#include - -#include #include "AnimationObject.h" #include "MenuItemProperties.h" @@ -37,6 +37,7 @@ VoxelsScriptingInterface ScriptEngine::_voxelsScriptingInterface; ParticlesScriptingInterface ScriptEngine::_particlesScriptingInterface; +ModelsScriptingInterface ScriptEngine::_modelsScriptingInterface; static QScriptValue soundConstructor(QScriptContext* context, QScriptEngine* engine) { QUrl soundURL = QUrl(context->argument(0).toString()); @@ -204,6 +205,11 @@ void ScriptEngine::init() { qScriptRegisterMetaType(&_engine, ParticlePropertiesToScriptValue, ParticlePropertiesFromScriptValue); qScriptRegisterMetaType(&_engine, ParticleIDtoScriptValue, ParticleIDfromScriptValue); qScriptRegisterSequenceMetaType >(&_engine); + + qScriptRegisterMetaType(&_engine, ModelItemPropertiesToScriptValue, ModelItemPropertiesFromScriptValue); + qScriptRegisterMetaType(&_engine, ModelItemIDtoScriptValue, ModelItemIDfromScriptValue); + qScriptRegisterSequenceMetaType >(&_engine); + qScriptRegisterSequenceMetaType >(&_engine); qScriptRegisterSequenceMetaType >(&_engine); qScriptRegisterSequenceMetaType >(&_engine); @@ -224,6 +230,7 @@ void ScriptEngine::init() { registerGlobalObject("Script", this); registerGlobalObject("Audio", &_audioScriptingInterface); registerGlobalObject("Controller", _controllerScriptingInterface); + registerGlobalObject("Models", &_modelsScriptingInterface); registerGlobalObject("Particles", &_particlesScriptingInterface); registerGlobalObject("Quat", &_quatLibrary); registerGlobalObject("Vec3", &_vec3Library); diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 61c3c559c5..09d41e3e2e 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -30,7 +30,9 @@ #include "ScriptUUID.h" #include "Vec3.h" +class ModelsScriptingInterface; class ParticlesScriptingInterface; +class VoxelsScriptingInterface; const QString NO_SCRIPT(""); @@ -52,6 +54,9 @@ public: /// Access the ParticlesScriptingInterface in order to initialize it with a custom packet sender and jurisdiction listener static ParticlesScriptingInterface* getParticlesScriptingInterface() { return &_particlesScriptingInterface; } + /// Access the ModelsScriptingInterface in order to initialize it with a custom packet sender and jurisdiction listener + static ModelsScriptingInterface* getModelsScriptingInterface() { return &_modelsScriptingInterface; } + /// sets the script contents, will return false if failed, will fail if script is already running bool setScriptContents(const QString& scriptContents, const QString& fileNameString = QString("")); @@ -128,6 +133,7 @@ private: static VoxelsScriptingInterface _voxelsScriptingInterface; static ParticlesScriptingInterface _particlesScriptingInterface; + static ModelsScriptingInterface _modelsScriptingInterface; AbstractControllerScriptingInterface* _controllerScriptingInterface; AudioScriptingInterface _audioScriptingInterface;