diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 07d15c9e26..3ff7eafd6b 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -469,7 +469,7 @@ void Agent::aboutToFinish() { } // our entity tree is going to go away so tell that to the EntityScriptingInterface - DependencyManager::get()->setEntityTree(NULL); + DependencyManager::get()->setEntityTree(nullptr); // cleanup the AssetClient thread QThread* assetThread = DependencyManager::get()->thread(); diff --git a/assignment-client/src/AssignmentClient.cpp b/assignment-client/src/AssignmentClient.cpp index 3bd38c4ae7..85a2c95b4d 100644 --- a/assignment-client/src/AssignmentClient.cpp +++ b/assignment-client/src/AssignmentClient.cpp @@ -60,7 +60,7 @@ AssignmentClient::AssignmentClient(Assignment::Type requestAssignmentType, QStri auto nodeList = DependencyManager::set(NodeType::Unassigned, listenPort); auto animationCache = DependencyManager::set(); - auto entityScriptingInterface = DependencyManager::set(); + auto entityScriptingInterface = DependencyManager::set(false); DependencyManager::registerInheritance(); auto actionFactory = DependencyManager::set(); diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 54032e993f..227ac843bb 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -458,7 +458,7 @@ int AudioMixer::prepareMixForListeningNode(Node* node) { if (otherNodeStream->getType() == PositionalAudioStream::Microphone) { streamUUID = otherNode->getUUID(); } - + // clear out the pre-mix samples before filling it up with this source memset(_preMixSamples, 0, sizeof(_preMixSamples)); @@ -498,7 +498,7 @@ void AudioMixer::sendAudioEnvironmentPacket(SharedNodePointer node) { break; } } - + AudioMixerClientData* nodeData = static_cast(node->getLinkedData()); AvatarAudioStream* stream = nodeData->getAvatarAudioStream(); bool dataChanged = (stream->hasReverb() != hasReverb) || @@ -550,18 +550,25 @@ void AudioMixer::handleNodeAudioPacket(QSharedPointer message, void AudioMixer::handleMuteEnvironmentPacket(QSharedPointer message, SharedNodePointer sendingNode) { auto nodeList = DependencyManager::get(); - + if (sendingNode->isAllowedEditor()) { - qDebug() << "Received a mute environment packet of" << message->getSize() << "bytes"; - - auto newPacket = NLPacket::create(PacketType::MuteEnvironment, message->getSize()); - // Copy payload - newPacket->write(message->getRawMessage(), message->getSize()); + glm::vec3 position; + float radius; + + auto newPacket = NLPacket::create(PacketType::MuteEnvironment, sizeof(position) + sizeof(radius)); + + // read the position and radius from the sent packet + message->readPrimitive(&position); + message->readPrimitive(&radius); + + // write them to our packet + newPacket->writePrimitive(position); + newPacket->writePrimitive(radius); nodeList->eachNode([&](const SharedNodePointer& node){ if (node->getType() == NodeType::Agent && node->getActiveSocket() && node->getLinkedData() && node != sendingNode) { - nodeList->sendPacket(std::move(newPacket), *node); + nodeList->sendUnreliablePacket(*newPacket, *node); } }); } @@ -649,185 +656,185 @@ void AudioMixer::sendStatsPacket() { } void AudioMixer::run() { - + qDebug() << "Waiting for connection to domain to request settings from domain-server."; - + // wait until we have the domain-server settings, otherwise we bail DomainHandler& domainHandler = DependencyManager::get()->getDomainHandler(); connect(&domainHandler, &DomainHandler::settingsReceived, this, &AudioMixer::domainSettingsRequestComplete); connect(&domainHandler, &DomainHandler::settingsReceiveFail, this, &AudioMixer::domainSettingsRequestFailed); - + ThreadedAssignment::commonInit(AUDIO_MIXER_LOGGING_TARGET_NAME, NodeType::AudioMixer); } void AudioMixer::domainSettingsRequestComplete() { auto nodeList = DependencyManager::get(); - + nodeList->addNodeTypeToInterestSet(NodeType::Agent); - + nodeList->linkedDataCreateCallback = [](Node* node) { node->setLinkedData(std::unique_ptr { new AudioMixerClientData }); }; - + DomainHandler& domainHandler = nodeList->getDomainHandler(); const QJsonObject& settingsObject = domainHandler.getSettingsObject(); - + // check the settings object to see if we have anything we can parse out parseSettingsObject(settingsObject); - + // queue up a connection to start broadcasting mixes now that we're ready to go QMetaObject::invokeMethod(this, "broadcastMixes", Qt::QueuedConnection); } void AudioMixer::broadcastMixes() { - auto nodeList = DependencyManager::get(); - + auto nodeList = DependencyManager::get(); + int64_t nextFrame = 0; QElapsedTimer timer; timer.start(); - + int64_t usecToSleep = AudioConstants::NETWORK_FRAME_USECS; - + const int TRAILING_AVERAGE_FRAMES = 100; int framesSinceCutoffEvent = TRAILING_AVERAGE_FRAMES; - + while (!_isFinished) { const float STRUGGLE_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD = 0.10f; const float BACK_OFF_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD = 0.20f; - + const float RATIO_BACK_OFF = 0.02f; - + const float CURRENT_FRAME_RATIO = 1.0f / TRAILING_AVERAGE_FRAMES; const float PREVIOUS_FRAMES_RATIO = 1.0f - CURRENT_FRAME_RATIO; - + if (usecToSleep < 0) { usecToSleep = 0; } - + _trailingSleepRatio = (PREVIOUS_FRAMES_RATIO * _trailingSleepRatio) + (usecToSleep * CURRENT_FRAME_RATIO / (float) AudioConstants::NETWORK_FRAME_USECS); - + float lastCutoffRatio = _performanceThrottlingRatio; bool hasRatioChanged = false; - + if (framesSinceCutoffEvent >= TRAILING_AVERAGE_FRAMES) { if (_trailingSleepRatio <= STRUGGLE_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD) { // we're struggling - change our min required loudness to reduce some load _performanceThrottlingRatio = _performanceThrottlingRatio + (0.5f * (1.0f - _performanceThrottlingRatio)); - + qDebug() << "Mixer is struggling, sleeping" << _trailingSleepRatio * 100 << "% of frame time. Old cutoff was" << lastCutoffRatio << "and is now" << _performanceThrottlingRatio; hasRatioChanged = true; } else if (_trailingSleepRatio >= BACK_OFF_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD && _performanceThrottlingRatio != 0) { // we've recovered and can back off the required loudness _performanceThrottlingRatio = _performanceThrottlingRatio - RATIO_BACK_OFF; - + if (_performanceThrottlingRatio < 0) { _performanceThrottlingRatio = 0; } - + qDebug() << "Mixer is recovering, sleeping" << _trailingSleepRatio * 100 << "% of frame time. Old cutoff was" << lastCutoffRatio << "and is now" << _performanceThrottlingRatio; hasRatioChanged = true; } - + if (hasRatioChanged) { // set out min audability threshold from the new ratio _minAudibilityThreshold = LOUDNESS_TO_DISTANCE_RATIO / (2.0f * (1.0f - _performanceThrottlingRatio)); qDebug() << "Minimum audability required to be mixed is now" << _minAudibilityThreshold; - + framesSinceCutoffEvent = 0; } } - + if (!hasRatioChanged) { ++framesSinceCutoffEvent; } - + quint64 now = usecTimestampNow(); if (now - _lastPerSecondCallbackTime > USECS_PER_SECOND) { perSecondActions(); _lastPerSecondCallbackTime = now; } - + nodeList->eachNode([&](const SharedNodePointer& node) { - + if (node->getLinkedData()) { AudioMixerClientData* nodeData = (AudioMixerClientData*)node->getLinkedData(); - + // this function will attempt to pop a frame from each audio stream. // a pointer to the popped data is stored as a member in InboundAudioStream. // That's how the popped audio data will be read for mixing (but only if the pop was successful) nodeData->checkBuffersBeforeFrameSend(); - + // if the stream should be muted, send mute packet if (nodeData->getAvatarAudioStream() && shouldMute(nodeData->getAvatarAudioStream()->getQuietestFrameLoudness())) { auto mutePacket = NLPacket::create(PacketType::NoisyMute, 0); nodeList->sendPacket(std::move(mutePacket), *node); } - + if (node->getType() == NodeType::Agent && node->getActiveSocket() && nodeData->getAvatarAudioStream()) { - + int streamsMixed = prepareMixForListeningNode(node.data()); - + std::unique_ptr mixPacket; - + if (streamsMixed > 0) { int mixPacketBytes = sizeof(quint16) + AudioConstants::NETWORK_FRAME_BYTES_STEREO; mixPacket = NLPacket::create(PacketType::MixedAudio, mixPacketBytes); - + // pack sequence number quint16 sequence = nodeData->getOutgoingSequenceNumber(); mixPacket->writePrimitive(sequence); - + // pack mixed audio samples mixPacket->write(reinterpret_cast(_mixSamples), AudioConstants::NETWORK_FRAME_BYTES_STEREO); } else { int silentPacketBytes = sizeof(quint16) + sizeof(quint16); mixPacket = NLPacket::create(PacketType::SilentAudioFrame, silentPacketBytes); - + // pack sequence number quint16 sequence = nodeData->getOutgoingSequenceNumber(); mixPacket->writePrimitive(sequence); - + // pack number of silent audio samples quint16 numSilentSamples = AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; mixPacket->writePrimitive(numSilentSamples); } - + // Send audio environment sendAudioEnvironmentPacket(node); - + // send mixed audio packet nodeList->sendPacket(std::move(mixPacket), *node); nodeData->incrementOutgoingMixedAudioSequenceNumber(); - + // send an audio stream stats packet if it's time if (_sendAudioStreamStats) { nodeData->sendAudioStreamStatsPackets(node); _sendAudioStreamStats = false; } - + ++_sumListeners; } } }); - + ++_numStatFrames; - + // since we're a while loop we need to help Qt's event processing QCoreApplication::processEvents(); - + if (_isFinished) { // at this point the audio-mixer is done // check if we have a deferred delete event to process (which we should once finished) QCoreApplication::sendPostedEvents(this, QEvent::DeferredDelete); break; } - + usecToSleep = (++nextFrame * AudioConstants::NETWORK_FRAME_USECS) - (timer.nsecsElapsed() / 1000); if (usecToSleep > 0) { @@ -1105,5 +1112,3 @@ void AudioMixer::parseSettingsObject(const QJsonObject &settingsObject) { } } } - - diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index 81e25cc7ba..31cab68cdf 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -289,6 +289,8 @@ void OctreeServer::initHTTPManager(int port) { _httpManager = new HTTPManager(QHostAddress::AnyIPv4, port, documentRoot, this, this); } +const QString PERSIST_FILE_DOWNLOAD_PATH = "/models.json.gz"; + bool OctreeServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler) { #ifdef FORCE_CRASH @@ -310,7 +312,6 @@ bool OctreeServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url #endif bool showStats = false; - QString persistFile = "/" + getPersistFilename(); if (connection->requestOperation() == QNetworkAccessManager::GetOperation) { if (url.path() == "/") { @@ -320,7 +321,7 @@ bool OctreeServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url _tree->resetEditStats(); resetSendingStats(); showStats = true; - } else if ((url.path() == persistFile) || (url.path() == persistFile + "/")) { + } else if ((url.path() == PERSIST_FILE_DOWNLOAD_PATH) || (url.path() == PERSIST_FILE_DOWNLOAD_PATH + "/")) { if (_persistFileDownload) { QByteArray persistFileContents = getPersistFileContents(); if (persistFileContents.length() > 0) { @@ -374,9 +375,9 @@ bool OctreeServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url statsString += "\r\n"; if (_persistFileDownload) { - statsString += QString("Persist file: %1\r\n").arg(persistFile); + statsString += QString("Persist file: Click to Download\r\n").arg(PERSIST_FILE_DOWNLOAD_PATH); } else { - statsString += QString("Persist file: %1\r\n").arg(persistFile); + statsString += QString("Persist file: %1\r\n").arg(_persistFilePath); } } else { @@ -1030,13 +1031,12 @@ void OctreeServer::readConfiguration() { qDebug() << "wantPersist=" << _wantPersist; if (_wantPersist) { - QString persistFilename; - if (!readOptionString(QString("persistFilename"), settingsSectionObject, persistFilename)) { - persistFilename = getMyDefaultPersistFilename(); + if (!readOptionString("persistFilePath", settingsSectionObject, _persistFilePath) + && !readOptionString("persistFilename", settingsSectionObject, _persistFilePath)) { + _persistFilePath = getMyDefaultPersistFilename(); } - strcpy(_persistFilename, qPrintable(persistFilename)); - qDebug("persistFilename=%s", _persistFilename); + qDebug() << "persistFilePath=" << _persistFilePath; _persistAsFileType = "json.gz"; @@ -1145,8 +1145,26 @@ void OctreeServer::domainSettingsRequestComplete() { if (_wantPersist) { // If persist filename does not exist, let's see if there is one beside the application binary // If there is, let's copy it over to our target persist directory - auto persistPath = ServerPathUtils::getDataFilePath("entities/" + QString(_persistFilename)); - if (!QFile::exists(persistPath)) { + QDir persistPath { _persistFilePath }; + QString absoluteFilePath = persistPath.absolutePath(); + + if (persistPath.isRelative()) { + // if the domain settings passed us a relative path, make an absolute path that is relative to the + // default data directory + absoluteFilePath = QDir(ServerPathUtils::getDataFilePath("entities/")).absoluteFilePath(_persistFilePath); + } + + static const QString ENTITY_PERSIST_EXTENSION = ".json.gz"; + + // force the persist file to end with .json.gz + if (!absoluteFilePath.endsWith(ENTITY_PERSIST_EXTENSION, Qt::CaseInsensitive)) { + absoluteFilePath += ENTITY_PERSIST_EXTENSION; + } else { + // make sure the casing of .json.gz is correct + absoluteFilePath.replace(ENTITY_PERSIST_EXTENSION, ENTITY_PERSIST_EXTENSION, Qt::CaseInsensitive); + } + + if (!QFile::exists(absoluteFilePath)) { qDebug() << "Persist file does not exist, checking for existence of persist file next to application"; static const QString OLD_DEFAULT_PERSIST_FILENAME = "resources/models.json.gz"; @@ -1154,7 +1172,7 @@ void OctreeServer::domainSettingsRequestComplete() { // This is the old persist path, based on the current persist filename, which could // be a custom filename set by the user. - auto oldPersistPath = QDir(oldResourcesDirectory).absoluteFilePath(_persistFilename); + auto oldPersistPath = QDir(oldResourcesDirectory).absoluteFilePath(_persistFilePath); // This is the old default persist path. auto oldDefaultPersistPath = QDir(oldResourcesDirectory).absoluteFilePath(OLD_DEFAULT_PERSIST_FILENAME); @@ -1172,23 +1190,24 @@ void OctreeServer::domainSettingsRequestComplete() { pathToCopyFrom = oldDefaultPersistPath; } - QDir persistFileDirectory = QDir(persistPath).filePath(".."); + QDir persistFileDirectory { QDir::cleanPath(absoluteFilePath + "/..") }; + if (!persistFileDirectory.exists()) { qDebug() << "Creating data directory " << persistFileDirectory.absolutePath(); persistFileDirectory.mkpath("."); } if (shouldCopy) { - qDebug() << "Old persist file found, copying from " << pathToCopyFrom << " to " << persistPath; + qDebug() << "Old persist file found, copying from " << pathToCopyFrom << " to " << absoluteFilePath; - QFile::copy(pathToCopyFrom, persistPath); + QFile::copy(pathToCopyFrom, absoluteFilePath); } else { qDebug() << "No existing persist file found"; } } // now set up PersistThread - _persistThread = new OctreePersistThread(_tree, persistPath, _persistInterval, + _persistThread = new OctreePersistThread(_tree, absoluteFilePath, _persistInterval, _wantBackup, _settings, _debugTimestampNow, _persistAsFileType); _persistThread->initialize(true); } diff --git a/assignment-client/src/octree/OctreeServer.h b/assignment-client/src/octree/OctreeServer.h index f1aa9531e8..1430715571 100644 --- a/assignment-client/src/octree/OctreeServer.h +++ b/assignment-client/src/octree/OctreeServer.h @@ -169,7 +169,7 @@ protected: int _statusPort; QString _statusHost; - char _persistFilename[MAX_FILENAME_LENGTH]; + QString _persistFilePath; QString _persistAsFileType; int _packetsPerClientPerInterval; int _packetsTotalPerInterval; diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index e38aa2a75a..870573ef6c 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1,5 +1,5 @@ { - "version": 1.0, + "version": 1.1, "settings": [ { "name": "metaverse", @@ -383,9 +383,9 @@ "assignment-types": [6], "settings": [ { - "name": "persistFilename", - "label": "Entities Filename", - "help": "the path to the file entities are stored in. Make sure the path exists.", + "name": "persistFilePath", + "label": "Entities File Path", + "help": "The path to the file entities are stored in. If this path is relative it will be relative to the application data directory. The filename must end in .json.gz.", "placeholder": "models.json.gz", "default": "models.json.gz", "advanced": true diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index e10007784f..98bb63241e 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -124,6 +124,41 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList // reload the master and user config so that the merged config is right _configMap.loadMasterAndUserConfig(argumentList); } + } else if (oldVersion < 1.1) { + static const QString ENTITY_SERVER_SETTINGS_KEY = "entity_server_settings"; + static const QString ENTITY_FILE_NAME_KEY = "persistFilename"; + static const QString ENTITY_FILE_PATH_KEYPATH = ENTITY_SERVER_SETTINGS_KEY + ".persistFilePath"; + + // this was prior to change of poorly named entitiesFileName to entitiesFilePath + QVariant* persistFileNameVariant = valueForKeyPath(_configMap.getMergedConfig(), + ENTITY_SERVER_SETTINGS_KEY + "." + ENTITY_FILE_NAME_KEY); + if (persistFileNameVariant && persistFileNameVariant->canConvert(QMetaType::QString)) { + QString persistFileName = persistFileNameVariant->toString(); + + qDebug() << "Migrating persistFilename to persistFilePath for entity-server settings"; + + // grab the persistFilePath option, create it if it doesn't exist + QVariant* persistFilePath = valueForKeyPath(_configMap.getUserConfig(), ENTITY_FILE_PATH_KEYPATH, true); + + // write the migrated value + *persistFilePath = persistFileName; + + // remove the old setting + QVariant* entityServerVariant = valueForKeyPath(_configMap.getUserConfig(), ENTITY_SERVER_SETTINGS_KEY); + if (entityServerVariant && entityServerVariant->canConvert(QMetaType::QVariantMap)) { + QVariantMap entityServerMap = entityServerVariant->toMap(); + entityServerMap.remove(ENTITY_FILE_NAME_KEY); + + *entityServerVariant = entityServerMap; + } + + // write the new settings to the json file + persistToFile(); + + // reload the master and user config so that the merged config is right + _configMap.loadMasterAndUserConfig(argumentList); + } + } } diff --git a/examples/attachedEntitiesManager.js b/examples/attachedEntitiesManager.js index f44564d6dc..1cdd61ad39 100644 --- a/examples/attachedEntitiesManager.js +++ b/examples/attachedEntitiesManager.js @@ -22,41 +22,44 @@ var MINIMUM_DROP_DISTANCE_FROM_JOINT = 0.4; var ATTACHED_ENTITY_SEARCH_DISTANCE = 10.0; var ATTACHED_ENTITIES_SETTINGS_KEY = "ATTACHED_ENTITIES"; var DRESSING_ROOM_DISTANCE = 2.0; +var SHOW_TOOL_BAR = false; // tool bar -HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; -var BUTTON_SIZE = 32; -var PADDING = 3; -Script.include(["libraries/toolBars.js"]); -var toolBar = new ToolBar(0, 0, ToolBar.VERTICAL, "highfidelity.attachedEntities.toolbar", function(screenSize) { - return { - x: (BUTTON_SIZE + PADDING), - y: (screenSize.y / 2 - BUTTON_SIZE * 2 + PADDING) - }; -}); -var saveButton = toolBar.addOverlay("image", { - width: BUTTON_SIZE, - height: BUTTON_SIZE, - imageURL: "http://headache.hungry.com/~seth/hifi/save.png", - color: { - red: 255, - green: 255, - blue: 255 - }, - alpha: 1 -}); -var loadButton = toolBar.addOverlay("image", { - width: BUTTON_SIZE, - height: BUTTON_SIZE, - imageURL: "http://headache.hungry.com/~seth/hifi/load.png", - color: { - red: 255, - green: 255, - blue: 255 - }, - alpha: 1 -}); +if (SHOW_TOOL_BAR) { + HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; + var BUTTON_SIZE = 32; + var PADDING = 3; + Script.include(["libraries/toolBars.js"]); + var toolBar = new ToolBar(0, 0, ToolBar.VERTICAL, "highfidelity.attachedEntities.toolbar", function(screenSize) { + return { + x: (BUTTON_SIZE + PADDING), + y: (screenSize.y / 2 - BUTTON_SIZE * 2 + PADDING) + }; + }); + var saveButton = toolBar.addOverlay("image", { + width: BUTTON_SIZE, + height: BUTTON_SIZE, + imageURL: ".../save.png", + color: { + red: 255, + green: 255, + blue: 255 + }, + alpha: 1 + }); + var loadButton = toolBar.addOverlay("image", { + width: BUTTON_SIZE, + height: BUTTON_SIZE, + imageURL: ".../load.png", + color: { + red: 255, + green: 255, + blue: 255 + }, + alpha: 1 + }); +} function mousePressEvent(event) { @@ -73,10 +76,14 @@ function mousePressEvent(event) { } function scriptEnding() { - toolBar.cleanup(); + if (SHOW_TOOL_BAR) { + toolBar.cleanup(); + } } -Controller.mousePressEvent.connect(mousePressEvent); +if (SHOW_TOOL_BAR) { + Controller.mousePressEvent.connect(mousePressEvent); +} Script.scriptEnding.connect(scriptEnding); @@ -116,6 +123,7 @@ function AttachedEntitiesManager() { manager.checkIfWearable(parsedMessage.grabbedEntity, parsedMessage.joint) // manager.saveAttachedEntities(); } else if (parsedMessage.action === 'shared-release') { + manager.updateRelativeOffsets(parsedMessage.grabbedEntity); // manager.saveAttachedEntities(); } else if (parsedMessage.action === 'equip') { // manager.saveAttachedEntities(); @@ -173,11 +181,12 @@ function AttachedEntitiesManager() { parentJointIndex: bestJointIndex }; - if (!this.avatarIsInDressingRoom() && - bestJointOffset && bestJointOffset.constructor === Array && bestJointOffset.length > 1) { - // don't snap the entity to the preferred position if the avatar is in the dressing room. - wearProps.localPosition = bestJointOffset[0]; - wearProps.localRotation = bestJointOffset[1]; + if (bestJointOffset && bestJointOffset.constructor === Array && bestJointOffset.length > 1) { + if (!this.avatarIsInDressingRoom()) { + // don't snap the entity to the preferred position if the avatar is in the dressing room. + wearProps.localPosition = bestJointOffset[0]; + wearProps.localRotation = bestJointOffset[1]; + } } Entities.editEntity(grabbedEntity, wearProps); } else if (props.parentID != NULL_UUID) { @@ -189,12 +198,19 @@ function AttachedEntitiesManager() { } } - this.updateRelativeOffsets = function(entityID, props) { + this.updateRelativeOffsets = function(entityID) { // save the preferred (current) relative position and rotation into the user-data of the entity - var wearableData = getEntityCustomData('wearable', entityID, DEFAULT_WEARABLE_DATA); - var currentJointName = MyAvatar.getJointNames()[props.parentJointIndex]; - wearableData.joints[currentJointName] = [props.localPosition, props.localRotation]; - setEntityCustomData('wearable', entityID, wearableData); + var props = Entities.getEntityProperties(entityID); + if (props.parentID == MyAvatar.sessionUUID) { + grabData = getEntityCustomData('grabKey', entityID, {}); + grabbableData = getEntityCustomData('grabbableKey', entityID, {}); + var wearableData = getEntityCustomData('wearable', entityID, DEFAULT_WEARABLE_DATA); + var currentJointName = MyAvatar.getJointNames()[props.parentJointIndex]; + wearableData.joints[currentJointName] = [props.localPosition, props.localRotation]; + setEntityCustomData('wearable', entityID, wearableData); + return true; + } + return false; } this.saveAttachedEntities = function() { @@ -203,12 +219,8 @@ function AttachedEntitiesManager() { var nearbyEntities = Entities.findEntities(MyAvatar.position, ATTACHED_ENTITY_SEARCH_DISTANCE); for (i = 0; i < nearbyEntities.length; i++) { var entityID = nearbyEntities[i]; - var props = Entities.getEntityProperties(entityID); - if (props.parentID == MyAvatar.sessionUUID) { - grabData = getEntityCustomData('grabKey', entityID, {}); - grabbableData = getEntityCustomData('grabbableKey', entityID, {}); - this.updateRelativeOffsets(entityID, props); - props = Entities.getEntityProperties(entityID); // refresh, because updateRelativeOffsets changed them + if (this.updateRelativeOffsets(entityID)) { + var props = Entities.getEntityProperties(entityID); // refresh, because updateRelativeOffsets changed them this.scrubProperties(props); saveData.push(props); } diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index 074b72c9f4..4b022e5a84 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -69,13 +69,14 @@ var PICK_MAX_DISTANCE = 500; // max length of pick-ray // near grabbing // -var GRAB_RADIUS = 0.03; // if the ray misses but an object is this close, it will still be selected +var GRAB_RADIUS = 0.06; // if the ray misses but an object is this close, it will still be selected var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position var NEAR_GRABBING_VELOCITY_SMOOTH_RATIO = 1.0; // adjust time-averaging of held object's velocity. 1.0 to disable. var NEAR_PICK_MAX_DISTANCE = 0.3; // max length of pick-ray for close grabbing to be selected var RELEASE_VELOCITY_MULTIPLIER = 1.5; // affects throwing things var PICK_BACKOFF_DISTANCE = 0.2; // helps when hand is intersecting the grabble object var NEAR_GRABBING_KINEMATIC = true; // force objects to be kinematic when near-grabbed +var SHOW_GRAB_SPHERE = false; // draw a green sphere to show the grab search position and size // // equip @@ -256,6 +257,7 @@ function MyController(hand) { //for visualizations this.overlayLine = null; this.particleBeamObject = null; + this.grabSphere = null; //for lights this.spotlight = null; @@ -336,6 +338,7 @@ function MyController(hand) { } this.setState = function(newState) { + this.grabSphereOff(); if (WANT_DEBUG || WANT_DEBUG_STATE) { print("STATE (" + this.hand + "): " + stateToName(this.state) + " --> " + stateToName(newState) + ", hand: " + this.hand); @@ -416,6 +419,37 @@ function MyController(hand) { } } + this.grabSphereOn = function() { + var color = {red: 0, green: 255, blue: 0}; + if (this.grabSphere === null) { + var sphereProperties = { + position: this.getHandPosition(), + size: GRAB_RADIUS*2, + color: color, + alpha: 0.1, + solid: true, + visible: true + } + this.grabSphere = Overlays.addOverlay("sphere", sphereProperties); + } else { + Overlays.editOverlay(this.grabSphere, { + position: this.getHandPosition(), + size: GRAB_RADIUS*2, + color: color, + alpha: 0.1, + solid: true, + visible: true + }); + } + } + + this.grabSphereOff = function() { + if (this.grabSphere !== null) { + Overlays.deleteOverlay(this.grabSphere); + this.grabSphere = null; + } + }; + this.overlayLineOn = function(closePoint, farPoint, color) { if (this.overlayLine === null) { var lineProperties = { @@ -644,7 +678,6 @@ function MyController(hand) { this.searchSphereDistance = DEFAULT_SEARCH_SPHERE_DISTANCE; this.intersectionDistance = 0.0; } - }; this.particleBeamOff = function() { @@ -749,6 +782,11 @@ function MyController(hand) { // the trigger is being pressed, so do a ray test to see what we are hitting var handPosition = this.getHandPosition(); + + if (SHOW_GRAB_SPHERE) { + this.grabSphereOn(); + } + var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; var currentHandRotation = Controller.getPoseValue(controllerHandInput).rotation; var handDeltaRotation = Quat.multiply(currentHandRotation, Quat.inverse(this.startingHandRotation)); @@ -1221,11 +1259,14 @@ function MyController(hand) { this.hasPresetOffsets = function() { var wearableData = getEntityCustomData('wearable', this.grabbedEntity, {joints: {}}); - var allowedJoints = wearableData.joints; - var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; - if (handJointName in allowedJoints) { - return true; + if ("joints" in wearableData) { + var allowedJoints = wearableData.joints; + var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; + if (handJointName in allowedJoints) { + return true; + } } + return false; } this.getPresetPosition = function() { diff --git a/examples/defaultScripts.js b/examples/defaultScripts.js index 5ca62470ee..35af5f4eae 100644 --- a/examples/defaultScripts.js +++ b/examples/defaultScripts.js @@ -21,3 +21,4 @@ Script.load("controllers/squeezeHands.js"); Script.load("grab.js"); Script.load("directory.js"); Script.load("dialTone.js"); +Script.load("attachedEntitiesManager.js"); diff --git a/examples/edit.js b/examples/edit.js index cc5921efd1..3e46160522 100644 --- a/examples/edit.js +++ b/examples/edit.js @@ -23,7 +23,6 @@ Script.include([ "libraries/ToolTip.js", - "libraries/entityPropertyDialogBox.js", "libraries/entityCameraTool.js", "libraries/gridTool.js", "libraries/entityList.js", @@ -32,7 +31,6 @@ Script.include([ var selectionDisplay = SelectionDisplay; var selectionManager = SelectionManager; -var entityPropertyDialogBox = EntityPropertyDialogBox; var lightOverlayManager = new LightOverlayManager(); diff --git a/examples/example/ui/MyEnergyBar.js b/examples/example/ui/MyEnergyBar.js new file mode 100644 index 0000000000..2d01a66317 --- /dev/null +++ b/examples/example/ui/MyEnergyBar.js @@ -0,0 +1,63 @@ +Script.include("../../libraries/utils.js"); +var energyColor = {red: 0, green: 200, blue: 0}; +var lowEnergyColor = {red: 255, green: 0, blue: 0}; +var totalWidth = 200; +var paddingRight = 50; +var xPosition = Window.innerWidth - totalWidth - paddingRight; +var lowEnergyThreshold = 0.3; +var currentEnergy = 1.0; +var energyLossRate = 0.003; +var energyChargeRate = 0.003; +var isGrabbing = false; +var refractoryPeriod = 2000; + +var lastAvatarVelocity = MyAvatar.getVelocity(); +var lastAvatarPosition = MyAvatar.position; + +var background = Overlays.addOverlay("text", { + x: xPosition, + y: 20, + width: totalWidth, + height: 10, + backgroundColor: {red: 255, green: 0, blue: 0} +}) + +var bar = Overlays.addOverlay("text", { + x: xPosition, + y: 20, + width: totalWidth, + height: 10, + backgroundColor: energyColor +}); + + +// Takes an energy value between 0 and 1 and sets energy bar width appropriately +function setEnergy(energy) { + energy = clamp(energy, 0, 1); + var barWidth = totalWidth * energy; + var color = energy <= lowEnergyThreshold ? lowEnergyColor: energyColor; + Overlays.editOverlay(bar, { width: barWidth, backgroundColor: color}); +} + +function update() { + currentEnergy = clamp(MyAvatar.energy, 0, 1); + setEnergy(currentEnergy); +} + +function cleanup() { + Overlays.deleteOverlay(background); + Overlays.deleteOverlay(bar); +} + +function energyChanged(newValue) { + Entities.currentAvatarEnergy = newValue; +} + +function debitAvatarEnergy(value) { + MyAvatar.energy = MyAvatar.energy - value; +} +Entities.costMultiplier = 0.02; +Entities.debitEnergySource.connect(debitAvatarEnergy); +MyAvatar.energyChanged.connect(energyChanged); +Script.update.connect(update); +Script.scriptEnding.connect(cleanup); diff --git a/examples/example/ui/energyBar.js b/examples/example/ui/energyBar.js index a45b09f6d4..498eef2751 100644 --- a/examples/example/ui/energyBar.js +++ b/examples/example/ui/energyBar.js @@ -51,45 +51,8 @@ function setEnergy(energy) { Overlays.editOverlay(bar, { width: barWidth, backgroundColor: color}); } -function avatarAccelerationEnergy() { - var AVATAR_MOVEMENT_ENERGY_CONSTANT = 0.001; - var velocity = MyAvatar.getVelocity(); - var dV = Math.abs(Vec3.length(velocity) - Vec3.length(lastAvatarVelocity)); - var dE = Vec3.length(lastAvatarVelocity) * dV * AVATAR_MOVEMENT_ENERGY_CONSTANT; - lastAvatarVelocity = velocity; - return dE; -} - -function teleported() { - var MAX_AVATAR_MOVEMENT_PER_FRAME = 30.0; - var position = MyAvatar.position; - var dP = Vec3.length(Vec3.subtract(position, lastAvatarPosition)); - lastAvatarPosition = position; - return (dP > MAX_AVATAR_MOVEMENT_PER_FRAME); -} - -function audioEnergy() { - var AUDIO_ENERGY_CONSTANT = 0.000001; - return MyAvatar.audioLoudness * AUDIO_ENERGY_CONSTANT; -} - function update() { - // refill energy - currentEnergy += energyChargeRate; - - // Avatar acceleration - currentEnergy -= avatarAccelerationEnergy(); - - // Teleport cost - if (teleported()) { - currentEnergy = 0; - } - - // Making sounds - currentEnergy -= audioEnergy(); - - - currentEnergy = clamp(currentEnergy, 0, 1); + currentEnergy = clamp(MyAvatar.energy, 0, 1); setEnergy(currentEnergy); } @@ -98,5 +61,10 @@ function cleanup() { Overlays.deleteOverlay(bar); } +function energyChanged(newValue) { + Entities.currentAvatarEnergy = newValue; +} + +MyAvatar.energyChanged.connect(energyChanged); Script.update.connect(update); Script.scriptEnding.connect(cleanup); diff --git a/examples/libraries/entityPropertyDialogBox.js b/examples/libraries/entityPropertyDialogBox.js deleted file mode 100644 index b04d797b77..0000000000 --- a/examples/libraries/entityPropertyDialogBox.js +++ /dev/null @@ -1,457 +0,0 @@ -// -// entityPropertyDialogBox.js -// examples -// -// Created by Brad hefta-Gaub on 10/1/14. -// Copyright 2014 High Fidelity, Inc. -// -// This script implements a class useful for building tools for editing entities. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -var DEGREES_TO_RADIANS = Math.PI / 180.0; -var RADIANS_TO_DEGREES = 180.0 / Math.PI; - -EntityPropertyDialogBox = (function () { - var that = {}; - - var propertiesForEditedEntity; - var editEntityFormArray; - var decimals = 3; - var dimensionX; - var dimensionY; - var dimensionZ; - var rescalePercentage; - var editModelID = -1; - var previousAnimationIsPlaying; - var previousAnimationCurrentFrame; - - that.cleanup = function () { - }; - - that.openDialog = function (entityID) { - print(" Edit Properties.... about to edit properties..."); - - editModelID = entityID; - propertiesForEditedEntity = Entities.getEntityProperties(editModelID); - var properties = propertiesForEditedEntity; - - var array = new Array(); - var index = 0; - - array.push({ label: "Entity Type:" + properties.type, type: "header" }); - index++; - array.push({ label: "ID:", value: properties.id }); - index++; - array.push({ label: "Locked:", type: "checkbox", value: properties.locked }); - index++; - - if (properties.type == "Model") { - array.push({ label: "Model URL:", value: properties.modelURL }); - index++; - array.push({ label: "Shape Type:", value: properties.shapeType }); - index++; - array.push({ label: "Compound Shape URL:", value: properties.compoundShapeURL }); - index++; - array.push({ label: "Animation URL:", value: properties.animation.url }); - index++; - array.push({ label: "Animation is playing:", type: "checkbox", value: properties.animation.running }); - previousAnimationIsPlaying = properties.animation.running; - index++; - array.push({ label: "Animation FPS:", value: properties.animation.fps }); - index++; - array.push({ label: "Animation Frame:", value: properties.animation.currentFrame }); - previousAnimationCurrentFrame = properties.animation.currentFrame; - index++; - array.push({ label: "Textures:", value: properties.textures }); - index++; - array.push({ label: "Original Textures:\n" + properties.originalTextures, type: "header" }); - index++; - } - - if (properties.type == "Text") { - array.push({ label: "Text:", value: properties.text }); - index++; - array.push({ label: "Line Height:", value: properties.lineHeight }); - index++; - array.push({ label: "Text Color:", type: "header" }); - index++; - array.push({ label: "Red:", value: properties.textColor.red }); - index++; - array.push({ label: "Green:", value: properties.textColor.green }); - index++; - array.push({ label: "Blue:", value: properties.textColor.blue }); - index++; - array.push({ label: "Background Color:", type: "header" }); - index++; - array.push({ label: "Red:", value: properties.backgroundColor.red }); - index++; - array.push({ label: "Green:", value: properties.backgroundColor.green }); - index++; - array.push({ label: "Blue:", value: properties.backgroundColor.blue }); - index++; - } - - if (properties.type == "PolyVox") { - array.push({ label: "Voxel Space Size:", type: "header" }); - index++; - - array.push({ label: "X:", value: properties.voxelVolumeSize.x.toFixed(decimals) }); - index++; - array.push({ label: "Y:", value: properties.voxelVolumeSize.y.toFixed(decimals) }); - index++; - array.push({ label: "Z:", value: properties.voxelVolumeSize.z.toFixed(decimals) }); - index++; - - array.push({ label: "Surface Extractor", value: properties.voxelSurfaceStyle }); - index++; - - array.push({ label: "X-axis Texture URL:", value: properties.xTextureURL }); - index++; - array.push({ label: "Y-axis Texture URL:", value: properties.yTextureURL }); - index++; - array.push({ label: "Z-axis Texture URL:", value: properties.zTextureURL }); - index++; - } - - array.push({ label: "Position:", type: "header" }); - index++; - array.push({ label: "X:", value: properties.position.x.toFixed(decimals) }); - index++; - array.push({ label: "Y:", value: properties.position.y.toFixed(decimals) }); - index++; - array.push({ label: "Z:", value: properties.position.z.toFixed(decimals) }); - index++; - - array.push({ label: "Registration X:", value: properties.registrationPoint.x.toFixed(decimals) }); - index++; - array.push({ label: "Registration Y:", value: properties.registrationPoint.y.toFixed(decimals) }); - index++; - array.push({ label: "Registration Z:", value: properties.registrationPoint.z.toFixed(decimals) }); - index++; - - array.push({ label: "Rotation:", type: "header" }); - index++; - var angles = Quat.safeEulerAngles(properties.rotation); - array.push({ label: "Pitch:", value: angles.x.toFixed(decimals) }); - index++; - array.push({ label: "Yaw:", value: angles.y.toFixed(decimals) }); - index++; - array.push({ label: "Roll:", value: angles.z.toFixed(decimals) }); - index++; - - array.push({ label: "Dimensions:", type: "header" }); - index++; - array.push({ label: "Width:", value: properties.dimensions.x.toFixed(decimals) }); - dimensionX = index; - index++; - array.push({ label: "Height:", value: properties.dimensions.y.toFixed(decimals) }); - dimensionY = index; - index++; - array.push({ label: "Depth:", value: properties.dimensions.z.toFixed(decimals) }); - dimensionZ = index; - index++; - array.push({ label: "", type: "inlineButton", buttonLabel: "Reset to Natural Dimensions", name: "resetDimensions" }); - index++; - array.push({ label: "Rescale Percentage:", value: 100 }); - rescalePercentage = index; - index++; - array.push({ label: "", type: "inlineButton", buttonLabel: "Rescale", name: "rescaleDimensions" }); - index++; - - array.push({ label: "Velocity:", type: "header" }); - index++; - array.push({ label: "Linear X:", value: properties.velocity.x.toFixed(decimals) }); - index++; - array.push({ label: "Linear Y:", value: properties.velocity.y.toFixed(decimals) }); - index++; - array.push({ label: "Linear Z:", value: properties.velocity.z.toFixed(decimals) }); - index++; - array.push({ label: "Linear Damping:", value: properties.damping.toFixed(decimals) }); - index++; - // NOTE: angular velocity is in radians/sec but we display degrees/sec for users - array.push({ label: "Angular Pitch:", value: (properties.angularVelocity.x * RADIANS_TO_DEGREES).toFixed(decimals) }); - index++; - array.push({ label: "Angular Yaw:", value: (properties.angularVelocity.y * RADIANS_TO_DEGREES).toFixed(decimals) }); - index++; - array.push({ label: "Angular Roll:", value: (properties.angularVelocity.z * RADIANS_TO_DEGREES).toFixed(decimals) }); - index++; - array.push({ label: "Angular Damping:", value: properties.angularDamping.toFixed(decimals) }); - index++; - - array.push({ label: "Gravity X:", value: properties.gravity.x.toFixed(decimals) }); - index++; - array.push({ label: "Gravity Y:", value: properties.gravity.y.toFixed(decimals) }); - index++; - array.push({ label: "Gravity Z:", value: properties.gravity.z.toFixed(decimals) }); - index++; - - array.push({ label: "Acceleration X:", value: properties.acceleration.x.toFixed(decimals) }); - index++; - array.push({ label: "Acceleration Y:", value: properties.acceleration.y.toFixed(decimals) }); - index++; - array.push({ label: "Acceleration Z:", value: properties.acceleration.z.toFixed(decimals) }); - index++; - - array.push({ label: "Collisions:", type: "header" }); - index++; - array.push({ label: "Density:", value: properties.density.toFixed(decimals) }); - index++; - array.push({ label: "Collisionless:", type: "checkbox", value: properties.collisionless }); - index++; - array.push({ label: "Dynamic:", type: "checkbox", value: properties.dynamic }); - index++; - array.push({ label: "Collision Sound URL:", value: properties.collisionSoundURL }); - index++; - - array.push({ label: "Lifetime:", value: properties.lifetime.toFixed(decimals) }); - index++; - - array.push({ label: "Visible:", type: "checkbox", value: properties.visible }); - index++; - - array.push({ label: "Script:", value: properties.script }); - index++; - - if (properties.type == "Box" || properties.type == "Sphere") { - array.push({ label: "Color:", type: "header" }); - index++; - array.push({ label: "Red:", value: properties.color.red }); - index++; - array.push({ label: "Green:", value: properties.color.green }); - index++; - array.push({ label: "Blue:", value: properties.color.blue }); - index++; - } - - if (properties.type == "Light") { - array.push({ label: "Light Properties:", type: "header" }); - index++; - array.push({ label: "Is Spot Light:", value: properties.isSpotlight }); - index++; - array.push({ label: "Diffuse Red:", value: properties.diffuseColor.red }); - index++; - array.push({ label: "Diffuse Green:", value: properties.diffuseColor.green }); - index++; - array.push({ label: "Diffuse Blue:", value: properties.diffuseColor.blue }); - index++; - array.push({ label: "Ambient Red:", value: properties.ambientColor.red }); - index++; - array.push({ label: "Ambient Green:", value: properties.ambientColor.green }); - index++; - array.push({ label: "Ambient Blue:", value: properties.ambientColor.blue }); - index++; - array.push({ label: "Specular Red:", value: properties.specularColor.red }); - index++; - array.push({ label: "Specular Green:", value: properties.specularColor.green }); - index++; - array.push({ label: "Specular Blue:", value: properties.specularColor.blue }); - index++; - array.push({ label: "Constant Attenuation:", value: properties.constantAttenuation }); - index++; - array.push({ label: "Linear Attenuation:", value: properties.linearAttenuation }); - index++; - array.push({ label: "Quadratic Attenuation:", value: properties.quadraticAttenuation }); - index++; - array.push({ label: "Exponent:", value: properties.exponent }); - index++; - array.push({ label: "Cutoff (in degrees):", value: properties.cutoff }); - index++; - } - - array.push({ label: "", type: "inlineButton", buttonLabel: "Preview Camera", name: "previewCamera" }); - index++; - - array.push({ button: "Cancel" }); - index++; - - editEntityFormArray = array; - Window.nonBlockingForm("Edit Properties", array); - }; - - Window.inlineButtonClicked.connect(function (name) { - if (name == "previewCamera") { - Camera.mode = "entity"; - Camera.cameraEntity = propertiesForEditedEntity.id; - } - - if (name == "resetDimensions") { - Window.reloadNonBlockingForm([ - { value: propertiesForEditedEntity.naturalDimensions.x.toFixed(decimals), oldIndex: dimensionX }, - { value: propertiesForEditedEntity.naturalDimensions.y.toFixed(decimals), oldIndex: dimensionY }, - { value: propertiesForEditedEntity.naturalDimensions.z.toFixed(decimals), oldIndex: dimensionZ } - ]); - } - - if (name == "rescaleDimensions") { - var peekValues = editEntityFormArray; - Window.peekNonBlockingFormResult(peekValues); - var peekX = peekValues[dimensionX].value; - var peekY = peekValues[dimensionY].value; - var peekZ = peekValues[dimensionZ].value; - var peekRescale = peekValues[rescalePercentage].value; - var rescaledX = peekX * peekRescale / 100.0; - var rescaledY = peekY * peekRescale / 100.0; - var rescaledZ = peekZ * peekRescale / 100.0; - - Window.reloadNonBlockingForm([ - { value: rescaledX.toFixed(decimals), oldIndex: dimensionX }, - { value: rescaledY.toFixed(decimals), oldIndex: dimensionY }, - { value: rescaledZ.toFixed(decimals), oldIndex: dimensionZ }, - { value: 100, oldIndex: rescalePercentage } - ]); - } - - }); - Window.nonBlockingFormClosed.connect(function() { - array = editEntityFormArray; - if (Window.getNonBlockingFormResult(array)) { - var properties = propertiesForEditedEntity; - var index = 0; - index++; // skip type header - index++; // skip id item - properties.locked = array[index++].value; - if (properties.type == "Model") { - properties.modelURL = array[index++].value; - properties.shapeType = array[index++].value; - properties.compoundShapeURL = array[index++].value; - properties.animation.url = array[index++].value; - - var newAnimationIsPlaying = array[index++].value; - if (previousAnimationIsPlaying != newAnimationIsPlaying) { - properties.animation.running = newAnimationIsPlaying; - } else { - delete properties.animation.running; - } - - properties.animation.fps = array[index++].value; - - var newAnimationCurrentFrame = array[index++].value; - if (previousAnimationCurrentFrame != newAnimationCurrentFrame) { - properties.animation.currentFrame = newAnimationCurrentFrame; - } else { - delete properties.animation.currentFrame; - } - - properties.textures = array[index++].value; - index++; // skip textureNames label - } - - if (properties.type == "Text") { - properties.text = array[index++].value; - properties.lineHeight = array[index++].value; - - index++; // skip header - properties.textColor.red = array[index++].value; - properties.textColor.green = array[index++].value; - properties.textColor.blue = array[index++].value; - - index++; // skip header - properties.backgroundColor.red = array[index++].value; - properties.backgroundColor.green = array[index++].value; - properties.backgroundColor.blue = array[index++].value; - } - - if (properties.type == "PolyVox") { - properties.shapeType = array[index++].value; - - index++; // skip header - properties.voxelVolumeSize.x = array[index++].value; - properties.voxelVolumeSize.y = array[index++].value; - properties.voxelVolumeSize.z = array[index++].value; - properties.voxelSurfaceStyle = array[index++].value; - properties.xTextureURL = array[index++].value; - properties.yTextureURL = array[index++].value; - properties.zTextureURL = array[index++].value; - } - - index++; // skip header - properties.position.x = array[index++].value; - properties.position.y = array[index++].value; - properties.position.z = array[index++].value; - properties.registrationPoint.x = array[index++].value; - properties.registrationPoint.y = array[index++].value; - properties.registrationPoint.z = array[index++].value; - - index++; // skip header - var angles = Quat.safeEulerAngles(properties.rotation); - angles.x = array[index++].value; - angles.y = array[index++].value; - angles.z = array[index++].value; - properties.rotation = Quat.fromVec3Degrees(angles); - - index++; // skip header - properties.dimensions.x = array[index++].value; - properties.dimensions.y = array[index++].value; - properties.dimensions.z = array[index++].value; - index++; // skip reset button - index++; // skip rescale percentage - index++; // skip rescale button - - index++; // skip header - properties.velocity.x = array[index++].value; - properties.velocity.y = array[index++].value; - properties.velocity.z = array[index++].value; - properties.damping = array[index++].value; - - // NOTE: angular velocity is in radians/sec but we display degrees/sec for users - properties.angularVelocity.x = array[index++].value * DEGREES_TO_RADIANS; - properties.angularVelocity.y = array[index++].value * DEGREES_TO_RADIANS; - properties.angularVelocity.z = array[index++].value * DEGREES_TO_RADIANS; - properties.angularDamping = array[index++].value; - - properties.gravity.x = array[index++].value; - properties.gravity.y = array[index++].value; - properties.gravity.z = array[index++].value; - - properties.acceleration.x = array[index++].value; - properties.acceleration.y = array[index++].value; - properties.acceleration.z = array[index++].value; - - index++; // skip header - properties.density = array[index++].value; - properties.collisionless = array[index++].value; - properties.dynamic = array[index++].value; - - properties.lifetime = array[index++].value; - properties.visible = array[index++].value; - properties.script = array[index++].value; - - if (properties.type == "Box" || properties.type == "Sphere") { - index++; // skip header - properties.color.red = array[index++].value; - properties.color.green = array[index++].value; - properties.color.blue = array[index++].value; - } - if (properties.type == "Light") { - index++; // skip header - properties.isSpotlight = array[index++].value; - properties.diffuseColor.red = array[index++].value; - properties.diffuseColor.green = array[index++].value; - properties.diffuseColor.blue = array[index++].value; - properties.ambientColor.red = array[index++].value; - properties.ambientColor.green = array[index++].value; - properties.ambientColor.blue = array[index++].value; - properties.specularColor.red = array[index++].value; - properties.specularColor.green = array[index++].value; - properties.specularColor.blue = array[index++].value; - properties.constantAttenuation = array[index++].value; - properties.linearAttenuation = array[index++].value; - properties.quadraticAttenuation = array[index++].value; - properties.exponent = array[index++].value; - properties.cutoff = array[index++].value; - } - - Entities.editEntity(editModelID, properties); - if (typeof(selectionDisplay) != "undefined") { - selectionDisplay.select(editModelID, false); - } - } - modelSelected = false; - }); - - return that; - -}()); diff --git a/examples/users.js b/examples/users.js index f63184625d..0f9be42835 100644 --- a/examples/users.js +++ b/examples/users.js @@ -235,7 +235,12 @@ var usersWindow = (function () { FRIENDS_BUTTON_HEIGHT = FRIENDS_BUTTON_SVG_HEIGHT, FRIENDS_BUTTON_COLOR = { red: 225, green: 225, blue: 225 }, FRIENDS_BUTTON_ALPHA = 0.95, + FRIENDS_WINDOW_URL = "https://metaverse.highfidelity.com/user/friends", + FRIENDS_WINDOW_WIDTH = 290, + FRIENDS_WINDOW_HEIGHT = 500, + FRIENDS_WINDOW_TITLE = "Add/Remove Friends", friendsButton, + friendsWindow, OPTION_BACKGROUND_COLOR = { red: 60, green: 60, blue: 60 }, OPTION_BACKGROUND_ALPHA = 0.1, @@ -643,7 +648,17 @@ var usersWindow = (function () { } if (clickedOverlay === friendsButton) { - GlobalServices.editFriends(); + if (!friendsWindow) { + friendsWindow = new OverlayWebWindow({ + title: FRIENDS_WINDOW_TITLE, + width: FRIENDS_WINDOW_WIDTH, + height: FRIENDS_WINDOW_HEIGHT, + visible: false + }); + } + friendsWindow.setURL(FRIENDS_WINDOW_URL); + friendsWindow.setVisible(true); + friendsWindow.raise(); } } diff --git a/interface/resources/meshes/defaultAvatar_full/avatar-animation.json b/interface/resources/meshes/defaultAvatar_full/avatar-animation.json index 42ff64abc6..8980abf740 100644 --- a/interface/resources/meshes/defaultAvatar_full/avatar-animation.json +++ b/interface/resources/meshes/defaultAvatar_full/avatar-animation.json @@ -247,7 +247,11 @@ { "var": "isTurningRight", "state": "turnRight" }, { "var": "isTurningLeft", "state": "turnLeft" }, { "var": "isAway", "state": "awayIntro" }, - { "var": "isFlying", "state": "fly" } + { "var": "isFlying", "state": "fly" }, + { "var": "isTakeoffStand", "state": "takeoffStand" }, + { "var": "isTakeoffRun", "state": "takeoffRun" }, + { "var": "isInAirStand", "state": "inAirStand" }, + { "var": "isInAirRun", "state": "inAirRun" } ] }, { @@ -263,7 +267,11 @@ { "var": "isTurningRight", "state": "turnRight" }, { "var": "isTurningLeft", "state": "turnLeft" }, { "var": "isAway", "state": "awayIntro" }, - { "var": "isFlying", "state": "fly" } + { "var": "isFlying", "state": "fly" }, + { "var": "isTakeoffStand", "state": "takeoffStand" }, + { "var": "isTakeoffRun", "state": "takeoffRun" }, + { "var": "isInAirStand", "state": "inAirStand" }, + { "var": "isInAirRun", "state": "inAirRun" } ] }, { @@ -278,7 +286,11 @@ { "var": "isTurningRight", "state": "turnRight" }, { "var": "isTurningLeft", "state": "turnLeft" }, { "var": "isAway", "state": "awayIntro" }, - { "var": "isFlying", "state": "fly" } + { "var": "isFlying", "state": "fly" }, + { "var": "isTakeoffStand", "state": "takeoffStand" }, + { "var": "isTakeoffRun", "state": "takeoffRun" }, + { "var": "isInAirStand", "state": "inAirStand" }, + { "var": "isInAirRun", "state": "inAirRun" } ] }, { @@ -293,7 +305,11 @@ { "var": "isTurningRight", "state": "turnRight" }, { "var": "isTurningLeft", "state": "turnLeft" }, { "var": "isAway", "state": "awayIntro" }, - { "var": "isFlying", "state": "fly" } + { "var": "isFlying", "state": "fly" }, + { "var": "isTakeoffStand", "state": "takeoffStand" }, + { "var": "isTakeoffRun", "state": "takeoffRun" }, + { "var": "isInAirStand", "state": "inAirStand" }, + { "var": "isInAirRun", "state": "inAirRun" } ] }, { @@ -308,7 +324,11 @@ { "var": "isTurningRight", "state": "turnRight" }, { "var": "isTurningLeft", "state": "turnLeft" }, { "var": "isAway", "state": "awayIntro" }, - { "var": "isFlying", "state": "fly" } + { "var": "isFlying", "state": "fly" }, + { "var": "isTakeoffStand", "state": "takeoffStand" }, + { "var": "isTakeoffRun", "state": "takeoffRun" }, + { "var": "isInAirStand", "state": "inAirStand" }, + { "var": "isInAirRun", "state": "inAirRun" } ] }, { @@ -323,7 +343,11 @@ { "var": "isTurningRight", "state": "turnRight" }, { "var": "isTurningLeft", "state": "turnLeft" }, { "var": "isAway", "state": "awayIntro" }, - { "var": "isFlying", "state": "fly" } + { "var": "isFlying", "state": "fly" }, + { "var": "isTakeoffStand", "state": "takeoffStand" }, + { "var": "isTakeoffRun", "state": "takeoffRun" }, + { "var": "isInAirStand", "state": "inAirStand" }, + { "var": "isInAirRun", "state": "inAirRun" } ] }, { @@ -338,7 +362,11 @@ { "var": "isMovingLeft", "state": "strafeLeft" }, { "var": "isTurningLeft", "state": "turnLeft" }, { "var": "isAway", "state": "awayIntro" }, - { "var": "isFlying", "state": "fly" } + { "var": "isFlying", "state": "fly" }, + { "var": "isTakeoffStand", "state": "takeoffStand" }, + { "var": "isTakeoffRun", "state": "takeoffRun" }, + { "var": "isInAirStand", "state": "inAirStand" }, + { "var": "isInAirRun", "state": "inAirRun" } ] }, { @@ -353,7 +381,11 @@ { "var": "isMovingLeft", "state": "strafeLeft" }, { "var": "isTurningRight", "state": "turnRight" }, { "var": "isAway", "state": "awayIntro" }, - { "var": "isFlying", "state": "fly" } + { "var": "isFlying", "state": "fly" }, + { "var": "isTakeoffStand", "state": "takeoffStand" }, + { "var": "isTakeoffRun", "state": "takeoffRun" }, + { "var": "isInAirStand", "state": "inAirStand" }, + { "var": "isInAirRun", "state": "inAirRun" } ] }, { @@ -361,6 +393,7 @@ "interpTarget": 30, "interpDuration": 30, "transitions": [ + { "var": "isNotAway", "state": "awayOutro" }, { "var": "awayIntroOnDone", "state": "away"} ] }, @@ -385,8 +418,47 @@ "interpTarget": 6, "interpDuration": 6, "transitions": [ + { "var": "isAway", "state": "awayIntro" }, { "var": "isNotFlying", "state": "idle" } ] + }, + { + "id": "takeoffStand", + "interpTarget": 0, + "interpDuration": 6, + "transitions": [ + { "var": "isAway", "state": "awayIntro" }, + { "var": "isNotTakeoff", "state": "inAirStand" } + ] + }, + { + "id": "takeoffRun", + "interpTarget": 0, + "interpDuration": 6, + "transitions": [ + { "var": "isAway", "state": "awayIntro" }, + { "var": "isNotTakeoff", "state": "inAirRun" } + ] + }, + { + "id": "inAirStand", + "interpTarget": 0, + "interpDuration": 6, + "interpType": "snapshotPrev", + "transitions": [ + { "var": "isAway", "state": "awayIntro" }, + { "var": "isNotInAir", "state": "idle" } + ] + }, + { + "id": "inAirRun", + "interpTarget": 0, + "interpDuration": 6, + "interpType": "snapshotPrev", + "transitions": [ + { "var": "isAway", "state": "awayIntro" }, + { "var": "isNotInAir", "state": "idle" } + ] } ] }, @@ -685,6 +757,122 @@ "loopFlag": true }, "children": [] + }, + { + "id": "takeoffStand", + "type": "clip", + "data": { + "url": "http://hifi-content.s3.amazonaws.com/ozan/dev/anim/standard_anims_160127/jump_standing_takeoff.fbx", + "startFrame": 17.0, + "endFrame": 25.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "takeoffRun", + "type": "clip", + "data": { + "url": "http://hifi-content.s3.amazonaws.com/ozan/dev/anim/standard_anims_160127/jump_takeoff.fbx", + "startFrame": 1.0, + "endFrame": 2.5, + "timeScale": 0.01, + "loopFlag": false + }, + "children": [] + }, + { + "id": "inAirStand", + "type": "blendLinear", + "data": { + "alpha": 0.0, + "alphaVar": "inAirAlpha" + }, + "children": [ + { + "id": "inAirStandPreApex", + "type": "clip", + "data": { + "url": "http://hifi-content.s3.amazonaws.com/ozan/dev/anim/standard_anims_160127/jump_standing_apex.fbx", + "startFrame": 0.0, + "endFrame": 0.0, + "timeScale": 0.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "inAirStandApex", + "type": "clip", + "data": { + "url": "http://hifi-content.s3.amazonaws.com/ozan/dev/anim/standard_anims_160127/jump_standing_apex.fbx", + "startFrame": 1.0, + "endFrame": 1.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "inAirStandPostApex", + "type": "clip", + "data": { + "url": "http://hifi-content.s3.amazonaws.com/ozan/dev/anim/standard_anims_160127/jump_standing_apex.fbx", + "startFrame": 2.0, + "endFrame": 2.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + } + ] + }, + { + "id": "inAirRun", + "type": "blendLinear", + "data": { + "alpha": 0.0, + "alphaVar": "inAirAlpha" + }, + "children": [ + { + "id": "inAirRunPreApex", + "type": "clip", + "data": { + "url": "http://hifi-content.s3.amazonaws.com/ozan/dev/anim/standard_anims_160127/jump_in_air.fbx", + "startFrame": 0.0, + "endFrame": 0.0, + "timeScale": 0.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "inAirRunApex", + "type": "clip", + "data": { + "url": "http://hifi-content.s3.amazonaws.com/ozan/dev/anim/standard_anims_160127/jump_in_air.fbx", + "startFrame": 6.0, + "endFrame": 6.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "inAirRunPostApex", + "type": "clip", + "data": { + "url": "http://hifi-content.s3.amazonaws.com/ozan/dev/anim/standard_anims_160127/jump_in_air.fbx", + "startFrame": 11.0, + "endFrame": 11.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + } + ] } ] } diff --git a/interface/resources/qml/controls/CheckBox.qml b/interface/resources/qml/controls/CheckBox.qml index fe836a0e89..91586299a7 100644 --- a/interface/resources/qml/controls/CheckBox.qml +++ b/interface/resources/qml/controls/CheckBox.qml @@ -10,7 +10,8 @@ Original.CheckBox { } label: Text { text: control.text + renderType: Text.QtRendering } } - -} \ No newline at end of file + +} diff --git a/interface/resources/qml/controls/TextField.qml b/interface/resources/qml/controls/TextField.qml new file mode 100644 index 0000000000..df536a1de5 --- /dev/null +++ b/interface/resources/qml/controls/TextField.qml @@ -0,0 +1,7 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 + +TextField { + style: TextFieldStyle { renderType: Text.QtRendering } +} diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index 7fb5cd3127..0286c45ac3 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -262,11 +262,10 @@ FocusScope { } Component { id: fileDialogBuilder; FileDialog { } } - function fileOpenDialog(properties) { + function fileDialog(properties) { return fileDialogBuilder.createObject(desktop, properties); } - MenuMouseHandler { id: menuPopperUpper } function popupMenu(point) { menuPopperUpper.popup(desktop, rootMenu.items, point); diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 96a0c95e88..10dd4897e3 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -2,6 +2,8 @@ import QtQuick 2.0 import QtQuick.Controls 1.4 import Qt.labs.folderlistmodel 2.1 import Qt.labs.settings 1.0 +import QtQuick.Controls.Styles 1.4 +import QtQuick.Dialogs 1.2 as OriginalDialogs import ".." import "../windows" @@ -34,18 +36,26 @@ ModalWindow { // Set from OffscreenUi::getOpenFile() property int options; // <-- FIXME unused + property bool selectDirectory: false; property bool showHidden: false; // FIXME implement property bool multiSelect: false; - // FIXME implement property bool saveDialog: false; property var helper: fileDialogHelper property alias model: fileTableView.model + property var drives: helper.drives() signal selectedFile(var file); signal canceled(); + Component.onCompleted: { + console.log("Helper " + helper + " drives " + drives) + drivesSelector.onCurrentTextChanged.connect(function(){ + root.dir = helper.pathToUrl(drivesSelector.currentText); + }) + } + Rectangle { anchors.fill: parent color: "white" @@ -77,11 +87,22 @@ ModalWindow { size: 32 onClicked: d.navigateHome(); } + + VrControls.ComboBox { + id: drivesSelector + width: 48 + height: homeButton.height + model: drives + visible: drives.length > 1 + currentIndex: 0 + + } } TextField { id: currentDirectory height: homeButton.height + style: TextFieldStyle { renderType: Text.QtRendering } anchors { left: navControls.right; right: parent.right; top: parent.top; margins: 8 } property var lastValidFolder: helper.urlToPath(model.folder) onLastValidFolderChanged: text = lastValidFolder; @@ -122,6 +143,8 @@ ModalWindow { currentSelectionIsFolder = fileTableView.model.isFolder(row); if (root.selectDirectory || !currentSelectionIsFolder) { currentSelection.text = helper.urlToPath(currentSelectionUrl); + } else { + currentSelection.text = "" } } @@ -173,17 +196,19 @@ ModalWindow { if (isFolder) { fileTableView.model.folder = file } else { - root.selectedFile(file); - root.destroy(); + okAction.trigger(); } } } TextField { id: currentSelection + style: TextFieldStyle { renderType: Text.QtRendering } anchors { right: root.selectDirectory ? parent.right : selectionType.left; rightMargin: 8; left: parent.left; leftMargin: 8; top: selectionType.top } - readOnly: true - activeFocusOnTab: false + readOnly: !root.saveDialog + activeFocusOnTab: !readOnly + onActiveFocusChanged: if (activeFocus) { selectAll(); } + onAccepted: okAction.trigger(); } FileTypeSelection { @@ -203,25 +228,91 @@ ModalWindow { spacing: 8 Button { id: openButton - text: root.selectDirectory ? "Choose" : "Open" - enabled: currentSelection.text ? true : false - onClicked: { selectedFile(d.currentSelectionUrl); root.visible = false; } - Keys.onReturnPressed: { selectedFile(d.currentSelectionUrl); root.visible = false; } - + action: okAction + Keys.onReturnPressed: okAction.trigger() KeyNavigation.up: selectionType KeyNavigation.left: selectionType KeyNavigation.right: cancelButton } Button { id: cancelButton - text: "Cancel" + action: cancelAction KeyNavigation.up: selectionType KeyNavigation.left: openButton KeyNavigation.right: fileTableView.contentItem Keys.onReturnPressed: { canceled(); root.enabled = false } - onClicked: { canceled(); root.visible = false; } } } + + Action { + id: okAction + text: root.saveDialog ? "Save" : (root.selectDirectory ? "Choose" : "Open") + enabled: currentSelection.text ? true : false + onTriggered: okActionTimer.start(); + } + + Timer { + id: okActionTimer + interval: 50 + running: false + repeat: false + onTriggered: { + if (!root.saveDialog) { + selectedFile(d.currentSelectionUrl); + root.destroy() + return; + } + + + // Handle the ambiguity between different cases + // * typed name (with or without extension) + // * full path vs relative vs filename only + var selection = helper.saveHelper(currentSelection.text, root.dir, selectionType.currentFilter); + + if (!selection) { + desktop.messageBox({ icon: OriginalDialogs.StandardIcon.Warning, text: "Unable to parse selection" }) + return; + } + + if (helper.urlIsDir(selection)) { + root.dir = selection; + currentSelection.text = ""; + return; + } + + // Check if the file is a valid target + if (!helper.urlIsWritable(selection)) { + desktop.messageBox({ + icon: OriginalDialogs.StandardIcon.Warning, + buttons: OriginalDialogs.StandardButton.Yes | OriginalDialogs.StandardButton.No, + text: "Unable to write to location " + selection + }) + return; + } + + if (helper.urlExists(selection)) { + var messageBox = desktop.messageBox({ + icon: OriginalDialogs.StandardIcon.Question, + buttons: OriginalDialogs.StandardButton.Yes | OriginalDialogs.StandardButton.No, + text: "Do you wish to overwrite " + selection + "?", + }); + var result = messageBox.exec(); + if (OriginalDialogs.StandardButton.Yes !== result) { + return; + } + } + + console.log("Selecting " + selection) + selectedFile(selection); + root.destroy(); + } + } + + Action { + id: cancelAction + text: "Cancel" + onTriggered: { canceled(); root.visible = false; } + } } Keys.onPressed: { diff --git a/interface/resources/qml/dialogs/MessageDialog.qml b/interface/resources/qml/dialogs/MessageDialog.qml index ca00da1aa3..3b7cc2c9a8 100644 --- a/interface/resources/qml/dialogs/MessageDialog.qml +++ b/interface/resources/qml/dialogs/MessageDialog.qml @@ -25,6 +25,10 @@ ModalWindow { destroy(); } + function exec() { + return OffscreenUi.waitForMessageBoxResult(root); + } + property alias detailedText: detailedText.text property alias text: mainTextContainer.text property alias informativeText: informativeTextContainer.text diff --git a/interface/resources/qml/dialogs/QueryDialog.qml b/interface/resources/qml/dialogs/QueryDialog.qml index fecd69a4c6..948bbb1295 100644 --- a/interface/resources/qml/dialogs/QueryDialog.qml +++ b/interface/resources/qml/dialogs/QueryDialog.qml @@ -62,7 +62,7 @@ ModalWindow { Item { anchors { top: mainTextContainer.bottom; bottom: buttons.top; left: parent.left; right: parent.right; margins: d.spacing } // FIXME make a text field type that can be bound to a history for autocompletion - TextField { + VrControls.TextField { id: textResult focus: items ? false : true visible: items ? false : true diff --git a/interface/resources/qml/dialogs/RunningScripts.qml b/interface/resources/qml/dialogs/RunningScripts.qml index 8b148c6bef..c9c9062bbd 100644 --- a/interface/resources/qml/dialogs/RunningScripts.qml +++ b/interface/resources/qml/dialogs/RunningScripts.qml @@ -219,7 +219,7 @@ Window { } } - TextField { + HifiControls.TextField { id: filterEdit anchors.left: parent.left anchors.right: parent.right @@ -252,7 +252,7 @@ Window { TableViewColumn { title: "Name"; role: "display"; } } - TextField { + HifiControls.TextField { id: selectedScript readOnly: true anchors.left: parent.left diff --git a/interface/resources/qml/dialogs/fileDialog/FileTypeSelection.qml b/interface/resources/qml/dialogs/fileDialog/FileTypeSelection.qml index a3ab652e32..57ad2028ad 100644 --- a/interface/resources/qml/dialogs/fileDialog/FileTypeSelection.qml +++ b/interface/resources/qml/dialogs/fileDialog/FileTypeSelection.qml @@ -17,9 +17,13 @@ VrControls.ComboBox { onCurrentTextChanged: { var globRegex = /\((.*)\)$/ var globs = globRegex.exec(currentText); - if (!globs[1]) { - console.warn("Unable to parse filter " + currentText); - return; + if (!globs || !globs[1]) { + globRegex = /^(\*.*)$/ + globs = globRegex.exec(currentText); + if (!globs || !globs[1]) { + console.warn("Unable to parse filter " + currentText); + return; + } } currentFilter = globs[1].split(" "); } diff --git a/interface/resources/qml/dialogs/preferences/AvatarPreference.qml b/interface/resources/qml/dialogs/preferences/AvatarPreference.qml index 1375fc392a..a92392799d 100644 --- a/interface/resources/qml/dialogs/preferences/AvatarPreference.qml +++ b/interface/resources/qml/dialogs/preferences/AvatarPreference.qml @@ -1,5 +1,6 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 Preference { id: root @@ -50,6 +51,7 @@ Preference { id: dataTextField placeholderText: root.placeholderText text: preference.value + style: TextFieldStyle { renderType: Text.QtRendering } anchors { top: labelText.bottom left: parent.left diff --git a/interface/resources/qml/dialogs/preferences/BrowsablePreference.qml b/interface/resources/qml/dialogs/preferences/BrowsablePreference.qml index 7d310be468..637cc6c02c 100644 --- a/interface/resources/qml/dialogs/preferences/BrowsablePreference.qml +++ b/interface/resources/qml/dialogs/preferences/BrowsablePreference.qml @@ -1,5 +1,6 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 import "../../dialogs" @@ -30,6 +31,7 @@ Preference { id: dataTextField placeholderText: root.placeholderText text: preference.value + style: TextFieldStyle { renderType: Text.QtRendering } anchors { top: labelText.bottom left: parent.left diff --git a/interface/resources/qml/dialogs/preferences/CheckBoxPreference.qml b/interface/resources/qml/dialogs/preferences/CheckBoxPreference.qml index ab37f30427..f28f3ab90b 100644 --- a/interface/resources/qml/dialogs/preferences/CheckBoxPreference.qml +++ b/interface/resources/qml/dialogs/preferences/CheckBoxPreference.qml @@ -1,5 +1,6 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 +import "../../controls" Preference { id: root diff --git a/interface/resources/qml/dialogs/preferences/EditablePreference.qml b/interface/resources/qml/dialogs/preferences/EditablePreference.qml index 53dc7ed5d0..ddd5600bca 100644 --- a/interface/resources/qml/dialogs/preferences/EditablePreference.qml +++ b/interface/resources/qml/dialogs/preferences/EditablePreference.qml @@ -1,5 +1,6 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 Preference { id: root @@ -24,6 +25,7 @@ Preference { TextField { id: dataTextField placeholderText: preference.placeholderText + style: TextFieldStyle { renderType: Text.QtRendering } anchors { top: labelText.bottom left: parent.left diff --git a/interface/resources/qml/hifi/Desktop.qml b/interface/resources/qml/hifi/Desktop.qml index 9d527c697a..5951101194 100644 --- a/interface/resources/qml/hifi/Desktop.qml +++ b/interface/resources/qml/hifi/Desktop.qml @@ -1,5 +1,6 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 +import QtWebEngine 1.1; import "../desktop" import ".." @@ -7,6 +8,13 @@ import ".." Desktop { id: desktop + Component.onCompleted: { + WebEngine.settings.javascriptCanOpenWindows = false; + WebEngine.settings.javascriptCanAccessClipboard = false; + WebEngine.settings.spatialNavigationEnabled = true; + WebEngine.settings.localContentCanAccessRemoteUrls = true; + } + // The tool window, one instance property alias toolWindow: toolWindow ToolWindow { id: toolWindow } diff --git a/interface/resources/qml/hifi/dialogs/AvatarAttachmentsDialog.qml b/interface/resources/qml/hifi/dialogs/AvatarAttachmentsDialog.qml index 252e4c629e..5de0864df5 100644 --- a/interface/resources/qml/hifi/dialogs/AvatarAttachmentsDialog.qml +++ b/interface/resources/qml/hifi/dialogs/AvatarAttachmentsDialog.qml @@ -1,6 +1,7 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 import QtQuick.XmlListModel 2.0 +import QtQuick.Controls.Styles 1.4 import "../../windows" import "../../js/Utils.js" as Utils @@ -27,6 +28,7 @@ ModalWindow { TextField { id: filterEdit anchors { left: parent.left; right: parent.right; top: parent.top } + style: TextFieldStyle { renderType: Text.QtRendering } placeholderText: "filter" onTextChanged: tableView.model.filter = text } diff --git a/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml b/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml index 252e4c629e..b103279d4c 100644 --- a/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml +++ b/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml @@ -1,6 +1,7 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 import QtQuick.XmlListModel 2.0 +import QtQuick.Controls.Styles 1.4 import "../../windows" import "../../js/Utils.js" as Utils @@ -26,6 +27,7 @@ ModalWindow { TextField { id: filterEdit + style: TextFieldStyle { renderType: Text.QtRendering } anchors { left: parent.left; right: parent.right; top: parent.top } placeholderText: "filter" onTextChanged: tableView.model.filter = text diff --git a/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml b/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml index 31a1895e58..8dc5d8ba4b 100644 --- a/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml +++ b/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml @@ -1,5 +1,6 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 import "../../../windows" import "../../../controls" as VrControls @@ -31,7 +32,7 @@ Item { height: modelChooserButton.height anchors { left: parent.left; right: parent.right; } Text { id: urlLabel; text: "Model URL:"; width: 80; anchors.verticalCenter: modelUrl.verticalCenter } - TextField { + VrControls.TextField { id: modelUrl; height: jointChooser.height; anchors { left: urlLabel.right; leftMargin: 8; rightMargin: 8; right: modelChooserButton.left } diff --git a/interface/resources/qml/windows/DefaultFrame.qml b/interface/resources/qml/windows/DefaultFrame.qml index a082db5a92..c58f9ca545 100644 --- a/interface/resources/qml/windows/DefaultFrame.qml +++ b/interface/resources/qml/windows/DefaultFrame.qml @@ -6,8 +6,10 @@ import "../controls" Frame { id: frame + property bool wideTopMargin: (window && (window.closable || window.title)); + Rectangle { - anchors { margins: -iconSize; topMargin: -iconSize * ((window && window.closable) ? 2 : 1); } + anchors { margins: -iconSize; topMargin: -iconSize * (wideTopMargin ? 2 : 1); } anchors.fill: parent; color: "#7f7f7f7f"; radius: 3; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index ab5edec527..bf145d0e29 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -348,7 +348,7 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); - DependencyManager::set(); + DependencyManager::set(true); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -1362,6 +1362,9 @@ void Application::paintGL() { renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; renderArgs._blitFramebuffer = DependencyManager::get()->getSelfieFramebuffer(); + auto inputs = AvatarInputs::getInstance(); + _mirrorViewRect.moveTo(inputs->x(), inputs->y()); + renderRearViewMirror(&renderArgs, _mirrorViewRect); renderArgs._blitFramebuffer.reset(); @@ -4612,22 +4615,6 @@ void Application::activeChanged(Qt::ApplicationState state) { break; } } -void Application::showFriendsWindow() { - const QString FRIENDS_WINDOW_OBJECT_NAME = "FriendsWindow"; - const QString FRIENDS_WINDOW_TITLE = "Add/Remove Friends"; - const QString FRIENDS_WINDOW_URL = "https://metaverse.highfidelity.com/user/friends"; - const int FRIENDS_WINDOW_WIDTH = 290; - const int FRIENDS_WINDOW_HEIGHT = 500; - auto webWindowClass = _window->findChildren(FRIENDS_WINDOW_OBJECT_NAME); - if (webWindowClass.empty()) { - auto friendsWindow = new WebWindowClass(FRIENDS_WINDOW_TITLE, FRIENDS_WINDOW_URL, FRIENDS_WINDOW_WIDTH, - FRIENDS_WINDOW_HEIGHT); - friendsWindow->setParent(_window); - friendsWindow->setObjectName(FRIENDS_WINDOW_OBJECT_NAME); - connect(friendsWindow, &WebWindowClass::closed, &WebWindowClass::deleteLater); - friendsWindow->setVisible(true); - } -} void Application::postLambdaEvent(std::function f) { if (this->thread() == QThread::currentThread()) { diff --git a/interface/src/Application.h b/interface/src/Application.h index 4b32d8879b..8aa2de53a5 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -167,11 +167,11 @@ public: virtual controller::ScriptingInterface* getControllerScriptingInterface() { return _controllerScriptingInterface; } virtual void registerScriptEngineWithApplicationServices(ScriptEngine* scriptEngine) override; - virtual ViewFrustum* getCurrentViewFrustum() { return getDisplayViewFrustum(); } - virtual QThread* getMainThread() { return thread(); } - virtual PickRay computePickRay(float x, float y) const; - virtual glm::vec3 getAvatarPosition() const; - virtual qreal getDevicePixelRatio(); + virtual ViewFrustum* getCurrentViewFrustum() override { return getDisplayViewFrustum(); } + virtual QThread* getMainThread() override { return thread(); } + virtual PickRay computePickRay(float x, float y) const override; + virtual glm::vec3 getAvatarPosition() const override; + virtual qreal getDevicePixelRatio() override; void setActiveDisplayPlugin(const QString& pluginName); @@ -245,8 +245,6 @@ public slots: void handleLocalServerConnection(); void readArgumentsFromLocalSocket(); - void showFriendsWindow(); - void packageModel(); void openUrl(const QUrl& url); diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index a552b77f1f..b67d176852 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -214,7 +214,7 @@ void Avatar::simulate(float deltaTime) { Head* head = getHead(); head->setPosition(headPosition); head->setScale(getUniformScale()); - head->simulate(deltaTime, false, _shouldAnimate); + head->simulate(deltaTime, false, !_shouldAnimate); } } @@ -1132,8 +1132,12 @@ glm::quat Avatar::getRightPalmRotation() const { glm::vec3 Avatar::getUncachedLeftPalmPosition() const { assert(QThread::currentThread() == thread()); // main thread access only glm::quat leftPalmRotation; - getSkeletonModel().getJointRotationInWorldFrame(getSkeletonModel().getLeftHandJointIndex(), leftPalmRotation); glm::vec3 leftPalmPosition; + if (_skeletonModel.getLeftGrabPosition(leftPalmPosition)) { + return leftPalmPosition; + } + // avatar didn't have a LeftHandMiddle1 joint, fall back on this: + getSkeletonModel().getJointRotationInWorldFrame(getSkeletonModel().getLeftHandJointIndex(), leftPalmRotation); getSkeletonModel().getLeftHandPosition(leftPalmPosition); leftPalmPosition += HAND_TO_PALM_OFFSET * glm::inverse(leftPalmRotation); return leftPalmPosition; @@ -1149,8 +1153,12 @@ glm::quat Avatar::getUncachedLeftPalmRotation() const { glm::vec3 Avatar::getUncachedRightPalmPosition() const { assert(QThread::currentThread() == thread()); // main thread access only glm::quat rightPalmRotation; - getSkeletonModel().getJointRotationInWorldFrame(getSkeletonModel().getRightHandJointIndex(), rightPalmRotation); glm::vec3 rightPalmPosition; + if (_skeletonModel.getRightGrabPosition(rightPalmPosition)) { + return rightPalmPosition; + } + // avatar didn't have a RightHandMiddle1 joint, fall back on this: + getSkeletonModel().getJointRotationInWorldFrame(getSkeletonModel().getRightHandJointIndex(), rightPalmRotation); getSkeletonModel().getRightHandPosition(rightPalmPosition); rightPalmPosition += HAND_TO_PALM_OFFSET * glm::inverse(rightPalmRotation); return rightPalmPosition; diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index db247f3e85..b23b64b384 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -164,10 +164,10 @@ public: virtual void setOrientation(const glm::quat& orientation) override; // these call through to the SpatiallyNestable versions, but they are here to expose these to javascript. - Q_INVOKABLE virtual const QUuid getParentID() const { return SpatiallyNestable::getParentID(); } - Q_INVOKABLE virtual void setParentID(const QUuid& parentID); - Q_INVOKABLE virtual quint16 getParentJointIndex() const { return SpatiallyNestable::getParentJointIndex(); } - Q_INVOKABLE virtual void setParentJointIndex(quint16 parentJointIndex); + Q_INVOKABLE virtual const QUuid getParentID() const override { return SpatiallyNestable::getParentID(); } + Q_INVOKABLE virtual void setParentID(const QUuid& parentID) override; + Q_INVOKABLE virtual quint16 getParentJointIndex() const override { return SpatiallyNestable::getParentJointIndex(); } + Q_INVOKABLE virtual void setParentJointIndex(quint16 parentJointIndex) override; // NOT thread safe, must be called on main thread. glm::vec3 getUncachedLeftPalmPosition() const; diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 833ed26cc9..e12ff8f857 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -268,17 +268,6 @@ QVector AvatarManager::getLocalLights() const { return _localLights; } -QVector AvatarManager::getAvatarIdentifiers() { - QReadLocker locker(&_hashLock); - return _avatarHash.keys().toVector(); -} - -AvatarData* AvatarManager::getAvatar(QUuid avatarID) { - // Null/Default-constructed QUuids will return MyAvatar - return getAvatarBySessionID(avatarID).get(); -} - - void AvatarManager::getObjectsToRemoveFromPhysics(VectorOfMotionStates& result) { result.clear(); result.swap(_motionStatesToRemoveFromPhysics); diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 72fcb3f862..2aff98a1d2 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -39,7 +39,7 @@ public: void init(); MyAvatar* getMyAvatar() { return _myAvatar.get(); } - AvatarSharedPointer getAvatarBySessionID(const QUuid& sessionID); + AvatarSharedPointer getAvatarBySessionID(const QUuid& sessionID) override; void updateMyAvatar(float deltaTime); void updateOtherAvatars(float deltaTime); @@ -56,9 +56,6 @@ public: Q_INVOKABLE void setLocalLights(const QVector& localLights); Q_INVOKABLE QVector getLocalLights() const; - // Currently, your own avatar will be included as the null avatar id. - Q_INVOKABLE QVector getAvatarIdentifiers(); - Q_INVOKABLE AvatarData* getAvatar(QUuid avatarID); void getObjectsToRemoveFromPhysics(VectorOfMotionStates& motionStates); diff --git a/interface/src/avatar/AvatarMotionState.h b/interface/src/avatar/AvatarMotionState.h index 715c38186b..04aa5ea57c 100644 --- a/interface/src/avatar/AvatarMotionState.h +++ b/interface/src/avatar/AvatarMotionState.h @@ -61,7 +61,7 @@ public: void addDirtyFlags(uint32_t flags) { _dirtyFlags |= flags; } - virtual void computeCollisionGroupAndMask(int16_t& group, int16_t& mask) const; + virtual void computeCollisionGroupAndMask(int16_t& group, int16_t& mask) const override; friend class AvatarManager; friend class Avatar; @@ -72,7 +72,7 @@ protected: ~AvatarMotionState(); virtual bool isReadyToComputeShape() const override { return true; } - virtual btCollisionShape* computeNewShape(); + virtual btCollisionShape* computeNewShape() override; // The AvatarMotionState keeps a RAW backpointer to its Avatar because all AvatarMotionState // instances are "owned" by their corresponding Avatar instance and are deleted in the Avatar dtor. diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 0c0b51bda2..a2c74603ed 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -298,8 +298,20 @@ void MyAvatar::update(float deltaTime) { auto audio = DependencyManager::get(); head->setAudioLoudness(audio->getLastInputLoudness()); head->setAudioAverageLoudness(audio->getAudioAverageInputLoudness()); - - simulate(deltaTime); + + simulate(deltaTime); + + currentEnergy += energyChargeRate; + currentEnergy -= getAccelerationEnergy(); + currentEnergy -= getAudioEnergy(); + + if(didTeleport()) { + currentEnergy = 0.0f; + } + currentEnergy = max(0.0f, min(currentEnergy,1.0f)); + emit energyChanged(currentEnergy); + + } extern QByteArray avatarStateToFrame(const AvatarData* _avatar); @@ -1311,21 +1323,22 @@ void MyAvatar::preRender(RenderArgs* renderArgs) { _prevShouldDrawHead = shouldDrawHead; } -const float RENDER_HEAD_CUTOFF_DISTANCE = 0.50f; +const float RENDER_HEAD_CUTOFF_DISTANCE = 0.6f; bool MyAvatar::cameraInsideHead() const { - const Head* head = getHead(); const glm::vec3 cameraPosition = qApp->getCamera()->getPosition(); - return glm::length(cameraPosition - head->getEyePosition()) < (RENDER_HEAD_CUTOFF_DISTANCE * getUniformScale()); + return glm::length(cameraPosition - getDefaultEyePosition()) < (RENDER_HEAD_CUTOFF_DISTANCE * getUniformScale()); } bool MyAvatar::shouldRenderHead(const RenderArgs* renderArgs) const { - return ((renderArgs->_renderMode != RenderArgs::DEFAULT_RENDER_MODE) || - (qApp->getCamera()->getMode() != CAMERA_MODE_FIRST_PERSON) || - !cameraInsideHead()); + bool defaultMode = renderArgs->_renderMode == RenderArgs::DEFAULT_RENDER_MODE; + bool firstPerson = qApp->getCamera()->getMode() == CAMERA_MODE_FIRST_PERSON; + bool insideHead = cameraInsideHead(); + return !defaultMode || !firstPerson || !insideHead; } void MyAvatar::updateOrientation(float deltaTime) { + // Smoothly rotate body with arrow keys float targetSpeed = _driveKeys[YAW] * _yawSpeed; if (targetSpeed != 0.0f) { @@ -1360,6 +1373,37 @@ void MyAvatar::updateOrientation(float deltaTime) { totalBodyYaw += _driveKeys[STEP_YAW]; } + // use head/HMD orientation to turn while flying + if (getCharacterController()->getState() == CharacterController::State::Hover) { + + // This is the direction the user desires to fly in. + glm::vec3 desiredFacing = getHead()->getCameraOrientation() * Vectors::UNIT_Z; + desiredFacing.y = 0.0f; + + // This is our reference frame, it is captured when the user begins to move. + glm::vec3 referenceFacing = transformVector(_sensorToWorldMatrix, _hoverReferenceCameraFacing); + referenceFacing.y = 0.0f; + referenceFacing = glm::normalize(referenceFacing); + glm::vec3 referenceRight(referenceFacing.z, 0.0f, -referenceFacing.x); + const float HOVER_FLY_ROTATION_PERIOD = 0.5f; + float tau = glm::clamp(deltaTime / HOVER_FLY_ROTATION_PERIOD, 0.0f, 1.0f); + + // new facing is a linear interpolation between the desired and reference vectors. + glm::vec3 newFacing = glm::normalize((1.0f - tau) * referenceFacing + tau * desiredFacing); + + // calcualte the signed delta yaw angle to apply so that we match our newFacing. + float sign = copysignf(1.0f, glm::dot(desiredFacing, referenceRight)); + float deltaAngle = sign * acosf(glm::clamp(glm::dot(referenceFacing, newFacing), -1.0f, 1.0f)); + + // speedFactor is 0 when we are at rest adn 1.0 when we are at max flying speed. + const float MAX_FLYING_SPEED = 30.0f; + float speedFactor = glm::min(glm::length(getVelocity()) / MAX_FLYING_SPEED, 1.0f); + + // apply our delta, but scale it by the speed factor, so we turn faster when we are flying faster. + totalBodyYaw += (speedFactor * deltaAngle * (180.0f / PI)); + } + + // update body orientation by movement inputs setOrientation(getOrientation() * glm::quat(glm::radians(glm::vec3(0.0f, totalBodyYaw, 0.0f)))); @@ -1510,7 +1554,8 @@ void MyAvatar::updatePosition(float deltaTime) { // rotate velocity into camera frame glm::quat rotation = getHead()->getCameraOrientation(); glm::vec3 localVelocity = glm::inverse(rotation) * _targetVelocity; - glm::vec3 newLocalVelocity = applyKeyboardMotor(deltaTime, localVelocity, isHovering()); + bool isHovering = _characterController.getState() == CharacterController::State::Hover; + glm::vec3 newLocalVelocity = applyKeyboardMotor(deltaTime, localVelocity, isHovering); newLocalVelocity = applyScriptedMotor(deltaTime, newLocalVelocity); // rotate back into world-frame @@ -1535,6 +1580,16 @@ void MyAvatar::updatePosition(float deltaTime) { // update _moving flag based on speed const float MOVING_SPEED_THRESHOLD = 0.01f; _moving = speed > MOVING_SPEED_THRESHOLD; + + + // capture the head rotation, in sensor space, when the user first indicates they would like to move/fly. + if (!_hoverReferenceCameraFacingIsCaptured && (fabs(_driveKeys[TRANSLATE_Z]) > 0.1f || fabs(_driveKeys[TRANSLATE_X]) > 0.1f)) { + _hoverReferenceCameraFacingIsCaptured = true; + // transform the camera facing vector into sensor space. + _hoverReferenceCameraFacing = transformVector(glm::inverse(_sensorToWorldMatrix), getHead()->getCameraOrientation() * Vectors::UNIT_Z); + } else if (_hoverReferenceCameraFacingIsCaptured && (fabs(_driveKeys[TRANSLATE_Z]) <= 0.1f && fabs(_driveKeys[TRANSLATE_X]) <= 0.1f)) { + _hoverReferenceCameraFacingIsCaptured = false; + } } void MyAvatar::updateCollisionSound(const glm::vec3 &penetration, float deltaTime, float frequency) { @@ -1579,10 +1634,6 @@ bool findAvatarAvatarPenetration(const glm::vec3 positionA, float radiusA, float return false; } -bool MyAvatar::isHovering() const { - return _characterController.isHovering(); -} - void MyAvatar::increaseSize() { if ((1.0f + SCALING_RATIO) * _targetScale < MAX_AVATAR_SCALE) { _targetScale *= (1.0f + SCALING_RATIO); @@ -1853,3 +1904,31 @@ glm::mat4 MyAvatar::FollowHelper::postPhysicsUpdate(const MyAvatar& myAvatar, co } } +float MyAvatar::getAccelerationEnergy() { + glm::vec3 velocity = getVelocity(); + int changeInVelocity = abs(velocity.length() - priorVelocity.length()); + float changeInEnergy = priorVelocity.length() * changeInVelocity * AVATAR_MOVEMENT_ENERGY_CONSTANT; + priorVelocity = velocity; + + return changeInEnergy; +} + +float MyAvatar::getEnergy() { + return currentEnergy; +} + +void MyAvatar::setEnergy(float value) { + currentEnergy = value; +} + +float MyAvatar::getAudioEnergy() { + return getAudioLoudness() * AUDIO_ENERGY_CONSTANT; +} + +bool MyAvatar::didTeleport() { + glm::vec3 pos = getPosition(); + glm::vec3 changeInPosition = pos - lastPosition; + lastPosition = pos; + return (changeInPosition.length() > MAX_AVATAR_MOVEMENT_PER_FRAME); +} + diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 760f390de9..2b1136e417 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -78,6 +78,7 @@ class MyAvatar : public Avatar { Q_PROPERTY(controller::Pose rightHandPose READ getRightHandPose) Q_PROPERTY(controller::Pose leftHandTipPose READ getLeftHandTipPose) Q_PROPERTY(controller::Pose rightHandTipPose READ getRightHandTipPose) + Q_PROPERTY(float energy READ getEnergy WRITE setEnergy) public: MyAvatar(RigPointer rig); @@ -239,8 +240,6 @@ public: glm::quat getCustomListenOrientation() { return _customListenOrientation; } void setCustomListenOrientation(glm::quat customListenOrientation) { _customListenOrientation = customListenOrientation; } - bool isHovering() const; - public slots: void increaseSize(); void decreaseSize(); @@ -278,8 +277,10 @@ signals: void transformChanged(); void newCollisionSoundURL(const QUrl& url); void collisionWithEntity(const Collision& collision); + void energyChanged(float newEnergy); void positionGoneTo(); + private: glm::vec3 getWorldBodyPosition() const; @@ -415,6 +416,21 @@ private: AtRestDetector _hmdAtRestDetector; bool _lastIsMoving { false }; + bool _hoverReferenceCameraFacingIsCaptured { false }; + glm::vec3 _hoverReferenceCameraFacing; // hmd sensor space + + float AVATAR_MOVEMENT_ENERGY_CONSTANT { 0.001f }; + float AUDIO_ENERGY_CONSTANT { 0.000001f }; + float MAX_AVATAR_MOVEMENT_PER_FRAME { 30.0f }; + float currentEnergy { 0.0f }; + float energyChargeRate { 0.003f }; + glm::vec3 priorVelocity; + glm::vec3 lastPosition; + float getAudioEnergy(); + float getAccelerationEnergy(); + float getEnergy(); + void setEnergy(float value); + bool didTeleport(); }; QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode); diff --git a/interface/src/avatar/MyCharacterController.cpp b/interface/src/avatar/MyCharacterController.cpp index c7f2945757..07156e9294 100644 --- a/interface/src/avatar/MyCharacterController.cpp +++ b/interface/src/avatar/MyCharacterController.cpp @@ -67,7 +67,7 @@ void MyCharacterController::updateShapeIfNecessary() { _rigidBody->setAngularFactor(0.0f); _rigidBody->setWorldTransform(btTransform(glmToBullet(_avatar->getOrientation()), glmToBullet(_avatar->getPosition()))); - if (_isHovering) { + if (_state == State::Hover) { _rigidBody->setGravity(btVector3(0.0f, 0.0f, 0.0f)); } else { _rigidBody->setGravity(DEFAULT_CHARACTER_GRAVITY * _currentUp); diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 6198a1b0a0..28ae27d11f 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -72,6 +72,20 @@ void SkeletonModel::initJointStates() { emit skeletonLoaded(); } +Rig::CharacterControllerState convertCharacterControllerState(CharacterController::State state) { + switch (state) { + default: + case CharacterController::State::Ground: + return Rig::CharacterControllerState::Ground; + case CharacterController::State::Takeoff: + return Rig::CharacterControllerState::Takeoff; + case CharacterController::State::InAir: + return Rig::CharacterControllerState::InAir; + case CharacterController::State::Hover: + return Rig::CharacterControllerState::Hover; + }; +} + // Called within Model::simulate call, below. void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { Head* head = _owningAvatar->getHead(); @@ -133,7 +147,8 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { _rig->updateFromHandParameters(handParams, deltaTime); - _rig->computeMotionAnimationState(deltaTime, _owningAvatar->getPosition(), _owningAvatar->getVelocity(), _owningAvatar->getOrientation(), myAvatar->isHovering()); + Rig::CharacterControllerState ccState = convertCharacterControllerState(myAvatar->getCharacterController()->getState()); + _rig->computeMotionAnimationState(deltaTime, _owningAvatar->getPosition(), _owningAvatar->getVelocity(), _owningAvatar->getOrientation(), ccState); // evaluate AnimGraph animation and update jointStates. Model::updateRig(deltaTime, parentTransform); @@ -235,6 +250,56 @@ void SkeletonModel::applyPalmData(int jointIndex, const PalmData& palm) { } } +bool SkeletonModel::getLeftGrabPosition(glm::vec3& position) const { + int knuckleIndex = _rig->indexOfJoint("LeftHandMiddle1"); + int handIndex = _rig->indexOfJoint("LeftHand"); + if (knuckleIndex >= 0 && handIndex >= 0) { + glm::quat handRotation; + glm::vec3 knucklePosition; + glm::vec3 handPosition; + if (!getJointPositionInWorldFrame(knuckleIndex, knucklePosition)) { + return false; + } + if (!getJointPositionInWorldFrame(handIndex, handPosition)) { + return false; + } + if (!getJointRotationInWorldFrame(handIndex, handRotation)) { + return false; + } + float halfPalmLength = glm::distance(knucklePosition, handPosition) * 0.5f; + // z azis is standardized to be out of the palm. move from the knuckle-joint away from the palm + // by 1/2 the palm length. + position = knucklePosition + handRotation * (glm::vec3(0.0f, 0.0f, 1.0f) * halfPalmLength); + return true; + } + return false; +} + +bool SkeletonModel::getRightGrabPosition(glm::vec3& position) const { + int knuckleIndex = _rig->indexOfJoint("RightHandMiddle1"); + int handIndex = _rig->indexOfJoint("RightHand"); + if (knuckleIndex >= 0 && handIndex >= 0) { + glm::quat handRotation; + glm::vec3 knucklePosition; + glm::vec3 handPosition; + if (!getJointPositionInWorldFrame(knuckleIndex, knucklePosition)) { + return false; + } + if (!getJointPositionInWorldFrame(handIndex, handPosition)) { + return false; + } + if (!getJointRotationInWorldFrame(handIndex, handRotation)) { + return false; + } + float halfPalmLength = glm::distance(knucklePosition, handPosition) * 0.5f; + // z azis is standardized to be out of the palm. move from the knuckle-joint away from the palm + // by 1/2 the palm length. + position = knucklePosition + handRotation * (glm::vec3(0.0f, 0.0f, 1.0f) * halfPalmLength); + return true; + } + return false; +} + bool SkeletonModel::getLeftHandPosition(glm::vec3& position) const { return getJointPositionInWorldFrame(getLeftHandJointIndex(), position); } diff --git a/interface/src/avatar/SkeletonModel.h b/interface/src/avatar/SkeletonModel.h index 7541a002dc..b57d54020d 100644 --- a/interface/src/avatar/SkeletonModel.h +++ b/interface/src/avatar/SkeletonModel.h @@ -39,6 +39,9 @@ public: /// Returns the index of the right hand joint, or -1 if not found. int getRightHandJointIndex() const { return isActive() ? _geometry->getFBXGeometry().rightHandJointIndex : -1; } + bool getLeftGrabPosition(glm::vec3& position) const; + bool getRightGrabPosition(glm::vec3& position) const; + /// Retrieve the position of the left hand /// \return true whether or not the position was found bool getLeftHandPosition(glm::vec3& position) const; diff --git a/interface/src/scripting/GlobalServicesScriptingInterface.cpp b/interface/src/scripting/GlobalServicesScriptingInterface.cpp index e764473107..7dac0247bd 100644 --- a/interface/src/scripting/GlobalServicesScriptingInterface.cpp +++ b/interface/src/scripting/GlobalServicesScriptingInterface.cpp @@ -144,6 +144,3 @@ void GlobalServicesScriptingInterface::updateDownloadInfo() { emit downloadInfoChanged(getDownloadInfo()); } -void GlobalServicesScriptingInterface::editFriends() { - QMetaObject::invokeMethod(qApp, "showFriendsWindow"); -} diff --git a/interface/src/scripting/GlobalServicesScriptingInterface.h b/interface/src/scripting/GlobalServicesScriptingInterface.h index e38bfc0eb6..11d8735187 100644 --- a/interface/src/scripting/GlobalServicesScriptingInterface.h +++ b/interface/src/scripting/GlobalServicesScriptingInterface.h @@ -45,8 +45,7 @@ public: public slots: DownloadInfoResult getDownloadInfo(); void updateDownloadInfo(); - void editFriends(); - + private slots: void loggedOut(); void checkDownloadInfo(); diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 73c0098995..3e8d0d0360 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -9,29 +9,20 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include -#include -#include -#include +#include #include #include -#include #include "Application.h" #include "DomainHandler.h" #include "MainWindow.h" #include "Menu.h" #include "OffscreenUi.h" -#include "ui/ModelsBrowser.h" #include "WebWindowClass.h" #include "WindowScriptingInterface.h" -WindowScriptingInterface::WindowScriptingInterface() : - _editDialog(NULL), - _nonBlockingFormActive(false), - _formResult(QDialog::Rejected) -{ +WindowScriptingInterface::WindowScriptingInterface() { const DomainHandler& domainHandler = DependencyManager::get()->getDomainHandler(); connect(&domainHandler, &DomainHandler::connectedToDomain, this, &WindowScriptingInterface::domainChanged); connect(qApp, &Application::domainConnectionRefused, this, &WindowScriptingInterface::domainConnectionRefused); @@ -85,544 +76,31 @@ QScriptValue WindowScriptingInterface::getCursorPositionY() { return QCursor::pos().y(); } -QScriptValue WindowScriptingInterface::alert(const QString& message) { - QScriptValue retVal; - QMetaObject::invokeMethod(this, "showAlert", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QScriptValue, retVal), Q_ARG(const QString&, message)); - return retVal; -} - -QScriptValue WindowScriptingInterface::confirm(const QString& message) { - QScriptValue retVal; - QMetaObject::invokeMethod(this, "showConfirm", Qt::BlockingQueuedConnection, - Q_RETURN_ARG(QScriptValue, retVal), Q_ARG(const QString&, message)); - return retVal; -} - -QScriptValue WindowScriptingInterface::form(const QString& title, QScriptValue form) { - QScriptValue retVal; - QMetaObject::invokeMethod(this, "showForm", Qt::BlockingQueuedConnection, - Q_RETURN_ARG(QScriptValue, retVal), - Q_ARG(const QString&, title), Q_ARG(QScriptValue, form)); - return retVal; -} - -QScriptValue WindowScriptingInterface::prompt(const QString& message, const QString& defaultText) { - QScriptValue retVal; - QMetaObject::invokeMethod(this, "showPrompt", Qt::BlockingQueuedConnection, - Q_RETURN_ARG(QScriptValue, retVal), - Q_ARG(const QString&, message), Q_ARG(const QString&, defaultText)); - return retVal; -} - -QScriptValue WindowScriptingInterface::browse(const QString& title, const QString& directory, const QString& nameFilter) { - QScriptValue retVal; - QMetaObject::invokeMethod(this, "showBrowse", Qt::BlockingQueuedConnection, - Q_RETURN_ARG(QScriptValue, retVal), - Q_ARG(const QString&, title), Q_ARG(const QString&, directory), Q_ARG(const QString&, nameFilter)); - return retVal; -} - -QScriptValue WindowScriptingInterface::save(const QString& title, const QString& directory, const QString& nameFilter) { - QScriptValue retVal; - QMetaObject::invokeMethod(this, "showBrowse", Qt::BlockingQueuedConnection, - Q_RETURN_ARG(QScriptValue, retVal), - Q_ARG(const QString&, title), Q_ARG(const QString&, directory), Q_ARG(const QString&, nameFilter), - Q_ARG(QFileDialog::AcceptMode, QFileDialog::AcceptSave)); - return retVal; -} - -QScriptValue WindowScriptingInterface::s3Browse(const QString& nameFilter) { - QScriptValue retVal; - QMetaObject::invokeMethod(this, "showS3Browse", Qt::BlockingQueuedConnection, - Q_RETURN_ARG(QScriptValue, retVal), - Q_ARG(const QString&, nameFilter)); - return retVal; -} - -void WindowScriptingInterface::nonBlockingForm(const QString& title, QScriptValue form) { - QMetaObject::invokeMethod(this, "showNonBlockingForm", Qt::BlockingQueuedConnection, - Q_ARG(const QString&, title), Q_ARG(QScriptValue, form)); -} - -void WindowScriptingInterface::reloadNonBlockingForm(QScriptValue newValues) { - QMetaObject::invokeMethod(this, "doReloadNonBlockingForm", Qt::BlockingQueuedConnection, - Q_ARG(QScriptValue, newValues)); -} - - -QScriptValue WindowScriptingInterface::getNonBlockingFormResult(QScriptValue form) { - QScriptValue retVal; - QMetaObject::invokeMethod(this, "doGetNonBlockingFormResult", Qt::BlockingQueuedConnection, - Q_RETURN_ARG(QScriptValue, retVal), - Q_ARG(QScriptValue, form)); - return retVal; -} - -QScriptValue WindowScriptingInterface::peekNonBlockingFormResult(QScriptValue form) { - QScriptValue retVal; - QMetaObject::invokeMethod(this, "doPeekNonBlockingFormResult", Qt::BlockingQueuedConnection, - Q_RETURN_ARG(QScriptValue, retVal), - Q_ARG(QScriptValue, form)); - return retVal; -} - /// Display an alert box /// \param const QString& message message to display /// \return QScriptValue::UndefinedValue -QScriptValue WindowScriptingInterface::showAlert(const QString& message) { +void WindowScriptingInterface::alert(const QString& message) { OffscreenUi::warning("", message); - return QScriptValue::UndefinedValue; } /// Display a confirmation box with the options 'Yes' and 'No' /// \param const QString& message message to display /// \return QScriptValue `true` if 'Yes' was clicked, `false` otherwise -QScriptValue WindowScriptingInterface::showConfirm(const QString& message) { - bool confirm = false; - if (QMessageBox::Yes == OffscreenUi::question("", message)) { - confirm = true; - } - return QScriptValue(confirm); -} - -void WindowScriptingInterface::chooseDirectory() { - QPushButton* button = reinterpret_cast(sender()); - - QString title = button->property("title").toString(); - QString path = button->property("path").toString(); - QRegExp displayAs = button->property("displayAs").toRegExp(); - QRegExp validateAs = button->property("validateAs").toRegExp(); - QString errorMessage = button->property("errorMessage").toString(); - - QString directory = QFileDialog::getExistingDirectory(button, title, path); - if (directory.isEmpty()) { - return; - } - - if (!validateAs.exactMatch(directory)) { - OffscreenUi::warning(NULL, "Invalid Directory", errorMessage); - return; - } - - button->setProperty("path", directory); - - displayAs.indexIn(directory); - QString buttonText = displayAs.cap(1) != "" ? displayAs.cap(1) : "."; - button->setText(buttonText); -} - -void WindowScriptingInterface::inlineButtonClicked() { - QPushButton* button = reinterpret_cast(sender()); - QString name = button->property("name").toString(); - emit inlineButtonClicked(name); -} - -QString WindowScriptingInterface::jsRegExp2QtRegExp(QString string) { - // Converts string representation of RegExp from JavaScript format to Qt format. - return string.mid(1, string.length() - 2) // No enclosing slashes. - .replace("\\/", "/"); // No escaping of forward slash. -} - -void WindowScriptingInterface::showNonBlockingForm(const QString& title, QScriptValue form) { - if (!form.isArray() || (form.isArray() && form.property("length").toInt32() <= 0)) { - return; - } - - // what should we do if someone calls us while we still think we have a dialog showing??? - if (_nonBlockingFormActive) { - qDebug() << "Show Non-Blocking Form called when form already active."; - return; - } - - _form = form; - _editDialog = createForm(title, _form); - _nonBlockingFormActive = true; - - connect(_editDialog, SIGNAL(accepted()), this, SLOT(nonBlockingFormAccepted())); - connect(_editDialog, SIGNAL(rejected()), this, SLOT(nonBlockingFormRejected())); - - _editDialog->setModal(true); - _editDialog->show(); -} - -void WindowScriptingInterface::doReloadNonBlockingForm(QScriptValue newValues) { - if (!newValues.isArray() || (newValues.isArray() && newValues.property("length").toInt32() <= 0)) { - return; - } - - // what should we do if someone calls us while we still think we have a dialog showing??? - if (!_editDialog) { - qDebug() << "Reload Non-Blocking Form called when no form is active."; - return; - } - - for (int i = 0; i < newValues.property("length").toInt32(); ++i) { - QScriptValue item = newValues.property(i); - - if (item.property("oldIndex").isValid()) { - int oldIndex = item.property("oldIndex").toInt32(); - QScriptValue oldItem = _form.property(oldIndex); - if (oldItem.isValid()) { - QLineEdit* originalEdit = _edits[oldItem.property("editIndex").toInt32()]; - originalEdit->setText(item.property("value").toString()); - } - } - } -} - - -bool WindowScriptingInterface::nonBlockingFormActive() { - return _nonBlockingFormActive; -} - -QScriptValue WindowScriptingInterface::doPeekNonBlockingFormResult(QScriptValue array) { - QScriptValue retVal; - - int e = -1; - int d = -1; - int c = -1; - int h = -1; - for (int i = 0; i < _form.property("length").toInt32(); ++i) { - QScriptValue item = _form.property(i); - QScriptValue value = item.property("value"); - - if (item.property("button").toString() != "") { - // Nothing to do - } else if (item.property("type").toString() == "inlineButton") { - // Nothing to do - } else if (item.property("type").toString() == "header") { - // Nothing to do - } else if (item.property("directory").toString() != "") { - d += 1; - value = _directories.at(d)->property("path").toString(); - item.setProperty("directory", value); - _form.setProperty(i, item); - } else if (item.property("options").isArray()) { - c += 1; - item.setProperty("value", - _combos.at(c)->currentIndex() < item.property("options").property("length").toInt32() ? - item.property("options").property(_combos.at(c)->currentIndex()) : - array.engine()->undefinedValue() - ); - _form.setProperty(i, item); - } else if (item.property("type").toString() == "checkbox") { - h++; - value = _checks.at(h)->checkState() == Qt::Checked; - item.setProperty("value", value); - _form.setProperty(i, item); - } else { - e += 1; - bool ok = true; - if (value.isNumber()) { - value = _edits.at(e)->text().toDouble(&ok); - } else if (value.isString()) { - value = _edits.at(e)->text(); - } else if (value.isBool()) { - if (_edits.at(e)->text() == "true") { - value = true; - } else if (_edits.at(e)->text() == "false") { - value = false; - } else { - ok = false; - } - } - if (ok) { - item.setProperty("value", value); - _form.setProperty(i, item); - } - } - } - - array = _form; - return (_formResult == QDialog::Accepted); -} - -QScriptValue WindowScriptingInterface::doGetNonBlockingFormResult(QScriptValue array) { - QScriptValue retVal; - - if (_formResult == QDialog::Accepted) { - int e = -1; - int d = -1; - int c = -1; - int h = -1; - for (int i = 0; i < _form.property("length").toInt32(); ++i) { - QScriptValue item = _form.property(i); - QScriptValue value = item.property("value"); - - if (item.property("button").toString() != "") { - // Nothing to do - } else if (item.property("type").toString() == "inlineButton") { - // Nothing to do - } else if (item.property("type").toString() == "header") { - // Nothing to do - } else if (item.property("directory").toString() != "") { - d += 1; - value = _directories.at(d)->property("path").toString(); - item.setProperty("directory", value); - _form.setProperty(i, item); - } else if (item.property("options").isArray()) { - c += 1; - item.setProperty("value", - _combos.at(c)->currentIndex() < item.property("options").property("length").toInt32() ? - item.property("options").property(_combos.at(c)->currentIndex()) : - array.engine()->undefinedValue() - ); - _form.setProperty(i, item); - } else if (item.property("type").toString() == "checkbox") { - h++; - value = _checks.at(h)->checkState() == Qt::Checked; - item.setProperty("value", value); - _form.setProperty(i, item); - } else { - e += 1; - bool ok = true; - if (value.isNumber()) { - value = _edits.at(e)->text().toDouble(&ok); - } else if (value.isString()) { - value = _edits.at(e)->text(); - } else if (value.isBool()) { - if (_edits.at(e)->text() == "true") { - value = true; - } else if (_edits.at(e)->text() == "false") { - value = false; - } else { - ok = false; - } - } - if (ok) { - item.setProperty("value", value); - _form.setProperty(i, item); - } - } - } - } - - delete _editDialog; - _editDialog = NULL; - _form = QScriptValue(); - _edits.clear(); - _directories.clear(); - _combos.clear(); - _checks.clear(); - - array = _form; - return (_formResult == QDialog::Accepted); -} - - -/// Display a form layout with an edit box -/// \param const QString& title title to display -/// \param const QScriptValue form to display as an array of objects: -/// - label, value -/// - label, directory, title, display regexp, validate regexp, error message -/// - button ("Cancel") -/// \return QScriptValue `true` if 'OK' was clicked, `false` otherwise -QScriptValue WindowScriptingInterface::showForm(const QString& title, QScriptValue form) { - if (form.isArray() && form.property("length").toInt32() <= 0) { - return false; - } - QDialog* editDialog = createForm(title, form); - - int result = editDialog->exec(); - - if (result == QDialog::Accepted) { - int e = -1; - int d = -1; - int c = -1; - int h = -1; - for (int i = 0; i < form.property("length").toInt32(); ++i) { - QScriptValue item = form.property(i); - QScriptValue value = item.property("value"); - - if (item.property("button").toString() != "") { - // Nothing to do - } else if (item.property("type").toString() == "inlineButton") { - // Nothing to do - } else if (item.property("type").toString() == "header") { - // Nothing to do - } else if (item.property("directory").toString() != "") { - d += 1; - value = _directories.at(d)->property("path").toString(); - item.setProperty("directory", value); - form.setProperty(i, item); - } else if (item.property("options").isArray()) { - c += 1; - item.setProperty("value", - _combos.at(c)->currentIndex() < item.property("options").property("length").toInt32() ? - item.property("options").property(_combos.at(c)->currentIndex()) : - form.engine()->undefinedValue() - ); - form.setProperty(i, item); - } else if (item.property("type").toString() == "checkbox") { - h++; - value = _checks.at(h)->checkState() == Qt::Checked; - item.setProperty("value", value); - form.setProperty(i, item); - } else { - e += 1; - bool ok = true; - if (value.isNumber()) { - value = _edits.at(e)->text().toDouble(&ok); - } else if (value.isString()) { - value = _edits.at(e)->text(); - } else if (value.isBool()) { - if (_edits.at(e)->text() == "true") { - value = true; - } else if (_edits.at(e)->text() == "false") { - value = false; - } else { - ok = false; - } - } - if (ok) { - item.setProperty("value", value); - form.setProperty(i, item); - } - } - } - } - - delete editDialog; - _combos.clear(); - _checks.clear(); - _edits.clear(); - _directories.clear(); - return (result == QDialog::Accepted); -} - - - -QDialog* WindowScriptingInterface::createForm(const QString& title, QScriptValue form) { - QDialog* editDialog = new QDialog(qApp->getWindow()); - editDialog->setWindowTitle(title); - - bool cancelButton = false; - - QVBoxLayout* layout = new QVBoxLayout(); - editDialog->setLayout(layout); - - QScrollArea* area = new QScrollArea(); - layout->addWidget(area); - area->setWidgetResizable(true); - QWidget* container = new QWidget(); - QFormLayout* formLayout = new QFormLayout(); - container->setLayout(formLayout); - container->sizePolicy().setHorizontalStretch(1); - formLayout->setRowWrapPolicy(QFormLayout::DontWrapRows); - formLayout->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow); - formLayout->setFormAlignment(Qt::AlignHCenter | Qt::AlignTop); - formLayout->setLabelAlignment(Qt::AlignLeft); - - area->setWidget(container); - - for (int i = 0; i < form.property("length").toInt32(); ++i) { - QScriptValue item = form.property(i); - - if (item.property("button").toString() != "") { - cancelButton = cancelButton || item.property("button").toString().toLower() == "cancel"; - - } else if (item.property("directory").toString() != "") { - QString path = item.property("directory").toString(); - QString title = item.property("title").toString(); - if (title == "") { - title = "Choose Directory"; - } - QString displayAsString = item.property("displayAs").toString(); - QRegExp displayAs = QRegExp(displayAsString != "" ? jsRegExp2QtRegExp(displayAsString) : "^(.*)$"); - QString validateAsString = item.property("validateAs").toString(); - QRegExp validateAs = QRegExp(validateAsString != "" ? jsRegExp2QtRegExp(validateAsString) : ".*"); - QString errorMessage = item.property("errorMessage").toString(); - if (errorMessage == "") { - errorMessage = "Invalid directory"; - } - - QPushButton* directory = new QPushButton(displayAs.cap(1)); - directory->setProperty("title", title); - directory->setProperty("path", path); - directory->setProperty("displayAs", displayAs); - directory->setProperty("validateAs", validateAs); - directory->setProperty("errorMessage", errorMessage); - displayAs.indexIn(path); - directory->setText(displayAs.cap(1) != "" ? displayAs.cap(1) : "."); - - directory->setMinimumWidth(200); - _directories.push_back(directory); - - formLayout->addRow(new QLabel(item.property("label").toString()), directory); - connect(directory, SIGNAL(clicked(bool)), SLOT(chooseDirectory())); - - } else if (item.property("type").toString() == "inlineButton") { - QString buttonLabel = item.property("buttonLabel").toString(); - - QPushButton* inlineButton = new QPushButton(buttonLabel); - inlineButton->setMinimumWidth(200); - inlineButton->setProperty("name", item.property("name").toString()); - formLayout->addRow(new QLabel(item.property("label").toString()), inlineButton); - connect(inlineButton, SIGNAL(clicked(bool)), SLOT(inlineButtonClicked())); - - } else if (item.property("type").toString() == "header") { - formLayout->addRow(new QLabel(item.property("label").toString())); - } else if (item.property("options").isArray()) { - QComboBox* combo = new QComboBox(); - combo->setMinimumWidth(200); - qint32 options_count = item.property("options").property("length").toInt32(); - for (qint32 i = 0; i < options_count; i++) { - combo->addItem(item.property("options").property(i).toString()); - } - _combos.push_back(combo); - formLayout->addRow(new QLabel(item.property("label").toString()), combo); - } else if (item.property("type").toString() == "checkbox") { - QCheckBox* check = new QCheckBox(); - check->setTristate(false); - check->setCheckState(item.property("value").toString() == "true" ? Qt::Checked : Qt::Unchecked); - _checks.push_back(check); - formLayout->addRow(new QLabel(item.property("label").toString()), check); - } else { - QLineEdit* edit = new QLineEdit(item.property("value").toString()); - edit->setMinimumWidth(200); - int editIndex = _edits.size(); - _edits.push_back(edit); - item.setProperty("editIndex", editIndex); - formLayout->addRow(new QLabel(item.property("label").toString()), edit); - } - } - - QDialogButtonBox* buttons = new QDialogButtonBox( - QDialogButtonBox::Ok - | (cancelButton ? QDialogButtonBox::Cancel : QDialogButtonBox::NoButton) - ); - connect(buttons, SIGNAL(accepted()), editDialog, SLOT(accept())); - connect(buttons, SIGNAL(rejected()), editDialog, SLOT(reject())); - layout->addWidget(buttons); - - return editDialog; +QScriptValue WindowScriptingInterface::confirm(const QString& message) { + return QScriptValue((QMessageBox::Yes == OffscreenUi::question("", message))); } /// Display a prompt with a text box /// \param const QString& message message to display /// \param const QString& defaultText default text in the text box /// \return QScriptValue string text value in text box if the dialog was accepted, `null` otherwise. -QScriptValue WindowScriptingInterface::showPrompt(const QString& message, const QString& defaultText) { - QInputDialog promptDialog(qApp->getWindow()); - promptDialog.setWindowTitle(""); - promptDialog.setLabelText(message); - promptDialog.setTextValue(defaultText); - promptDialog.setFixedSize(600, 200); - - if (promptDialog.exec() == QDialog::Accepted) { - return QScriptValue(promptDialog.textValue()); - } - return QScriptValue::NullValue; +QScriptValue WindowScriptingInterface::prompt(const QString& message, const QString& defaultText) { + bool ok = false; + QString result = OffscreenUi::getText(nullptr, "", message, QLineEdit::Normal, defaultText, &ok); + return ok ? QScriptValue(result) : QScriptValue::NullValue; } -/// Display a file dialog. If `directory` is an invalid file or directory the browser will start at the current -/// working directory. -/// \param const QString& title title of the window -/// \param const QString& directory directory to start the file browser at -/// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog` -/// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue` -QScriptValue WindowScriptingInterface::showBrowse(const QString& title, const QString& directory, const QString& nameFilter, - QFileDialog::AcceptMode acceptMode) { +QString fixupPathForMac(const QString& directory) { // On OS X `directory` does not work as expected unless a file is included in the path, so we append a bogus // filename if the directory is valid. QString path = ""; @@ -631,35 +109,31 @@ QScriptValue WindowScriptingInterface::showBrowse(const QString& title, const QS fileInfo.setFile(directory, "__HIFI_INVALID_FILE__"); path = fileInfo.filePath(); } - - QFileDialog fileDialog(qApp->getWindow(), title, path, nameFilter); - fileDialog.setAcceptMode(acceptMode); - QUrl fileUrl(directory); - if (acceptMode == QFileDialog::AcceptSave) { - // TODO -- Setting this breaks the dialog on Linux. Does it help something on other platforms? - // fileDialog.setFileMode(QFileDialog::Directory); - fileDialog.selectFile(fileUrl.fileName()); - } - if (fileDialog.exec()) { - return QScriptValue(fileDialog.selectedFiles().first()); - } - return QScriptValue::NullValue; + return path; } -/// Display a browse window for S3 models -/// \param const QString& nameFilter filter to filter filenames +/// Display an open file dialog. If `directory` is an invalid file or directory the browser will start at the current +/// working directory. +/// \param const QString& title title of the window +/// \param const QString& directory directory to start the file browser at +/// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog` /// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue` -QScriptValue WindowScriptingInterface::showS3Browse(const QString& nameFilter) { - ModelsBrowser browser(FSTReader::ENTITY_MODEL); - if (nameFilter != "") { - browser.setNameFilter(nameFilter); - } - QEventLoop loop; - connect(&browser, &ModelsBrowser::selected, &loop, &QEventLoop::quit); - QMetaObject::invokeMethod(&browser, "browse", Qt::QueuedConnection); - loop.exec(); - - return browser.getSelectedFile(); +QScriptValue WindowScriptingInterface::browse(const QString& title, const QString& directory, const QString& nameFilter) { + QString path = fixupPathForMac(directory); + QString result = OffscreenUi::getOpenFileName(nullptr, title, path, nameFilter); + return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result); +} + +/// Display a save file dialog. If `directory` is an invalid file or directory the browser will start at the current +/// working directory. +/// \param const QString& title title of the window +/// \param const QString& directory directory to start the file browser at +/// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog` +/// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue` +QScriptValue WindowScriptingInterface::save(const QString& title, const QString& directory, const QString& nameFilter) { + QString path = fixupPathForMac(directory); + QString result = OffscreenUi::getSaveFileName(nullptr, title, path, nameFilter); + return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result); } int WindowScriptingInterface::getInnerWidth() { diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 4b287a24e9..d00e63b49e 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -12,12 +12,9 @@ #ifndef hifi_WindowScriptingInterface_h #define hifi_WindowScriptingInterface_h -#include -#include -#include -#include -#include -#include +#include +#include +#include class WebWindowClass; @@ -41,61 +38,19 @@ public slots: QScriptValue hasFocus(); void setFocus(); void raiseMainWindow(); - QScriptValue alert(const QString& message = ""); + void alert(const QString& message = ""); QScriptValue confirm(const QString& message = ""); - QScriptValue form(const QString& title, QScriptValue array); QScriptValue prompt(const QString& message = "", const QString& defaultText = ""); QScriptValue browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); - QScriptValue s3Browse(const QString& nameFilter = ""); - - void nonBlockingForm(const QString& title, QScriptValue array); - void reloadNonBlockingForm(QScriptValue array); - QScriptValue getNonBlockingFormResult(QScriptValue array); - QScriptValue peekNonBlockingFormResult(QScriptValue array); signals: void domainChanged(const QString& domainHostname); - void inlineButtonClicked(const QString& name); - void nonBlockingFormClosed(); void svoImportRequested(const QString& url); void domainConnectionRefused(const QString& reason); private slots: - QScriptValue showAlert(const QString& message); - QScriptValue showConfirm(const QString& message); - QScriptValue showForm(const QString& title, QScriptValue form); - QScriptValue showPrompt(const QString& message, const QString& defaultText); - QScriptValue showBrowse(const QString& title, const QString& directory, const QString& nameFilter, - QFileDialog::AcceptMode acceptMode = QFileDialog::AcceptOpen); - QScriptValue showS3Browse(const QString& nameFilter); - - void showNonBlockingForm(const QString& title, QScriptValue array); - void doReloadNonBlockingForm(QScriptValue array); - bool nonBlockingFormActive(); - QScriptValue doGetNonBlockingFormResult(QScriptValue array); - QScriptValue doPeekNonBlockingFormResult(QScriptValue array); - - void chooseDirectory(); - void inlineButtonClicked(); - - void nonBlockingFormAccepted() { _nonBlockingFormActive = false; _formResult = QDialog::Accepted; emit nonBlockingFormClosed(); } - void nonBlockingFormRejected() { _nonBlockingFormActive = false; _formResult = QDialog::Rejected; emit nonBlockingFormClosed(); } - WebWindowClass* doCreateWebWindow(const QString& title, const QString& url, int width, int height); - -private: - QString jsRegExp2QtRegExp(QString string); - QDialog* createForm(const QString& title, QScriptValue form); - - QDialog* _editDialog; - QScriptValue _form; - bool _nonBlockingFormActive; - int _formResult; - QVector _combos; - QVector _checks; - QVector _edits; - QVector _directories; }; #endif // hifi_WindowScriptingInterface_h diff --git a/interface/src/ui/overlays/ImageOverlay.h b/interface/src/ui/overlays/ImageOverlay.h index 224cb42045..40da461822 100644 --- a/interface/src/ui/overlays/ImageOverlay.h +++ b/interface/src/ui/overlays/ImageOverlay.h @@ -20,7 +20,7 @@ class ImageOverlay : public QmlOverlay { public: static QString const TYPE; - virtual QString getType() const { return TYPE; } + virtual QString getType() const override { return TYPE; } static QUrl const URL; ImageOverlay(); diff --git a/interface/src/ui/overlays/TextOverlay.h b/interface/src/ui/overlays/TextOverlay.h index 7c6e133ebd..53c1805345 100644 --- a/interface/src/ui/overlays/TextOverlay.h +++ b/interface/src/ui/overlays/TextOverlay.h @@ -30,7 +30,7 @@ public: void setText(const QString& text); - TextOverlay* createClone() const; + TextOverlay* createClone() const override; QSizeF textSize(const QString& text) const; // Pixels }; diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index 90cd85e727..e8f429f22c 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -15,12 +15,13 @@ bool AnimClip::usePreAndPostPoseFromAnim = false; -AnimClip::AnimClip(const QString& id, const QString& url, float startFrame, float endFrame, float timeScale, bool loopFlag) : +AnimClip::AnimClip(const QString& id, const QString& url, float startFrame, float endFrame, float timeScale, bool loopFlag, bool mirrorFlag) : AnimNode(AnimNode::Type::Clip, id), _startFrame(startFrame), _endFrame(endFrame), _timeScale(timeScale), _loopFlag(loopFlag), + _mirrorFlag(mirrorFlag), _frame(startFrame) { loadURL(url); @@ -37,6 +38,7 @@ const AnimPoseVec& AnimClip::evaluate(const AnimVariantMap& animVars, float dt, _endFrame = animVars.lookup(_endFrameVar, _endFrame); _timeScale = animVars.lookup(_timeScaleVar, _timeScale); _loopFlag = animVars.lookup(_loopFlagVar, _loopFlag); + _mirrorFlag = animVars.lookup(_mirrorFlagVar, _mirrorFlag); float frame = animVars.lookup(_frameVar, _frame); _frame = ::accumulateTime(_startFrame, _endFrame, _timeScale, frame, dt, _loopFlag, _id, triggersOut); @@ -49,6 +51,12 @@ const AnimPoseVec& AnimClip::evaluate(const AnimVariantMap& animVars, float dt, } if (_anim.size()) { + + // lazy creation of mirrored animation frames. + if (_mirrorFlag && _anim.size() != _mirrorAnim.size()) { + buildMirrorAnim(); + } + int prevIndex = (int)glm::floor(_frame); int nextIndex; if (_loopFlag && _frame >= _endFrame) { @@ -63,8 +71,8 @@ const AnimPoseVec& AnimClip::evaluate(const AnimVariantMap& animVars, float dt, prevIndex = std::min(std::max(0, prevIndex), frameCount - 1); nextIndex = std::min(std::max(0, nextIndex), frameCount - 1); - const AnimPoseVec& prevFrame = _anim[prevIndex]; - const AnimPoseVec& nextFrame = _anim[nextIndex]; + const AnimPoseVec& prevFrame = _mirrorFlag ? _mirrorAnim[prevIndex] : _anim[prevIndex]; + const AnimPoseVec& nextFrame = _mirrorFlag ? _mirrorAnim[nextIndex] : _anim[nextIndex]; float alpha = glm::fract(_frame); ::blend(_poses.size(), &prevFrame[0], &nextFrame[0], alpha, &_poses[0]); @@ -162,9 +170,22 @@ void AnimClip::copyFromNetworkAnim() { } } + // mirrorAnim will be re-built on demand, if needed. + _mirrorAnim.clear(); + _poses.resize(skeletonJointCount); } +void AnimClip::buildMirrorAnim() { + assert(_skeleton); + + _mirrorAnim.clear(); + _mirrorAnim.reserve(_anim.size()); + for (auto& relPoses : _anim) { + _mirrorAnim.push_back(relPoses); + _skeleton->mirrorRelativePoses(_mirrorAnim.back()); + } +} const AnimPoseVec& AnimClip::getPosesInternal() const { return _poses; diff --git a/libraries/animation/src/AnimClip.h b/libraries/animation/src/AnimClip.h index 7d58ae4f9a..7989f6d172 100644 --- a/libraries/animation/src/AnimClip.h +++ b/libraries/animation/src/AnimClip.h @@ -27,7 +27,7 @@ public: static bool usePreAndPostPoseFromAnim; - AnimClip(const QString& id, const QString& url, float startFrame, float endFrame, float timeScale, bool loopFlag); + AnimClip(const QString& id, const QString& url, float startFrame, float endFrame, float timeScale, bool loopFlag, bool mirrorFlag); virtual ~AnimClip() override; virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override; @@ -36,6 +36,7 @@ public: void setEndFrameVar(const QString& endFrameVar) { _endFrameVar = endFrameVar; } void setTimeScaleVar(const QString& timeScaleVar) { _timeScaleVar = timeScaleVar; } void setLoopFlagVar(const QString& loopFlagVar) { _loopFlagVar = loopFlagVar; } + void setMirrorFlagVar(const QString& mirrorFlagVar) { _mirrorFlagVar = mirrorFlagVar; } void setFrameVar(const QString& frameVar) { _frameVar = frameVar; } float getStartFrame() const { return _startFrame; } @@ -49,12 +50,16 @@ public: bool getLoopFlag() const { return _loopFlag; } void setLoopFlag(bool loopFlag) { _loopFlag = loopFlag; } + bool getMirrorFlag() const { return _mirrorFlag; } + void setMirrorFlag(bool mirrorFlag) { _mirrorFlag = mirrorFlag; } + void loadURL(const QString& url); protected: virtual void setCurrentFrameInternal(float frame) override; void copyFromNetworkAnim(); + void buildMirrorAnim(); // for AnimDebugDraw rendering virtual const AnimPoseVec& getPosesInternal() const override; @@ -64,18 +69,21 @@ protected: // _anim[frame][joint] std::vector _anim; + std::vector _mirrorAnim; QString _url; float _startFrame; float _endFrame; float _timeScale; bool _loopFlag; + bool _mirrorFlag; float _frame; QString _startFrameVar; QString _endFrameVar; QString _timeScaleVar; QString _loopFlagVar; + QString _mirrorFlagVar; QString _frameVar; // no copies diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index 9f08ce455a..92d8240510 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -590,17 +590,24 @@ void AnimInverseKinematics::initConstraints() { stConstraint->setReferenceRotation(_defaultRelativePoses[i].rot); stConstraint->setTwistLimits(-PI / 4.0f, PI / 4.0f); - // these directions are approximate swing limits in root-frame - // NOTE: they don't need to be normalized std::vector swungDirections; - swungDirections.push_back(glm::vec3(mirror * 0.25f, 0.0f, 1.0f)); - swungDirections.push_back(glm::vec3(mirror * -0.5f, 0.0f, 1.0f)); - swungDirections.push_back(glm::vec3(mirror * -1.0f, 0.0f, 1.0f)); - swungDirections.push_back(glm::vec3(mirror * -1.0f, 0.0f, 0.0f)); - swungDirections.push_back(glm::vec3(mirror * -0.5f, -0.5f, -1.0f)); - swungDirections.push_back(glm::vec3(mirror * 0.0f, -0.75f, -1.0f)); - swungDirections.push_back(glm::vec3(mirror * 0.25f, -1.0f, 0.0f)); - swungDirections.push_back(glm::vec3(mirror * 0.25f, -1.0f, 1.0f)); + float deltaTheta = PI / 4.0f; + float theta = 0.0f; + swungDirections.push_back(glm::vec3(mirror * cosf(theta), 0.25f, sinf(theta))); + theta += deltaTheta; + swungDirections.push_back(glm::vec3(mirror * cosf(theta), 0.0f, sinf(theta))); + theta += deltaTheta; + swungDirections.push_back(glm::vec3(mirror * cosf(theta), -0.25f, sinf(theta))); // posterior + theta += deltaTheta; + swungDirections.push_back(glm::vec3(mirror * cosf(theta), 0.0f, sinf(theta))); + theta += deltaTheta; + swungDirections.push_back(glm::vec3(mirror * cosf(theta), 0.25f, sinf(theta))); + theta += deltaTheta; + swungDirections.push_back(glm::vec3(mirror * cosf(theta), 0.5f, sinf(theta))); + theta += deltaTheta; + swungDirections.push_back(glm::vec3(mirror * cosf(theta), 0.5f, sinf(theta))); // anterior + theta += deltaTheta; + swungDirections.push_back(glm::vec3(mirror * cosf(theta), 0.5f, sinf(theta))); // rotate directions into joint-frame glm::quat invAbsoluteRotation = glm::inverse(absolutePoses[i].rot); @@ -755,7 +762,7 @@ void AnimInverseKinematics::initConstraints() { // we determine the max/min angles by rotating the swing limit lines from parent- to child-frame // then measure the angles to swing the yAxis into alignment const float MIN_KNEE_ANGLE = 0.0f; - const float MAX_KNEE_ANGLE = 3.0f * PI / 4.0f; + const float MAX_KNEE_ANGLE = 7.0f * PI / 8.0f; glm::quat invReferenceRotation = glm::inverse(referenceRotation); glm::vec3 minSwingAxis = invReferenceRotation * glm::angleAxis(MIN_KNEE_ANGLE, hingeAxis) * Vectors::UNIT_Y; glm::vec3 maxSwingAxis = invReferenceRotation * glm::angleAxis(MAX_KNEE_ANGLE, hingeAxis) * Vectors::UNIT_Y; diff --git a/libraries/animation/src/AnimNodeLoader.cpp b/libraries/animation/src/AnimNodeLoader.cpp index 568da8dd63..e222de54f9 100644 --- a/libraries/animation/src/AnimNodeLoader.cpp +++ b/libraries/animation/src/AnimNodeLoader.cpp @@ -67,6 +67,16 @@ static AnimNode::Type stringToAnimNodeType(const QString& str) { return AnimNode::Type::NumTypes; } +static AnimStateMachine::InterpType stringToInterpType(const QString& str) { + if (str == "snapshotBoth") { + return AnimStateMachine::InterpType::SnapshotBoth; + } else if (str == "snapshotPrev") { + return AnimStateMachine::InterpType::SnapshotPrev; + } else { + return AnimStateMachine::InterpType::NumTypes; + } +} + static const char* animManipulatorJointVarTypeToString(AnimManipulator::JointVar::Type type) { switch (type) { case AnimManipulator::JointVar::Type::AbsoluteRotation: return "absoluteRotation"; @@ -145,6 +155,14 @@ static NodeProcessFunc animNodeTypeToProcessFunc(AnimNode::Type type) { } \ bool NAME = NAME##_VAL.toBool() +#define READ_OPTIONAL_BOOL(NAME, JSON_OBJ, DEFAULT) \ + auto NAME##_VAL = JSON_OBJ.value(#NAME); \ + bool NAME = DEFAULT; \ + if (NAME##_VAL.isBool()) { \ + NAME = NAME##_VAL.toBool(); \ + } \ + do {} while (0) + #define READ_FLOAT(NAME, JSON_OBJ, ID, URL, ERROR_RETURN) \ auto NAME##_VAL = JSON_OBJ.value(#NAME); \ if (!NAME##_VAL.isDouble()) { \ @@ -222,13 +240,15 @@ static AnimNode::Pointer loadClipNode(const QJsonObject& jsonObj, const QString& READ_FLOAT(endFrame, jsonObj, id, jsonUrl, nullptr); READ_FLOAT(timeScale, jsonObj, id, jsonUrl, nullptr); READ_BOOL(loopFlag, jsonObj, id, jsonUrl, nullptr); + READ_OPTIONAL_BOOL(mirrorFlag, jsonObj, false); READ_OPTIONAL_STRING(startFrameVar, jsonObj); READ_OPTIONAL_STRING(endFrameVar, jsonObj); READ_OPTIONAL_STRING(timeScaleVar, jsonObj); READ_OPTIONAL_STRING(loopFlagVar, jsonObj); + READ_OPTIONAL_STRING(mirrorFlagVar, jsonObj); - auto node = std::make_shared(id, url, startFrame, endFrame, timeScale, loopFlag); + auto node = std::make_shared(id, url, startFrame, endFrame, timeScale, loopFlag, mirrorFlag); if (!startFrameVar.isEmpty()) { node->setStartFrameVar(startFrameVar); @@ -242,6 +262,9 @@ static AnimNode::Pointer loadClipNode(const QJsonObject& jsonObj, const QString& if (!loopFlagVar.isEmpty()) { node->setLoopFlagVar(loopFlagVar); } + if (!mirrorFlagVar.isEmpty()) { + node->setMirrorFlagVar(mirrorFlagVar); + } return node; } @@ -465,9 +488,11 @@ bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, READ_STRING(id, stateObj, nodeId, jsonUrl, false); READ_FLOAT(interpTarget, stateObj, nodeId, jsonUrl, false); READ_FLOAT(interpDuration, stateObj, nodeId, jsonUrl, false); + READ_OPTIONAL_STRING(interpType, stateObj); READ_OPTIONAL_STRING(interpTargetVar, stateObj); READ_OPTIONAL_STRING(interpDurationVar, stateObj); + READ_OPTIONAL_STRING(interpTypeVar, stateObj); auto iter = childMap.find(id); if (iter == childMap.end()) { @@ -475,7 +500,16 @@ bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, return false; } - auto statePtr = std::make_shared(id, iter->second, interpTarget, interpDuration); + AnimStateMachine::InterpType interpTypeEnum = AnimStateMachine::InterpType::SnapshotBoth; // default value + if (!interpType.isEmpty()) { + interpTypeEnum = stringToInterpType(interpType); + if (interpTypeEnum == AnimStateMachine::InterpType::NumTypes) { + qCCritical(animation) << "AnimNodeLoader, bad interpType on stateMachine state, nodeId = " << nodeId << "stateId =" << id << "url = " << jsonUrl.toDisplayString(); + return false; + } + } + + auto statePtr = std::make_shared(id, iter->second, interpTarget, interpDuration, interpTypeEnum); assert(statePtr); if (!interpTargetVar.isEmpty()) { @@ -484,6 +518,9 @@ bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, if (!interpDurationVar.isEmpty()) { statePtr->setInterpDurationVar(interpDurationVar); } + if (!interpTypeVar.isEmpty()) { + statePtr->setInterpTypeVar(interpTypeVar); + } smNode->addState(statePtr); stateMap.insert(StateMap::value_type(statePtr->getID(), statePtr)); diff --git a/libraries/animation/src/AnimPose.cpp b/libraries/animation/src/AnimPose.cpp index 0c6af2d5bd..439ef5336f 100644 --- a/libraries/animation/src/AnimPose.cpp +++ b/libraries/animation/src/AnimPose.cpp @@ -51,6 +51,11 @@ AnimPose AnimPose::inverse() const { return AnimPose(glm::inverse(static_cast(*this))); } +// mirror about x-axis without applying negative scale. +AnimPose AnimPose::mirror() const { + return AnimPose(scale, glm::quat(rot.w, rot.x, -rot.y, -rot.z), glm::vec3(-trans.x, trans.y, trans.z)); +} + AnimPose::operator glm::mat4() const { glm::vec3 xAxis = rot * glm::vec3(scale.x, 0.0f, 0.0f); glm::vec3 yAxis = rot * glm::vec3(0.0f, scale.y, 0.0f); diff --git a/libraries/animation/src/AnimPose.h b/libraries/animation/src/AnimPose.h index 852d84ec1b..6ffa9bb321 100644 --- a/libraries/animation/src/AnimPose.h +++ b/libraries/animation/src/AnimPose.h @@ -30,6 +30,7 @@ struct AnimPose { AnimPose operator*(const AnimPose& rhs) const; AnimPose inverse() const; + AnimPose mirror() const; operator glm::mat4() const; glm::vec3 scale; diff --git a/libraries/animation/src/AnimSkeleton.cpp b/libraries/animation/src/AnimSkeleton.cpp index 8f45b785d1..2d37be9b87 100644 --- a/libraries/animation/src/AnimSkeleton.cpp +++ b/libraries/animation/src/AnimSkeleton.cpp @@ -87,7 +87,8 @@ AnimPose AnimSkeleton::getAbsolutePose(int jointIndex, const AnimPoseVec& poses) void AnimSkeleton::convertRelativePosesToAbsolute(AnimPoseVec& poses) const { // poses start off relative and leave in absolute frame - for (int i = 0; i < (int)poses.size() && i < (int)_joints.size(); ++i) { + int lastIndex = std::min((int)poses.size(), (int)_joints.size()); + for (int i = 0; i < lastIndex; ++i) { int parentIndex = _joints[i].parentIndex; if (parentIndex != -1) { poses[i] = poses[parentIndex] * poses[i]; @@ -95,6 +96,30 @@ void AnimSkeleton::convertRelativePosesToAbsolute(AnimPoseVec& poses) const { } } +void AnimSkeleton::convertAbsolutePosesToRelative(AnimPoseVec& poses) const { + // poses start off absolute and leave in relative frame + int lastIndex = std::min((int)poses.size(), (int)_joints.size()); + for (int i = lastIndex - 1; i >= 0; --i) { + int parentIndex = _joints[i].parentIndex; + if (parentIndex != -1) { + poses[i] = poses[parentIndex].inverse() * poses[i]; + } + } +} + +void AnimSkeleton::mirrorRelativePoses(AnimPoseVec& poses) const { + convertRelativePosesToAbsolute(poses); + mirrorAbsolutePoses(poses); + convertAbsolutePosesToRelative(poses); +} + +void AnimSkeleton::mirrorAbsolutePoses(AnimPoseVec& poses) const { + AnimPoseVec temp = poses; + for (int i = 0; i < (int)poses.size(); i++) { + poses[_mirrorMap[i]] = temp[i].mirror(); + } +} + void AnimSkeleton::buildSkeletonFromJoints(const std::vector& joints) { _joints = joints; @@ -150,6 +175,24 @@ void AnimSkeleton::buildSkeletonFromJoints(const std::vector& joints) } } } + + // build mirror map. + _mirrorMap.reserve(_joints.size()); + for (int i = 0; i < (int)joints.size(); i++) { + int mirrorJointIndex = -1; + if (_joints[i].name.startsWith("Left")) { + QString mirrorJointName = QString(_joints[i].name).replace(0, 4, "Right"); + mirrorJointIndex = nameToJointIndex(mirrorJointName); + } else if (_joints[i].name.startsWith("Right")) { + QString mirrorJointName = QString(_joints[i].name).replace(0, 5, "Left"); + mirrorJointIndex = nameToJointIndex(mirrorJointName); + } + if (mirrorJointIndex >= 0) { + _mirrorMap.push_back(mirrorJointIndex); + } else { + _mirrorMap.push_back(i); + } + } } #ifndef NDEBUG diff --git a/libraries/animation/src/AnimSkeleton.h b/libraries/animation/src/AnimSkeleton.h index 757f4e5c3e..fc246bc4c0 100644 --- a/libraries/animation/src/AnimSkeleton.h +++ b/libraries/animation/src/AnimSkeleton.h @@ -53,6 +53,10 @@ public: AnimPose getAbsolutePose(int jointIndex, const AnimPoseVec& poses) const; void convertRelativePosesToAbsolute(AnimPoseVec& poses) const; + void convertAbsolutePosesToRelative(AnimPoseVec& poses) const; + + void mirrorRelativePoses(AnimPoseVec& poses) const; + void mirrorAbsolutePoses(AnimPoseVec& poses) const; #ifndef NDEBUG void dump() const; @@ -69,6 +73,7 @@ protected: AnimPoseVec _absoluteDefaultPoses; AnimPoseVec _relativePreRotationPoses; AnimPoseVec _relativePostRotationPoses; + std::vector _mirrorMap; // no copies AnimSkeleton(const AnimSkeleton&) = delete; diff --git a/libraries/animation/src/AnimStateMachine.cpp b/libraries/animation/src/AnimStateMachine.cpp index 3f5a81a861..e8f9c944b7 100644 --- a/libraries/animation/src/AnimStateMachine.cpp +++ b/libraries/animation/src/AnimStateMachine.cpp @@ -52,8 +52,25 @@ const AnimPoseVec& AnimStateMachine::evaluate(const AnimVariantMap& animVars, fl if (_duringInterp) { _alpha += _alphaVel * dt; if (_alpha < 1.0f) { - if (_poses.size() > 0 && _nextPoses.size() > 0 && _prevPoses.size() > 0) { - ::blend(_poses.size(), &_prevPoses[0], &_nextPoses[0], _alpha, &_poses[0]); + AnimPoseVec* nextPoses = nullptr; + AnimPoseVec* prevPoses = nullptr; + AnimPoseVec localNextPoses; + if (_interpType == InterpType::SnapshotBoth) { + // interp between both snapshots + prevPoses = &_prevPoses; + nextPoses = &_nextPoses; + } else if (_interpType == InterpType::SnapshotPrev) { + // interp between the prev snapshot and evaluated next target. + // this is useful for interping into a blend + localNextPoses = currentStateNode->evaluate(animVars, dt, triggersOut); + prevPoses = &_prevPoses; + nextPoses = &localNextPoses; + } else { + assert(false); + } + + if (_poses.size() > 0 && nextPoses && prevPoses && nextPoses->size() > 0 && prevPoses->size() > 0) { + ::blend(_poses.size(), &(prevPoses->at(0)), &(nextPoses->at(0)), _alpha, &_poses[0]); } } else { _duringInterp = false; @@ -86,16 +103,32 @@ void AnimStateMachine::switchState(const AnimVariantMap& animVars, State::Pointe _alpha = 0.0f; float duration = std::max(0.001f, animVars.lookup(desiredState->_interpDurationVar, desiredState->_interpDuration)); _alphaVel = FRAMES_PER_SECOND / duration; - _prevPoses = _poses; - nextStateNode->setCurrentFrame(desiredState->_interpTarget); + _interpType = (InterpType)animVars.lookup(desiredState->_interpTypeVar, (int)desiredState->_interpType); // because dt is 0, we should not encounter any triggers const float dt = 0.0f; Triggers triggers; - _nextPoses = nextStateNode->evaluate(animVars, dt, triggers); + + if (_interpType == InterpType::SnapshotBoth) { + // snapshot previous pose. + _prevPoses = _poses; + // snapshot next pose at the target frame. + nextStateNode->setCurrentFrame(desiredState->_interpTarget); + _nextPoses = nextStateNode->evaluate(animVars, dt, triggers); + } else if (_interpType == InterpType::SnapshotPrev) { + // snapshot previoius pose + _prevPoses = _poses; + // no need to evaluate _nextPoses we will do it dynamically during the interp, + // however we need to set the current frame. + nextStateNode->setCurrentFrame(desiredState->_interpTarget - duration); + } else { + assert(false); + } + #if WANT_DEBUG - qCDebug(animation) << "AnimStateMachine::switchState:" << _currentState->getID() << "->" << desiredState->getID() << "duration =" << duration << "targetFrame =" << desiredState->_interpTarget; + qCDebug(animation) << "AnimStateMachine::switchState:" << _currentState->getID() << "->" << desiredState->getID() << "duration =" << duration << "targetFrame =" << desiredState->_interpTarget << "interpType = " << (int)_interpType; #endif + _currentState = desiredState; } diff --git a/libraries/animation/src/AnimStateMachine.h b/libraries/animation/src/AnimStateMachine.h index dda56235d5..6a28ef1825 100644 --- a/libraries/animation/src/AnimStateMachine.h +++ b/libraries/animation/src/AnimStateMachine.h @@ -31,13 +31,27 @@ // visible after interpolation is complete. // * interpDuration - (frames) The total length of time it will take to interp between the current pose and the // interpTarget frame. +// * interpType - How the interpolation is performed. +// * SnapshotBoth: Stores two snapshots, the previous animation before interpolation begins and the target state at the +// interTarget frame. Then during the interpolation period the two snapshots are interpolated to produce smooth motion between them. +// * SnapshotPrev: Stores a snapshot of the previous animation before interpolation begins. However the target state is +// evaluated dynamically. During the interpolation period the previous snapshot is interpolated with the target pose +// to produce smooth motion between them. This mode is useful for interping into a blended animation where the actual +// blend factor is not known at the start of the interp or is might change dramatically during the interp. class AnimStateMachine : public AnimNode { public: friend class AnimNodeLoader; friend bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& nodeId, const QUrl& jsonUrl); + enum class InterpType { + SnapshotBoth = 0, + SnapshotPrev, + NumTypes + }; + protected: + class State { public: friend AnimStateMachine; @@ -55,14 +69,16 @@ protected: State::Pointer _state; }; - State(const QString& id, int childIndex, float interpTarget, float interpDuration) : + State(const QString& id, int childIndex, float interpTarget, float interpDuration, InterpType interpType) : _id(id), _childIndex(childIndex), _interpTarget(interpTarget), - _interpDuration(interpDuration) {} + _interpDuration(interpDuration), + _interpType(interpType) {} void setInterpTargetVar(const QString& interpTargetVar) { _interpTargetVar = interpTargetVar; } void setInterpDurationVar(const QString& interpDurationVar) { _interpDurationVar = interpDurationVar; } + void setInterpTypeVar(const QString& interpTypeVar) { _interpTypeVar = interpTypeVar; } int getChildIndex() const { return _childIndex; } const QString& getID() const { return _id; } @@ -78,9 +94,11 @@ protected: int _childIndex; float _interpTarget; // frames float _interpDuration; // frames + InterpType _interpType; QString _interpTargetVar; QString _interpDurationVar; + QString _interpTypeVar; std::vector _transitions; @@ -115,6 +133,7 @@ protected: // interpolation state bool _duringInterp = false; + InterpType _interpType { InterpType::SnapshotBoth }; float _alphaVel = 0.0f; float _alpha = 0.0f; AnimPoseVec _prevPoses; diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 4d72bfbaea..7034d0f253 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -120,7 +120,7 @@ void Rig::overrideRoleAnimation(const QString& role, const QString& url, float f _origRoleAnimations[role] = node; const float REFERENCE_FRAMES_PER_SECOND = 30.0f; float timeScale = fps / REFERENCE_FRAMES_PER_SECOND; - auto clipNode = std::make_shared(role, url, firstFrame, lastFrame, timeScale, loop); + auto clipNode = std::make_shared(role, url, firstFrame, lastFrame, timeScale, loop, false); AnimNode::Pointer parent = node->getParent(); parent->replaceChild(node, clipNode); } else { @@ -152,7 +152,7 @@ void Rig::prefetchAnimation(const QString& url) { // This will begin loading the NetworkGeometry for the given URL. // which should speed us up if we request it later via overrideAnimation. - auto clipNode = std::make_shared("prefetch", url, 0, 0, 1.0, false); + auto clipNode = std::make_shared("prefetch", url, 0, 0, 1.0, false, false); _prefetchedAnimations.push_back(clipNode); } @@ -504,7 +504,7 @@ static const std::vector FORWARD_SPEEDS = { 0.4f, 1.4f, 4.5f }; // m/s static const std::vector BACKWARD_SPEEDS = { 0.6f, 1.45f }; // m/s static const std::vector LATERAL_SPEEDS = { 0.2f, 0.65f }; // m/s -void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation, bool isHovering) { +void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation, CharacterControllerState ccState) { glm::vec3 front = worldRotation * IDENTITY_FRONT; @@ -572,11 +572,21 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos const float TURN_ENTER_SPEED_THRESHOLD = 0.5f; // rad/sec const float TURN_EXIT_SPEED_THRESHOLD = 0.2f; // rad/sec - if (isHovering) { + if (ccState == CharacterControllerState::Hover) { if (_desiredState != RigRole::Hover) { _desiredStateAge = 0.0f; } _desiredState = RigRole::Hover; + } else if (ccState == CharacterControllerState::InAir) { + if (_desiredState != RigRole::InAir) { + _desiredStateAge = 0.0f; + } + _desiredState = RigRole::InAir; + } else if (ccState == CharacterControllerState::Takeoff) { + if (_desiredState != RigRole::Takeoff) { + _desiredStateAge = 0.0f; + } + _desiredState = RigRole::Takeoff; } else { float moveThresh; if (_state != RigRole::Move) { @@ -614,6 +624,15 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos const float STATE_CHANGE_HYSTERESIS_TIMER = 0.1f; + // Skip hystersis timer for jump transitions. + if (_desiredState == RigRole::Takeoff) { + _desiredStateAge = STATE_CHANGE_HYSTERESIS_TIMER; + } else if (_state == RigRole::Takeoff && _desiredState == RigRole::InAir) { + _desiredStateAge = STATE_CHANGE_HYSTERESIS_TIMER; + } else if (_state == RigRole::InAir && _desiredState != RigRole::InAir) { + _desiredStateAge = STATE_CHANGE_HYSTERESIS_TIMER; + } + if ((_desiredStateAge >= STATE_CHANGE_HYSTERESIS_TIMER) && _desiredState != _state) { _state = _desiredState; _desiredStateAge = 0.0f; @@ -662,6 +681,12 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos _animVars.set("isNotTurning", true); _animVars.set("isFlying", false); _animVars.set("isNotFlying", true); + _animVars.set("isTakeoffStand", false); + _animVars.set("isTakeoffRun", false); + _animVars.set("isNotTakeoff", true); + _animVars.set("isInAirStand", false); + _animVars.set("isInAirRun", false); + _animVars.set("isNotInAir", true); } } else if (_state == RigRole::Turn) { if (turningSpeed > 0.0f) { @@ -682,6 +707,13 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos _animVars.set("isNotMoving", true); _animVars.set("isFlying", false); _animVars.set("isNotFlying", true); + _animVars.set("isTakeoffStand", false); + _animVars.set("isTakeoffRun", false); + _animVars.set("isNotTakeoff", true); + _animVars.set("isInAirStand", false); + _animVars.set("isInAirRun", false); + _animVars.set("isNotInAir", true); + } else if (_state == RigRole::Idle ) { // default anim vars to notMoving and notTurning _animVars.set("isMovingForward", false); @@ -694,7 +726,14 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos _animVars.set("isNotTurning", true); _animVars.set("isFlying", false); _animVars.set("isNotFlying", true); - } else { + _animVars.set("isTakeoffStand", false); + _animVars.set("isTakeoffRun", false); + _animVars.set("isNotTakeoff", true); + _animVars.set("isInAirStand", false); + _animVars.set("isInAirRun", false); + _animVars.set("isNotInAir", true); + + } else if (_state == RigRole::Hover) { // flying. _animVars.set("isMovingForward", false); _animVars.set("isMovingBackward", false); @@ -706,15 +745,82 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos _animVars.set("isNotTurning", true); _animVars.set("isFlying", true); _animVars.set("isNotFlying", false); + _animVars.set("isTakeoffStand", false); + _animVars.set("isTakeoffRun", false); + _animVars.set("isNotTakeoff", true); + _animVars.set("isInAirStand", false); + _animVars.set("isInAirRun", false); + _animVars.set("isNotInAir", true); + + } else if (_state == RigRole::Takeoff) { + // jumping in-air + _animVars.set("isMovingForward", false); + _animVars.set("isMovingBackward", false); + _animVars.set("isMovingLeft", false); + _animVars.set("isMovingRight", false); + _animVars.set("isNotMoving", true); + _animVars.set("isTurningLeft", false); + _animVars.set("isTurningRight", false); + _animVars.set("isNotTurning", true); + _animVars.set("isFlying", false); + _animVars.set("isNotFlying", true); + + bool takeOffRun = forwardSpeed > 0.1f; + if (takeOffRun) { + _animVars.set("isTakeoffStand", false); + _animVars.set("isTakeoffRun", true); + } else { + _animVars.set("isTakeoffStand", true); + _animVars.set("isTakeoffRun", false); + } + + _animVars.set("isNotTakeoff", false); + _animVars.set("isInAirStand", false); + _animVars.set("isInAirRun", false); + _animVars.set("isNotInAir", false); + + } else if (_state == RigRole::InAir) { + // jumping in-air + _animVars.set("isMovingForward", false); + _animVars.set("isMovingBackward", false); + _animVars.set("isMovingLeft", false); + _animVars.set("isMovingRight", false); + _animVars.set("isNotMoving", true); + _animVars.set("isTurningLeft", false); + _animVars.set("isTurningRight", false); + _animVars.set("isNotTurning", true); + _animVars.set("isFlying", false); + _animVars.set("isNotFlying", true); + _animVars.set("isTakeoffStand", false); + _animVars.set("isTakeoffRun", false); + _animVars.set("isNotTakeoff", true); + + bool inAirRun = forwardSpeed > 0.1f; + if (inAirRun) { + _animVars.set("isInAirStand", false); + _animVars.set("isInAirRun", true); + } else { + _animVars.set("isInAirStand", true); + _animVars.set("isInAirRun", false); + } + _animVars.set("isNotInAir", false); + + // compute blend based on velocity + const float JUMP_SPEED = 3.5f; + float alpha = glm::clamp(-worldVelocity.y / JUMP_SPEED, -1.0f, 1.0f) + 1.0f; + _animVars.set("inAirAlpha", alpha); } t += deltaTime; - if (_enableInverseKinematics) { - _animVars.set("ikOverlayAlpha", 1.0f); - } else { - _animVars.set("ikOverlayAlpha", 0.0f); + if (_enableInverseKinematics != _lastEnableInverseKinematics) { + if (_enableInverseKinematics) { + _animVars.set("ikOverlayAlpha", 1.0f); + } else { + _animVars.set("ikOverlayAlpha", 0.0f); + } } + _lastEnableInverseKinematics = _enableInverseKinematics; } _lastFront = front; diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index cf96c6dc16..e4668d6c2a 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -73,6 +73,13 @@ public: glm::quat rightOrientation = glm::quat(); // rig space (z forward) }; + enum class CharacterControllerState { + Ground = 0, + Takeoff, + InAir, + Hover + }; + virtual ~Rig() {} void destroyAnimGraph(); @@ -141,7 +148,7 @@ public: glm::mat4 getJointTransform(int jointIndex) const; // Start or stop animations as needed. - void computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation, bool isHovering); + void computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation, CharacterControllerState ccState); // Regardless of who started the animations or how many, update the joints. void updateAnimations(float deltaTime, glm::mat4 rootTransform); @@ -271,7 +278,9 @@ public: Idle = 0, Turn, Move, - Hover + Hover, + Takeoff, + InAir }; RigRole _state { RigRole::Idle }; RigRole _desiredState { RigRole::Idle }; @@ -292,6 +301,7 @@ public: std::map _origRoleAnimations; std::vector _prefetchedAnimations; + bool _lastEnableInverseKinematics { true }; bool _enableInverseKinematics { true }; private: diff --git a/libraries/animation/src/SwingTwistConstraint.h b/libraries/animation/src/SwingTwistConstraint.h index f36dc851ea..f73bbfb233 100644 --- a/libraries/animation/src/SwingTwistConstraint.h +++ b/libraries/animation/src/SwingTwistConstraint.h @@ -51,7 +51,7 @@ public: virtual bool apply(glm::quat& rotation) const override; void setLowerSpine(bool lowerSpine) { _lowerSpine = lowerSpine; } - virtual bool isLowerSpine() const { return _lowerSpine; } + virtual bool isLowerSpine() const override { return _lowerSpine; } // SwingLimitFunction is an implementation of the constraint check described in the paper: // "The Parameterization of Joint Rotation with the Unit Quaternion" by Quang Liu and Edmond C. Prakash diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 845a6a6245..ef7ff9684a 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -22,6 +22,16 @@ AvatarHashMap::AvatarHashMap() { connect(DependencyManager::get().data(), &NodeList::uuidChanged, this, &AvatarHashMap::sessionUUIDChanged); } +QVector AvatarHashMap::getAvatarIdentifiers() { + QReadLocker locker(&_hashLock); + return _avatarHash.keys().toVector(); +} + +AvatarData* AvatarHashMap::getAvatar(QUuid avatarID) { + // Null/Default-constructed QUuids will return MyAvatar + return getAvatarBySessionID(avatarID).get(); +} + bool AvatarHashMap::isAvatarInRange(const glm::vec3& position, const float range) { auto hashCopy = getHashCopy(); foreach(const AvatarSharedPointer& sharedAvatar, hashCopy) { diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index cb6c6cb0cc..ee1197367c 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -34,6 +34,12 @@ public: AvatarHash getHashCopy() { QReadLocker lock(&_hashLock); return _avatarHash; } int size() { return _avatarHash.size(); } + // Currently, your own avatar will be included as the null avatar id. + Q_INVOKABLE QVector getAvatarIdentifiers(); + Q_INVOKABLE AvatarData* getAvatar(QUuid avatarID); + + virtual AvatarSharedPointer getAvatarBySessionID(const QUuid& sessionID) { return findAvatar(sessionID); } + signals: void avatarAddedEvent(const QUuid& sessionUUID); void avatarRemovedEvent(const QUuid& sessionUUID); diff --git a/libraries/entities-renderer/src/RenderableBoxEntityItem.h b/libraries/entities-renderer/src/RenderableBoxEntityItem.h index 9addfd813a..67f881dbd8 100644 --- a/libraries/entities-renderer/src/RenderableBoxEntityItem.h +++ b/libraries/entities-renderer/src/RenderableBoxEntityItem.h @@ -22,8 +22,8 @@ public: static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties); RenderableBoxEntityItem(const EntityItemID& entityItemID) : BoxEntityItem(entityItemID) { } - virtual void render(RenderArgs* args); - virtual void setUserData(const QString& value); + virtual void render(RenderArgs* args) override; + virtual void setUserData(const QString& value) override; SIMPLE_RENDERABLE() private: diff --git a/libraries/entities-renderer/src/RenderableLightEntityItem.h b/libraries/entities-renderer/src/RenderableLightEntityItem.h index aac1a4a998..2db913db0d 100644 --- a/libraries/entities-renderer/src/RenderableLightEntityItem.h +++ b/libraries/entities-renderer/src/RenderableLightEntityItem.h @@ -20,12 +20,12 @@ public: static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties); RenderableLightEntityItem(const EntityItemID& entityItemID) : LightEntityItem(entityItemID) { } - virtual void render(RenderArgs* args); - virtual bool supportsDetailedRayIntersection() const { return true; } + virtual void render(RenderArgs* args) override; + virtual bool supportsDetailedRayIntersection() const override { return true; } virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const; + void** intersectedObject, bool precisionPicking) const override; SIMPLE_RENDERABLE(); }; diff --git a/libraries/entities-renderer/src/RenderableLineEntityItem.h b/libraries/entities-renderer/src/RenderableLineEntityItem.h index 9af8c0c8ba..1227c6e63d 100644 --- a/libraries/entities-renderer/src/RenderableLineEntityItem.h +++ b/libraries/entities-renderer/src/RenderableLineEntityItem.h @@ -24,7 +24,7 @@ public: _lineVerticesID(GeometryCache::UNKNOWN_ID) { } - virtual void render(RenderArgs* args); + virtual void render(RenderArgs* args) override; SIMPLE_RENDERABLE(); diff --git a/libraries/entities-renderer/src/RenderableSphereEntityItem.h b/libraries/entities-renderer/src/RenderableSphereEntityItem.h index 737bee3134..5efe49854a 100644 --- a/libraries/entities-renderer/src/RenderableSphereEntityItem.h +++ b/libraries/entities-renderer/src/RenderableSphereEntityItem.h @@ -22,8 +22,8 @@ public: static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties); RenderableSphereEntityItem(const EntityItemID& entityItemID) : SphereEntityItem(entityItemID) { } - virtual void render(RenderArgs* args); - virtual void setUserData(const QString& value); + virtual void render(RenderArgs* args) override; + virtual void setUserData(const QString& value) override; SIMPLE_RENDERABLE(); diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.h b/libraries/entities-renderer/src/RenderableTextEntityItem.h index 149df946f7..cbe2b11c27 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.h +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.h @@ -25,7 +25,7 @@ public: RenderableTextEntityItem(const EntityItemID& entityItemID) : TextEntityItem(entityItemID) { } ~RenderableTextEntityItem() { delete _textRenderer; } - virtual void render(RenderArgs* args); + virtual void render(RenderArgs* args) override; SIMPLE_RENDERABLE(); diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.h b/libraries/entities-renderer/src/RenderableWebEntityItem.h index 799a414151..da1ddbf1a1 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.h +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.h @@ -25,8 +25,8 @@ public: RenderableWebEntityItem(const EntityItemID& entityItemID); ~RenderableWebEntityItem(); - virtual void render(RenderArgs* args); - virtual void setSourceUrl(const QString& value); + virtual void render(RenderArgs* args) override; + virtual void setSourceUrl(const QString& value) override; void setProxyWindow(QWindow* proxyWindow); QObject* getEventHandler(); diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 99dff84407..a4a646f532 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -652,6 +652,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef } if (_simulationOwner.set(newSimOwner)) { _dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID; + somethingChanged = true; } } { // When we own the simulation we don't accept updates to the entity's transform/velocities @@ -987,7 +988,7 @@ EntityTreePointer EntityItem::getTree() const { return tree; } -bool EntityItem::wantTerseEditLogging() { +bool EntityItem::wantTerseEditLogging() const { EntityTreePointer tree = getTree(); return tree ? tree->wantTerseEditLogging() : false; } diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index c8bf62f83e..8271aedb15 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -104,7 +104,7 @@ public: quint64 getLastBroadcast() const { return _lastBroadcast; } void setLastBroadcast(quint64 lastBroadcast) { _lastBroadcast = lastBroadcast; } - void markAsChangedOnServer() { _changedOnServer = usecTimestampNow(); } + void markAsChangedOnServer() { _changedOnServer = usecTimestampNow(); } quint64 getLastChangedOnServer() const { return _changedOnServer; } // TODO: eventually only include properties changed since the params.lastViewFrustumSent time @@ -351,14 +351,14 @@ public: void setPhysicsInfo(void* data) { _physicsInfo = data; } EntityTreeElementPointer getElement() const { return _element; } EntityTreePointer getTree() const; - bool wantTerseEditLogging(); + bool wantTerseEditLogging() const; glm::mat4 getEntityToWorldMatrix() const; glm::mat4 getWorldToEntityMatrix() const; glm::vec3 worldToEntity(const glm::vec3& point) const; glm::vec3 entityToWorld(const glm::vec3& point) const; - quint64 getLastEditedFromRemote() { return _lastEditedFromRemote; } + quint64 getLastEditedFromRemote() const { return _lastEditedFromRemote; } void getAllTerseUpdateProperties(EntityItemProperties& properties) const; diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 0a72789485..8e0983f62a 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -910,7 +910,7 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem success = packetData->startSubTree(octcode); delete[] octcode; - // assuming we have rome to fit our octalCode, proceed... + // assuming we have room to fit our octalCode, proceed... if (success) { // Now add our edit content details... diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index be9010cba5..3d252abc11 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -8,7 +8,6 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // - #include "EntityScriptingInterface.h" #include "EntityItemID.h" @@ -25,8 +24,9 @@ #include "ZoneEntityItem.h" -EntityScriptingInterface::EntityScriptingInterface() : - _entityTree(NULL) +EntityScriptingInterface::EntityScriptingInterface(bool bidOnSimulationOwnership) : + _entityTree(NULL), + _bidOnSimulationOwnership(bidOnSimulationOwnership) { auto nodeList = DependencyManager::get(); connect(nodeList.data(), &NodeList::isAllowedEditorChanged, this, &EntityScriptingInterface::canAdjustLocksChanged); @@ -122,6 +122,20 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties EntityItemProperties propertiesWithSimID = convertLocationFromScriptSemantics(properties); propertiesWithSimID.setDimensionsInitialized(properties.dimensionsChanged()); + auto dimensions = propertiesWithSimID.getDimensions(); + float volume = dimensions.x * dimensions.y * dimensions.z; + auto density = propertiesWithSimID.getDensity(); + auto newVelocity = propertiesWithSimID.getVelocity().length(); + float cost = calculateCost(density * volume, 0, newVelocity); + cost *= costMultiplier; + + if(cost > _currentAvatarEnergy) { + return QUuid(); + } else { + //debit the avatar energy and continue + emit debitEnergySource(cost); + } + EntityItemID id = EntityItemID(QUuid::createUuid()); // If we have a local entity tree set, then also update it. @@ -130,17 +144,20 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties _entityTree->withWriteLock([&] { EntityItemPointer entity = _entityTree->addEntity(id, propertiesWithSimID); if (entity) { - // This Node is creating a new object. If it's in motion, set this Node as the simulator. - auto nodeList = DependencyManager::get(); - const QUuid myNodeID = nodeList->getSessionUUID(); - propertiesWithSimID.setSimulationOwner(myNodeID, SCRIPT_EDIT_SIMULATION_PRIORITY); if (propertiesWithSimID.parentRelatedPropertyChanged()) { // due to parenting, the server may not know where something is in world-space, so include the bounding cube. propertiesWithSimID.setQueryAACube(entity->getQueryAACube()); } - // and make note of it now, so we can act on it right away. - entity->setSimulationOwner(myNodeID, SCRIPT_EDIT_SIMULATION_PRIORITY); + if (_bidOnSimulationOwnership) { + // This Node is creating a new object. If it's in motion, set this Node as the simulator. + auto nodeList = DependencyManager::get(); + const QUuid myNodeID = nodeList->getSessionUUID(); + + // and make note of it now, so we can act on it right away. + propertiesWithSimID.setSimulationOwner(myNodeID, SCRIPT_EDIT_SIMULATION_PRIORITY); + entity->setSimulationOwner(myNodeID, SCRIPT_EDIT_SIMULATION_PRIORITY); + } entity->setLastBroadcast(usecTimestampNow()); } else { @@ -211,9 +228,28 @@ EntityItemProperties EntityScriptingInterface::getEntityProperties(QUuid identit QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& scriptSideProperties) { EntityItemProperties properties = scriptSideProperties; + + auto dimensions = properties.getDimensions(); + float volume = dimensions.x * dimensions.y * dimensions.z; + auto density = properties.getDensity(); + auto newVelocity = properties.getVelocity().length(); + float oldVelocity = { 0.0f }; + EntityItemID entityID(id); if (!_entityTree) { queueEntityMessage(PacketType::EntityEdit, entityID, properties); + + //if there is no local entity entity tree, no existing velocity, use 0. + float cost = calculateCost(density * volume, oldVelocity, newVelocity); + cost *= costMultiplier; + + if(cost > _currentAvatarEnergy) { + return QUuid(); + } else { + //debit the avatar energy and continue + emit debitEnergySource(cost); + } + return id; } // If we have a local entity tree set, then also update it. @@ -227,6 +263,9 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& if (!entity) { return; } + //existing entity, retrieve old velocity for check down below + oldVelocity = entity->getVelocity().length(); + if (!scriptSideProperties.parentIDChanged()) { properties.setParentID(entity->getParentID()); } @@ -242,6 +281,16 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& } properties = convertLocationFromScriptSemantics(properties); updatedEntity = _entityTree->updateEntity(entityID, properties); + + float cost = calculateCost(density * volume, oldVelocity, newVelocity); + cost *= costMultiplier; + + if(cost > _currentAvatarEnergy) { + updatedEntity = false; + } else { + //debit the avatar energy and continue + emit debitEnergySource(cost); + } }); if (!updatedEntity) { @@ -255,7 +304,7 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& properties.setType(entity->getType()); bool hasTerseUpdateChanges = properties.hasTerseUpdateChanges(); bool hasPhysicsChanges = properties.hasMiscPhysicsChanges() || hasTerseUpdateChanges; - if (hasPhysicsChanges) { + if (_bidOnSimulationOwnership && hasPhysicsChanges) { auto nodeList = DependencyManager::get(); const QUuid myNodeID = nodeList->getSessionUUID(); @@ -316,6 +365,21 @@ void EntityScriptingInterface::deleteEntity(QUuid id) { _entityTree->withWriteLock([&] { EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID); if (entity) { + + auto dimensions = entity->getDimensions(); + float volume = dimensions.x * dimensions.y * dimensions.z; + auto density = entity->getDensity(); + auto velocity = entity->getVelocity().length(); + float cost = calculateCost(density * volume, velocity, 0); + cost *= costMultiplier; + + if(cost > _currentAvatarEnergy) { + return; + } else { + //debit the avatar energy and continue + emit debitEnergySource(cost); + } + if (entity->getLocked()) { shouldDelete = false; } else { @@ -992,3 +1056,20 @@ QStringList EntityScriptingInterface::getJointNames(const QUuid& entityID) { Q_RETURN_ARG(QStringList, result), Q_ARG(QUuid, entityID)); return result; } + +float EntityScriptingInterface::calculateCost(float mass, float oldVelocity, float newVelocity) { + return std::abs(mass * (newVelocity - oldVelocity)); +} + +void EntityScriptingInterface::setCurrentAvatarEnergy(float energy) { + // qCDebug(entities) << "NEW AVATAR ENERGY IN ENTITY SCRIPTING INTERFACE: " << energy; + _currentAvatarEnergy = energy; +} + +float EntityScriptingInterface::getCostMultiplier() { + return costMultiplier; +} + +void EntityScriptingInterface::setCostMultiplier(float value) { + costMultiplier = value; +} diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 641da7518e..fef000cc3d 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -16,6 +16,8 @@ #include #include +#include +#include #include #include @@ -57,8 +59,11 @@ void RayToEntityIntersectionResultFromScriptValue(const QScriptValue& object, Ra /// handles scripting of Entity commands from JS passed to assigned clients class EntityScriptingInterface : public OctreeScriptingInterface, public Dependency { Q_OBJECT + + Q_PROPERTY(float currentAvatarEnergy READ getCurrentAvatarEnergy WRITE setCurrentAvatarEnergy) + Q_PROPERTY(float costMultiplier READ getCostMultiplier WRITE setCostMultiplier) public: - EntityScriptingInterface(); + EntityScriptingInterface(bool bidOnSimulationOwnership); EntityEditPacketSender* getEntityPacketSender() const { return (EntityEditPacketSender*)getPacketSender(); } virtual NodeType_t getServerNodeType() const { return NodeType::EntityServer; } @@ -67,7 +72,7 @@ public: void setEntityTree(EntityTreePointer modelTree); EntityTreePointer getEntityTree() { return _entityTree; } void setEntitiesScriptEngine(EntitiesScriptEngineProvider* engine) { _entitiesScriptEngine = engine; } - + float calculateCost(float mass, float oldVelocity, float newVelocity); public slots: // returns true if the DomainServer will allow this Node/Avatar to make changes @@ -163,6 +168,7 @@ public slots: Q_INVOKABLE int getJointIndex(const QUuid& entityID, const QString& name); Q_INVOKABLE QStringList getJointNames(const QUuid& entityID); + signals: void collisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, const Collision& collision); @@ -188,6 +194,7 @@ signals: void deletingEntity(const EntityItemID& entityID); void addingEntity(const EntityItemID& entityID); void clearingEntities(); + void debitEnergySource(float value); private: bool actionWorker(const QUuid& entityID, std::function actor); @@ -204,7 +211,16 @@ private: bool precisionPicking, const QVector& entityIdsToInclude, const QVector& entityIdsToDiscard); EntityTreePointer _entityTree; - EntitiesScriptEngineProvider* _entitiesScriptEngine = nullptr; + EntitiesScriptEngineProvider* _entitiesScriptEngine { nullptr }; + + bool _bidOnSimulationOwnership { false }; + float _currentAvatarEnergy = { FLT_MAX }; + float getCurrentAvatarEnergy() { return _currentAvatarEnergy; } + void setCurrentAvatarEnergy(float energy); + + float costMultiplier = { 0.01f }; + float getCostMultiplier(); + void setCostMultiplier(float value); }; #endif // hifi_EntityScriptingInterface_h diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 3c638fd1a2..d7a079f0ce 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -194,7 +194,7 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI // the entire update is suspect --> ignore it return false; } - } else { + } else if (simulationBlocked) { simulationBlocked = senderID != entity->getSimulatorID(); } if (simulationBlocked) { diff --git a/libraries/entities/src/SimpleEntitySimulation.cpp b/libraries/entities/src/SimpleEntitySimulation.cpp index 120b536161..bdf27f4440 100644 --- a/libraries/entities/src/SimpleEntitySimulation.cpp +++ b/libraries/entities/src/SimpleEntitySimulation.cpp @@ -11,38 +11,58 @@ //#include -#include "EntityItem.h" #include "SimpleEntitySimulation.h" + +#include + +#include "EntityItem.h" #include "EntitiesLogging.h" -const quint64 AUTO_REMOVE_SIMULATION_OWNER_USEC = 2 * USECS_PER_SECOND; +const quint64 MIN_SIMULATION_OWNERSHIP_UPDATE_PERIOD = 2 * USECS_PER_SECOND; void SimpleEntitySimulation::updateEntitiesInternal(const quint64& now) { - // If an Entity has a simulation owner and we don't get an update for some amount of time, - // clear the owner. This guards against an interface failing to release the Entity when it - // has finished simulating it. - auto nodeList = DependencyManager::get(); + if (_entitiesWithSimulator.size() == 0) { + return; + } + + if (now < _nextSimulationExpiry) { + // nothing has expired yet + return; + } + + // If an Entity has a simulation owner but there has been no update for a while: clear the owner. + // If an Entity goes ownerless for too long: zero velocity and remove from _entitiesWithSimulator. + _nextSimulationExpiry = now + MIN_SIMULATION_OWNERSHIP_UPDATE_PERIOD; QMutexLocker lock(&_mutex); SetOfEntities::iterator itemItr = _entitiesWithSimulator.begin(); while (itemItr != _entitiesWithSimulator.end()) { EntityItemPointer entity = *itemItr; - if (entity->getSimulatorID().isNull()) { - itemItr = _entitiesWithSimulator.erase(itemItr); - } else if (now - entity->getLastChangedOnServer() >= AUTO_REMOVE_SIMULATION_OWNER_USEC) { - SharedNodePointer ownerNode = nodeList->nodeWithUUID(entity->getSimulatorID()); - if (ownerNode.isNull() || !ownerNode->isAlive()) { - qCDebug(entities) << "auto-removing simulation owner" << entity->getSimulatorID(); - entity->clearSimulationOwnership(); - itemItr = _entitiesWithSimulator.erase(itemItr); + quint64 expiry = entity->getLastChangedOnServer() + MIN_SIMULATION_OWNERSHIP_UPDATE_PERIOD; + if (expiry < now) { + if (entity->getSimulatorID().isNull()) { + // no simulators are volunteering // zero the velocity on this entity so that it doesn't drift far away - entity->setVelocity(glm::vec3(0.0f)); + entity->setVelocity(Vectors::ZERO); + entity->setAngularVelocity(Vectors::ZERO); + entity->setAcceleration(Vectors::ZERO); + // remove from list + itemItr = _entitiesWithSimulator.erase(itemItr); + continue; } else { - ++itemItr; + // the simulator has stopped updating this object + // clear ownership and restart timer, giving nearby simulators time to volunteer + qCDebug(entities) << "auto-removing simulation owner " << entity->getSimulatorID(); + entity->clearSimulationOwnership(); } - } else { - ++itemItr; + entity->markAsChangedOnServer(); + // dirty all the tree elements that contain the entity + DirtyOctreeElementOperator op(entity->getElement()); + getEntityTree()->recurseTreeWithOperator(&op); + } else if (expiry < _nextSimulationExpiry) { + _nextSimulationExpiry = expiry; } + ++itemItr; } } diff --git a/libraries/entities/src/SimpleEntitySimulation.h b/libraries/entities/src/SimpleEntitySimulation.h index 83c51525a8..53a7574bf2 100644 --- a/libraries/entities/src/SimpleEntitySimulation.h +++ b/libraries/entities/src/SimpleEntitySimulation.h @@ -29,6 +29,7 @@ protected: virtual void clearEntitiesInternal() override; SetOfEntities _entitiesWithSimulator; + quint64 _nextSimulationExpiry { 0 }; }; #endif // hifi_SimpleEntitySimulation_h diff --git a/libraries/entities/src/SimulationOwner.cpp b/libraries/entities/src/SimulationOwner.cpp index 24f6784954..669d811df9 100644 --- a/libraries/entities/src/SimulationOwner.cpp +++ b/libraries/entities/src/SimulationOwner.cpp @@ -16,11 +16,11 @@ #include -// static +// static const int SimulationOwner::NUM_BYTES_ENCODED = NUM_BYTES_RFC4122_UUID + 1; -SimulationOwner::SimulationOwner(const SimulationOwner& other) +SimulationOwner::SimulationOwner(const SimulationOwner& other) : _id(other._id), _priority(other._priority), _expiry(other._expiry) { } @@ -48,11 +48,6 @@ void SimulationOwner::clear() { void SimulationOwner::setPriority(quint8 priority) { _priority = priority; - if (_priority == 0) { - // when priority is zero we clear everything - _expiry = 0; - _id = QUuid(); - } } void SimulationOwner::promotePriority(quint8 priority) { diff --git a/libraries/entities/src/SimulationOwner.h b/libraries/entities/src/SimulationOwner.h index a848ad6e84..bd17128003 100644 --- a/libraries/entities/src/SimulationOwner.h +++ b/libraries/entities/src/SimulationOwner.h @@ -18,10 +18,10 @@ #include #include -const quint8 NO_PRORITY = 0x00; +const quint8 ZERO_SIMULATION_PRIORITY = 0x00; // Simulation observers will bid to simulate unowned active objects at the lowest possible priority -// which is VOLUNTEER. If the server accepts a VOLUNTEER bid it will automatically bump it +// which is VOLUNTEER. If the server accepts a VOLUNTEER bid it will automatically bump it // to RECRUIT priority so that other volunteers don't accidentally take over. const quint8 VOLUNTEER_SIMULATION_PRIORITY = 0x01; const quint8 RECRUIT_SIMULATION_PRIORITY = VOLUNTEER_SIMULATION_PRIORITY + 1; diff --git a/libraries/networking/src/udt/Connection.cpp b/libraries/networking/src/udt/Connection.cpp index e63d44e72f..3b5b26d05d 100644 --- a/libraries/networking/src/udt/Connection.cpp +++ b/libraries/networking/src/udt/Connection.cpp @@ -82,8 +82,12 @@ void Connection::resetRTT() { SendQueue& Connection::getSendQueue() { if (!_sendQueue) { + + // we may have a sequence number from the previous inactive queue - re-use that so that the + // receiver is getting the sequence numbers it expects (given that the connection must still be active) + // Lasily create send queue - _sendQueue = SendQueue::create(_parentSocket, _destination); + _sendQueue = SendQueue::create(_parentSocket, _destination, _inactiveSendQueueSequenceNumber); #ifdef UDT_CONNECTION_DEBUG qCDebug(networking) << "Created SendQueue for connection to" << _destination; @@ -105,6 +109,10 @@ SendQueue& Connection::getSendQueue() { } void Connection::queueInactive() { + // get the current sequence number from the send queue, this is to be re-used if the send + // queue is re-activated for this connection + _inactiveSendQueueSequenceNumber = _sendQueue->getCurrentSequenceNumber(); + // tell our current send queue to go down and reset our ptr to it to null stopSendQueue(); diff --git a/libraries/networking/src/udt/Connection.h b/libraries/networking/src/udt/Connection.h index f8c4acd7d9..b58b7ec570 100644 --- a/libraries/networking/src/udt/Connection.h +++ b/libraries/networking/src/udt/Connection.h @@ -139,6 +139,8 @@ private: SequenceNumber _lastSentACK; // The last sent ACK SequenceNumber _lastSentACK2; // The last sent ACK sub-sequence number in an ACK2 + + SequenceNumber _inactiveSendQueueSequenceNumber { 0 }; int _acksDuringSYN { 1 }; // The number of non-SYN ACKs sent during SYN int _lightACKsDuringSYN { 1 }; // The number of lite ACKs sent during SYN interval diff --git a/libraries/networking/src/udt/SendQueue.cpp b/libraries/networking/src/udt/SendQueue.cpp index ba99efdcea..4d6e431b45 100644 --- a/libraries/networking/src/udt/SendQueue.cpp +++ b/libraries/networking/src/udt/SendQueue.cpp @@ -52,11 +52,11 @@ private: Mutex2& _mutex2; }; -std::unique_ptr SendQueue::create(Socket* socket, HifiSockAddr destination) { +std::unique_ptr SendQueue::create(Socket* socket, HifiSockAddr destination, SequenceNumber currentSequenceNumber) { Q_ASSERT_X(socket, "SendQueue::create", "Must be called with a valid Socket*"); - auto queue = std::unique_ptr(new SendQueue(socket, destination)); - + auto queue = std::unique_ptr(new SendQueue(socket, destination, currentSequenceNumber)); + // Setup queue private thread QThread* thread = new QThread; thread->setObjectName("Networking: SendQueue " + destination.objectName()); // Name thread for easier debug @@ -74,10 +74,12 @@ std::unique_ptr SendQueue::create(Socket* socket, HifiSockAddr destin return queue; } -SendQueue::SendQueue(Socket* socket, HifiSockAddr dest) : +SendQueue::SendQueue(Socket* socket, HifiSockAddr dest, SequenceNumber currentSequenceNumber) : _socket(socket), - _destination(dest) + _destination(dest), + _currentSequenceNumber(currentSequenceNumber) { + } void SendQueue::queuePacket(std::unique_ptr packet) { @@ -389,6 +391,7 @@ bool SendQueue::isInactive(bool sentAPacket) { static const int NUM_TIMEOUTS_BEFORE_INACTIVE = 16; static const int MIN_SECONDS_BEFORE_INACTIVE_MS = 5 * 1000; if (_timeoutExpiryCount >= NUM_TIMEOUTS_BEFORE_INACTIVE && + _lastReceiverResponse > 0 && (QDateTime::currentMSecsSinceEpoch() - _lastReceiverResponse) > MIN_SECONDS_BEFORE_INACTIVE_MS) { // If the flow window has been full for over CONSIDER_INACTIVE_AFTER, // then signal the queue is inactive and return so it can be cleaned up diff --git a/libraries/networking/src/udt/SendQueue.h b/libraries/networking/src/udt/SendQueue.h index 3fe4006139..5428e7a26d 100644 --- a/libraries/networking/src/udt/SendQueue.h +++ b/libraries/networking/src/udt/SendQueue.h @@ -50,7 +50,8 @@ public: Stopped }; - static std::unique_ptr create(Socket* socket, HifiSockAddr destination); + static std::unique_ptr create(Socket* socket, HifiSockAddr destination, + SequenceNumber currentSequenceNumber = SequenceNumber()); void queuePacket(std::unique_ptr packet); void queuePacketList(std::unique_ptr packetList); @@ -83,7 +84,7 @@ private slots: void run(); private: - SendQueue(Socket* socket, HifiSockAddr dest); + SendQueue(Socket* socket, HifiSockAddr dest, SequenceNumber currentSequenceNumber); SendQueue(SendQueue& other) = delete; SendQueue(SendQueue&& other) = delete; @@ -108,7 +109,7 @@ private: std::atomic _lastACKSequenceNumber { 0 }; // Last ACKed sequence number - SequenceNumber _currentSequenceNumber; // Last sequence number sent out + SequenceNumber _currentSequenceNumber { 0 }; // Last sequence number sent out std::atomic _atomicCurrentSequenceNumber { 0 }; // Atomic for last sequence number sent out std::atomic _packetSendPeriod { 0 }; // Interval between two packet send event in microseconds, set from CC diff --git a/libraries/octree/src/DirtyOctreeElementOperator.cpp b/libraries/octree/src/DirtyOctreeElementOperator.cpp new file mode 100644 index 0000000000..0f7e6d9f04 --- /dev/null +++ b/libraries/octree/src/DirtyOctreeElementOperator.cpp @@ -0,0 +1,30 @@ +// +// DirtyOctreeElementOperator.cpp +// libraries/entities/src +// +// Created by Andrew Meawdows 2016.02.04 +// Copyright 2016 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 "DirtyOctreeElementOperator.h" + +DirtyOctreeElementOperator::DirtyOctreeElementOperator(OctreeElementPointer element) + : _element(element) { + assert(_element.get()); + _point = _element->getAACube().calcCenter(); +} + +bool DirtyOctreeElementOperator::preRecursion(OctreeElementPointer element) { + if (element == _element) { + return false; + } + return element->getAACube().contains(_point); +} + +bool DirtyOctreeElementOperator::postRecursion(OctreeElementPointer element) { + element->markWithChangedTime(); + return true; +} diff --git a/libraries/octree/src/DirtyOctreeElementOperator.h b/libraries/octree/src/DirtyOctreeElementOperator.h new file mode 100644 index 0000000000..a5eec780a0 --- /dev/null +++ b/libraries/octree/src/DirtyOctreeElementOperator.h @@ -0,0 +1,30 @@ +// +// DirtyOctreeElementOperator.h +// libraries/entities/src +// +// Created by Andrew Meawdows 2016.02.04 +// Copyright 2016 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_DirtyOctreeElementOperator_h +#define hifi_DirtyOctreeElementOperator_h + +#include "Octree.h" + +class DirtyOctreeElementOperator : public RecurseOctreeOperator { +public: + DirtyOctreeElementOperator(OctreeElementPointer element); + + ~DirtyOctreeElementOperator() {} + + virtual bool preRecursion(OctreeElementPointer element); + virtual bool postRecursion(OctreeElementPointer element); +private: + glm::vec3 _point; + OctreeElementPointer _element; +}; + +#endif // hifi_DirtyOctreeElementOperator_h diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp index a30feec150..1f61bb9a59 100644 --- a/libraries/physics/src/CharacterController.cpp +++ b/libraries/physics/src/CharacterController.cpp @@ -15,11 +15,18 @@ #include "PhysicsCollisionGroups.h" #include "ObjectMotionState.h" +#include "PhysicsLogging.h" const btVector3 LOCAL_UP_AXIS(0.0f, 1.0f, 0.0f); const float JUMP_SPEED = 3.5f; const float MAX_FALL_HEIGHT = 20.0f; +#ifdef DEBUG_STATE_CHANGE +#define SET_STATE(desiredState, reason) setState(desiredState, reason) +#else +#define SET_STATE(desiredState, reason) setState(desiredState) +#endif + // helper class for simple ray-traces from character class ClosestNotMe : public btCollisionWorld::ClosestRayResultCallback { public: @@ -51,19 +58,18 @@ CharacterController::CharacterController() { _followDesiredBodyTransform.setIdentity(); _followTimeRemaining = 0.0f; _jumpSpeed = JUMP_SPEED; - _isOnGround = false; - _isJumping = false; - _isFalling = false; - _isHovering = true; + _state = State::Hover; _isPushingUp = false; - _jumpToHoverStart = 0; + _rayHitStartTime = 0; + _takeoffToInAirStartTime = 0; + _jumpButtonDownStartTime = 0; + _jumpButtonDownCount = 0; _followTime = 0.0f; _followLinearDisplacement = btVector3(0, 0, 0); _followAngularDisplacement = btQuaternion::getIdentity(); _hasSupport = false; _pendingFlags = PENDING_FLAG_UPDATE_SHAPE; - } bool CharacterController::needsRemoval() const { @@ -107,6 +113,8 @@ void CharacterController::setDynamicsWorld(btDynamicsWorld* world) { } } +static const float COS_PI_OVER_THREE = cosf(PI / 3.0f); + bool CharacterController::checkForSupport(btCollisionWorld* collisionWorld) const { int numManifolds = collisionWorld->getDispatcher()->getNumManifolds(); for (int i = 0; i < numManifolds; i++) { @@ -119,8 +127,10 @@ bool CharacterController::checkForSupport(btCollisionWorld* collisionWorld) cons btManifoldPoint& pt = contactManifold->getContactPoint(j); // check to see if contact point is touching the bottom sphere of the capsule. + // and the contact normal is not slanted too much. float contactPointY = (obA == _rigidBody) ? pt.m_localPointA.getY() : pt.m_localPointB.getY(); - if (contactPointY < -_halfHeight) { + btVector3 normal = (obA == _rigidBody) ? pt.m_normalWorldOnB : -pt.m_normalWorldOnB; + if (contactPointY < -_halfHeight && normal.dot(_currentUp) > COS_PI_OVER_THREE) { return true; } } @@ -153,72 +163,61 @@ void CharacterController::preStep(btCollisionWorld* collisionWorld) { } void CharacterController::playerStep(btCollisionWorld* dynaWorld, btScalar dt) { - btVector3 actualVelocity = _rigidBody->getLinearVelocity(); - btScalar actualSpeed = actualVelocity.length(); - - btVector3 desiredVelocity = _walkVelocity; - btScalar desiredSpeed = desiredVelocity.length(); - - const btScalar MIN_UP_PUSH = 0.1f; - if (desiredVelocity.dot(_currentUp) < MIN_UP_PUSH) { - _isPushingUp = false; - } const btScalar MIN_SPEED = 0.001f; - if (_isHovering) { - if (desiredSpeed < MIN_SPEED) { - if (actualSpeed < MIN_SPEED) { - _rigidBody->setLinearVelocity(btVector3(0.0f, 0.0f, 0.0f)); - } else { - const btScalar HOVER_BRAKING_TIMESCALE = 0.1f; - btScalar tau = glm::max(dt / HOVER_BRAKING_TIMESCALE, 1.0f); - _rigidBody->setLinearVelocity((1.0f - tau) * actualVelocity); - } - } else { - const btScalar HOVER_ACCELERATION_TIMESCALE = 0.1f; - btScalar tau = dt / HOVER_ACCELERATION_TIMESCALE; - _rigidBody->setLinearVelocity(actualVelocity - tau * (actualVelocity - desiredVelocity)); + + btVector3 actualVelocity = _rigidBody->getLinearVelocity(); + if (actualVelocity.length() < MIN_SPEED) { + actualVelocity = btVector3(0.0f, 0.0f, 0.0f); + } + + btVector3 desiredVelocity = _walkVelocity; + if (desiredVelocity.length() < MIN_SPEED) { + desiredVelocity = btVector3(0.0f, 0.0f, 0.0f); + } + + // decompose into horizontal and vertical components. + btVector3 actualVertVelocity = actualVelocity.dot(_currentUp) * _currentUp; + btVector3 actualHorizVelocity = actualVelocity - actualVertVelocity; + btVector3 desiredVertVelocity = desiredVelocity.dot(_currentUp) * _currentUp; + btVector3 desiredHorizVelocity = desiredVelocity - desiredVertVelocity; + + btVector3 finalVelocity; + + switch (_state) { + case State::Ground: + case State::Takeoff: + { + // horizontal ground control + const btScalar WALK_ACCELERATION_TIMESCALE = 0.1f; + btScalar tau = dt / WALK_ACCELERATION_TIMESCALE; + finalVelocity = tau * desiredHorizVelocity + (1.0f - tau) * actualHorizVelocity + actualVertVelocity; } - } else { - if (onGround()) { - // walking on ground - if (desiredSpeed < MIN_SPEED) { - if (actualSpeed < MIN_SPEED) { - _rigidBody->setLinearVelocity(btVector3(0.0f, 0.0f, 0.0f)); - } else { - const btScalar HOVER_BRAKING_TIMESCALE = 0.1f; - btScalar tau = dt / HOVER_BRAKING_TIMESCALE; - _rigidBody->setLinearVelocity((1.0f - tau) * actualVelocity); - } - } else { - // TODO: modify desiredVelocity using floor normal - const btScalar WALK_ACCELERATION_TIMESCALE = 0.1f; - btScalar tau = dt / WALK_ACCELERATION_TIMESCALE; - btVector3 velocityCorrection = tau * (desiredVelocity - actualVelocity); - // subtract vertical component - velocityCorrection -= velocityCorrection.dot(_currentUp) * _currentUp; - _rigidBody->setLinearVelocity(actualVelocity + velocityCorrection); - } - } else { - // transitioning to flying - btVector3 velocityCorrection = desiredVelocity - actualVelocity; + break; + case State::InAir: + { + // horizontal air control + const btScalar IN_AIR_ACCELERATION_TIMESCALE = 2.0f; + btScalar tau = dt / IN_AIR_ACCELERATION_TIMESCALE; + finalVelocity = tau * desiredHorizVelocity + (1.0f - tau) * actualHorizVelocity + actualVertVelocity; + } + break; + case State::Hover: + { + // vertical and horizontal air control const btScalar FLY_ACCELERATION_TIMESCALE = 0.2f; btScalar tau = dt / FLY_ACCELERATION_TIMESCALE; - if (!_isPushingUp) { - // actually falling --> compute a different velocity attenuation factor - const btScalar FALL_ACCELERATION_TIMESCALE = 2.0f; - tau = dt / FALL_ACCELERATION_TIMESCALE; - // zero vertical component - velocityCorrection -= velocityCorrection.dot(_currentUp) * _currentUp; - } - _rigidBody->setLinearVelocity(actualVelocity + tau * velocityCorrection); + finalVelocity = tau * desiredVelocity + (1.0f - tau) * actualVelocity; } + break; } + _rigidBody->setLinearVelocity(finalVelocity); + // Dynamicaly compute a follow velocity to move this body toward the _followDesiredBodyTransform. // Rather then add this velocity to velocity the RigidBody, we explicitly teleport the RigidBody towards its goal. // This mirrors the computation done in MyAvatar::FollowHelper::postPhysicsUpdate(). - // These two computations must be kept in sync. + const float MINIMUM_TIME_REMAINING = 0.005f; const float MAX_DISPLACEMENT = 0.5f * _radius; _followTimeRemaining -= dt; @@ -254,21 +253,7 @@ void CharacterController::playerStep(btCollisionWorld* dynaWorld, btScalar dt) { } void CharacterController::jump() { - // check for case where user is holding down "jump" key... - // we'll eventually tansition to "hover" - if (!_isJumping) { - if (!_isHovering) { - _jumpToHoverStart = usecTimestampNow(); - _pendingFlags |= PENDING_FLAG_JUMP; - } - } else { - quint64 now = usecTimestampNow(); - const quint64 JUMP_TO_HOVER_PERIOD = 75 * (USECS_PER_SECOND / 100); - if (now - _jumpToHoverStart > JUMP_TO_HOVER_PERIOD) { - _isPushingUp = true; - setHovering(true); - } - } + _pendingFlags |= PENDING_FLAG_JUMP; } bool CharacterController::onGround() const { @@ -276,18 +261,44 @@ bool CharacterController::onGround() const { return _floorDistance < FLOOR_PROXIMITY_THRESHOLD || _hasSupport; } -void CharacterController::setHovering(bool hover) { - if (hover != _isHovering) { - _isHovering = hover; - _isJumping = false; +#ifdef DEBUG_STATE_CHANGE +static const char* stateToStr(CharacterController::State state) { + switch (state) { + case CharacterController::State::Ground: + return "Ground"; + case CharacterController::State::Takeoff: + return "Takeoff"; + case CharacterController::State::InAir: + return "InAir"; + case CharacterController::State::Hover: + return "Hover"; + default: + return "Unknown"; + } +} +#endif // #ifdef DEBUG_STATE_CHANGE - if (_rigidBody) { - if (hover) { +#ifdef DEBUG_STATE_CHANGE +void CharacterController::setState(State desiredState, const char* reason) { +#else +void CharacterController::setState(State desiredState) { +#endif + if (desiredState != _state) { +#ifdef DEBUG_STATE_CHANGE + qCDebug(physics) << "CharacterController::setState" << stateToStr(desiredState) << "from" << stateToStr(_state) << "," << reason; +#endif + if (desiredState == State::Hover && _state != State::Hover) { + // hover enter + if (_rigidBody) { _rigidBody->setGravity(btVector3(0.0f, 0.0f, 0.0f)); - } else { + } + } else if (_state == State::Hover && desiredState != State::Hover) { + // hover exit + if (_rigidBody) { _rigidBody->setGravity(DEFAULT_CHARACTER_GRAVITY * _currentUp); } } + _state = desiredState; } } @@ -335,9 +346,8 @@ void CharacterController::setEnabled(bool enabled) { _pendingFlags |= PENDING_FLAG_REMOVE_FROM_SIMULATION; } _pendingFlags &= ~ PENDING_FLAG_ADD_TO_SIMULATION; - _isOnGround = false; } - setHovering(true); + SET_STATE(State::Hover, "setEnabled"); _enabled = enabled; } } @@ -345,7 +355,7 @@ void CharacterController::setEnabled(bool enabled) { void CharacterController::updateUpAxis(const glm::quat& rotation) { btVector3 oldUp = _currentUp; _currentUp = quatRotate(glmToBullet(rotation), LOCAL_UP_AXIS); - if (!_isHovering) { + if (_state != State::Hover) { const btScalar MIN_UP_ERROR = 0.01f; if (oldUp.distance(_currentUp) > MIN_UP_ERROR) { _rigidBody->setGravity(DEFAULT_CHARACTER_GRAVITY * _currentUp); @@ -408,8 +418,14 @@ glm::vec3 CharacterController::getLinearVelocity() const { void CharacterController::preSimulation() { if (_enabled && _dynamicsWorld) { + quint64 now = usecTimestampNow(); + // slam body to where it is supposed to be _rigidBody->setWorldTransform(_characterBodyTransform); + btVector3 velocity = _rigidBody->getLinearVelocity(); + + btVector3 actualVertVelocity = velocity.dot(_currentUp) * _currentUp; + btVector3 actualHorizVelocity = velocity - actualVertVelocity; // scan for distant floor // rayStart is at center of bottom sphere @@ -419,37 +435,82 @@ void CharacterController::preSimulation() { btScalar rayLength = _radius + MAX_FALL_HEIGHT; btVector3 rayEnd = rayStart - rayLength * _currentUp; + const btScalar JUMP_PROXIMITY_THRESHOLD = 0.1f * _radius; + const quint64 TAKE_OFF_TO_IN_AIR_PERIOD = 250 * MSECS_PER_SECOND; + const btScalar MIN_HOVER_HEIGHT = 2.5f; + const quint64 JUMP_TO_HOVER_PERIOD = 1100 * MSECS_PER_SECOND; + const btScalar MAX_WALKING_SPEED = 2.5f; + const quint64 RAY_HIT_START_PERIOD = 500 * MSECS_PER_SECOND; + ClosestNotMe rayCallback(_rigidBody); rayCallback.m_closestHitFraction = 1.0f; _dynamicsWorld->rayTest(rayStart, rayEnd, rayCallback); - if (rayCallback.hasHit()) { + bool rayHasHit = rayCallback.hasHit(); + if (rayHasHit) { + _rayHitStartTime = now; _floorDistance = rayLength * rayCallback.m_closestHitFraction - _radius; - const btScalar MIN_HOVER_HEIGHT = 3.0f; - if (_isHovering && _floorDistance < MIN_HOVER_HEIGHT && !_isPushingUp) { - setHovering(false); - } - // TODO: use collision events rather than ray-trace test to disable jumping - const btScalar JUMP_PROXIMITY_THRESHOLD = 0.1f * _radius; - if (_floorDistance < JUMP_PROXIMITY_THRESHOLD) { - _isJumping = false; - } - } else if (!_hasSupport) { + } else if ((now - _rayHitStartTime) < RAY_HIT_START_PERIOD) { + rayHasHit = true; + } else { _floorDistance = FLT_MAX; - setHovering(true); } - if (_pendingFlags & PENDING_FLAG_JUMP) { - _pendingFlags &= ~ PENDING_FLAG_JUMP; - if (onGround()) { - _isJumping = true; - btVector3 velocity = _rigidBody->getLinearVelocity(); + // record a time stamp when the jump button was first pressed. + if ((_previousFlags & PENDING_FLAG_JUMP) != (_pendingFlags & PENDING_FLAG_JUMP)) { + if (_pendingFlags & PENDING_FLAG_JUMP) { + _jumpButtonDownStartTime = now; + _jumpButtonDownCount++; + } + } + + bool jumpButtonHeld = _pendingFlags & PENDING_FLAG_JUMP; + bool flyingFast = _state == State::Hover && actualHorizVelocity.length() > (MAX_WALKING_SPEED * 0.75f); + + switch (_state) { + case State::Ground: + if (!rayHasHit && !_hasSupport) { + SET_STATE(State::Hover, "no ground detected"); + } else if (_pendingFlags & PENDING_FLAG_JUMP && _jumpButtonDownCount != _takeoffJumpButtonID) { + _takeoffJumpButtonID = _jumpButtonDownCount; + _takeoffToInAirStartTime = now; + SET_STATE(State::Takeoff, "jump pressed"); + } else if (rayHasHit && !_hasSupport && _floorDistance > JUMP_PROXIMITY_THRESHOLD) { + SET_STATE(State::InAir, "falling"); + } + break; + case State::Takeoff: + if (!rayHasHit && !_hasSupport) { + SET_STATE(State::Hover, "no ground"); + } else if ((now - _takeoffToInAirStartTime) > TAKE_OFF_TO_IN_AIR_PERIOD) { + SET_STATE(State::InAir, "takeoff done"); velocity += _jumpSpeed * _currentUp; _rigidBody->setLinearVelocity(velocity); } + break; + case State::InAir: { + if ((velocity.dot(_currentUp) <= (JUMP_SPEED / 2.0f)) && ((_floorDistance < JUMP_PROXIMITY_THRESHOLD) || _hasSupport)) { + SET_STATE(State::Ground, "hit ground"); + } else if (jumpButtonHeld && (_takeoffJumpButtonID != _jumpButtonDownCount)) { + SET_STATE(State::Hover, "double jump button"); + } else if (jumpButtonHeld && (now - _jumpButtonDownStartTime) > JUMP_TO_HOVER_PERIOD) { + SET_STATE(State::Hover, "jump button held"); + } + break; + } + case State::Hover: + if ((_floorDistance < MIN_HOVER_HEIGHT) && !jumpButtonHeld && !flyingFast) { + SET_STATE(State::InAir, "near ground"); + } else if (((_floorDistance < JUMP_PROXIMITY_THRESHOLD) || _hasSupport) && !flyingFast) { + SET_STATE(State::Ground, "touching ground"); + } + break; } } - _followTime = 0.0f; + _previousFlags = _pendingFlags; + _pendingFlags &= ~PENDING_FLAG_JUMP; + + _followTime = 0.0f; _followLinearDisplacement = btVector3(0, 0, 0); _followAngularDisplacement = btQuaternion::getIdentity(); } diff --git a/libraries/physics/src/CharacterController.h b/libraries/physics/src/CharacterController.h index c1c64a1a02..86ef350812 100644 --- a/libraries/physics/src/CharacterController.h +++ b/libraries/physics/src/CharacterController.h @@ -31,6 +31,8 @@ class btRigidBody; class btCollisionWorld; class btDynamicsWorld; +//#define DEBUG_STATE_CHANGE + class CharacterController : public btCharacterControllerInterface { public: CharacterController(); @@ -75,8 +77,14 @@ public: glm::vec3 getLinearVelocity() const; - bool isHovering() const { return _isHovering; } - void setHovering(bool enabled); + enum class State { + Ground = 0, + Takeoff, + InAir, + Hover + }; + + State getState() const { return _state; } void setLocalBoundingBox(const glm::vec3& corner, const glm::vec3& scale); @@ -86,6 +94,12 @@ public: bool getRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation); protected: +#ifdef DEBUG_STATE_CHANGE + void setState(State state, const char* reason); +#else + void setState(State state); +#endif + void updateUpAxis(const glm::quat& rotation); bool checkForSupport(btCollisionWorld* collisionWorld) const; @@ -100,7 +114,11 @@ protected: glm::vec3 _boxScale; // used to compute capsule shape - quint64 _jumpToHoverStart; + quint64 _rayHitStartTime; + quint64 _takeoffToInAirStartTime; + quint64 _jumpButtonDownStartTime; + quint32 _jumpButtonDownCount; + quint32 _takeoffJumpButtonID; btScalar _halfHeight; btScalar _radius; @@ -116,16 +134,13 @@ protected: btQuaternion _followAngularDisplacement; bool _enabled; - bool _isOnGround; - bool _isJumping; - bool _isFalling; - bool _isHovering; + State _state; bool _isPushingUp; btDynamicsWorld* _dynamicsWorld { nullptr }; btRigidBody* _rigidBody { nullptr }; uint32_t _pendingFlags { 0 }; - + uint32_t _previousFlags { 0 }; }; #endif // hifi_CharacterControllerInterface_h diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 84d2282e7a..155186e891 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -26,10 +26,7 @@ #include "EntityTree.h" #endif -static const float ACCELERATION_EQUIVALENT_EPSILON_RATIO = 0.1f; -static const quint8 STEPS_TO_DECIDE_BALLISTIC = 4; - -const uint32_t LOOPS_FOR_SIMULATION_ORPHAN = 50; +const uint8_t LOOPS_FOR_SIMULATION_ORPHAN = 50; const quint64 USECS_BETWEEN_OWNERSHIP_BIDS = USECS_PER_SECOND / 5; #ifdef WANT_DEBUG_ENTITY_TREE_LOCKS @@ -52,8 +49,6 @@ EntityMotionState::EntityMotionState(btCollisionShape* shape, EntityItemPointer ObjectMotionState(shape), _entityPtr(entity), _entity(entity.get()), - _sentInactive(true), - _lastStep(0), _serverPosition(0.0f), _serverRotation(), _serverVelocity(0.0f), @@ -61,13 +56,16 @@ EntityMotionState::EntityMotionState(btCollisionShape* shape, EntityItemPointer _serverGravity(0.0f), _serverAcceleration(0.0f), _serverActionData(QByteArray()), - _lastMeasureStep(0), _lastVelocity(glm::vec3(0.0f)), _measuredAcceleration(glm::vec3(0.0f)), - _measuredDeltaTime(0.0f), - _accelerationNearlyGravityCount(0), _nextOwnershipBid(0), - _loopsWithoutOwner(0) + _measuredDeltaTime(0.0f), + _lastMeasureStep(0), + _lastStep(0), + _loopsWithoutOwner(0), + _accelerationNearlyGravityCount(0), + _numInactiveUpdates(1), + _outgoingPriority(ZERO_SIMULATION_PRIORITY) { _type = MOTIONSTATE_TYPE_ENTITY; assert(_entity); @@ -102,27 +100,35 @@ bool EntityMotionState::handleEasyChanges(uint32_t& flags) { ObjectMotionState::handleEasyChanges(flags); if (flags & Simulation::DIRTY_SIMULATOR_ID) { - _loopsWithoutOwner = 0; if (_entity->getSimulatorID().isNull()) { - // simulation ownership is being removed - // remove the ACTIVATION flag because this object is coming to rest - // according to a remote simulation and we don't want to wake it up again - flags &= ~Simulation::DIRTY_PHYSICS_ACTIVATION; - // hint to Bullet that the object is deactivating - _body->setActivationState(WANTS_DEACTIVATION); - _outgoingPriority = NO_PRORITY; - } else { + // simulation ownership has been removed by an external simulator + if (glm::length2(_entity->getVelocity()) == 0.0f) { + // this object is coming to rest --> clear the ACTIVATION flag and outgoing priority + flags &= ~Simulation::DIRTY_PHYSICS_ACTIVATION; + _body->setActivationState(WANTS_DEACTIVATION); + _outgoingPriority = ZERO_SIMULATION_PRIORITY; + _loopsWithoutOwner = 0; + } else { + // unowned object is still moving --> we should volunteer to own it + // TODO? put a delay in here proportional to distance from object? + setOutgoingPriority(VOLUNTEER_SIMULATION_PRIORITY); + _loopsWithoutOwner = LOOPS_FOR_SIMULATION_ORPHAN; + _nextOwnershipBid = 0; + } + } else { + // this entity's simulation is owned by someone, so we push its ownership expiry into the future _nextOwnershipBid = usecTimestampNow() + USECS_BETWEEN_OWNERSHIP_BIDS; if (Physics::getSessionUUID() == _entity->getSimulatorID() || _entity->getSimulationPriority() >= _outgoingPriority) { - // we own the simulation or our priority looses to (or ties with) remote - _outgoingPriority = NO_PRORITY; + // either we already own the simulation or our old outgoing priority momentarily looses to current owner + // so we clear it + _outgoingPriority = ZERO_SIMULATION_PRIORITY; } } } if (flags & Simulation::DIRTY_SIMULATOR_OWNERSHIP) { - // (DIRTY_SIMULATOR_OWNERSHIP really means "we should bid for ownership with SCRIPT priority") - // we're manipulating this object directly via script, so we artificially - // manipulate the logic to trigger an immediate bid for ownership + // The DIRTY_SIMULATOR_OWNERSHIP bit really means "we should bid for ownership at SCRIPT priority". + // Since that bit is set there must be a local script that is updating the physics properties of the objects + // therefore we upgrade _outgoingPriority to trigger a bid for ownership. setOutgoingPriority(SCRIPT_EDIT_SIMULATION_PRIORITY); } if ((flags & Simulation::DIRTY_PHYSICS_ACTIVATION) && !_body->isActive()) { @@ -203,7 +209,6 @@ void EntityMotionState::setWorldTransform(const btTransform& worldTrans) { _loopsWithoutOwner++; if (_loopsWithoutOwner > LOOPS_FOR_SIMULATION_ORPHAN && usecTimestampNow() > _nextOwnershipBid) { - //qDebug() << "Warning -- claiming something I saw moving." << getName(); setOutgoingPriority(VOLUNTEER_SIMULATION_PRIORITY); } } @@ -235,14 +240,14 @@ btCollisionShape* EntityMotionState::computeNewShape() { } bool EntityMotionState::isCandidateForOwnership(const QUuid& sessionID) const { - if (!_body || !_entity) { - return false; - } + assert(_body); + assert(_entity); assert(entityTreeIsLocked()); - return _outgoingPriority != NO_PRORITY || sessionID == _entity->getSimulatorID() || _entity->actionDataNeedsTransmit(); + return _outgoingPriority != ZERO_SIMULATION_PRIORITY || sessionID == _entity->getSimulatorID() || _entity->actionDataNeedsTransmit(); } bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { + // NOTE: we only get here if we think we own the simulation assert(_body); // if we've never checked before, our _lastStep will be 0, and we need to initialize our state if (_lastStep == 0) { @@ -253,7 +258,7 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { _serverAngularVelocity = bulletToGLM(_body->getAngularVelocity()); _lastStep = simulationStep; _serverActionData = _entity->getActionData(); - _sentInactive = true; + _numInactiveUpdates = 1; return false; } @@ -266,16 +271,21 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { int numSteps = simulationStep - _lastStep; float dt = (float)(numSteps) * PHYSICS_ENGINE_FIXED_SUBSTEP; - const float INACTIVE_UPDATE_PERIOD = 0.5f; - if (_sentInactive) { + if (_numInactiveUpdates > 0) { + const uint8_t MAX_NUM_INACTIVE_UPDATES = 3; + if (_numInactiveUpdates > MAX_NUM_INACTIVE_UPDATES) { + // clear local ownership (stop sending updates) and let the server clear itself + _entity->clearSimulationOwnership(); + return false; + } // we resend the inactive update every INACTIVE_UPDATE_PERIOD // until it is removed from the outgoing updates // (which happens when we don't own the simulation and it isn't touching our simulation) + const float INACTIVE_UPDATE_PERIOD = 0.5f; return (dt > INACTIVE_UPDATE_PERIOD); } - bool isActive = _body->isActive(); - if (!isActive) { + if (!_body->isActive()) { // object has gone inactive but our last send was moving --> send non-moving update immediately return true; } @@ -374,11 +384,12 @@ bool EntityMotionState::shouldSendUpdate(uint32_t simulationStep, const QUuid& s } if (_entity->getSimulatorID() != sessionID) { - // we don't own the simulation, but maybe we should... - if (_outgoingPriority != NO_PRORITY) { + // we don't own the simulation + if (_outgoingPriority != ZERO_SIMULATION_PRIORITY) { + // but we would like to own it if (_outgoingPriority < _entity->getSimulationPriority()) { - // our priority loses to remote, so we don't bother to bid - _outgoingPriority = NO_PRORITY; + // but our priority loses to remote, so we don't bother trying + _outgoingPriority = ZERO_SIMULATION_PRIORITY; return false; } return usecTimestampNow() > _nextOwnershipBid; @@ -400,10 +411,12 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, const Q _entity->setVelocity(zero); _entity->setAngularVelocity(zero); _entity->setAcceleration(zero); - _sentInactive = true; + _numInactiveUpdates++; } else { + const uint8_t STEPS_TO_DECIDE_BALLISTIC = 4; float gravityLength = glm::length(_entity->getGravity()); float accVsGravity = glm::abs(glm::length(_measuredAcceleration) - gravityLength); + const float ACCELERATION_EQUIVALENT_EPSILON_RATIO = 0.1f; if (accVsGravity < ACCELERATION_EQUIVALENT_EPSILON_RATIO * gravityLength) { // acceleration measured during the most recent simulation step was close to gravity. if (getAccelerationNearlyGravityCount() < STEPS_TO_DECIDE_BALLISTIC) { @@ -440,7 +453,7 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, const Q _entity->setVelocity(zero); _entity->setAngularVelocity(zero); } - _sentInactive = false; + _numInactiveUpdates = 0; } // remember properties for local server prediction @@ -488,12 +501,12 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, const Q // we own the simulation but the entity has stopped, so we tell the server that we're clearing simulatorID // but we remember that we do still own it... and rely on the server to tell us that we don't properties.clearSimulationOwner(); - _outgoingPriority = NO_PRORITY; + _outgoingPriority = ZERO_SIMULATION_PRIORITY; } // else the ownership is not changing so we don't bother to pack it } else { // we don't own the simulation for this entity yet, but we're sending a bid for it - properties.setSimulationOwner(sessionID, glm::max(_outgoingPriority, VOLUNTEER_SIMULATION_PRIORITY)); + properties.setSimulationOwner(sessionID, glm::max(_outgoingPriority, VOLUNTEER_SIMULATION_PRIORITY)); _nextOwnershipBid = now + USECS_BETWEEN_OWNERSHIP_BIDS; } @@ -558,7 +571,7 @@ void EntityMotionState::clearIncomingDirtyFlags() { } // virtual -quint8 EntityMotionState::getSimulationPriority() const { +uint8_t EntityMotionState::getSimulationPriority() const { return _entity->getSimulationPriority(); } @@ -568,7 +581,7 @@ QUuid EntityMotionState::getSimulatorID() const { return _entity->getSimulatorID(); } -void EntityMotionState::bump(quint8 priority) { +void EntityMotionState::bump(uint8_t priority) { setOutgoingPriority(glm::max(VOLUNTEER_SIMULATION_PRIORITY, --priority)); } @@ -601,7 +614,7 @@ void EntityMotionState::measureBodyAcceleration() { if (numSubsteps > PHYSICS_ENGINE_MAX_NUM_SUBSTEPS) { _loopsWithoutOwner = 0; _lastStep = ObjectMotionState::getWorldSimulationStep(); - _sentInactive = false; + _numInactiveUpdates = 0; } } } @@ -631,6 +644,6 @@ void EntityMotionState::computeCollisionGroupAndMask(int16_t& group, int16_t& ma _entity->computeCollisionGroupAndFinalMask(group, mask); } -void EntityMotionState::setOutgoingPriority(quint8 priority) { - _outgoingPriority = glm::max(_outgoingPriority, priority); +void EntityMotionState::setOutgoingPriority(uint8_t priority) { + _outgoingPriority = glm::max(_outgoingPriority, priority); } diff --git a/libraries/physics/src/EntityMotionState.h b/libraries/physics/src/EntityMotionState.h index 152ae7be23..cab8448dd9 100644 --- a/libraries/physics/src/EntityMotionState.h +++ b/libraries/physics/src/EntityMotionState.h @@ -29,13 +29,13 @@ public: virtual ~EntityMotionState(); void updateServerPhysicsVariables(); - virtual bool handleEasyChanges(uint32_t& flags); - virtual bool handleHardAndEasyChanges(uint32_t& flags, PhysicsEngine* engine); + virtual bool handleEasyChanges(uint32_t& flags) override; + virtual bool handleHardAndEasyChanges(uint32_t& flags, PhysicsEngine* engine) override; /// \return PhysicsMotionType based on params set in EntityItem - virtual PhysicsMotionType computePhysicsMotionType() const; + virtual PhysicsMotionType computePhysicsMotionType() const override; - virtual bool isMoving() const; + virtual bool isMoving() const override; // this relays incoming position/rotation to the RigidBody virtual void getWorldTransform(btTransform& worldTrans) const override; @@ -48,12 +48,12 @@ public: bool shouldSendUpdate(uint32_t simulationStep, const QUuid& sessionID); void sendUpdate(OctreeEditPacketSender* packetSender, const QUuid& sessionID, uint32_t step); - virtual uint32_t getIncomingDirtyFlags(); - virtual void clearIncomingDirtyFlags(); + virtual uint32_t getIncomingDirtyFlags() override; + virtual void clearIncomingDirtyFlags() override; void incrementAccelerationNearlyGravityCount() { _accelerationNearlyGravityCount++; } void resetAccelerationNearlyGravityCount() { _accelerationNearlyGravityCount = 0; } - quint8 getAccelerationNearlyGravityCount() { return _accelerationNearlyGravityCount; } + uint8_t getAccelerationNearlyGravityCount() { return _accelerationNearlyGravityCount; } virtual float getObjectRestitution() const override { return _entity->getRestitution(); } virtual float getObjectFriction() const override { return _entity->getFriction(); } @@ -69,9 +69,9 @@ public: virtual const QUuid getObjectID() const override { return _entity->getID(); } - virtual quint8 getSimulationPriority() const override; + virtual uint8_t getSimulationPriority() const override; virtual QUuid getSimulatorID() const override; - virtual void bump(quint8 priority) override; + virtual void bump(uint8_t priority) override; EntityItemPointer getEntity() const { return _entityPtr.lock(); } @@ -80,10 +80,10 @@ public: virtual QString getName() const override; - virtual void computeCollisionGroupAndMask(int16_t& group, int16_t& mask) const; + virtual void computeCollisionGroupAndMask(int16_t& group, int16_t& mask) const override; // eternal logic can suggest a simuator priority bid for the next outgoing update - void setOutgoingPriority(quint8 priority); + void setOutgoingPriority(uint8_t priority); friend class PhysicalEntitySimulation; @@ -93,7 +93,7 @@ protected: #endif virtual bool isReadyToComputeShape() const override; - virtual btCollisionShape* computeNewShape(); + virtual btCollisionShape* computeNewShape() override; virtual void setMotionType(PhysicsMotionType motionType); // In the glorious future (when entities lib depends on physics lib) the EntityMotionState will be @@ -106,10 +106,6 @@ protected: // Meanwhile we also keep a raw EntityItem* for internal stuff where the pointer is guaranteed valid. EntityItem* _entity; - bool _sentInactive; // true if body was inactive when we sent last update - - // these are for the prediction of the remote server's simple extrapolation - uint32_t _lastStep; // last step of server extrapolation glm::vec3 _serverPosition; // in simulation-frame (not world-frame) glm::quat _serverRotation; glm::vec3 _serverVelocity; @@ -118,15 +114,18 @@ protected: glm::vec3 _serverAcceleration; QByteArray _serverActionData; - uint32_t _lastMeasureStep; glm::vec3 _lastVelocity; glm::vec3 _measuredAcceleration; - float _measuredDeltaTime; + quint64 _nextOwnershipBid { 0 }; - quint8 _accelerationNearlyGravityCount; - quint64 _nextOwnershipBid = NO_PRORITY; - uint32_t _loopsWithoutOwner; - quint8 _outgoingPriority = NO_PRORITY; + float _measuredDeltaTime; + uint32_t _lastMeasureStep; + uint32_t _lastStep; // last step of server extrapolation + + uint8_t _loopsWithoutOwner; + uint8_t _accelerationNearlyGravityCount; + uint8_t _numInactiveUpdates { 1 }; + uint8_t _outgoingPriority { ZERO_SIMULATION_PRIORITY }; }; #endif // hifi_EntityMotionState_h diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index d2c91f29dd..71c78b8b86 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -249,6 +249,7 @@ void PhysicalEntitySimulation::getObjectsToChange(VectorOfMotionStates& result) void PhysicalEntitySimulation::handleOutgoingChanges(const VectorOfMotionStates& motionStates, const QUuid& sessionID) { QMutexLocker lock(&_mutex); + // walk the motionStates looking for those that correspond to entities for (auto stateItr : motionStates) { ObjectMotionState* state = &(*stateItr); @@ -273,13 +274,15 @@ void PhysicalEntitySimulation::handleOutgoingChanges(const VectorOfMotionStates& return; } - // send outgoing packets + // look for entities to prune or update QSet::iterator stateItr = _outgoingChanges.begin(); while (stateItr != _outgoingChanges.end()) { EntityMotionState* state = *stateItr; if (!state->isCandidateForOwnership(sessionID)) { + // prune stateItr = _outgoingChanges.erase(stateItr); } else if (state->shouldSendUpdate(numSubsteps, sessionID)) { + // update state->sendUpdate(_entityPacketSender, sessionID, numSubsteps); ++stateItr; } else { diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index d76d0a77de..e21b8ce799 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -4,7 +4,7 @@ // render-utils/src/ // // Created by Sam Gateau on 5/29/15. -// Copyright 20154 High Fidelity, Inc. +// Copyright 2016 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 @@ -15,7 +15,6 @@ #include #include #include -#include #include "DebugDeferredBuffer.h" #include "DeferredLightingEffect.h" @@ -31,34 +30,11 @@ #include "RenderDeferredTask.h" -#include "model_vert.h" -#include "model_shadow_vert.h" -#include "model_normal_map_vert.h" -#include "model_lightmap_vert.h" -#include "model_lightmap_normal_map_vert.h" -#include "skin_model_vert.h" -#include "skin_model_shadow_vert.h" -#include "skin_model_normal_map_vert.h" - -#include "model_frag.h" -#include "model_shadow_frag.h" -#include "model_normal_map_frag.h" -#include "model_normal_specular_map_frag.h" -#include "model_specular_map_frag.h" -#include "model_lightmap_frag.h" -#include "model_lightmap_normal_map_frag.h" -#include "model_lightmap_normal_specular_map_frag.h" -#include "model_lightmap_specular_map_frag.h" -#include "model_translucent_frag.h" - -#include "overlay3D_vert.h" -#include "overlay3D_frag.h" - -#include "drawOpaqueStencil_frag.h" - using namespace render; -void initDeferredPipelines(render::ShapePlumber& plumber); +extern void initStencilPipeline(gpu::PipelinePointer& pipeline); +extern void initOverlay3DPipelines(render::ShapePlumber& plumber); +extern void initDeferredPipelines(render::ShapePlumber& plumber); void PrepareDeferred::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) { DependencyManager::get()->prepare(renderContext->args); @@ -128,7 +104,7 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { addJob("DrawStatus", opaques, DrawStatus(statusIconMap)); } - addJob("DrawOverlay3D", shapePlumber); + addJob("DrawOverlay3D"); addJob("HitEffect"); @@ -180,22 +156,8 @@ void DrawDeferred::run(const SceneContextPointer& sceneContext, const RenderCont }); } -// TODO: Move this to the shapePlumber -gpu::PipelinePointer DrawOverlay3D::_opaquePipeline; -const gpu::PipelinePointer& DrawOverlay3D::getOpaquePipeline() { - if (!_opaquePipeline) { - auto vs = gpu::Shader::createVertex(std::string(overlay3D_vert)); - auto ps = gpu::Shader::createPixel(std::string(overlay3D_frag)); - auto program = gpu::Shader::createProgram(vs, ps); - - auto state = std::make_shared(); - state->setDepthTest(false); - // additive blending - state->setBlendFunction(true, gpu::State::ONE, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - - _opaquePipeline = gpu::Pipeline::create(program, state); - } - return _opaquePipeline; +DrawOverlay3D::DrawOverlay3D() : _shapePlumber{ std::make_shared() } { + initOverlay3DPipelines(*_shapePlumber); } void DrawOverlay3D::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) { @@ -246,9 +208,8 @@ void DrawOverlay3D::run(const SceneContextPointer& sceneContext, const RenderCon batch.setViewTransform(viewMat); batch.setViewportTransform(args->_viewport); batch.setStateScissorRect(args->_viewport); - - batch.setPipeline(getOpaquePipeline()); batch.setResourceTexture(0, args->_whiteTexture); + renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn); }); args->_batch = nullptr; @@ -259,20 +220,7 @@ void DrawOverlay3D::run(const SceneContextPointer& sceneContext, const RenderCon gpu::PipelinePointer DrawStencilDeferred::_opaquePipeline; const gpu::PipelinePointer& DrawStencilDeferred::getOpaquePipeline() { if (!_opaquePipeline) { - const gpu::int8 STENCIL_OPAQUE = 1; - auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); - auto ps = gpu::Shader::createPixel(std::string(drawOpaqueStencil_frag)); - auto program = gpu::Shader::createProgram(vs, ps); - - - gpu::Shader::makeProgram((*program)); - - auto state = std::make_shared(); - state->setDepthTest(true, false, gpu::LESS_EQUAL); - state->setStencilTest(true, 0xFF, gpu::State::StencilTest(STENCIL_OPAQUE, 0xFF, gpu::ALWAYS, gpu::State::STENCIL_OP_REPLACE, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_REPLACE)); - state->setColorWriteMask(0); - - _opaquePipeline = gpu::Pipeline::create(program, state); + initStencilPipeline(_opaquePipeline); } return _opaquePipeline; } @@ -413,166 +361,3 @@ void Blit::run(const SceneContextPointer& sceneContext, const RenderContextPoint } }); } - -void pipelineBatchSetter(const ShapePipeline& pipeline, gpu::Batch& batch) { - if (pipeline.locations->normalFittingMapUnit > -1) { - batch.setResourceTexture(pipeline.locations->normalFittingMapUnit, - DependencyManager::get()->getNormalFittingTexture()); - } -} - -void initDeferredPipelines(render::ShapePlumber& plumber) { - using Key = render::ShapeKey; - using ShaderPointer = gpu::ShaderPointer; - - auto addPipeline = [&plumber](const Key& key, const ShaderPointer& vertexShader, const ShaderPointer& pixelShader) { - auto state = std::make_shared(); - - // Cull backface - state->setCullMode(gpu::State::CULL_BACK); - - // Z test depends on transparency - state->setDepthTest(true, !key.isTranslucent(), gpu::LESS_EQUAL); - - // Blend if transparent - state->setBlendFunction(key.isTranslucent(), - // For transparency, keep the highlight intensity - gpu::State::ONE, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - - ShaderPointer program = gpu::Shader::createProgram(vertexShader, pixelShader); - plumber.addPipeline(key, program, state, &pipelineBatchSetter); - - // Add a wireframe version - if (!key.isWireFrame()) { - auto wireFrameKey = Key::Builder(key).withWireframe(); - auto wireFrameState = std::make_shared(state->getValues()); - - wireFrameState->setFillMode(gpu::State::FILL_LINE); - - plumber.addPipeline(wireFrameKey, program, wireFrameState, &pipelineBatchSetter); - } - }; - - // Vertex shaders - auto modelVertex = gpu::Shader::createVertex(std::string(model_vert)); - auto modelNormalMapVertex = gpu::Shader::createVertex(std::string(model_normal_map_vert)); - auto modelLightmapVertex = gpu::Shader::createVertex(std::string(model_lightmap_vert)); - auto modelLightmapNormalMapVertex = gpu::Shader::createVertex(std::string(model_lightmap_normal_map_vert)); - auto modelShadowVertex = gpu::Shader::createVertex(std::string(model_shadow_vert)); - auto skinModelVertex = gpu::Shader::createVertex(std::string(skin_model_vert)); - auto skinModelNormalMapVertex = gpu::Shader::createVertex(std::string(skin_model_normal_map_vert)); - auto skinModelShadowVertex = gpu::Shader::createVertex(std::string(skin_model_shadow_vert)); - - // Pixel shaders - auto modelPixel = gpu::Shader::createPixel(std::string(model_frag)); - auto modelNormalMapPixel = gpu::Shader::createPixel(std::string(model_normal_map_frag)); - auto modelSpecularMapPixel = gpu::Shader::createPixel(std::string(model_specular_map_frag)); - auto modelNormalSpecularMapPixel = gpu::Shader::createPixel(std::string(model_normal_specular_map_frag)); - auto modelTranslucentPixel = gpu::Shader::createPixel(std::string(model_translucent_frag)); - auto modelShadowPixel = gpu::Shader::createPixel(std::string(model_shadow_frag)); - auto modelLightmapPixel = gpu::Shader::createPixel(std::string(model_lightmap_frag)); - auto modelLightmapNormalMapPixel = gpu::Shader::createPixel(std::string(model_lightmap_normal_map_frag)); - auto modelLightmapSpecularMapPixel = gpu::Shader::createPixel(std::string(model_lightmap_specular_map_frag)); - auto modelLightmapNormalSpecularMapPixel = gpu::Shader::createPixel(std::string(model_lightmap_normal_specular_map_frag)); - - // Fill the pipelineLib - addPipeline( - Key::Builder(), - modelVertex, modelPixel); - - addPipeline( - Key::Builder().withTangents(), - modelNormalMapVertex, modelNormalMapPixel); - - addPipeline( - Key::Builder().withSpecular(), - modelVertex, modelSpecularMapPixel); - - addPipeline( - Key::Builder().withTangents().withSpecular(), - modelNormalMapVertex, modelNormalSpecularMapPixel); - - - addPipeline( - Key::Builder().withTranslucent(), - modelVertex, modelTranslucentPixel); - // FIXME Ignore lightmap for translucents meshpart - addPipeline( - Key::Builder().withTranslucent().withLightmap(), - modelVertex, modelTranslucentPixel); - - addPipeline( - Key::Builder().withTangents().withTranslucent(), - modelNormalMapVertex, modelTranslucentPixel); - - addPipeline( - Key::Builder().withSpecular().withTranslucent(), - modelVertex, modelTranslucentPixel); - - addPipeline( - Key::Builder().withTangents().withSpecular().withTranslucent(), - modelNormalMapVertex, modelTranslucentPixel); - - - addPipeline( - Key::Builder().withLightmap(), - modelLightmapVertex, modelLightmapPixel); - - addPipeline( - Key::Builder().withLightmap().withTangents(), - modelLightmapNormalMapVertex, modelLightmapNormalMapPixel); - - addPipeline( - Key::Builder().withLightmap().withSpecular(), - modelLightmapVertex, modelLightmapSpecularMapPixel); - - addPipeline( - Key::Builder().withLightmap().withTangents().withSpecular(), - modelLightmapNormalMapVertex, modelLightmapNormalSpecularMapPixel); - - - addPipeline( - Key::Builder().withSkinned(), - skinModelVertex, modelPixel); - - addPipeline( - Key::Builder().withSkinned().withTangents(), - skinModelNormalMapVertex, modelNormalMapPixel); - - addPipeline( - Key::Builder().withSkinned().withSpecular(), - skinModelVertex, modelSpecularMapPixel); - - addPipeline( - Key::Builder().withSkinned().withTangents().withSpecular(), - skinModelNormalMapVertex, modelNormalSpecularMapPixel); - - - addPipeline( - Key::Builder().withSkinned().withTranslucent(), - skinModelVertex, modelTranslucentPixel); - - addPipeline( - Key::Builder().withSkinned().withTangents().withTranslucent(), - skinModelNormalMapVertex, modelTranslucentPixel); - - addPipeline( - Key::Builder().withSkinned().withSpecular().withTranslucent(), - skinModelVertex, modelTranslucentPixel); - - addPipeline( - Key::Builder().withSkinned().withTangents().withSpecular().withTranslucent(), - skinModelNormalMapVertex, modelTranslucentPixel); - - - addPipeline( - Key::Builder().withDepthOnly(), - modelShadowVertex, modelShadowPixel); - - - addPipeline( - Key::Builder().withSkinned().withDepthOnly(), - skinModelShadowVertex, modelShadowPixel); -} - diff --git a/libraries/render-utils/src/RenderDeferredTask.h b/libraries/render-utils/src/RenderDeferredTask.h index 8d773a9b21..0be2e0e808 100755 --- a/libraries/render-utils/src/RenderDeferredTask.h +++ b/libraries/render-utils/src/RenderDeferredTask.h @@ -68,11 +68,10 @@ protected: class DrawStencilDeferred { public: - static const gpu::PipelinePointer& getOpaquePipeline(); + using JobModel = render::Job::Model; void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); - - using JobModel = render::Job::Model; + static const gpu::PipelinePointer& getOpaquePipeline(); protected: static gpu::PipelinePointer _opaquePipeline; //lazy evaluation hence mutable @@ -106,15 +105,12 @@ public: using Config = DrawOverlay3DConfig; using JobModel = render::Job::Model; - DrawOverlay3D(render::ShapePlumberPointer shapePlumber) : _shapePlumber{ shapePlumber } {} + DrawOverlay3D(); void configure(const Config& config) { _maxDrawn = config.maxDrawn; } void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); - static const gpu::PipelinePointer& getOpaquePipeline(); - protected: - static gpu::PipelinePointer _opaquePipeline; //lazy evaluation hence mutable render::ShapePlumberPointer _shapePlumber; int _maxDrawn; // initialized by Config }; diff --git a/libraries/render-utils/src/RenderPipelines.cpp b/libraries/render-utils/src/RenderPipelines.cpp new file mode 100644 index 0000000000..d2e880aea3 --- /dev/null +++ b/libraries/render-utils/src/RenderPipelines.cpp @@ -0,0 +1,234 @@ + +// +// RenderPipelines.cpp +// render-utils/src/ +// +// Created by Zach Pomerantz on 1/28/2016. +// Copyright 2016 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 "TextureCache.h" +#include "render/DrawTask.h" + +#include "model_vert.h" +#include "model_shadow_vert.h" +#include "model_normal_map_vert.h" +#include "model_lightmap_vert.h" +#include "model_lightmap_normal_map_vert.h" +#include "skin_model_vert.h" +#include "skin_model_shadow_vert.h" +#include "skin_model_normal_map_vert.h" + +#include "model_frag.h" +#include "model_shadow_frag.h" +#include "model_normal_map_frag.h" +#include "model_normal_specular_map_frag.h" +#include "model_specular_map_frag.h" +#include "model_lightmap_frag.h" +#include "model_lightmap_normal_map_frag.h" +#include "model_lightmap_normal_specular_map_frag.h" +#include "model_lightmap_specular_map_frag.h" +#include "model_translucent_frag.h" + +#include "overlay3D_vert.h" +#include "overlay3D_frag.h" + +#include "drawOpaqueStencil_frag.h" + +using namespace render; + +void initStencilPipeline(gpu::PipelinePointer& pipeline) { + const gpu::int8 STENCIL_OPAQUE = 1; + auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); + auto ps = gpu::Shader::createPixel(std::string(drawOpaqueStencil_frag)); + auto program = gpu::Shader::createProgram(vs, ps); + gpu::Shader::makeProgram((*program)); + + auto state = std::make_shared(); + state->setDepthTest(true, false, gpu::LESS_EQUAL); + state->setStencilTest(true, 0xFF, gpu::State::StencilTest(STENCIL_OPAQUE, 0xFF, gpu::ALWAYS, gpu::State::STENCIL_OP_REPLACE, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_REPLACE)); + state->setColorWriteMask(0); + + pipeline = gpu::Pipeline::create(program, state); +} + +void initOverlay3DPipelines(ShapePlumber& plumber) { + auto vs = gpu::Shader::createVertex(std::string(overlay3D_vert)); + auto ps = gpu::Shader::createPixel(std::string(overlay3D_frag)); + auto program = gpu::Shader::createProgram(vs, ps); + + auto opaqueState = std::make_shared(); + opaqueState->setDepthTest(false); + opaqueState->setBlendFunction(false); + + plumber.addPipeline(ShapeKey::Filter::Builder().withOpaque(), program, opaqueState); +} + +void pipelineBatchSetter(const ShapePipeline& pipeline, gpu::Batch& batch) { + if (pipeline.locations->normalFittingMapUnit > -1) { + batch.setResourceTexture(pipeline.locations->normalFittingMapUnit, + DependencyManager::get()->getNormalFittingTexture()); + } +} + +void initDeferredPipelines(render::ShapePlumber& plumber) { + using Key = render::ShapeKey; + using ShaderPointer = gpu::ShaderPointer; + + auto addPipeline = [&plumber](const Key& key, const ShaderPointer& vertexShader, const ShaderPointer& pixelShader) { + auto state = std::make_shared(); + + // Cull backface + state->setCullMode(gpu::State::CULL_BACK); + + // Z test depends on transparency + state->setDepthTest(true, !key.isTranslucent(), gpu::LESS_EQUAL); + + // Blend if transparent + state->setBlendFunction(key.isTranslucent(), + // For transparency, keep the highlight intensity + gpu::State::ONE, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + + ShaderPointer program = gpu::Shader::createProgram(vertexShader, pixelShader); + plumber.addPipeline(key, program, state, &pipelineBatchSetter); + + // Add a wireframe version + if (!key.isWireFrame()) { + auto wireFrameKey = Key::Builder(key).withWireframe(); + auto wireFrameState = std::make_shared(state->getValues()); + + wireFrameState->setFillMode(gpu::State::FILL_LINE); + + plumber.addPipeline(wireFrameKey, program, wireFrameState, &pipelineBatchSetter); + } + }; + + // Vertex shaders + auto modelVertex = gpu::Shader::createVertex(std::string(model_vert)); + auto modelNormalMapVertex = gpu::Shader::createVertex(std::string(model_normal_map_vert)); + auto modelLightmapVertex = gpu::Shader::createVertex(std::string(model_lightmap_vert)); + auto modelLightmapNormalMapVertex = gpu::Shader::createVertex(std::string(model_lightmap_normal_map_vert)); + auto modelShadowVertex = gpu::Shader::createVertex(std::string(model_shadow_vert)); + auto skinModelVertex = gpu::Shader::createVertex(std::string(skin_model_vert)); + auto skinModelNormalMapVertex = gpu::Shader::createVertex(std::string(skin_model_normal_map_vert)); + auto skinModelShadowVertex = gpu::Shader::createVertex(std::string(skin_model_shadow_vert)); + + // Pixel shaders + auto modelPixel = gpu::Shader::createPixel(std::string(model_frag)); + auto modelNormalMapPixel = gpu::Shader::createPixel(std::string(model_normal_map_frag)); + auto modelSpecularMapPixel = gpu::Shader::createPixel(std::string(model_specular_map_frag)); + auto modelNormalSpecularMapPixel = gpu::Shader::createPixel(std::string(model_normal_specular_map_frag)); + auto modelTranslucentPixel = gpu::Shader::createPixel(std::string(model_translucent_frag)); + auto modelShadowPixel = gpu::Shader::createPixel(std::string(model_shadow_frag)); + auto modelLightmapPixel = gpu::Shader::createPixel(std::string(model_lightmap_frag)); + auto modelLightmapNormalMapPixel = gpu::Shader::createPixel(std::string(model_lightmap_normal_map_frag)); + auto modelLightmapSpecularMapPixel = gpu::Shader::createPixel(std::string(model_lightmap_specular_map_frag)); + auto modelLightmapNormalSpecularMapPixel = gpu::Shader::createPixel(std::string(model_lightmap_normal_specular_map_frag)); + + // Fill the pipelineLib + addPipeline( + Key::Builder(), + modelVertex, modelPixel); + + addPipeline( + Key::Builder().withTangents(), + modelNormalMapVertex, modelNormalMapPixel); + + addPipeline( + Key::Builder().withSpecular(), + modelVertex, modelSpecularMapPixel); + + addPipeline( + Key::Builder().withTangents().withSpecular(), + modelNormalMapVertex, modelNormalSpecularMapPixel); + + + addPipeline( + Key::Builder().withTranslucent(), + modelVertex, modelTranslucentPixel); + // FIXME Ignore lightmap for translucents meshpart + addPipeline( + Key::Builder().withTranslucent().withLightmap(), + modelVertex, modelTranslucentPixel); + + addPipeline( + Key::Builder().withTangents().withTranslucent(), + modelNormalMapVertex, modelTranslucentPixel); + + addPipeline( + Key::Builder().withSpecular().withTranslucent(), + modelVertex, modelTranslucentPixel); + + addPipeline( + Key::Builder().withTangents().withSpecular().withTranslucent(), + modelNormalMapVertex, modelTranslucentPixel); + + + addPipeline( + Key::Builder().withLightmap(), + modelLightmapVertex, modelLightmapPixel); + + addPipeline( + Key::Builder().withLightmap().withTangents(), + modelLightmapNormalMapVertex, modelLightmapNormalMapPixel); + + addPipeline( + Key::Builder().withLightmap().withSpecular(), + modelLightmapVertex, modelLightmapSpecularMapPixel); + + addPipeline( + Key::Builder().withLightmap().withTangents().withSpecular(), + modelLightmapNormalMapVertex, modelLightmapNormalSpecularMapPixel); + + + addPipeline( + Key::Builder().withSkinned(), + skinModelVertex, modelPixel); + + addPipeline( + Key::Builder().withSkinned().withTangents(), + skinModelNormalMapVertex, modelNormalMapPixel); + + addPipeline( + Key::Builder().withSkinned().withSpecular(), + skinModelVertex, modelSpecularMapPixel); + + addPipeline( + Key::Builder().withSkinned().withTangents().withSpecular(), + skinModelNormalMapVertex, modelNormalSpecularMapPixel); + + + addPipeline( + Key::Builder().withSkinned().withTranslucent(), + skinModelVertex, modelTranslucentPixel); + + addPipeline( + Key::Builder().withSkinned().withTangents().withTranslucent(), + skinModelNormalMapVertex, modelTranslucentPixel); + + addPipeline( + Key::Builder().withSkinned().withSpecular().withTranslucent(), + skinModelVertex, modelTranslucentPixel); + + addPipeline( + Key::Builder().withSkinned().withTangents().withSpecular().withTranslucent(), + skinModelNormalMapVertex, modelTranslucentPixel); + + + addPipeline( + Key::Builder().withDepthOnly(), + modelShadowVertex, modelShadowPixel); + + + addPipeline( + Key::Builder().withSkinned().withDepthOnly(), + skinModelShadowVertex, modelShadowPixel); +} + diff --git a/libraries/render/src/render/Task.h b/libraries/render/src/render/Task.h index 148117eed6..93f3681c1c 100644 --- a/libraries/render/src/render/Task.h +++ b/libraries/render/src/render/Task.h @@ -257,7 +257,13 @@ public: QConfigPointer config = _jobs.back().getConfiguration(); config->setParent(_config.get()); config->setObjectName(name.c_str()); - QObject::connect(config.get(), SIGNAL(dirty()), _config.get(), SLOT(refresh())); + + // Connect dirty->refresh if defined + static const char* DIRTY_SIGNAL = "dirty()"; + if (config->metaObject()->indexOfSignal(DIRTY_SIGNAL) != -1) { + QObject::connect(config.get(), SIGNAL(dirty()), _config.get(), SLOT(refresh())); + } + return _jobs.back().getOutput(); } template const Varying addJob(std::string name, A&&... args) { diff --git a/libraries/ui/src/FileDialogHelper.cpp b/libraries/ui/src/FileDialogHelper.cpp index f8a0929702..82ad877573 100644 --- a/libraries/ui/src/FileDialogHelper.cpp +++ b/libraries/ui/src/FileDialogHelper.cpp @@ -12,6 +12,8 @@ #include #include +#include +#include QUrl FileDialogHelper::home() { @@ -26,7 +28,11 @@ QString FileDialogHelper::urlToPath(const QUrl& url) { return url.toLocalFile(); } -bool FileDialogHelper::validPath(const QString& path) { +bool FileDialogHelper::fileExists(const QString& path) { + return QFile(path).exists(); +} + +bool FileDialogHelper::validPath(const QString& path) { return QFile(path).exists(); } @@ -38,3 +44,62 @@ QUrl FileDialogHelper::pathToUrl(const QString& path) { return QUrl::fromLocalFile(path); } + +QUrl FileDialogHelper::saveHelper(const QString& saveText, const QUrl& currentFolder, const QStringList& selectionFilters) { + qDebug() << "Calling save helper with " << saveText << " " << currentFolder << " " << selectionFilters; + + QFileInfo fileInfo(saveText); + + // Check if it's a relative path and if it is resolve to the absolute path + { + if (fileInfo.isRelative()) { + fileInfo = QFileInfo(currentFolder.toLocalFile() + "/" + fileInfo.filePath()); + } + } + + // Check if we need to append an extension, but only if the current resolved path isn't a directory + if (!fileInfo.isDir()) { + QString fileName = fileInfo.fileName(); + if (!fileName.contains(".") && selectionFilters.size() == 1) { + const QRegularExpression extensionRe{ ".*(\\.[a-zA-Z0-9]+)$" }; + QString filter = selectionFilters[0]; + auto match = extensionRe.match(filter); + if (match.hasMatch()) { + fileInfo = QFileInfo(fileInfo.filePath() + match.captured(1)); + } + } + } + + return QUrl::fromLocalFile(fileInfo.absoluteFilePath()); +} + +bool FileDialogHelper::urlIsDir(const QUrl& url) { + return QFileInfo(url.toLocalFile()).isDir(); +} + +bool FileDialogHelper::urlIsFile(const QUrl& url) { + return QFileInfo(url.toLocalFile()).isFile(); +} + +bool FileDialogHelper::urlExists(const QUrl& url) { + return QFileInfo(url.toLocalFile()).exists(); +} + +bool FileDialogHelper::urlIsWritable(const QUrl& url) { + QFileInfo fileInfo(url.toLocalFile()); + // Is the file writable? + if (fileInfo.exists()) { + return fileInfo.isWritable(); + } + + // No file, get the parent directory and check if writable + return QFileInfo(fileInfo.absoluteDir().absolutePath()).isWritable(); +} + +QStringList FileDialogHelper::drives() { + QStringList result; + for (const auto& drive : QDir::drives()) { + result << drive.absolutePath(); + } + return result; +} diff --git a/libraries/ui/src/FileDialogHelper.h b/libraries/ui/src/FileDialogHelper.h index edb702eeda..2119b77917 100644 --- a/libraries/ui/src/FileDialogHelper.h +++ b/libraries/ui/src/FileDialogHelper.h @@ -47,10 +47,17 @@ public: Q_INVOKABLE QUrl home(); Q_INVOKABLE QStringList standardPath(StandardLocation location); + Q_INVOKABLE QStringList drives(); Q_INVOKABLE QString urlToPath(const QUrl& url); + Q_INVOKABLE bool urlIsDir(const QUrl& url); + Q_INVOKABLE bool urlIsFile(const QUrl& url); + Q_INVOKABLE bool urlExists(const QUrl& url); + Q_INVOKABLE bool urlIsWritable(const QUrl& url); + Q_INVOKABLE bool fileExists(const QString& path); Q_INVOKABLE bool validPath(const QString& path); Q_INVOKABLE bool validFolder(const QString& path); Q_INVOKABLE QUrl pathToUrl(const QString& path); + Q_INVOKABLE QUrl saveHelper(const QString& saveText, const QUrl& currentFolder, const QStringList& selectionFilters); }; diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 8e52507243..fa40fedb9b 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -45,6 +45,20 @@ private: bool _navigationFocused { false }; }; +QString fixupHifiUrl(const QString& urlString) { + static const QString ACCESS_TOKEN_PARAMETER = "access_token"; + static const QString ALLOWED_HOST = "metaverse.highfidelity.com"; + QUrl url(urlString); + QUrlQuery query(url); + if (url.host() == ALLOWED_HOST && query.allQueryItemValues(ACCESS_TOKEN_PARAMETER).empty()) { + AccountManager& accountManager = AccountManager::getInstance(); + query.addQueryItem(ACCESS_TOKEN_PARAMETER, accountManager.getAccountInfo().getAccessToken().token); + url.setQuery(query.query()); + return url.toString(); + } + return urlString; +} + class UrlHandler : public QObject { Q_OBJECT public: @@ -60,20 +74,7 @@ public: // FIXME hack for authentication, remove when we migrate to Qt 5.6 Q_INVOKABLE QString fixupUrl(const QString& originalUrl) { - static const QString ACCESS_TOKEN_PARAMETER = "access_token"; - static const QString ALLOWED_HOST = "metaverse.highfidelity.com"; - QString result = originalUrl; - QUrl url(originalUrl); - QUrlQuery query(url); - if (url.host() == ALLOWED_HOST && query.allQueryItemValues(ACCESS_TOKEN_PARAMETER).empty()) { - qDebug() << "Updating URL with auth token"; - AccountManager& accountManager = AccountManager::getInstance(); - query.addQueryItem(ACCESS_TOKEN_PARAMETER, accountManager.getAccountInfo().getAccessToken().token); - url.setQuery(query.query()); - result = url.toString(); - } - - return result; + return fixupHifiUrl(originalUrl); } }; @@ -104,6 +105,7 @@ void OffscreenUi::create(QOpenGLContext* context) { OffscreenQmlSurface::create(context); auto rootContext = getRootContext(); + rootContext->setContextProperty("OffscreenUi", this); rootContext->setContextProperty("offscreenFlags", offscreenFlags = new OffscreenFlags()); rootContext->setContextProperty("urlHandler", new UrlHandler()); rootContext->setContextProperty("fileDialogHelper", new FileDialogHelper()); @@ -218,7 +220,7 @@ QQuickItem* OffscreenUi::createMessageBox(QMessageBox::Icon icon, const QString& return qvariant_cast(result); } -QMessageBox::StandardButton OffscreenUi::waitForMessageBoxResult(QQuickItem* messageBox) { +int OffscreenUi::waitForMessageBoxResult(QQuickItem* messageBox) { if (!messageBox) { return QMessageBox::NoButton; } @@ -240,7 +242,7 @@ QMessageBox::StandardButton OffscreenUi::messageBox(QMessageBox::Icon icon, cons return result; } - return waitForMessageBoxResult(createMessageBox(icon, title, text, buttons, defaultButton)); + return static_cast(waitForMessageBoxResult(createMessageBox(icon, title, text, buttons, defaultButton))); } QMessageBox::StandardButton OffscreenUi::critical(const QString& title, const QString& text, @@ -477,6 +479,26 @@ private slots: } }; + +QString OffscreenUi::fileDialog(const QVariantMap& properties) { + QVariant buildDialogResult; + bool invokeResult = QMetaObject::invokeMethod(_desktop, "fileDialog", + Q_RETURN_ARG(QVariant, buildDialogResult), + Q_ARG(QVariant, QVariant::fromValue(properties))); + + if (!invokeResult) { + qWarning() << "Failed to create file open dialog"; + return QString(); + } + + QVariant result = FileDialogListener(qvariant_cast(buildDialogResult)).waitForResult(); + if (!result.isValid()) { + return QString(); + } + qDebug() << result.toString(); + return result.toUrl().toLocalFile(); +} + QString OffscreenUi::fileOpenDialog(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { if (QThread::currentThread() != thread()) { QString result; @@ -496,28 +518,40 @@ QString OffscreenUi::fileOpenDialog(const QString& caption, const QString& dir, map.insert("dir", QUrl::fromLocalFile(dir)); map.insert("filter", filter); map.insert("options", static_cast(options)); + return fileDialog(map); +} - QVariant buildDialogResult; - bool invokeResult = QMetaObject::invokeMethod(_desktop, "fileOpenDialog", - Q_RETURN_ARG(QVariant, buildDialogResult), - Q_ARG(QVariant, QVariant::fromValue(map))); - - if (!invokeResult) { - qWarning() << "Failed to create file open dialog"; - return QString(); +QString OffscreenUi::fileSaveDialog(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { + if (QThread::currentThread() != thread()) { + QString result; + QMetaObject::invokeMethod(this, "fileSaveDialog", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QString, result), + Q_ARG(QString, caption), + Q_ARG(QString, dir), + Q_ARG(QString, filter), + Q_ARG(QString*, selectedFilter), + Q_ARG(QFileDialog::Options, options)); + return result; } - QVariant result = FileDialogListener(qvariant_cast(buildDialogResult)).waitForResult(); - if (!result.isValid()) { - return QString(); - } - qDebug() << result.toString(); - return result.toUrl().toLocalFile(); + // FIXME support returning the selected filter... somehow? + QVariantMap map; + map.insert("caption", caption); + map.insert("dir", QUrl::fromLocalFile(dir)); + map.insert("filter", filter); + map.insert("options", static_cast(options)); + map.insert("saveDialog", true); + + return fileDialog(map); } QString OffscreenUi::getOpenFileName(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { return DependencyManager::get()->fileOpenDialog(caption, dir, filter, selectedFilter, options); } +QString OffscreenUi::getSaveFileName(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { + return DependencyManager::get()->fileSaveDialog(caption, dir, filter, selectedFilter, options); +} + #include "OffscreenUi.moc" diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index ec5ba433cc..de479853f3 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -47,7 +47,7 @@ public: // Must be called from the main thread QQuickItem* createMessageBox(QMessageBox::Icon icon, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton); // Must be called from the main thread - QMessageBox::StandardButton waitForMessageBoxResult(QQuickItem* messageBox); + Q_INVOKABLE int waitForMessageBoxResult(QQuickItem* messageBox); /// Same design as QMessageBox::critical(), will block, returns result static QMessageBox::StandardButton critical(void* ignored, const QString& title, const QString& text, @@ -87,10 +87,13 @@ public: QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); - // file dialog compatibility Q_INVOKABLE QString fileOpenDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + Q_INVOKABLE QString fileSaveDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + // Compatibility with QFileDialog::getOpenFileName static QString getOpenFileName(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + // Compatibility with QFileDialog::getSaveFileName + static QString getSaveFileName(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); // input dialog compatibility @@ -104,6 +107,8 @@ public: static QString getItem(void *ignored, const QString & title, const QString & label, const QStringList & items, int current = 0, bool editable = true, bool * ok = 0, Qt::WindowFlags flags = 0, Qt::InputMethodHints inputMethodHints = Qt::ImhNone); private: + QString fileDialog(const QVariantMap& properties); + QQuickItem* _desktop { nullptr }; QQuickItem* _toolWindow { nullptr }; }; diff --git a/libraries/ui/src/QmlWebWindowClass.cpp b/libraries/ui/src/QmlWebWindowClass.cpp index 96e99654c8..f12fb51b19 100644 --- a/libraries/ui/src/QmlWebWindowClass.cpp +++ b/libraries/ui/src/QmlWebWindowClass.cpp @@ -49,8 +49,11 @@ QString QmlWebWindowClass::getURL() const { return result.toString(); } +// HACK find a good place to declare and store this +extern QString fixupHifiUrl(const QString& urlString); + void QmlWebWindowClass::setURL(const QString& urlString) { DependencyManager::get()->executeOnUiThread([=] { - _qmlWindow->setProperty(URL_PROPERTY, urlString); + _qmlWindow->setProperty(URL_PROPERTY, fixupHifiUrl(urlString)); }); } diff --git a/libraries/ui/src/QmlWindowClass.cpp b/libraries/ui/src/QmlWindowClass.cpp index 3874b85a12..0e834fa379 100644 --- a/libraries/ui/src/QmlWindowClass.cpp +++ b/libraries/ui/src/QmlWindowClass.cpp @@ -220,18 +220,7 @@ QmlWindowClass::QmlWindowClass(QObject* qmlWindow) } QmlWindowClass::~QmlWindowClass() { - if (_qmlWindow) { - if (_toolWindow) { - auto offscreenUi = DependencyManager::get(); - auto toolWindow = offscreenUi->getToolWindow(); - auto invokeResult = QMetaObject::invokeMethod(toolWindow, "removeTabForUrl", Qt::QueuedConnection, - Q_ARG(QVariant, _source)); - Q_ASSERT(invokeResult); - } else { - _qmlWindow->deleteLater(); - } - _qmlWindow = nullptr; - } + close(); } void QmlWindowClass::registerObject(const QString& name, QObject* object) { @@ -331,14 +320,18 @@ void QmlWindowClass::setTitle(const QString& title) { } void QmlWindowClass::close() { - DependencyManager::get()->executeOnUiThread([this] { - if (_qmlWindow) { - _qmlWindow->setProperty("destroyOnInvisible", true); - _qmlWindow->setProperty("visible", false); + if (_qmlWindow) { + if (_toolWindow) { + auto offscreenUi = DependencyManager::get(); + auto toolWindow = offscreenUi->getToolWindow(); + auto invokeResult = QMetaObject::invokeMethod(toolWindow, "removeTabForUrl", Qt::QueuedConnection, + Q_ARG(QVariant, _source)); + Q_ASSERT(invokeResult); + } else { _qmlWindow->deleteLater(); - _qmlWindow = nullptr; } - }); + _qmlWindow = nullptr; + } } void QmlWindowClass::hasClosed() { diff --git a/plugins/hifiSixense/src/SixenseManager.cpp b/plugins/hifiSixense/src/SixenseManager.cpp index 3377aac14c..b23982f948 100644 --- a/plugins/hifiSixense/src/SixenseManager.cpp +++ b/plugins/hifiSixense/src/SixenseManager.cpp @@ -282,8 +282,8 @@ void SixenseManager::InputDevice::setDebugDrawCalibrated(bool flag) { // the calibration sequence is: // (1) reach arm straight out to the sides (xAxis is to the left) -// (2) press BUTTON_FWD on both hands and hold for one second -// (3) release both BUTTON_FWDs +// (2) press either BUTTON_1 or BUTTON_2 on both hands and hold for one second +// (3) release both buttons // // The code will: // (4) assume that the orb is on a flat surface (yAxis is UP) @@ -294,7 +294,8 @@ static const float MAXIMUM_NOISE_LEVEL = 0.05f; // meters static const quint64 LOCK_DURATION = USECS_PER_SECOND / 4; // time for lock to be acquired static bool calibrationRequested(SixenseControllerData* controllers) { - return (controllers[0].buttons == BUTTON_FWD && controllers[1].buttons == BUTTON_FWD); + return (((controllers[0].buttons == BUTTON_1) || (controllers[0].buttons == BUTTON_2)) && + ((controllers[1].buttons == BUTTON_1) || (controllers[1].buttons == BUTTON_2))); } void SixenseManager::InputDevice::updateCalibration(SixenseControllerData* controllers) { diff --git a/tests/ui/qml/main.qml b/tests/ui/qml/main.qml index d20b580b5a..a55f042227 100644 --- a/tests/ui/qml/main.qml +++ b/tests/ui/qml/main.qml @@ -118,13 +118,7 @@ ApplicationWindow { Button { text: "Open File" property var builder: Component { - FileDialog { - folder: "file:///C:/users/bdavis"; - filterModel: ListModel { - ListElement { text: "Javascript Files (*.js)"; filter: "*.js" } - ListElement { text: "All Files (*.*)"; filter: "*.*" } - } - } + FileDialog { } } onClicked: { diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/playBackgroundAudio.js b/unpublishedScripts/DomainContent/CellScience/Scripts/playBackgroundAudio.js index 0cb348c3f4..856b9f8f67 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/playBackgroundAudio.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/playBackgroundAudio.js @@ -8,7 +8,7 @@ (function() { var self = this; var baseURL = "https://hifi-content.s3.amazonaws.com/DomainContent/CellScience/"; - var version = 8; + var version = 9; this.preload = function(entityId) { self.soundPlaying = false; self.entityId = entityId; @@ -20,7 +20,7 @@ stereo: true, loop: true, localOnly: true, - volume: 0.5 + volume: 0.035 }; this.sound = SoundCache.getSound(self.soundURL); @@ -36,7 +36,6 @@ } } - this.enterEntity = function(entityID) { print("entering audio zone"); if (self.sound.downloaded) { @@ -49,7 +48,6 @@ } - this.leaveEntity = function(entityID) { print("leaving audio area " + self.userData.name); if (self.soundPlaying !== false) { diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js b/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js index e505a532e3..65fddd7ad3 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js @@ -24,7 +24,7 @@ stereo: true, loop: false, localOnly: true, - volume: 0.5 + volume: 0.035 }; this.sound = SoundCache.getSound(this.soundURL); } diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/showIdentification.js b/unpublishedScripts/DomainContent/CellScience/Scripts/showIdentification.js index c0b0cb54d4..2e37f3a51f 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/showIdentification.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/showIdentification.js @@ -22,7 +22,7 @@ stereo: true, loop: false, localOnly: true, - volume: 0.5, + volume: 0.035, position: this.position }; this.sound = SoundCache.getSound(this.soundURL); diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/zoom.js b/unpublishedScripts/DomainContent/CellScience/Scripts/zoom.js index d69f33e7c9..101ce54a5e 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/zoom.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/zoom.js @@ -25,8 +25,9 @@ loop: false, localOnly: false, position: this.position, - volume: 0.5 + volume: 0.035 }; + this.teleportSound = SoundCache.getSound("https://hifi-content.s3.amazonaws.com/DomainContent/CellScience/Audio/whoosh.wav"); //print('Script.clearTimeout PRELOADING A ZOOM ENTITY') print(" portal destination is " + portalDestination); diff --git a/unpublishedScripts/DomainContent/CellScience/importCellScience.js b/unpublishedScripts/DomainContent/CellScience/importCellScience.js index b456c4a1ce..58888584a9 100644 --- a/unpublishedScripts/DomainContent/CellScience/importCellScience.js +++ b/unpublishedScripts/DomainContent/CellScience/importCellScience.js @@ -5,7 +5,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var version = 1003; +var version = 1004; var cellLayout; var baseLocation = "https://hifi-content.s3.amazonaws.com/DomainContent/CellScience/"; diff --git a/unpublishedScripts/hiddenEntityReset.js b/unpublishedScripts/hiddenEntityReset.js index 995ebdaad6..4a64b34510 100644 --- a/unpublishedScripts/hiddenEntityReset.js +++ b/unpublishedScripts/hiddenEntityReset.js @@ -299,35 +299,35 @@ }, dynamic: true, userData: JSON.stringify({ - grabbableKey: { - wearable: { - joints: { - RightHand: [{ - x: 0.07079616189002991, - y: 0.20177987217903137, - z: 0.06374628841876984 - }, { - x: -0.5863648653030396, - y: -0.46007341146469116, - z: 0.46949487924575806, - w: -0.4733745753765106 - }], - LeftHand: [{ - x: 0.1802254319190979, - y: 0.13442856073379517, - z: 0.08504903316497803 - }, { - x: 0.2198076844215393, - y: -0.7377811074256897, - z: 0.2780133783817291, - w: 0.574519157409668 - }] - } - }, - invertSolidWhileHeld: true + "wearable": { + "joints": { + "RightHand": [{ + "x": 0.07079616189002991, + "y": 0.20177987217903137, + "z": 0.06374628841876984 + }, { + "x": -0.5863648653030396, + "y": -0.46007341146469116, + "z": 0.46949487924575806, + "w": -0.4733745753765106 + }], + "LeftHand": [{ + "x": 0.0012094751000404358, + "y": 0.1991066336631775, + "z": 0.079972043633461 + }, { + "x": 0.29249316453933716, + "y": -0.6115763187408447, + "z": 0.5668558478355408, + "w": 0.46807748079299927 + }] + } }, - resetMe: { - resetMe: true + "grabbableKey": { + "invertSolidWhileHeld": true + }, + "resetMe": { + "resetMe": true } }) }); diff --git a/unpublishedScripts/masterReset.js b/unpublishedScripts/masterReset.js index 8777ad5269..c5669b9ac0 100644 --- a/unpublishedScripts/masterReset.js +++ b/unpublishedScripts/masterReset.js @@ -283,35 +283,35 @@ MasterReset = function() { damping: 0.5, collisionSoundURL: "http://hifi-content.s3.amazonaws.com/james/pistol/sounds/drop.wav", userData: JSON.stringify({ - grabbableKey: { - wearable: { - joints: { - RightHand: [{ - x: 0.07079616189002991, - y: 0.20177987217903137, - z: 0.06374628841876984 - }, { - x: -0.5863648653030396, - y: -0.46007341146469116, - z: 0.46949487924575806, - w: -0.4733745753765106 - }], - LeftHand: [{ - x: 0.1802254319190979, - y: 0.13442856073379517, - z: 0.08504903316497803 - }, { - x: 0.2198076844215393, - y: -0.7377811074256897, - z: 0.2780133783817291, - w: 0.574519157409668 - }] - } - }, - invertSolidWhileHeld: true + "wearable": { + "joints": { + "RightHand": [{ + "x": 0.07079616189002991, + "y": 0.20177987217903137, + "z": 0.06374628841876984 + }, { + "x": -0.5863648653030396, + "y": -0.46007341146469116, + "z": 0.46949487924575806, + "w": -0.4733745753765106 + }], + "LeftHand": [{ + "x": 0.0012094751000404358, + "y": 0.1991066336631775, + "z": 0.079972043633461 + }, { + "x": 0.29249316453933716, + "y": -0.6115763187408447, + "z": 0.5668558478355408, + "w": 0.46807748079299927 + }] + } }, - resetMe: { - resetMe: true + "grabbableKey": { + "invertSolidWhileHeld": true + }, + "resetMe": { + "resetMe": true } }) });