diff --git a/BUILD_LINUX.md b/BUILD_LINUX.md index d40576a75d..038f53154c 100644 --- a/BUILD_LINUX.md +++ b/BUILD_LINUX.md @@ -1,7 +1,96 @@ +# Linux build guide + Please read the [general build guide](BUILD.md) for information on dependencies required for all platforms. Only Linux specific instructions are found in this file. -### Qt5 Dependencies +## Qt5 Dependencies Should you choose not to install Qt5 via a package manager that handles dependencies for you, you may be missing some Qt5 dependencies. On Ubuntu, for example, the following additional packages are required: libasound2 libxmu-dev libxi-dev freeglut3-dev libasound2-dev libjack0 libjack-dev libxrandr-dev libudev-dev libssl-dev + +## Ubuntu 16.04 specific build guide + +### Prepare environment + +Install qt: +```bash +wget http://debian.highfidelity.com/pool/h/hi/hifi-qt5.6.1_5.6.1_amd64.deb +sudo dpkg -i hifi-qt5.6.1_5.6.1_amd64.deb +``` + +Install build dependencies: +```bash +sudo apt-get install libasound2 libxmu-dev libxi-dev freeglut3-dev libasound2-dev libjack0 libjack-dev libxrandr-dev libudev-dev libssl-dev +``` + +To compile interface in a server you must install: +```bash +sudo apt -y install libpulse0 libnss3 libnspr4 libfontconfig1 libxcursor1 libxcomposite1 libxtst6 libxslt1.1 +``` + +Install build tools: +```bash +sudo apt install cmake +``` + +### Get code and checkout the tag you need + +Clone this repository: +```bash +git clone https://github.com/highfidelity/hifi.git +``` + +To compile a RELEASE version checkout the tag you need getting a list of all tags: +```bash +git fetch -a +git tags +``` + +Then checkout last tag with: +```bash +git checkout tags/RELEASE-6819 +``` + +Or go to the highfidelity download page (https://highfidelity.com/download) to get the release version. For example, if there is a BETA 6731 type: +```bash +git checkout tags/RELEASE-6731 +``` + +### Compiling + +Create the build directory: +```bash +mkdir -p hifi/build +cd hifi/build +``` + +Prepare makefiles: +```bash +cmake -DQT_CMAKE_PREFIX_PATH=/usr/local/Qt5.6.1/5.6/gcc_64/lib/cmake .. +``` + +Start compilation and get a cup of coffee: +```bash +make domain-server assignment-client interface +``` + +In a server does not make sense to compile interface + +### Running the software + +Running domain server: +```bash +./domain-server/domain-server +``` + +Running assignment client: +```bash +./assignment-client/assignment-client -n 6 +``` + +Running interface: +```bash +./interface/interface +``` + +Go to localhost in running interface. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a0d867ade9..4654c311cc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,7 +16,7 @@ Contributing git checkout -b new_branch_name ``` 4. Code - * Follow the [coding standard](https://wiki.highfidelity.com/wiki/Coding_Standards) + * Follow the [coding standard](https://docs.highfidelity.com/build-guide/coding-standards) 5. Commit * Use [well formed commit messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 6. Update your branch diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 1de7fa135b..c44fdf74ff 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -50,14 +51,14 @@ #include "RecordingScriptingInterface.h" #include "AbstractAudioInterface.h" -#include "AvatarAudioTimer.h" static const int RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES = 10; Agent::Agent(ReceivedMessage& message) : ThreadedAssignment(message), _receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES, RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES), - _audioGate(AudioConstants::SAMPLE_RATE, AudioConstants::MONO) + _audioGate(AudioConstants::SAMPLE_RATE, AudioConstants::MONO), + _avatarAudioTimer(this) { _entityEditSender.setPacketsPerSecond(DEFAULT_ENTITY_PPS_PER_SCRIPT); DependencyManager::get()->setPacketSender(&_entityEditSender); @@ -81,6 +82,9 @@ Agent::Agent(ReceivedMessage& message) : DependencyManager::set(); DependencyManager::set(); + // Needed to ensure the creation of the DebugDraw instance on the main thread + DebugDraw::getInstance(); + auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); @@ -92,6 +96,14 @@ Agent::Agent(ReceivedMessage& message) : this, "handleOctreePacket"); packetReceiver.registerListener(PacketType::Jurisdiction, this, "handleJurisdictionPacket"); packetReceiver.registerListener(PacketType::SelectedAudioFormat, this, "handleSelectedAudioFormat"); + + + // 100Hz timer for audio + const int TARGET_INTERVAL_MSEC = 10; // 10ms + connect(&_avatarAudioTimer, &QTimer::timeout, this, &Agent::processAgentAvatarAudio); + _avatarAudioTimer.setSingleShot(false); + _avatarAudioTimer.setInterval(TARGET_INTERVAL_MSEC); + _avatarAudioTimer.setTimerType(Qt::PreciseTimer); } void Agent::playAvatarSound(SharedSoundPointer sound) { @@ -471,14 +483,7 @@ void Agent::executeScript() { DependencyManager::set(_entityViewer.getTree()); - // 100Hz timer for audio - AvatarAudioTimer* audioTimerWorker = new AvatarAudioTimer(); - audioTimerWorker->moveToThread(&_avatarAudioTimerThread); - connect(audioTimerWorker, &AvatarAudioTimer::avatarTick, this, &Agent::processAgentAvatarAudio); - connect(this, &Agent::startAvatarAudioTimer, audioTimerWorker, &AvatarAudioTimer::start); - connect(this, &Agent::stopAvatarAudioTimer, audioTimerWorker, &AvatarAudioTimer::stop); - connect(&_avatarAudioTimerThread, &QThread::finished, audioTimerWorker, &QObject::deleteLater); - _avatarAudioTimerThread.start(); + QMetaObject::invokeMethod(&_avatarAudioTimer, "start"); // Agents should run at 45hz static const int AVATAR_DATA_HZ = 45; @@ -557,7 +562,7 @@ void Agent::setIsAvatar(bool isAvatar) { _avatarIdentityTimer->start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS); // FIXME - we shouldn't really need to constantly send identity packets // tell the avatarAudioTimer to start ticking - emit startAvatarAudioTimer(); + QMetaObject::invokeMethod(&_avatarAudioTimer, "start"); } @@ -586,7 +591,7 @@ void Agent::setIsAvatar(bool isAvatar) { nodeList->sendPacket(std::move(packet), *node); }); } - emit stopAvatarAudioTimer(); + QMetaObject::invokeMethod(&_avatarAudioTimer, "stop"); } } @@ -814,8 +819,7 @@ void Agent::aboutToFinish() { DependencyManager::destroy(); DependencyManager::destroy(); - emit stopAvatarAudioTimer(); - _avatarAudioTimerThread.quit(); + QMetaObject::invokeMethod(&_avatarAudioTimer, "stop"); // cleanup codec & encoder if (_codec && _encoder) { diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index 549a0858b7..a549df5fb3 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -81,9 +81,6 @@ private slots: void processAgentAvatar(); void processAgentAvatarAudio(); -signals: - void startAvatarAudioTimer(); - void stopAvatarAudioTimer(); private: void negotiateAudioFormat(); void selectAudioFormat(const QString& selectedCodecName); @@ -118,7 +115,7 @@ private: CodecPluginPointer _codec; QString _selectedCodecName; Encoder* _encoder { nullptr }; - QThread _avatarAudioTimerThread; + QTimer _avatarAudioTimer; bool _flushEncoder { false }; }; diff --git a/assignment-client/src/AssignmentClientMonitor.cpp b/assignment-client/src/AssignmentClientMonitor.cpp index 070034d54b..5539d6a0bb 100644 --- a/assignment-client/src/AssignmentClientMonitor.cpp +++ b/assignment-client/src/AssignmentClientMonitor.cpp @@ -91,9 +91,22 @@ void AssignmentClientMonitor::simultaneousWaitOnChildren(int waitMsecs) { } } -void AssignmentClientMonitor::childProcessFinished(qint64 pid) { +void AssignmentClientMonitor::childProcessFinished(qint64 pid, int exitCode, QProcess::ExitStatus exitStatus) { + auto message = "Child process " + QString::number(pid) + " has %1 with exit code " + QString::number(exitCode) + "."; + if (_childProcesses.remove(pid)) { - qDebug() << "Child process" << pid << "has finished. Removed from internal map."; + message.append(" Removed from internal map."); + } else { + message.append(" Could not find process in internal map."); + } + + switch (exitStatus) { + case QProcess::NormalExit: + qDebug() << qPrintable(message.arg("returned")); + break; + case QProcess::CrashExit: + qCritical() << qPrintable(message.arg("crashed")); + break; } } @@ -221,7 +234,9 @@ void AssignmentClientMonitor::spawnChildClient() { auto pid = assignmentClient->processId(); // make sure we hear that this process has finished when it does connect(assignmentClient, static_cast(&QProcess::finished), - this, [this, pid]() { childProcessFinished(pid); }); + this, [this, pid](int exitCode, QProcess::ExitStatus exitStatus) { + childProcessFinished(pid, exitCode, exitStatus); + }); qDebug() << "Spawned a child client with PID" << assignmentClient->processId(); diff --git a/assignment-client/src/AssignmentClientMonitor.h b/assignment-client/src/AssignmentClientMonitor.h index a7f69a559b..8848d503ae 100644 --- a/assignment-client/src/AssignmentClientMonitor.h +++ b/assignment-client/src/AssignmentClientMonitor.h @@ -44,7 +44,7 @@ public: void stopChildProcesses(); private slots: void checkSpares(); - void childProcessFinished(qint64 pid); + void childProcessFinished(qint64 pid, int exitCode, QProcess::ExitStatus exitStatus); void handleChildStatusPacket(QSharedPointer message); bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false) override; diff --git a/assignment-client/src/AvatarAudioTimer.cpp b/assignment-client/src/AvatarAudioTimer.cpp deleted file mode 100644 index d031b9d9f6..0000000000 --- a/assignment-client/src/AvatarAudioTimer.cpp +++ /dev/null @@ -1,36 +0,0 @@ -// -// AvatarAudioTimer.cpp -// assignment-client/src -// -// Created by David Kelly on 10/12/13. -// 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 "AvatarAudioTimer.h" - -// this should send a signal every 10ms, with pretty good precision. Hardcoding -// to 10ms since that's what you'd want for audio. -void AvatarAudioTimer::start() { - auto startTime = usecTimestampNow(); - quint64 frameCounter = 0; - const int TARGET_INTERVAL_USEC = 10000; // 10ms - while (!_quit) { - ++frameCounter; - - // tick every 10ms from startTime - quint64 targetTime = startTime + frameCounter * TARGET_INTERVAL_USEC; - quint64 now = usecTimestampNow(); - - // avoid quint64 underflow - if (now < targetTime) { - usleep(targetTime - now); - } - - emit avatarTick(); - } - qDebug() << "AvatarAudioTimer is finished"; -} diff --git a/assignment-client/src/AvatarAudioTimer.h b/assignment-client/src/AvatarAudioTimer.h deleted file mode 100644 index 1f6381b030..0000000000 --- a/assignment-client/src/AvatarAudioTimer.h +++ /dev/null @@ -1,31 +0,0 @@ -// -// AvatarAudioTimer.h -// assignment-client/src -// -// Created by David Kelly on 10/12/13. -// 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_AvatarAudioTimer_h -#define hifi_AvatarAudioTimer_h - -#include - -class AvatarAudioTimer : public QObject { - Q_OBJECT - -signals: - void avatarTick(); - -public slots: - void start(); - void stop() { _quit = true; } - -private: - bool _quit { false }; -}; - -#endif //hifi_AvatarAudioTimer_h diff --git a/assignment-client/src/audio/AudioMixerSlavePool.cpp b/assignment-client/src/audio/AudioMixerSlavePool.cpp index 643361ac5d..d2c19d97ba 100644 --- a/assignment-client/src/audio/AudioMixerSlavePool.cpp +++ b/assignment-client/src/audio/AudioMixerSlavePool.cpp @@ -76,7 +76,7 @@ void AudioMixerSlavePool::processPackets(ConstIter begin, ConstIter end) { void AudioMixerSlavePool::mix(ConstIter begin, ConstIter end, unsigned int frame, float throttlingRatio) { _function = &AudioMixerSlave::mix; - _configure = [&](AudioMixerSlave& slave) { + _configure = [=](AudioMixerSlave& slave) { slave.configureMix(_begin, _end, _frame, _throttlingRatio); }; _frame = frame; diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index 4d80bc7d17..a4bf8fa253 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -108,9 +108,6 @@ void AvatarMixerClientData::ignoreOther(SharedNodePointer self, SharedNodePointe void AvatarMixerClientData::removeFromRadiusIgnoringSet(SharedNodePointer self, const QUuid& other) { if (isRadiusIgnoring(other)) { _radiusIgnoredOthers.erase(other); - auto exitingSpaceBubblePacket = NLPacket::create(PacketType::ExitingSpaceBubble, NUM_BYTES_RFC4122_UUID); - exitingSpaceBubblePacket->write(other.toRfc4122()); - DependencyManager::get()->sendUnreliablePacket(*exitingSpaceBubblePacket, *self); } } diff --git a/assignment-client/src/avatars/AvatarMixerSlavePool.cpp b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp index cb5ae7735a..8afbc1cfe4 100644 --- a/assignment-client/src/avatars/AvatarMixerSlavePool.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp @@ -69,7 +69,7 @@ static AvatarMixerSlave slave; void AvatarMixerSlavePool::processIncomingPackets(ConstIter begin, ConstIter end) { _function = &AvatarMixerSlave::processIncomingPackets; - _configure = [&](AvatarMixerSlave& slave) { + _configure = [=](AvatarMixerSlave& slave) { slave.configure(begin, end); }; run(begin, end); @@ -79,7 +79,7 @@ void AvatarMixerSlavePool::broadcastAvatarData(ConstIter begin, ConstIter end, p_high_resolution_clock::time_point lastFrameTimestamp, float maxKbpsPerNode, float throttlingRatio) { _function = &AvatarMixerSlave::broadcastAvatarData; - _configure = [&](AvatarMixerSlave& slave) { + _configure = [=](AvatarMixerSlave& slave) { slave.configureBroadcast(begin, end, lastFrameTimestamp, maxKbpsPerNode, throttlingRatio); }; run(begin, end); diff --git a/assignment-client/src/entities/EntityServer.cpp b/assignment-client/src/entities/EntityServer.cpp index ac686e2e0a..2c8f8a9e37 100644 --- a/assignment-client/src/entities/EntityServer.cpp +++ b/assignment-client/src/entities/EntityServer.cpp @@ -50,6 +50,12 @@ EntityServer::~EntityServer() { tree->removeNewlyCreatedHook(this); } +void EntityServer::aboutToFinish() { + DependencyManager::get()->cleanup(); + + OctreeServer::aboutToFinish(); +} + void EntityServer::handleEntityPacket(QSharedPointer message, SharedNodePointer senderNode) { if (_octreeInboundPacketProcessor) { _octreeInboundPacketProcessor->queueReceivedPacket(message, senderNode); diff --git a/assignment-client/src/entities/EntityServer.h b/assignment-client/src/entities/EntityServer.h index 40676e79bd..26c2f149aa 100644 --- a/assignment-client/src/entities/EntityServer.h +++ b/assignment-client/src/entities/EntityServer.h @@ -59,6 +59,8 @@ public: virtual void trackSend(const QUuid& dataID, quint64 dataLastEdited, const QUuid& sessionID) override; virtual void trackViewerGone(const QUuid& sessionID) override; + virtual void aboutToFinish() override; + public slots: virtual void nodeAdded(SharedNodePointer node) override; virtual void nodeKilled(SharedNodePointer node) override; diff --git a/assignment-client/src/octree/OctreeSendThread.cpp b/assignment-client/src/octree/OctreeSendThread.cpp index 7db06f12c0..868b377ced 100644 --- a/assignment-client/src/octree/OctreeSendThread.cpp +++ b/assignment-client/src/octree/OctreeSendThread.cpp @@ -81,7 +81,6 @@ bool OctreeSendThread::process() { // don't do any send processing until the initial load of the octree is complete... if (_myServer->isInitialLoadComplete()) { if (auto node = _node.lock()) { - _nodeMissingCount = 0; OctreeQueryNode* nodeData = static_cast(node->getLinkedData()); // Sometimes the node data has not yet been linked, in which case we can't really do anything @@ -129,8 +128,7 @@ AtomicUIntStat OctreeSendThread::_totalSpecialBytes { 0 }; AtomicUIntStat OctreeSendThread::_totalSpecialPackets { 0 }; -int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* nodeData, int& trueBytesSent, - int& truePacketsSent, bool dontSuppressDuplicate) { +int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* nodeData, bool dontSuppressDuplicate) { OctreeServer::didHandlePacketSend(this); // if we're shutting down, then exit early @@ -141,15 +139,14 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* bool debug = _myServer->wantsDebugSending(); quint64 now = usecTimestampNow(); - bool packetSent = false; // did we send a packet? - int packetsSent = 0; + int numPackets = 0; // Here's where we check to see if this packet is a duplicate of the last packet. If it is, we will silently // obscure the packet and not send it. This allows the callers and upper level logic to not need to know about // this rate control savings. if (!dontSuppressDuplicate && nodeData->shouldSuppressDuplicatePacket()) { nodeData->resetOctreePacket(); // we still need to reset it though! - return packetsSent; // without sending... + return numPackets; // without sending... } // If we've got a stats message ready to send, then see if we can piggyback them together @@ -163,12 +160,15 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* // copy octree message to back of stats message statsPacket.write(nodeData->getPacket().getData(), nodeData->getPacket().getDataSize()); - // since a stats message is only included on end of scene, don't consider any of these bytes "wasted", since + int numBytes = statsPacket.getDataSize(); + _totalBytes += numBytes; + _totalPackets++; + // since a stats message is only included on end of scene, don't consider any of these bytes "wasted" // there was nothing else to send. int thisWastedBytes = 0; - _totalWastedBytes += thisWastedBytes; - _totalBytes += statsPacket.getDataSize(); - _totalPackets++; + //_totalWastedBytes += 0; + _trueBytesSent += numBytes; + numPackets++; if (debug) { NLPacket& sentPacket = nodeData->getPacket(); @@ -191,18 +191,22 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* // actually send it OctreeServer::didCallWriteDatagram(this); DependencyManager::get()->sendUnreliablePacket(statsPacket, *node); - packetSent = true; } else { // not enough room in the packet, send two packets + + // first packet OctreeServer::didCallWriteDatagram(this); DependencyManager::get()->sendUnreliablePacket(statsPacket, *node); - // since a stats message is only included on end of scene, don't consider any of these bytes "wasted", since + int numBytes = statsPacket.getDataSize(); + _totalBytes += numBytes; + _totalPackets++; + // since a stats message is only included on end of scene, don't consider any of these bytes "wasted" // there was nothing else to send. int thisWastedBytes = 0; - _totalWastedBytes += thisWastedBytes; - _totalBytes += statsPacket.getDataSize(); - _totalPackets++; + //_totalWastedBytes += 0; + _trueBytesSent += numBytes; + numPackets++; if (debug) { NLPacket& sentPacket = nodeData->getPacket(); @@ -221,19 +225,18 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* "] wasted bytes:" << thisWastedBytes << " [" << _totalWastedBytes << "]"; } - trueBytesSent += statsPacket.getDataSize(); - truePacketsSent++; - packetsSent++; - + // second packet OctreeServer::didCallWriteDatagram(this); DependencyManager::get()->sendUnreliablePacket(nodeData->getPacket(), *node); - packetSent = true; - int packetSizeWithHeader = nodeData->getPacket().getDataSize(); - thisWastedBytes = udt::MAX_PACKET_SIZE - packetSizeWithHeader; - _totalWastedBytes += thisWastedBytes; - _totalBytes += nodeData->getPacket().getDataSize(); + numBytes = nodeData->getPacket().getDataSize(); + _totalBytes += numBytes; _totalPackets++; + // we count wasted bytes here because we were unable to fit the stats packet + thisWastedBytes = udt::MAX_PACKET_SIZE - numBytes; + _totalWastedBytes += thisWastedBytes; + _trueBytesSent += numBytes; + numPackets++; if (debug) { NLPacket& sentPacket = nodeData->getPacket(); @@ -259,13 +262,14 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* // just send the octree packet OctreeServer::didCallWriteDatagram(this); DependencyManager::get()->sendUnreliablePacket(nodeData->getPacket(), *node); - packetSent = true; - int packetSizeWithHeader = nodeData->getPacket().getDataSize(); - int thisWastedBytes = udt::MAX_PACKET_SIZE - packetSizeWithHeader; - _totalWastedBytes += thisWastedBytes; - _totalBytes += packetSizeWithHeader; + int numBytes = nodeData->getPacket().getDataSize(); + _totalBytes += numBytes; _totalPackets++; + int thisWastedBytes = udt::MAX_PACKET_SIZE - numBytes; + _totalWastedBytes += thisWastedBytes; + numPackets++; + _trueBytesSent += numBytes; if (debug) { NLPacket& sentPacket = nodeData->getPacket(); @@ -280,23 +284,21 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* qDebug() << "Sending packet at " << now << " [" << _totalPackets <<"]: sequence: " << sequence << " timestamp: " << timestamp << - " size: " << packetSizeWithHeader << " [" << _totalBytes << + " size: " << numBytes << " [" << _totalBytes << "] wasted bytes:" << thisWastedBytes << " [" << _totalWastedBytes << "]"; } } } // remember to track our stats - if (packetSent) { + if (numPackets > 0) { nodeData->stats.packetSent(nodeData->getPacket().getPayloadSize()); - trueBytesSent += nodeData->getPacket().getPayloadSize(); - truePacketsSent++; - packetsSent++; nodeData->octreePacketSent(); nodeData->resetOctreePacket(); } - return packetsSent; + _truePacketsSent += numPackets; + return numPackets; } /// Version of octree element distributor that sends the deepest LOD level at once @@ -315,13 +317,9 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* preDistributionProcessing(); } - // calculate max number of packets that can be sent during this interval - int clientMaxPacketsPerInterval = std::max(1, (nodeData->getMaxQueryPacketsPerSecond() / INTERVALS_PER_SECOND)); - int maxPacketsPerInterval = std::min(clientMaxPacketsPerInterval, _myServer->getPacketsPerClientPerInterval()); - - int truePacketsSent = 0; - int trueBytesSent = 0; - int packetsSentThisInterval = 0; + _truePacketsSent = 0; + _trueBytesSent = 0; + _packetsSentThisInterval = 0; bool isFullScene = nodeData->shouldForceFullScene(); if (isFullScene) { @@ -334,17 +332,9 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* && ((!viewFrustumChanged && nodeData->getViewFrustumJustStoppedChanging()) || nodeData->hasLodChanged())); } - bool somethingToSend = true; // assume we have something - - // If our packet already has content in it, then we must use the color choice of the waiting packet. - // If we're starting a fresh packet, then... - // If we're moving, and the client asked for low res, then we force monochrome, otherwise, use - // the clients requested color state. - - // If we have a packet waiting, and our desired want color, doesn't match the current waiting packets color - // then let's just send that waiting packet. if (nodeData->isPacketWaiting()) { - packetsSentThisInterval += handlePacketSend(node, nodeData, trueBytesSent, truePacketsSent); + // send the waiting packet + _packetsSentThisInterval += handlePacketSend(node, nodeData); } else { nodeData->resetOctreePacket(); } @@ -375,8 +365,7 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* //unsigned long encodeTime = nodeData->stats.getTotalEncodeTime(); //unsigned long elapsedTime = nodeData->stats.getElapsedTime(); - int packetsJustSent = handlePacketSend(node, nodeData, trueBytesSent, truePacketsSent, isFullScene); - packetsSentThisInterval += packetsJustSent; + _packetsSentThisInterval += handlePacketSend(node, nodeData, isFullScene); // If we're starting a full scene, then definitely we want to empty the elementBag if (isFullScene) { @@ -404,185 +393,44 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* // If we have something in our elementBag, then turn them into packets and send them out... if (!nodeData->elementBag.isEmpty()) { - int bytesWritten = 0; quint64 start = usecTimestampNow(); - // TODO: add these to stats page - //quint64 startCompressTimeMsecs = OctreePacketData::getCompressContentTime() / 1000; - //quint64 startCompressCalls = OctreePacketData::getCompressContentCalls(); - - int extraPackingAttempts = 0; - bool completedScene = false; - - while (somethingToSend && packetsSentThisInterval < maxPacketsPerInterval && !nodeData->isShuttingDown()) { - float lockWaitElapsedUsec = OctreeServer::SKIP_TIME; - float encodeElapsedUsec = OctreeServer::SKIP_TIME; - float compressAndWriteElapsedUsec = OctreeServer::SKIP_TIME; - float packetSendingElapsedUsec = OctreeServer::SKIP_TIME; - - quint64 startInside = usecTimestampNow(); - - bool lastNodeDidntFit = false; // assume each node fits - if (!nodeData->elementBag.isEmpty()) { - - quint64 lockWaitStart = usecTimestampNow(); - _myServer->getOctree()->withReadLock([&]{ - quint64 lockWaitEnd = usecTimestampNow(); - lockWaitElapsedUsec = (float)(lockWaitEnd - lockWaitStart); - quint64 encodeStart = usecTimestampNow(); - - OctreeElementPointer subTree = nodeData->elementBag.extract(); - if (!subTree) { - return; - } - - float octreeSizeScale = nodeData->getOctreeSizeScale(); - int boundaryLevelAdjustClient = nodeData->getBoundaryLevelAdjust(); - - int boundaryLevelAdjust = boundaryLevelAdjustClient + - (viewFrustumChanged ? LOW_RES_MOVING_ADJUST : NO_BOUNDARY_ADJUST); - - EncodeBitstreamParams params(INT_MAX, WANT_EXISTS_BITS, DONT_CHOP, - viewFrustumChanged, boundaryLevelAdjust, octreeSizeScale, - isFullScene, _myServer->getJurisdiction(), nodeData); - nodeData->copyCurrentViewFrustum(params.viewFrustum); - if (viewFrustumChanged) { - nodeData->copyLastKnownViewFrustum(params.lastViewFrustum); - } - - // Our trackSend() function is implemented by the server subclass, and will be called back - // during the encodeTreeBitstream() as new entities/data elements are sent - params.trackSend = [this, node](const QUuid& dataID, quint64 dataEdited) { - _myServer->trackSend(dataID, dataEdited, node->getUUID()); - }; - - // TODO: should this include the lock time or not? This stat is sent down to the client, - // it seems like it may be a good idea to include the lock time as part of the encode time - // are reported to client. Since you can encode without the lock - nodeData->stats.encodeStarted(); - - bytesWritten = _myServer->getOctree()->encodeTreeBitstream(subTree, &_packetData, nodeData->elementBag, params); - - quint64 encodeEnd = usecTimestampNow(); - encodeElapsedUsec = (float)(encodeEnd - encodeStart); - - // If after calling encodeTreeBitstream() there are no nodes left to send, then we know we've - // sent the entire scene. We want to know this below so we'll actually write this content into - // the packet and send it - completedScene = nodeData->elementBag.isEmpty(); - - if (params.stopReason == EncodeBitstreamParams::DIDNT_FIT) { - lastNodeDidntFit = true; - extraPackingAttempts++; - } - - nodeData->stats.encodeStopped(); - }); - } else { - // If the bag was empty then we didn't even attempt to encode, and so we know the bytesWritten were 0 - bytesWritten = 0; - somethingToSend = false; // this will cause us to drop out of the loop... - } - - // If the last node didn't fit, but we're in compressed mode, then we actually want to see if we can fit a - // little bit more in this packet. To do this we write into the packet, but don't send it yet, we'll - // keep attempting to write in compressed mode to add more compressed segments - - // We only consider sending anything if there is something in the _packetData to send... But - // if bytesWritten == 0 it means either the subTree couldn't fit or we had an empty bag... Both cases - // mean we should send the previous packet contents and reset it. - if (completedScene || lastNodeDidntFit) { - - if (_packetData.hasContent()) { - quint64 compressAndWriteStart = usecTimestampNow(); - - // if for some reason the finalized size is greater than our available size, then probably the "compressed" - // form actually inflated beyond our padding, and in this case we will send the current packet, then - // write to out new packet... - unsigned int writtenSize = _packetData.getFinalizedSize() + sizeof(OCTREE_PACKET_INTERNAL_SECTION_SIZE); - - if (writtenSize > nodeData->getAvailable()) { - packetsSentThisInterval += handlePacketSend(node, nodeData, trueBytesSent, truePacketsSent); - } - - nodeData->writeToPacket(_packetData.getFinalizedData(), _packetData.getFinalizedSize()); - quint64 compressAndWriteEnd = usecTimestampNow(); - compressAndWriteElapsedUsec = (float)(compressAndWriteEnd - compressAndWriteStart); - } - - // If we're not running compressed, then we know we can just send now. Or if we're running compressed, but - // the packet doesn't have enough space to bother attempting to pack more... - bool sendNow = true; - - if (!completedScene && (nodeData->getAvailable() >= MINIMUM_ATTEMPT_MORE_PACKING && - extraPackingAttempts <= REASONABLE_NUMBER_OF_PACKING_ATTEMPTS)) { - sendNow = false; // try to pack more - } - - int targetSize = MAX_OCTREE_PACKET_DATA_SIZE; - if (sendNow) { - quint64 packetSendingStart = usecTimestampNow(); - packetsSentThisInterval += handlePacketSend(node, nodeData, trueBytesSent, truePacketsSent); - quint64 packetSendingEnd = usecTimestampNow(); - packetSendingElapsedUsec = (float)(packetSendingEnd - packetSendingStart); - - targetSize = nodeData->getAvailable() - sizeof(OCTREE_PACKET_INTERNAL_SECTION_SIZE); - extraPackingAttempts = 0; - } else { - // If we're in compressed mode, then we want to see if we have room for more in this wire packet. - // but we've finalized the _packetData, so we want to start a new section, we will do that by - // resetting the packet settings with the max uncompressed size of our current available space - // in the wire packet. We also include room for our section header, and a little bit of padding - // to account for the fact that whenc compressing small amounts of data, we sometimes end up with - // a larger compressed size then uncompressed size - targetSize = nodeData->getAvailable() - sizeof(OCTREE_PACKET_INTERNAL_SECTION_SIZE) - COMPRESS_PADDING; - } - _packetData.changeSettings(true, targetSize); // will do reset - NOTE: Always compressed - - } - OctreeServer::trackTreeWaitTime(lockWaitElapsedUsec); - OctreeServer::trackEncodeTime(encodeElapsedUsec); - OctreeServer::trackCompressAndWriteTime(compressAndWriteElapsedUsec); - OctreeServer::trackPacketSendingTime(packetSendingElapsedUsec); - - quint64 endInside = usecTimestampNow(); - quint64 elapsedInsideUsecs = endInside - startInside; - OctreeServer::trackInsideTime((float)elapsedInsideUsecs); - } - - if (somethingToSend && _myServer->wantsVerboseDebug()) { - qCDebug(octree) << "Hit PPS Limit, packetsSentThisInterval =" << packetsSentThisInterval - << " maxPacketsPerInterval = " << maxPacketsPerInterval - << " clientMaxPacketsPerInterval = " << clientMaxPacketsPerInterval; - } + traverseTreeAndSendContents(node, nodeData, viewFrustumChanged, isFullScene); // Here's where we can/should allow the server to send other data... // send the environment packet // TODO: should we turn this into a while loop to better handle sending multiple special packets if (_myServer->hasSpecialPacketsToSend(node) && !nodeData->isShuttingDown()) { int specialPacketsSent = 0; - trueBytesSent += _myServer->sendSpecialPackets(node, nodeData, specialPacketsSent); + int specialBytesSent = _myServer->sendSpecialPackets(node, nodeData, specialPacketsSent); nodeData->resetOctreePacket(); // because nodeData's _sequenceNumber has changed - truePacketsSent += specialPacketsSent; - packetsSentThisInterval += specialPacketsSent; + _truePacketsSent += specialPacketsSent; + _trueBytesSent += specialBytesSent; + _packetsSentThisInterval += specialPacketsSent; _totalPackets += specialPacketsSent; - _totalBytes += trueBytesSent; + _totalBytes += specialBytesSent; _totalSpecialPackets += specialPacketsSent; - _totalSpecialBytes += trueBytesSent; + _totalSpecialBytes += specialBytesSent; } + // calculate max number of packets that can be sent during this interval + int clientMaxPacketsPerInterval = std::max(1, (nodeData->getMaxQueryPacketsPerSecond() / INTERVALS_PER_SECOND)); + int maxPacketsPerInterval = std::min(clientMaxPacketsPerInterval, _myServer->getPacketsPerClientPerInterval()); + // Re-send packets that were nacked by the client - while (nodeData->hasNextNackedPacket() && packetsSentThisInterval < maxPacketsPerInterval) { + while (nodeData->hasNextNackedPacket() && _packetsSentThisInterval < maxPacketsPerInterval) { const NLPacket* packet = nodeData->getNextNackedPacket(); if (packet) { DependencyManager::get()->sendUnreliablePacket(*packet, *node); - truePacketsSent++; - packetsSentThisInterval++; + int numBytes = packet->getDataSize(); + _truePacketsSent++; + _trueBytesSent += numBytes; + _packetsSentThisInterval++; - _totalBytes += packet->getDataSize(); _totalPackets++; + _totalBytes += numBytes; _totalWastedBytes += udt::MAX_PACKET_SIZE - packet->getDataSize(); } } @@ -591,12 +439,6 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* int elapsedmsec = (end - start) / USECS_PER_MSEC; OctreeServer::trackLoopTime(elapsedmsec); - // TODO: add these to stats page - //quint64 endCompressCalls = OctreePacketData::getCompressContentCalls(); - //int elapsedCompressCalls = endCompressCalls - startCompressCalls; - //quint64 endCompressTimeMsecs = OctreePacketData::getCompressContentTime() / 1000; - //int elapsedCompressTimeMsecs = endCompressTimeMsecs - startCompressTimeMsecs; - // if after sending packets we've emptied our bag, then we want to remember that we've sent all // the octree elements from the current view frustum if (nodeData->elementBag.isEmpty()) { @@ -606,17 +448,147 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* // If this was a full scene then make sure we really send out a stats packet at this point so that // the clients will know the scene is stable if (isFullScene) { - int thisTrueBytesSent = 0; - int thisTruePacketsSent = 0; nodeData->stats.sceneCompleted(); - int packetsJustSent = handlePacketSend(node, nodeData, thisTrueBytesSent, thisTruePacketsSent, true); - _totalBytes += thisTrueBytesSent; - _totalPackets += thisTruePacketsSent; - truePacketsSent += packetsJustSent; + handlePacketSend(node, nodeData, true); } } } // end if bag wasn't empty, and so we sent stuff... - return truePacketsSent; + return _truePacketsSent; +} + +void OctreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged, bool isFullScene) { + // calculate max number of packets that can be sent during this interval + int clientMaxPacketsPerInterval = std::max(1, (nodeData->getMaxQueryPacketsPerSecond() / INTERVALS_PER_SECOND)); + int maxPacketsPerInterval = std::min(clientMaxPacketsPerInterval, _myServer->getPacketsPerClientPerInterval()); + + int extraPackingAttempts = 0; + bool completedScene = false; + + bool somethingToSend = true; // assume we have something + while (somethingToSend && _packetsSentThisInterval < maxPacketsPerInterval && !nodeData->isShuttingDown()) { + float lockWaitElapsedUsec = OctreeServer::SKIP_TIME; + float encodeElapsedUsec = OctreeServer::SKIP_TIME; + float compressAndWriteElapsedUsec = OctreeServer::SKIP_TIME; + float packetSendingElapsedUsec = OctreeServer::SKIP_TIME; + + quint64 startInside = usecTimestampNow(); + + bool lastNodeDidntFit = false; // assume each node fits + if (!nodeData->elementBag.isEmpty()) { + + quint64 lockWaitStart = usecTimestampNow(); + _myServer->getOctree()->withReadLock([&]{ + quint64 lockWaitEnd = usecTimestampNow(); + lockWaitElapsedUsec = (float)(lockWaitEnd - lockWaitStart); + quint64 encodeStart = usecTimestampNow(); + + OctreeElementPointer subTree = nodeData->elementBag.extract(); + if (!subTree) { + return; + } + + float octreeSizeScale = nodeData->getOctreeSizeScale(); + int boundaryLevelAdjustClient = nodeData->getBoundaryLevelAdjust(); + + int boundaryLevelAdjust = boundaryLevelAdjustClient + + (viewFrustumChanged ? LOW_RES_MOVING_ADJUST : NO_BOUNDARY_ADJUST); + + EncodeBitstreamParams params(INT_MAX, WANT_EXISTS_BITS, DONT_CHOP, + viewFrustumChanged, boundaryLevelAdjust, octreeSizeScale, + isFullScene, _myServer->getJurisdiction(), nodeData); + nodeData->copyCurrentViewFrustum(params.viewFrustum); + if (viewFrustumChanged) { + nodeData->copyLastKnownViewFrustum(params.lastViewFrustum); + } + + // Our trackSend() function is implemented by the server subclass, and will be called back + // during the encodeTreeBitstream() as new entities/data elements are sent + params.trackSend = [this](const QUuid& dataID, quint64 dataEdited) { + _myServer->trackSend(dataID, dataEdited, _nodeUuid); + }; + + // TODO: should this include the lock time or not? This stat is sent down to the client, + // it seems like it may be a good idea to include the lock time as part of the encode time + // are reported to client. Since you can encode without the lock + nodeData->stats.encodeStarted(); + + // NOTE: this is where the tree "contents" are actaully packed + _myServer->getOctree()->encodeTreeBitstream(subTree, &_packetData, nodeData->elementBag, params); + + quint64 encodeEnd = usecTimestampNow(); + encodeElapsedUsec = (float)(encodeEnd - encodeStart); + + // If after calling encodeTreeBitstream() there are no nodes left to send, then we know we've + // sent the entire scene. We want to know this below so we'll actually write this content into + // the packet and send it + completedScene = nodeData->elementBag.isEmpty(); + + if (params.stopReason == EncodeBitstreamParams::DIDNT_FIT) { + lastNodeDidntFit = true; + extraPackingAttempts++; + } + + nodeData->stats.encodeStopped(); + }); + } else { + somethingToSend = false; // this will cause us to drop out of the loop... + } + + if (completedScene || lastNodeDidntFit) { + // we probably want to flush what has accumulated in nodeData but: + // do we have more data to send? and is there room? + if (_packetData.hasContent()) { + // yes, more data to send + quint64 compressAndWriteStart = usecTimestampNow(); + unsigned int additionalSize = _packetData.getFinalizedSize() + sizeof(OCTREE_PACKET_INTERNAL_SECTION_SIZE); + if (additionalSize > nodeData->getAvailable()) { + // no room --> flush what we've got + _packetsSentThisInterval += handlePacketSend(node, nodeData); + } + + // either there is room, or we've flushed and reset nodeData's data buffer + // so we can transfer whatever is in _packetData to nodeData + nodeData->writeToPacket(_packetData.getFinalizedData(), _packetData.getFinalizedSize()); + compressAndWriteElapsedUsec = (float)(usecTimestampNow()- compressAndWriteStart); + } + + bool sendNow = completedScene || + nodeData->getAvailable() < MINIMUM_ATTEMPT_MORE_PACKING || + extraPackingAttempts > REASONABLE_NUMBER_OF_PACKING_ATTEMPTS; + + int targetSize = MAX_OCTREE_PACKET_DATA_SIZE; + if (sendNow) { + quint64 packetSendingStart = usecTimestampNow(); + _packetsSentThisInterval += handlePacketSend(node, nodeData); + quint64 packetSendingEnd = usecTimestampNow(); + packetSendingElapsedUsec = (float)(packetSendingEnd - packetSendingStart); + + targetSize = nodeData->getAvailable() - sizeof(OCTREE_PACKET_INTERNAL_SECTION_SIZE); + extraPackingAttempts = 0; + } else { + // We want to see if we have room for more in this wire packet but we've copied the _packetData, + // so we want to start a new section. We will do that by resetting the packet settings with the max + // size of our current available space in the wire packet plus room for our section header and a + // little bit of padding. + targetSize = nodeData->getAvailable() - sizeof(OCTREE_PACKET_INTERNAL_SECTION_SIZE) - COMPRESS_PADDING; + } + _packetData.changeSettings(true, targetSize); // will do reset - NOTE: Always compressed + } + OctreeServer::trackTreeWaitTime(lockWaitElapsedUsec); + OctreeServer::trackEncodeTime(encodeElapsedUsec); + OctreeServer::trackCompressAndWriteTime(compressAndWriteElapsedUsec); + OctreeServer::trackPacketSendingTime(packetSendingElapsedUsec); + + quint64 endInside = usecTimestampNow(); + quint64 elapsedInsideUsecs = endInside - startInside; + OctreeServer::trackInsideTime((float)elapsedInsideUsecs); + } + + if (somethingToSend && _myServer->wantsVerboseDebug()) { + qCDebug(octree) << "Hit PPS Limit, packetsSentThisInterval =" << _packetsSentThisInterval + << " maxPacketsPerInterval = " << maxPacketsPerInterval + << " clientMaxPacketsPerInterval = " << clientMaxPacketsPerInterval; + } } diff --git a/assignment-client/src/octree/OctreeSendThread.h b/assignment-client/src/octree/OctreeSendThread.h index 06c9b5f1d6..d158539f57 100644 --- a/assignment-client/src/octree/OctreeSendThread.h +++ b/assignment-client/src/octree/OctreeSendThread.h @@ -34,7 +34,7 @@ public: void setIsShuttingDown(); bool isShuttingDown() { return _isShuttingDown; } - + QUuid getNodeUuid() const { return _nodeUuid; } static AtomicUIntStat _totalBytes; @@ -53,20 +53,23 @@ protected: /// Called before a packetDistributor pass to allow for pre-distribution processing virtual void preDistributionProcessing() {}; + virtual void traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged, bool isFullScene); OctreeServer* _myServer { nullptr }; QWeakPointer _node; private: - int handlePacketSend(SharedNodePointer node, OctreeQueryNode* nodeData, int& trueBytesSent, int& truePacketsSent, bool dontSuppressDuplicate = false); + int handlePacketSend(SharedNodePointer node, OctreeQueryNode* nodeData, bool dontSuppressDuplicate = false); int packetDistributor(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged); - + QUuid _nodeUuid; OctreePacketData _packetData; - int _nodeMissingCount { 0 }; + int _truePacketsSent { 0 }; // available for debug stats + int _trueBytesSent { 0 }; // available for debug stats + int _packetsSentThisInterval { 0 }; // used for bandwidth throttle condition bool _isShuttingDown { false }; }; diff --git a/assignment-client/src/scripts/EntityScriptServer.cpp b/assignment-client/src/scripts/EntityScriptServer.cpp index 489478ff9a..e7433e7c05 100644 --- a/assignment-client/src/scripts/EntityScriptServer.cpp +++ b/assignment-client/src/scripts/EntityScriptServer.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -67,6 +68,9 @@ EntityScriptServer::EntityScriptServer(ReceivedMessage& message) : ThreadedAssig DependencyManager::set(); DependencyManager::set(ScriptEngine::ENTITY_SERVER_SCRIPT); + // Needed to ensure the creation of the DebugDraw instance on the main thread + DebugDraw::getInstance(); + auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); packetReceiver.registerListenerForTypes({ PacketType::OctreeStats, PacketType::EntityData, PacketType::EntityErase }, this, "handleOctreePacket"); diff --git a/cmake/templates/NSIS.template.in b/cmake/templates/NSIS.template.in index 710fd81316..f44c8185d8 100644 --- a/cmake/templates/NSIS.template.in +++ b/cmake/templates/NSIS.template.in @@ -49,7 +49,7 @@ Var STR_CONTAINS_VAR_3 Var STR_CONTAINS_VAR_4 Var STR_RETURN_VAR - + Function StrContains Exch $STR_NEEDLE Exch 1 @@ -438,6 +438,7 @@ Var DesktopServerCheckbox Var ServerStartupCheckbox Var LaunchServerNowCheckbox Var LaunchClientNowCheckbox +Var CleanInstallCheckbox Var CurrentOffset Var OffsetUnits Var CopyFromProductionCheckbox @@ -475,27 +476,18 @@ Function PostInstallOptionsPage ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for @INTERFACE_HF_SHORTCUT_NAME@" Pop $DesktopClientCheckbox IntOp $CurrentOffset $CurrentOffset + 15 - + ; set the checkbox state depending on what is present in the registry !insertmacro SetPostInstallOption $DesktopClientCheckbox @CLIENT_DESKTOP_SHORTCUT_REG_KEY@ ${BST_CHECKED} ${EndIf} - + ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for @CONSOLE_HF_SHORTCUT_NAME@" Pop $DesktopServerCheckbox - + IntOp $CurrentOffset $CurrentOffset + 15 + ; set the checkbox state depending on what is present in the registry !insertmacro SetPostInstallOption $DesktopServerCheckbox @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ ${BST_UNCHECKED} - - IntOp $CurrentOffset $CurrentOffset + 15 - - ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @CONSOLE_HF_SHORTCUT_NAME@ on startup" - Pop $ServerStartupCheckbox - - ; set the checkbox state depending on what is present in the registry - !insertmacro SetPostInstallOption $ServerStartupCheckbox @CONSOLE_STARTUP_REG_KEY@ ${BST_CHECKED} - - IntOp $CurrentOffset $CurrentOffset + 15 ${EndIf} ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} @@ -511,17 +503,33 @@ Function PostInstallOptionsPage IntOp $CurrentOffset $CurrentOffset + 15 ${EndIf} - + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} - ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @INTERFACE_HF_SHORTCUT_NAME@ after install" - Pop $LaunchClientNowCheckbox - - ; set the checkbox state depending on what is present in the registry - !insertmacro SetPostInstallOption $LaunchClientNowCheckbox @CLIENT_LAUNCH_NOW_REG_KEY@ ${BST_CHECKED} - ${StrContains} $substringResult "/forceNoLaunchClient" $CMDLINE - ${IfNot} $substringResult == "" - ${NSD_SetState} $LaunchClientNowCheckbox ${BST_UNCHECKED} - ${EndIf} + ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @INTERFACE_HF_SHORTCUT_NAME@ after install" + Pop $LaunchClientNowCheckbox + IntOp $CurrentOffset $CurrentOffset + 30 + + ; set the checkbox state depending on what is present in the registry + !insertmacro SetPostInstallOption $LaunchClientNowCheckbox @CLIENT_LAUNCH_NOW_REG_KEY@ ${BST_CHECKED} + ${StrContains} $substringResult "/forceNoLaunchClient" $CMDLINE + ${IfNot} $substringResult == "" + ${NSD_SetState} $LaunchClientNowCheckbox ${BST_UNCHECKED} + ${EndIf} + ${EndIf} + + ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} + ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @CONSOLE_HF_SHORTCUT_NAME@ on startup" + Pop $ServerStartupCheckbox + IntOp $CurrentOffset $CurrentOffset + 15 + + ; set the checkbox state depending on what is present in the registry + !insertmacro SetPostInstallOption $ServerStartupCheckbox @CONSOLE_STARTUP_REG_KEY@ ${BST_CHECKED} + ${EndIf} + + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} + ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Perform a clean install (Delete older settings and content)" + Pop $CleanInstallCheckbox + IntOp $CurrentOffset $CurrentOffset + 15 ${EndIf} ${If} @PR_BUILD@ == 1 @@ -543,7 +551,7 @@ Function PostInstallOptionsPage ${NSD_SetState} $CopyFromProductionCheckbox ${BST_CHECKED} ${EndIf} - + nsDialogs::Show FunctionEnd @@ -558,6 +566,7 @@ Var ServerStartupState Var LaunchServerNowState Var LaunchClientNowState Var CopyFromProductionState +Var CleanInstallState Function ReadPostInstallOptions ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} @@ -579,13 +588,18 @@ Function ReadPostInstallOptions ${EndIf} ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} - ; check if we need to launch the server post-install - ${NSD_GetState} $LaunchServerNowCheckbox $LaunchServerNowState + ; check if we need to launch the server post-install + ${NSD_GetState} $LaunchServerNowCheckbox $LaunchServerNowState ${EndIf} - + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} - ; check if we need to launch the client post-install - ${NSD_GetState} $LaunchClientNowCheckbox $LaunchClientNowState + ; check if we need to launch the client post-install + ${NSD_GetState} $LaunchClientNowCheckbox $LaunchClientNowState + ${EndIf} + + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} + ; check if the user asked for a clean install + ${NSD_GetState} $CleanInstallCheckbox $CleanInstallState ${EndIf} FunctionEnd @@ -628,6 +642,15 @@ Function HandlePostInstallOptions !insertmacro WritePostInstallOption @CONSOLE_STARTUP_REG_KEY@ NO ${EndIf} ${EndIf} + + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} + ; check if the user asked for a clean install + ${If} $CleanInstallState == ${BST_CHECKED} + SetShellVarContext current + RMDir /r "$APPDATA\@BUILD_ORGANIZATION@" + RMDir /r "$LOCALAPPDATA\@BUILD_ORGANIZATION@" + ${EndIf} + ${EndIf} ${If} @PR_BUILD@ == 1 diff --git a/interface/resources/icons/tablet-icons/edit-disabled.svg b/interface/resources/icons/tablet-icons/edit-disabled.svg new file mode 100644 index 0000000000..4869b30dd9 --- /dev/null +++ b/interface/resources/icons/tablet-icons/edit-disabled.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/spectator-a.svg b/interface/resources/icons/tablet-icons/spectator-a.svg new file mode 100644 index 0000000000..22ebde999b --- /dev/null +++ b/interface/resources/icons/tablet-icons/spectator-a.svg @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/spectator-i.svg b/interface/resources/icons/tablet-icons/spectator-i.svg new file mode 100644 index 0000000000..3e6c1a7dd9 --- /dev/null +++ b/interface/resources/icons/tablet-icons/spectator-i.svg @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/interface/resources/images/static.gif b/interface/resources/images/static.gif new file mode 100644 index 0000000000..fbe46f48e6 Binary files /dev/null and b/interface/resources/images/static.gif differ diff --git a/interface/resources/qml/controls-uit/CheckBox.qml b/interface/resources/qml/controls-uit/CheckBox.qml index 36161e3c3e..b279b7ca8d 100644 --- a/interface/resources/qml/controls-uit/CheckBox.qml +++ b/interface/resources/qml/controls-uit/CheckBox.qml @@ -18,7 +18,7 @@ Original.CheckBox { id: checkBox property int colorScheme: hifi.colorSchemes.light - property string color: hifi.colors.lightGray + property string color: hifi.colors.lightGrayText readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light property bool isRedCheck: false property int boxSize: 14 diff --git a/interface/resources/qml/controls-uit/Separator.qml b/interface/resources/qml/controls-uit/Separator.qml new file mode 100644 index 0000000000..5a775221f6 --- /dev/null +++ b/interface/resources/qml/controls-uit/Separator.qml @@ -0,0 +1,38 @@ +// +// Separator.qml +// +// Created by Zach Fox on 2017-06-06 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import "../styles-uit" + +Item { + // Size + height: 2; + Rectangle { + // Size + width: parent.width; + height: 1; + // Anchors + anchors.left: parent.left; + anchors.bottom: parent.bottom; + anchors.bottomMargin: height; + // Style + color: hifi.colors.baseGrayShadow; + } + Rectangle { + // Size + width: parent.width; + height: 1; + // Anchors + anchors.left: parent.left; + anchors.bottom: parent.bottom; + // Style + color: hifi.colors.baseGrayHighlight; + } +} diff --git a/interface/resources/qml/controls-uit/Slider.qml b/interface/resources/qml/controls-uit/Slider.qml index 39831546e1..89bae9bcde 100644 --- a/interface/resources/qml/controls-uit/Slider.qml +++ b/interface/resources/qml/controls-uit/Slider.qml @@ -36,7 +36,7 @@ Slider { Rectangle { width: parent.height - 2 - height: slider.value * (slider.width/(slider.maximumValue - slider.minimumValue)) - 1 + height: slider.width * (slider.value - slider.minimumValue) / (slider.maximumValue - slider.minimumValue) - 1 radius: height / 2 anchors { top: parent.top diff --git a/interface/resources/qml/controls-uit/Switch.qml b/interface/resources/qml/controls-uit/Switch.qml new file mode 100644 index 0000000000..d54f986717 --- /dev/null +++ b/interface/resources/qml/controls-uit/Switch.qml @@ -0,0 +1,156 @@ +// +// Switch.qml +// +// Created by Zach Fox on 2017-06-06 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 as Original +import QtQuick.Controls.Styles 1.4 + +import "../styles-uit" + +Item { + id: rootSwitch; + + property int colorScheme: hifi.colorSchemes.light; + readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light; + property int switchWidth: 70; + readonly property int switchRadius: height/2; + property string labelTextOff: ""; + property string labelGlyphOffText: ""; + property int labelGlyphOffSize: 32; + property string labelTextOn: ""; + property string labelGlyphOnText: ""; + property int labelGlyphOnSize: 32; + property alias checked: originalSwitch.checked; + signal onCheckedChanged; + signal clicked; + + Original.Switch { + id: originalSwitch; + activeFocusOnPress: true; + anchors.top: rootSwitch.top; + anchors.left: rootSwitch.left; + anchors.leftMargin: rootSwitch.width/2 - rootSwitch.switchWidth/2; + onCheckedChanged: rootSwitch.onCheckedChanged(); + onClicked: rootSwitch.clicked(); + + style: SwitchStyle { + + padding { + top: 3; + left: 3; + right: 3; + bottom: 3; + } + + groove: Rectangle { + color: "#252525"; + implicitWidth: rootSwitch.switchWidth; + implicitHeight: rootSwitch.height; + radius: rootSwitch.switchRadius; + } + + handle: Rectangle { + id: switchHandle; + implicitWidth: rootSwitch.height - padding.top - padding.bottom; + implicitHeight: implicitWidth; + radius: implicitWidth/2; + border.color: hifi.colors.lightGrayText; + color: hifi.colors.lightGray; + + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + onEntered: parent.color = hifi.colors.blueHighlight; + onExited: parent.color = hifi.colors.lightGray; + } + } + } + } + + // OFF Label + Item { + anchors.right: originalSwitch.left; + anchors.rightMargin: 10; + anchors.top: rootSwitch.top; + height: rootSwitch.height; + + RalewaySemiBold { + id: labelOff; + text: labelTextOff; + size: hifi.fontSizes.inputLabel; + color: originalSwitch.checked ? hifi.colors.lightGrayText : "#FFFFFF"; + anchors.top: parent.top; + anchors.right: parent.right; + width: paintedWidth; + height: parent.height; + verticalAlignment: Text.AlignVCenter; + } + + HiFiGlyphs { + id: labelGlyphOff; + text: labelGlyphOffText; + size: labelGlyphOffSize; + color: labelOff.color; + anchors.top: parent.top; + anchors.topMargin: 2; + anchors.right: labelOff.left; + anchors.rightMargin: 4; + } + + MouseArea { + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.left: labelGlyphOff.left; + anchors.right: labelOff.right; + onClicked: { + originalSwitch.checked = false; + } + } + } + + // ON Label + Item { + anchors.left: originalSwitch.right; + anchors.leftMargin: 10; + anchors.top: rootSwitch.top; + height: rootSwitch.height; + + RalewaySemiBold { + id: labelOn; + text: labelTextOn; + size: hifi.fontSizes.inputLabel; + color: originalSwitch.checked ? "#FFFFFF" : hifi.colors.lightGrayText; + anchors.top: parent.top; + anchors.left: parent.left; + width: paintedWidth; + height: parent.height; + verticalAlignment: Text.AlignVCenter; + } + + HiFiGlyphs { + id: labelGlyphOn; + text: labelGlyphOnText; + size: labelGlyphOnSize; + color: labelOn.color; + anchors.top: parent.top; + anchors.left: labelOn.right; + } + + MouseArea { + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.left: labelOn.left; + anchors.right: labelGlyphOn.right; + onClicked: { + originalSwitch.checked = true; + } + } + } +} diff --git a/interface/resources/qml/dialogs/preferences/Section.qml b/interface/resources/qml/dialogs/preferences/Section.qml index a813dc6b5f..3985c7d6f6 100644 --- a/interface/resources/qml/dialogs/preferences/Section.qml +++ b/interface/resources/qml/dialogs/preferences/Section.qml @@ -72,6 +72,7 @@ Preference { property var avatarBuilder: Component { AvatarPreference { } } property var buttonBuilder: Component { ButtonPreference { } } property var comboBoxBuilder: Component { ComboBoxPreference { } } + property var spinnerSliderBuilder: Component { SpinnerSliderPreference { } } property var preferences: [] property int checkBoxCount: 0 @@ -86,7 +87,7 @@ Preference { } function buildPreference(preference) { - console.log("\tPreference type " + preference.type + " name " + preference.name) + console.log("\tPreference type " + preference.type + " name " + preference.name); var builder; switch (preference.type) { case Preference.Editable: @@ -128,6 +129,11 @@ Preference { checkBoxCount = 0; builder = comboBoxBuilder; break; + + case Preference.SpinnerSlider: + checkBoxCount = 0; + builder = spinnerSliderBuilder; + break; }; if (builder) { diff --git a/interface/resources/qml/dialogs/preferences/SpinnerSliderPreference.qml b/interface/resources/qml/dialogs/preferences/SpinnerSliderPreference.qml new file mode 100644 index 0000000000..3cba67bc82 --- /dev/null +++ b/interface/resources/qml/dialogs/preferences/SpinnerSliderPreference.qml @@ -0,0 +1,111 @@ +// +// SpinnerSliderPreference.qml +// +// Created by Cain Kilgore on 11th July 2017 +// 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 +// + +import QtQuick 2.5 + +import "../../dialogs" +import "../../controls-uit" + +Preference { + id: root + property alias slider: slider + property alias spinner: spinner + height: control.height + hifi.dimensions.controlInterlineHeight + + Component.onCompleted: { + slider.value = preference.value; + spinner.value = preference.value; + } + + function save() { + preference.value = slider.value; + preference.save(); + } + + Item { + id: control + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + height: Math.max(labelText.height, slider.height, spinner.height, button.height) + + Label { + id: labelText + text: root.label + ":" + colorScheme: hifi.colorSchemes.dark + anchors { + left: parent.left + right: slider.left + rightMargin: hifi.dimensions.labelPadding + verticalCenter: parent.verticalCenter + } + horizontalAlignment: Text.AlignRight + wrapMode: Text.Wrap + } + + Slider { + id: slider + value: preference.value + width: 100 + minimumValue: MyAvatar.getDomainMinScale() + maximumValue: MyAvatar.getDomainMaxScale() + stepSize: preference.step + onValueChanged: { + spinner.value = value + } + anchors { + right: spinner.left + rightMargin: 10 + verticalCenter: parent.verticalCenter + } + colorScheme: hifi.colorSchemes.dark + } + + SpinBox { + id: spinner + decimals: preference.decimals + value: preference.value + minimumValue: MyAvatar.getDomainMinScale() + maximumValue: MyAvatar.getDomainMaxScale() + width: 100 + onValueChanged: { + slider.value = value; + } + anchors { + right: button.left + rightMargin: 10 + verticalCenter: parent.verticalCenter + } + colorScheme: hifi.colorSchemes.dark + } + + GlyphButton { + id: button + onClicked: { + if (spinner.maximumValue >= 1) { + spinner.value = 1 + slider.value = 1 + } else { + spinner.value = spinner.maximumValue + slider.value = spinner.maximumValue + } + } + width: 30 + glyph: hifi.glyphs.reload + anchors { + right: parent.right + verticalCenter: parent.verticalCenter + } + colorScheme: hifi.colorSchemes.dark + } + } +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/LetterboxMessage.qml b/interface/resources/qml/hifi/LetterboxMessage.qml index 754876b2c1..fa9d7aa6f0 100644 --- a/interface/resources/qml/hifi/LetterboxMessage.qml +++ b/interface/resources/qml/hifi/LetterboxMessage.qml @@ -32,14 +32,15 @@ Item { radius: popupRadius } Rectangle { - width: Math.max(parent.width * 0.75, 400) + id: textContainer; + width: Math.max(parent.width * 0.8, 400) height: contentContainer.height + 50 anchors.centerIn: parent radius: popupRadius color: "white" Item { id: contentContainer - width: parent.width - 60 + width: parent.width - 50 height: childrenRect.height anchors.centerIn: parent Item { @@ -92,7 +93,7 @@ Item { anchors.top: parent.top anchors.topMargin: -20 anchors.right: parent.right - anchors.rightMargin: -25 + anchors.rightMargin: -20 MouseArea { anchors.fill: closeGlyphButton hoverEnabled: true @@ -127,11 +128,51 @@ Item { color: hifi.colors.darkGray wrapMode: Text.WordWrap textFormat: Text.StyledText + onLinkActivated: { + Qt.openUrlExternally(link) + } } } } + // Left gray MouseArea MouseArea { - anchors.fill: parent + anchors.left: parent.left; + anchors.right: textContainer.left; + anchors.top: textContainer.top; + anchors.bottom: textContainer.bottom; + acceptedButtons: Qt.LeftButton + onClicked: { + letterbox.visible = false + } + } + // Right gray MouseArea + MouseArea { + anchors.left: textContainer.left; + anchors.right: parent.left; + anchors.top: textContainer.top; + anchors.bottom: textContainer.bottom; + acceptedButtons: Qt.LeftButton + onClicked: { + letterbox.visible = false + } + } + // Top gray MouseArea + MouseArea { + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: parent.top; + anchors.bottom: textContainer.top; + acceptedButtons: Qt.LeftButton + onClicked: { + letterbox.visible = false + } + } + // Bottom gray MouseArea + MouseArea { + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: textContainer.bottom; + anchors.bottom: parent.bottom; acceptedButtons: Qt.LeftButton onClicked: { letterbox.visible = false diff --git a/interface/resources/qml/hifi/SpectatorCamera.qml b/interface/resources/qml/hifi/SpectatorCamera.qml new file mode 100644 index 0000000000..3a8559ab1e --- /dev/null +++ b/interface/resources/qml/hifi/SpectatorCamera.qml @@ -0,0 +1,374 @@ +// +// SpectatorCamera.qml +// qml/hifi +// +// Spectator Camera +// +// Created by Zach Fox on 2017-06-05 +// 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 +// + +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "../styles-uit" +import "../controls-uit" as HifiControlsUit +import "../controls" as HifiControls + +// references HMD, XXX from root context + +Rectangle { + HifiConstants { id: hifi; } + + id: spectatorCamera; + // Style + color: hifi.colors.baseGray; + + // The letterbox used for popup messages + LetterboxMessage { + id: letterboxMessage; + z: 999; // Force the popup on top of everything else + } + function letterbox(headerGlyph, headerText, message) { + letterboxMessage.headerGlyph = headerGlyph; + letterboxMessage.headerText = headerText; + letterboxMessage.text = message; + letterboxMessage.visible = true; + letterboxMessage.popupRadius = 0; + } + + // + // TITLE BAR START + // + Item { + id: titleBarContainer; + // Size + width: spectatorCamera.width; + height: 50; + // Anchors + anchors.left: parent.left; + anchors.top: parent.top; + + // "Spectator" text + RalewaySemiBold { + id: titleBarText; + text: "Spectator"; + // Text size + size: hifi.fontSizes.overlayTitle; + // Anchors + anchors.fill: parent; + anchors.leftMargin: 16; + // Style + color: hifi.colors.lightGrayText; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + // Separator + HifiControlsUit.Separator { + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + } + } + // + // TITLE BAR END + // + + // + // SPECTATOR APP DESCRIPTION START + // + Item { + id: spectatorDescriptionContainer; + // Size + width: spectatorCamera.width; + height: childrenRect.height; + // Anchors + anchors.left: parent.left; + anchors.top: titleBarContainer.bottom; + + // (i) Glyph + HiFiGlyphs { + id: spectatorDescriptionGlyph; + text: hifi.glyphs.info; + // Size + width: 20; + height: parent.height; + size: 60; + // Anchors + anchors.left: parent.left; + anchors.leftMargin: 20; + anchors.top: parent.top; + anchors.topMargin: 0; + // Style + color: hifi.colors.lightGrayText; + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignTop; + } + + // "Spectator" app description text + RalewayLight { + id: spectatorDescriptionText; + text: "Spectator lets you change what your monitor displays while you're using a VR headset. Use Spectator when streaming and recording video."; + // Text size + size: 14; + // Size + width: 350; + height: paintedHeight; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 15; + anchors.left: spectatorDescriptionGlyph.right; + anchors.leftMargin: 40; + // Style + color: hifi.colors.lightGrayText; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + // "Learn More" text + RalewayRegular { + id: spectatorLearnMoreText; + text: "Learn More About Spectator"; + // Text size + size: 14; + // Size + width: paintedWidth; + height: paintedHeight; + // Anchors + anchors.top: spectatorDescriptionText.bottom; + anchors.topMargin: 10; + anchors.left: spectatorDescriptionText.anchors.left; + anchors.leftMargin: spectatorDescriptionText.anchors.leftMargin; + // Style + color: hifi.colors.blueAccent; + wrapMode: Text.WordWrap; + font.underline: true; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + + MouseArea { + anchors.fill: parent; + hoverEnabled: enabled; + onClicked: { + letterbox(hifi.glyphs.question, + "Spectator Camera", + "By default, your monitor shows a preview of what you're seeing in VR. " + + "Using the Spectator Camera app, your monitor can display the view " + + "from a virtual hand-held camera - perfect for taking selfies or filming " + + "your friends!
" + + "

Streaming and Recording

" + + "We recommend OBS for streaming and recording the contents of your monitor to services like " + + "Twitch, YouTube Live, and Facebook Live.

" + + "To get started using OBS, click this link now. The page will open in an external browser:
" + + 'OBS Official Overview Guide'); + } + onEntered: parent.color = hifi.colors.blueHighlight; + onExited: parent.color = hifi.colors.blueAccent; + } + } + + // Separator + HifiControlsUit.Separator { + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: spectatorLearnMoreText.bottom; + anchors.topMargin: spectatorDescriptionText.anchors.topMargin; + } + } + // + // SPECTATOR APP DESCRIPTION END + // + + + // + // SPECTATOR CONTROLS START + // + Item { + id: spectatorControlsContainer; + // Size + height: spectatorCamera.height - spectatorDescriptionContainer.height - titleBarContainer.height; + // Anchors + anchors.top: spectatorDescriptionContainer.bottom; + anchors.topMargin: 20; + anchors.left: parent.left; + anchors.leftMargin: 25; + anchors.right: parent.right; + anchors.rightMargin: anchors.leftMargin; + + // "Camera On" Checkbox + HifiControlsUit.CheckBox { + id: cameraToggleCheckBox; + colorScheme: hifi.colorSchemes.dark; + anchors.left: parent.left; + anchors.top: parent.top; + text: "Spectator Camera On"; + boxSize: 24; + onClicked: { + sendToScript({method: (checked ? 'spectatorCameraOn' : 'spectatorCameraOff')}); + spectatorCameraPreview.ready = checked; + } + } + + // Instructions or Preview + Rectangle { + id: spectatorCameraImageContainer; + anchors.left: parent.left; + anchors.top: cameraToggleCheckBox.bottom; + anchors.topMargin: 20; + anchors.right: parent.right; + height: 250; + color: cameraToggleCheckBox.checked ? "transparent" : "black"; + + AnimatedImage { + source: "../../images/static.gif" + visible: !cameraToggleCheckBox.checked; + anchors.fill: parent; + opacity: 0.15; + } + + // Instructions (visible when display texture isn't set) + FiraSansRegular { + id: spectatorCameraInstructions; + text: "Turn on Spectator Camera for a preview\nof what your monitor shows."; + size: 16; + color: hifi.colors.lightGrayText; + visible: !cameraToggleCheckBox.checked; + anchors.fill: parent; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + + // Spectator Camera Preview + Hifi.ResourceImageItem { + id: spectatorCameraPreview; + visible: cameraToggleCheckbox.checked; + url: monitorShowsSwitch.checked ? "resource://spectatorCameraFrame" : "resource://hmdPreviewFrame"; + ready: cameraToggleCheckBox.checked; + mirrorVertically: true; + anchors.fill: parent; + onVisibleChanged: { + ready = cameraToggleCheckBox.checked; + update(); + } + } + } + + + // "Monitor Shows" Switch Label Glyph + HiFiGlyphs { + id: monitorShowsSwitchLabelGlyph; + text: hifi.glyphs.screen; + size: 32; + color: hifi.colors.blueHighlight; + anchors.top: spectatorCameraImageContainer.bottom; + anchors.topMargin: 13; + anchors.left: parent.left; + } + // "Monitor Shows" Switch Label + RalewayLight { + id: monitorShowsSwitchLabel; + text: "MONITOR SHOWS:"; + anchors.top: spectatorCameraImageContainer.bottom; + anchors.topMargin: 20; + anchors.left: monitorShowsSwitchLabelGlyph.right; + anchors.leftMargin: 6; + size: 16; + width: paintedWidth; + height: paintedHeight; + color: hifi.colors.lightGrayText; + verticalAlignment: Text.AlignVCenter; + } + // "Monitor Shows" Switch + HifiControlsUit.Switch { + id: monitorShowsSwitch; + height: 30; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: monitorShowsSwitchLabel.bottom; + anchors.topMargin: 10; + labelTextOff: "HMD Preview"; + labelTextOn: "Camera View"; + labelGlyphOnText: hifi.glyphs.alert; + onCheckedChanged: { + sendToScript({method: 'setMonitorShowsCameraView', params: checked}); + } + } + + // "Switch View From Controller" Checkbox + HifiControlsUit.CheckBox { + id: switchViewFromControllerCheckBox; + colorScheme: hifi.colorSchemes.dark; + anchors.left: parent.left; + anchors.top: monitorShowsSwitch.bottom; + anchors.topMargin: 25; + text: ""; + boxSize: 24; + onClicked: { + sendToScript({method: 'changeSwitchViewFromControllerPreference', params: checked}); + } + } + } + // + // SPECTATOR CONTROLS END + // + + // + // FUNCTION DEFINITIONS START + // + // + // Function Name: fromScript() + // + // Relevant Variables: + // None + // + // Arguments: + // message: The message sent from the SpectatorCamera JavaScript. + // Messages are in format "{method, params}", like json-rpc. + // + // Description: + // Called when a message is received from spectatorCamera.js. + // + function fromScript(message) { + switch (message.method) { + case 'updateSpectatorCameraCheckbox': + cameraToggleCheckBox.checked = message.params; + break; + case 'updateMonitorShowsSwitch': + monitorShowsSwitch.checked = message.params; + break; + case 'updateControllerMappingCheckbox': + switchViewFromControllerCheckBox.checked = message.setting; + switchViewFromControllerCheckBox.enabled = true; + if (message.controller === "OculusTouch") { + switchViewFromControllerCheckBox.text = "Clicking Touch's Left Thumbstick Switches Monitor View"; + } else if (message.controller === "Vive") { + switchViewFromControllerCheckBox.text = "Clicking Left Thumb Pad Switches Monitor View"; + } else { + switchViewFromControllerCheckBox.text = "Pressing Ctrl+0 Switches Monitor View"; + switchViewFromControllerCheckBox.checked = true; + switchViewFromControllerCheckBox.enabled = false; + } + break; + case 'showPreviewTextureNotInstructions': + console.log('showPreviewTextureNotInstructions recvd', JSON.stringify(message)); + spectatorCameraPreview.url = message.url; + spectatorCameraPreview.visible = message.setting; + break; + default: + console.log('Unrecognized message from spectatorCamera.js:', JSON.stringify(message)); + } + } + signal sendToScript(var message); + + // + // FUNCTION DEFINITIONS END + // +} diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index 519499e35c..03d27e3831 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -117,26 +117,28 @@ Rectangle { delegate: Item { width: parent.width; height: 36; + + AudioControls.CheckBox { + id: checkbox + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + text: display; + wrap: false; + checked: selected; + enabled: false; + } - RowLayout { - width: parent.width; + MouseArea { + anchors.fill: checkbox + onClicked: Audio.setInputDevice(info); + } - AudioControls.CheckBox { - Layout.maximumWidth: parent.width - level.width - 40; - text: display; - wrap: false; - checked: selected; - onClicked: { - selected = checked; - checked = Qt.binding(function() { return selected; }); // restore binding - } - } - InputLevel { - id: level; - Layout.alignment: Qt.AlignRight; - Layout.rightMargin: 30; - visible: selected; - } + InputLevel { + id: level; + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + anchors.rightMargin: 30 + visible: selected; } } } @@ -174,13 +176,19 @@ Rectangle { delegate: Item { width: parent.width; height: 36; + AudioControls.CheckBox { + id: checkbox + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left text: display; checked: selected; - onClicked: { - selected = checked; - checked = Qt.binding(function() { return selected; }); // restore binding - } + enabled: false; + } + + MouseArea { + anchors.fill: checkbox + onClicked: Audio.setOutputDevice(info); } } } diff --git a/interface/resources/qml/hifi/tablet/NewModelDialog.qml b/interface/resources/qml/hifi/tablet/NewModelDialog.qml index 5040c043f4..d47c981440 100644 --- a/interface/resources/qml/hifi/tablet/NewModelDialog.qml +++ b/interface/resources/qml/hifi/tablet/NewModelDialog.qml @@ -65,7 +65,8 @@ Rectangle { onClicked: { newModelDialog.keyboardEnabled = HMD.active parent.focus = true; - parent.forceActiveFocus() + parent.forceActiveFocus(); + modelURL.cursorPosition = modelURL.positionAt(mouseX, mouseY, TextInput.CursorBetweenCharaters); } } } diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml index 9dd0956000..af1fbd0070 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml @@ -81,6 +81,7 @@ Preference { property var avatarBuilder: Component { AvatarPreference { } } property var buttonBuilder: Component { ButtonPreference { } } property var comboBoxBuilder: Component { ComboBoxPreference { } } + property var spinnerSliderBuilder: Component { SpinnerSliderPreference { } } property var preferences: [] property int checkBoxCount: 0 @@ -143,6 +144,10 @@ Preference { //to be not overlapped when drop down is active zpos = root.z + 1000 - itemNum break; + case Preference.SpinnerSlider: + checkBoxCount = 0; + builder = spinnerSliderBuilder; + break; }; if (builder) { diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index aa968c85ef..1556a9c0c0 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -50,7 +50,7 @@ Item { id: colors // Base colors - readonly property color baseGray: "#404040" + readonly property color baseGray: "#393939" readonly property color darkGray: "#121212" readonly property color baseGrayShadow: "#252525" readonly property color baseGrayHighlight: "#575757" diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index ddd1870723..dbb94cfdae 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -69,6 +69,7 @@ #include #include #include +#include #include #include #include @@ -167,6 +168,7 @@ #if defined(Q_OS_MAC) || defined(Q_OS_WIN) #include "SpeechRecognizer.h" #endif +#include "ui/ResourceImageItem.h" #include "ui/AddressBarDialog.h" #include "ui/AvatarInputs.h" #include "ui/DialogsManager.h" @@ -587,6 +589,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); return previousSessionCrashed; } @@ -1003,7 +1006,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo properties["processor_l2_cache_count"] = procInfo.numProcessorCachesL2; properties["processor_l3_cache_count"] = procInfo.numProcessorCachesL3; } - + properties["first_run"] = firstRun.get(); // add the user's machine ID to the launch event @@ -1466,6 +1469,14 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo properties["atp_mapping_requests"] = atpMappingRequests; properties["throttled"] = _displayPlugin ? _displayPlugin->isThrottled() : false; + + QJsonObject bytesDownloaded; + bytesDownloaded["atp"] = statTracker->getStat(STAT_ATP_RESOURCE_TOTAL_BYTES).toInt(); + bytesDownloaded["http"] = statTracker->getStat(STAT_HTTP_RESOURCE_TOTAL_BYTES).toInt(); + bytesDownloaded["file"] = statTracker->getStat(STAT_FILE_RESOURCE_TOTAL_BYTES).toInt(); + bytesDownloaded["total"] = bytesDownloaded["atp"].toInt() + bytesDownloaded["http"].toInt() + + bytesDownloaded["file"].toInt(); + properties["bytesDownloaded"] = bytesDownloaded; auto myAvatar = getMyAvatar(); glm::vec3 avatarPosition = myAvatar->getPosition(); @@ -1704,9 +1715,7 @@ QString Application::getUserAgent() { void Application::toggleTabletUI(bool shouldOpen) const { auto tabletScriptingInterface = DependencyManager::get(); auto hmd = DependencyManager::get(); - TabletProxy* tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); - bool messageOpen = tablet->isMessageDialogOpen(); - if ((!messageOpen || (messageOpen && !hmd->getShouldShowTablet())) && !(shouldOpen && hmd->getShouldShowTablet())) { + if (!(shouldOpen && hmd->getShouldShowTablet())) { auto HMD = DependencyManager::get(); HMD->toggleShouldShowTablet(); } @@ -1971,7 +1980,7 @@ void Application::initializeGL() { static const QString RENDER_FORWARD = "HIFI_RENDER_FORWARD"; bool isDeferred = !QProcessEnvironment::systemEnvironment().contains(RENDER_FORWARD); _renderEngine->addJob("UpdateScene"); - _renderEngine->addJob("SecondaryCameraFrame", cullFunctor); + _renderEngine->addJob("SecondaryCameraJob", cullFunctor); _renderEngine->addJob("RenderMainView", cullFunctor, isDeferred); _renderEngine->load(); _renderEngine->registerScene(_main3DScene); @@ -2019,6 +2028,7 @@ void Application::initializeUi() { LoginDialog::registerType(); Tooltip::registerType(); UpdateDialog::registerType(); + qmlRegisterType("Hifi", 1, 0, "ResourceImageItem"); qmlRegisterType("Hifi", 1, 0, "Preference"); auto offscreenUi = DependencyManager::get(); @@ -2114,6 +2124,7 @@ void Application::initializeUi() { surfaceContext->setContextProperty("ApplicationCompositor", &getApplicationCompositor()); surfaceContext->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); + surfaceContext->setContextProperty("HoverOverlay", DependencyManager::get().data()); if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { surfaceContext->setContextProperty("Steam", new SteamScriptingInterface(engine, steamClient.get())); @@ -2740,6 +2751,16 @@ bool Application::event(QEvent* event) { static_cast(event)->call(); return true; + // Explicit idle keeps the idle running at a lower interval, but without any rendering + // see (windowMinimizedChanged) + case Event::Idle: + { + float nsecsElapsed = (float)_lastTimeUpdated.nsecsElapsed(); + _lastTimeUpdated.start(); + idle(nsecsElapsed); + } + return true; + case Event::Present: if (!_renderRequested) { float nsecsElapsed = (float)_lastTimeUpdated.nsecsElapsed(); @@ -2749,7 +2770,7 @@ bool Application::event(QEvent* event) { idle(nsecsElapsed); postEvent(this, new QEvent(static_cast(Paint)), Qt::HighEventPriority); } - } + } return true; case Event::Paint: @@ -3725,8 +3746,8 @@ void updateCpuInformation() { // Update friendly structure auto& myCpuInfo = myCpuInfos[i]; myCpuInfo.update(cpuInfo); - PROFILE_COUNTER(app, myCpuInfo.name.c_str(), { - { "kernel", myCpuInfo.kernelUsage }, + PROFILE_COUNTER(app, myCpuInfo.name.c_str(), { + { "kernel", myCpuInfo.kernelUsage }, { "user", myCpuInfo.userUsage } }); } @@ -3793,7 +3814,7 @@ void getCpuUsage(vec3& systemAndUser) { void setupCpuMonitorThread() { initCpuUsage(); auto cpuMonitorThread = QThread::currentThread(); - + QTimer* timer = new QTimer(); timer->setInterval(50); QObject::connect(timer, &QTimer::timeout, [] { @@ -5427,6 +5448,10 @@ void Application::updateWindowTitle() const { qCDebug(interfaceapp, "Application title set to: %s", title.toStdString().c_str()); #endif _window->setWindowTitle(title); + + // updateTitleWindow gets called whenever there's a change regarding the domain, so rather + // than placing this within domainChanged, it's placed here to cover the other potential cases. + DependencyManager::get< MessagesClient >()->sendLocalMessage("Toolbar-DomainChanged", ""); } void Application::clearDomainOctreeDetails() { @@ -5812,6 +5837,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri auto entityScriptServerLog = DependencyManager::get(); scriptEngine->registerGlobalObject("EntityScriptServerLog", entityScriptServerLog.data()); scriptEngine->registerGlobalObject("AvatarInputs", AvatarInputs::getInstance()); + scriptEngine->registerGlobalObject("HoverOverlay", DependencyManager::get().data()); qScriptRegisterMetaType(scriptEngine, OverlayIDtoScriptValue, OverlayIDfromScriptValue); @@ -6583,11 +6609,11 @@ void Application::setPreviousScriptLocation(const QString& location) { } void Application::loadScriptURLDialog() const { - auto newScript = OffscreenUi::getText(OffscreenUi::ICON_NONE, "Open and Run Script", "Script URL"); + QString newScript = OffscreenUi::getText(OffscreenUi::ICON_NONE, "Open and Run Script", "Script URL"); if (QUrl(newScript).scheme() == "atp") { OffscreenUi::warning("Error Loading Script", "Cannot load client script over ATP"); } else if (!newScript.isEmpty()) { - DependencyManager::get()->loadScript(newScript); + DependencyManager::get()->loadScript(newScript.trimmed()); } } diff --git a/interface/src/Application.h b/interface/src/Application.h index cf0ae91a0f..ce27c4a70a 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -678,7 +678,7 @@ private: QTimer _addAssetToWorldErrorTimer; FileScriptingInterface* _fileDownload; - AudioInjector* _snapshotSoundInjector { nullptr }; + AudioInjectorPointer _snapshotSoundInjector; SharedSoundPointer _snapshotSound; DisplayPluginPointer _autoSwitchDisplayModeSupportedHMDPlugin; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 8b616c3029..c7223be208 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -680,7 +680,7 @@ Menu::Menu() { // Developer > Physics >>> MenuWrapper* physicsOptionsMenu = developerMenu->addMenu("Physics"); { - auto drawStatusConfig = qApp->getRenderEngine()->getConfiguration()->getConfig(); + auto drawStatusConfig = qApp->getRenderEngine()->getConfiguration()->getConfig("RenderMainView.DrawStatus"); addCheckableActionToQMenuAndActionHash(physicsOptionsMenu, MenuOption::PhysicsShowOwned, 0, false, drawStatusConfig, SLOT(setShowNetwork(bool))); } diff --git a/interface/src/SecondaryCamera.cpp b/interface/src/SecondaryCamera.cpp index f59d2fcc7a..56b8b3ef85 100644 --- a/interface/src/SecondaryCamera.cpp +++ b/interface/src/SecondaryCamera.cpp @@ -9,9 +9,11 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "Application.h" #include "SecondaryCamera.h" #include #include +#include using RenderArgsPointer = std::shared_ptr; @@ -27,39 +29,32 @@ void MainRenderTask::build(JobModel& task, const render::Varying& inputs, render } } -void SecondaryCameraRenderTaskConfig::resetSize(int width, int height) { // FIXME: Add an arg here for "destinationFramebuffer" - bool wasEnabled = isEnabled(); - setEnabled(false); - auto textureCache = DependencyManager::get(); - textureCache->resetSpectatorCameraFramebuffer(width, height); // FIXME: Call the correct reset function based on the "destinationFramebuffer" arg - setEnabled(wasEnabled); -} - -void SecondaryCameraRenderTaskConfig::resetSizeSpectatorCamera(int width, int height) { // Carefully adjust the framebuffer / texture. - resetSize(width, height); -} - -class BeginSecondaryCameraFrame { // Changes renderContext for our framebuffer and and view. +class SecondaryCameraJob { // Changes renderContext for our framebuffer and view. + QUuid _attachedEntityId{}; glm::vec3 _position{}; glm::quat _orientation{}; float _vFoV{}; float _nearClipPlaneDistance{}; float _farClipPlaneDistance{}; + EntityPropertyFlags _attachedEntityPropertyFlags; + QSharedPointer _entityScriptingInterface; public: - using Config = BeginSecondaryCameraFrameConfig; - using JobModel = render::Job::ModelO; - BeginSecondaryCameraFrame() { + using Config = SecondaryCameraJobConfig; + using JobModel = render::Job::ModelO; + SecondaryCameraJob() { _cachedArgsPointer = std::make_shared(_cachedArgs); + _entityScriptingInterface = DependencyManager::get(); + _attachedEntityPropertyFlags += PROP_POSITION; + _attachedEntityPropertyFlags += PROP_ROTATION; } void configure(const Config& config) { - if (config.enabled || config.alwaysEnabled) { - _position = config.position; - _orientation = config.orientation; - _vFoV = config.vFoV; - _nearClipPlaneDistance = config.nearClipPlaneDistance; - _farClipPlaneDistance = config.farClipPlaneDistance; - } + _attachedEntityId = config.attachedEntityId; + _position = config.position; + _orientation = config.orientation; + _vFoV = config.vFoV; + _nearClipPlaneDistance = config.nearClipPlaneDistance; + _farClipPlaneDistance = config.farClipPlaneDistance; } void run(const render::RenderContextPointer& renderContext, RenderArgsPointer& cachedArgs) { @@ -83,8 +78,14 @@ public: }); auto srcViewFrustum = args->getViewFrustum(); - srcViewFrustum.setPosition(_position); - srcViewFrustum.setOrientation(_orientation); + if (!_attachedEntityId.isNull()) { + EntityItemProperties entityProperties = _entityScriptingInterface->getEntityProperties(_attachedEntityId, _attachedEntityPropertyFlags); + srcViewFrustum.setPosition(entityProperties.getPosition()); + srcViewFrustum.setOrientation(entityProperties.getRotation()); + } else { + srcViewFrustum.setPosition(_position); + srcViewFrustum.setOrientation(_orientation); + } srcViewFrustum.setProjection(glm::perspective(glm::radians(_vFoV), ((float)args->_viewport.z / (float)args->_viewport.w), _nearClipPlaneDistance, _farClipPlaneDistance)); // Without calculating the bound planes, the secondary camera will use the same culling frustum as the main camera, // which is not what we want here. @@ -99,6 +100,41 @@ protected: RenderArgsPointer _cachedArgsPointer; }; +void SecondaryCameraJobConfig::setPosition(glm::vec3 pos) { + if (attachedEntityId.isNull()) { + position = pos; + emit dirty(); + } else { + qDebug() << "ERROR: Cannot set position of SecondaryCamera while attachedEntityId is set."; + } +} + +void SecondaryCameraJobConfig::setOrientation(glm::quat orient) { + if (attachedEntityId.isNull()) { + orientation = orient; + emit dirty(); + } else { + qDebug() << "ERROR: Cannot set orientation of SecondaryCamera while attachedEntityId is set."; + } +} + +void SecondaryCameraJobConfig::enableSecondaryCameraRenderConfigs(bool enabled) { + qApp->getRenderEngine()->getConfiguration()->getConfig()->setEnabled(enabled); + setEnabled(enabled); +} + +void SecondaryCameraJobConfig::resetSizeSpectatorCamera(int width, int height) { // Carefully adjust the framebuffer / texture. + qApp->getRenderEngine()->getConfiguration()->getConfig()->resetSize(width, height); +} + +void SecondaryCameraRenderTaskConfig::resetSize(int width, int height) { // FIXME: Add an arg here for "destinationFramebuffer" + bool wasEnabled = isEnabled(); + setEnabled(false); + auto textureCache = DependencyManager::get(); + textureCache->resetSpectatorCameraFramebuffer(width, height); // FIXME: Call the correct reset function based on the "destinationFramebuffer" arg + setEnabled(wasEnabled); +} + class EndSecondaryCameraFrame { // Restores renderContext. public: using JobModel = render::Job::ModelI; @@ -119,7 +155,7 @@ public: }; void SecondaryCameraRenderTask::build(JobModel& task, const render::Varying& inputs, render::Varying& outputs, render::CullFunctor cullFunctor) { - const auto cachedArg = task.addJob("BeginSecondaryCamera"); + const auto cachedArg = task.addJob("SecondaryCamera"); const auto items = task.addJob("FetchCullSort", cullFunctor); assert(items.canCast()); task.addJob("RenderDeferredTask", items); diff --git a/interface/src/SecondaryCamera.h b/interface/src/SecondaryCamera.h index 5ad19c9614..0941959c0a 100644 --- a/interface/src/SecondaryCamera.h +++ b/interface/src/SecondaryCamera.h @@ -28,34 +28,40 @@ public: void build(JobModel& task, const render::Varying& inputs, render::Varying& outputs, render::CullFunctor cullFunctor, bool isDeferred = true); }; -class BeginSecondaryCameraFrameConfig : public render::Task::Config { // Exposes secondary camera parameters to JavaScript. +class SecondaryCameraJobConfig : public render::Task::Config { // Exposes secondary camera parameters to JavaScript. Q_OBJECT - Q_PROPERTY(glm::vec3 position MEMBER position NOTIFY dirty) // of viewpoint to render from - Q_PROPERTY(glm::quat orientation MEMBER orientation NOTIFY dirty) // of viewpoint to render from + Q_PROPERTY(QUuid attachedEntityId MEMBER attachedEntityId NOTIFY dirty) // entity whose properties define camera position and orientation + Q_PROPERTY(glm::vec3 position READ getPosition WRITE setPosition) // of viewpoint to render from + Q_PROPERTY(glm::quat orientation READ getOrientation WRITE setOrientation) // of viewpoint to render from Q_PROPERTY(float vFoV MEMBER vFoV NOTIFY dirty) // Secondary camera's vertical field of view. In degrees. Q_PROPERTY(float nearClipPlaneDistance MEMBER nearClipPlaneDistance NOTIFY dirty) // Secondary camera's near clip plane distance. In meters. Q_PROPERTY(float farClipPlaneDistance MEMBER farClipPlaneDistance NOTIFY dirty) // Secondary camera's far clip plane distance. In meters. public: + QUuid attachedEntityId{}; glm::vec3 position{}; glm::quat orientation{}; - float vFoV{ 45.0f }; - float nearClipPlaneDistance{ 0.1f }; - float farClipPlaneDistance{ 100.0f }; - BeginSecondaryCameraFrameConfig() : render::Task::Config(false) {} + float vFoV{ DEFAULT_FIELD_OF_VIEW_DEGREES }; + float nearClipPlaneDistance{ DEFAULT_NEAR_CLIP }; + float farClipPlaneDistance{ DEFAULT_FAR_CLIP }; + SecondaryCameraJobConfig() : render::Task::Config(false) {} signals: void dirty(); +public slots: + glm::vec3 getPosition() { return position; } + void setPosition(glm::vec3 pos); + glm::quat getOrientation() { return orientation; } + void setOrientation(glm::quat orient); + void enableSecondaryCameraRenderConfigs(bool enabled); + void resetSizeSpectatorCamera(int width, int height); }; class SecondaryCameraRenderTaskConfig : public render::Task::Config { Q_OBJECT public: SecondaryCameraRenderTaskConfig() : render::Task::Config(false) {} -private: void resetSize(int width, int height); signals: void dirty(); -public slots: - void resetSizeSpectatorCamera(int width, int height); }; class SecondaryCameraRenderTask { diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index c46d61cf68..bd545c64e0 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -63,7 +63,6 @@ AvatarManager::AvatarManager(QObject* parent) : packetReceiver.registerListener(PacketType::BulkAvatarData, this, "processAvatarDataPacket"); packetReceiver.registerListener(PacketType::KillAvatar, this, "processKillAvatar"); packetReceiver.registerListener(PacketType::AvatarIdentity, this, "processAvatarIdentityPacket"); - packetReceiver.registerListener(PacketType::ExitingSpaceBubble, this, "processExitingSpaceBubble"); // when we hear that the user has ignored an avatar by session UUID // immediately remove that avatar instead of waiting for the absence of packets from avatar mixer @@ -320,9 +319,6 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble) { emit DependencyManager::get()->enteredIgnoreRadius(); - } - if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble || removalReason == YourAvatarEnteredTheirBubble) { - DependencyManager::get()->radiusIgnoreNodeBySessionID(avatar->getSessionUUID(), true); } else if (removalReason == KillAvatarReason::AvatarDisconnected) { // remove from node sets, if present DependencyManager::get()->removeFromIgnoreMuteSets(avatar->getSessionUUID()); @@ -434,8 +430,7 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents // but most avatars are roughly the same size, so let's not be so fancy yet. const float AVATAR_STRETCH_FACTOR = 1.0f; - - _collisionInjectors.remove_if([](QPointer& injector) { + _collisionInjectors.remove_if([](const AudioInjectorPointer& injector) { return !injector || injector->isFinished(); }); diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index f1e71f7367..30801807d6 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -22,11 +22,11 @@ #include #include #include +#include #include "AvatarMotionState.h" #include "MyAvatar.h" -class AudioInjector; class AvatarManager : public AvatarHashMap { Q_OBJECT @@ -104,7 +104,7 @@ private: std::shared_ptr _myAvatar; quint64 _lastSendAvatarDataTime = 0; // Controls MyAvatar send data rate. - std::list> _collisionInjectors; + std::list _collisionInjectors; RateCounter<> _myAvatarSendRate; int _numAvatarsUpdated { 0 }; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 568c9a87e6..f9a4d491c8 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1075,9 +1075,6 @@ void MyAvatar::loadData() { getHead()->setBasePitch(loadSetting(settings, "headPitch", 0.0f)); - _targetScale = loadSetting(settings, "scale", 1.0f); - setScale(glm::vec3(_targetScale)); - _prefOverrideAnimGraphUrl.set(QUrl(settings.value("animGraphURL", "").toString())); _fullAvatarURLFromPreferences = settings.value("fullAvatarURL", AvatarData::defaultFullAvatarModelUrl()).toUrl(); _fullAvatarModelName = settings.value("fullAvatarModelName", DEFAULT_FULL_AVATAR_MODEL_NAME).toString(); @@ -2227,6 +2224,14 @@ void MyAvatar::clampScaleChangeToDomainLimits(float desiredScale) { qCDebug(interfaceapp, "Changed scale to %f", (double)_targetScale); } +float MyAvatar::getDomainMinScale() { + return _domainMinimumScale; +} + +float MyAvatar::getDomainMaxScale() { + return _domainMaximumScale; +} + void MyAvatar::increaseSize() { // make sure we're starting from an allowable scale clampTargetScaleToDomainLimits(); @@ -2274,17 +2279,27 @@ void MyAvatar::restrictScaleFromDomainSettings(const QJsonObject& domainSettings if (_domainMinimumScale > _domainMaximumScale) { std::swap(_domainMinimumScale, _domainMaximumScale); } + // Set avatar current scale + Settings settings; + settings.beginGroup("Avatar"); + _targetScale = loadSetting(settings, "scale", 1.0f); - qCDebug(interfaceapp, "This domain requires a minimum avatar scale of %f and a maximum avatar scale of %f", - (double)_domainMinimumScale, (double)_domainMaximumScale); + qCDebug(interfaceapp) << "This domain requires a minimum avatar scale of " << _domainMinimumScale + << " and a maximum avatar scale of " << _domainMaximumScale + << ". Current avatar scale is " << _targetScale; // debug to log if this avatar's scale in this domain will be clamped - auto clampedScale = glm::clamp(_targetScale, _domainMinimumScale, _domainMaximumScale); + float clampedScale = glm::clamp(_targetScale, _domainMinimumScale, _domainMaximumScale); if (_targetScale != clampedScale) { - qCDebug(interfaceapp, "Avatar scale will be clamped to %f because %f is not allowed by current domain", - (double)clampedScale, (double)_targetScale); + qCDebug(interfaceapp) << "Current avatar scale is clamped to " << clampedScale + << " because " << _targetScale << " is not allowed by current domain"; + // The current scale of avatar should not be more than domain's max_avatar_scale and not less than domain's min_avatar_scale . + _targetScale = clampedScale; } + + setScale(glm::vec3(_targetScale)); + settings.endGroup(); } void MyAvatar::clearScaleRestriction() { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index da223fc999..648a5b5f29 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -556,6 +556,8 @@ public slots: void increaseSize(); void decreaseSize(); void resetSize(); + float getDomainMinScale(); + float getDomainMaxScale(); void goToLocation(const glm::vec3& newPosition, bool hasOrientation = false, const glm::quat& newOrientation = glm::quat(), diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 8125f9a9f0..9719c23885 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -133,4 +133,12 @@ void Audio::setReverb(bool enable) { void Audio::setReverbOptions(const AudioEffectOptions* options) { DependencyManager::get()->setReverbOptions(options); -} \ No newline at end of file +} + +void Audio::setInputDevice(const QAudioDeviceInfo& device) { + _devices.chooseInputDevice(device); +} + +void Audio::setOutputDevice(const QAudioDeviceInfo& device) { + _devices.chooseOutputDevice(device); +} diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index ca89521489..bd40de4303 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -50,6 +50,8 @@ public: void showMicMeter(bool show); void setInputVolume(float volume); + Q_INVOKABLE void setInputDevice(const QAudioDeviceInfo& device); + Q_INVOKABLE void setOutputDevice(const QAudioDeviceInfo& device); Q_INVOKABLE void setReverb(bool enable); Q_INVOKABLE void setReverbOptions(const AudioEffectOptions* options); @@ -79,7 +81,7 @@ private: float _inputVolume { 1.0f }; float _inputLevel { 0.0f }; bool _isMuted { false }; - bool _enableNoiseReduction; + bool _enableNoiseReduction { true }; // Match default value of AudioClient::_isNoiseGateEnabled. bool _contextIsHMD { false }; AudioDevices* getDevices() { return &_devices; } diff --git a/interface/src/scripting/AudioDevices.cpp b/interface/src/scripting/AudioDevices.cpp index 98b8a7c12c..a284e38dac 100644 --- a/interface/src/scripting/AudioDevices.cpp +++ b/interface/src/scripting/AudioDevices.cpp @@ -38,7 +38,8 @@ Setting::Handle& getSetting(bool contextIsHMD, QAudio::Mode mode) { QHash AudioDeviceList::_roles { { Qt::DisplayRole, "display" }, - { Qt::CheckStateRole, "selected" } + { Qt::CheckStateRole, "selected" }, + { Qt::UserRole, "info" } }; Qt::ItemFlags AudioDeviceList::_flags { Qt::ItemIsSelectable | Qt::ItemIsEnabled }; @@ -51,66 +52,24 @@ QVariant AudioDeviceList::data(const QModelIndex& index, int role) const { return _devices.at(index.row()).display; } else if (role == Qt::CheckStateRole) { return _devices.at(index.row()).selected; + } else if (role == Qt::UserRole) { + return QVariant::fromValue(_devices.at(index.row()).info); } else { return QVariant(); } } -bool AudioDeviceList::setData(const QModelIndex& index, const QVariant& value, int role) { - if (!index.isValid() || index.row() >= _devices.size() || role != Qt::CheckStateRole) { - return false; - } - - // only allow switching to a new device, not deactivating an in-use device - auto selected = value.toBool(); - if (!selected) { - return false; - } - - return setDevice(index.row(), true); -} - -bool AudioDeviceList::setDevice(int row, bool fromUser) { - bool success = false; - auto& device = _devices[row]; - _userSelection = fromUser; - - // skip if already selected - if (!device.selected) { - auto client = DependencyManager::get(); - QMetaObject::invokeMethod(client.data(), "switchAudioDevice", - Q_ARG(QAudio::Mode, _mode), - Q_ARG(const QAudioDeviceInfo&, device.info)); - } - - emit dataChanged(createIndex(0, 0), createIndex(rowCount() - 1, 0)); - return success; -} void AudioDeviceList::resetDevice(bool contextIsHMD, const QString& device) { - bool success { false }; - - // try to set the last selected device - if (!device.isNull()) { - auto i = 0; - for (; i < rowCount(); ++i) { - if (device == _devices[i].info.deviceName()) { - break; - } - } - if (i < rowCount()) { - success = setDevice(i, false); - } - - // the selection failed - reset it - if (!success) { - emit deviceSelected(); - } - } + auto client = DependencyManager::get().data(); + auto deviceName = getSetting(contextIsHMD, _mode).get(); + bool switchResult = false; + QMetaObject::invokeMethod(client, "switchAudioDevice", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, switchResult), + Q_ARG(QAudio::Mode, _mode), Q_ARG(QString, deviceName)); // try to set to the default device for this mode - if (!success) { - auto client = DependencyManager::get().data(); + if (!switchResult) { if (contextIsHMD) { QString deviceName; if (_mode == QAudio::AudioInput) { @@ -131,7 +90,6 @@ void AudioDeviceList::resetDevice(bool contextIsHMD, const QString& device) { void AudioDeviceList::onDeviceChanged(const QAudioDeviceInfo& device) { auto oldDevice = _selectedDevice; _selectedDevice = device; - QModelIndex index; for (auto i = 0; i < _devices.size(); ++i) { AudioDevice& device = _devices[i]; @@ -139,15 +97,9 @@ void AudioDeviceList::onDeviceChanged(const QAudioDeviceInfo& device) { device.selected = false; } else if (device.info == _selectedDevice) { device.selected = true; - index = createIndex(i, 0); } } - if (_userSelection) { - _userSelection = false; - emit deviceSelected(_selectedDevice, oldDevice); - } - emit deviceChanged(_selectedDevice); emit dataChanged(createIndex(0, 0), createIndex(rowCount() - 1, 0)); } @@ -182,13 +134,6 @@ AudioDevices::AudioDevices(bool& contextIsHMD) : _contextIsHMD(contextIsHMD) { _outputs.onDeviceChanged(client->getActiveAudioDevice(QAudio::AudioOutput)); _inputs.onDevicesChanged(client->getAudioDevices(QAudio::AudioInput)); _outputs.onDevicesChanged(client->getAudioDevices(QAudio::AudioOutput)); - - connect(&_inputs, &AudioDeviceList::deviceSelected, [&](const QAudioDeviceInfo& device, const QAudioDeviceInfo& previousDevice) { - onDeviceSelected(QAudio::AudioInput, device, previousDevice); - }); - connect(&_outputs, &AudioDeviceList::deviceSelected, [&](const QAudioDeviceInfo& device, const QAudioDeviceInfo& previousDevice) { - onDeviceSelected(QAudio::AudioOutput, device, previousDevice); - }); } void AudioDevices::onContextChanged(const QString& context) { @@ -244,22 +189,40 @@ void AudioDevices::onDeviceChanged(QAudio::Mode mode, const QAudioDeviceInfo& de } void AudioDevices::onDevicesChanged(QAudio::Mode mode, const QList& devices) { - static bool initialized { false }; - auto initialize = [&]{ - if (initialized) { - onContextChanged(QString()); - } else { - initialized = true; - } - }; - + static std::once_flag once; if (mode == QAudio::AudioInput) { _inputs.onDevicesChanged(devices); - static std::once_flag inputFlag; - std::call_once(inputFlag, initialize); } else { // if (mode == QAudio::AudioOutput) _outputs.onDevicesChanged(devices); - static std::once_flag outputFlag; - std::call_once(outputFlag, initialize); + } + std::call_once(once, [&] { onContextChanged(QString()); }); +} + + +void AudioDevices::chooseInputDevice(const QAudioDeviceInfo& device) { + auto client = DependencyManager::get(); + bool success = false; + QMetaObject::invokeMethod(client.data(), "switchAudioDevice", + Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, success), + Q_ARG(QAudio::Mode, QAudio::AudioInput), + Q_ARG(const QAudioDeviceInfo&, device)); + + if (success) { + onDeviceSelected(QAudio::AudioInput, device, _inputs._selectedDevice); + } +} + +void AudioDevices::chooseOutputDevice(const QAudioDeviceInfo& device) { + auto client = DependencyManager::get(); + bool success = false; + QMetaObject::invokeMethod(client.data(), "switchAudioDevice", + Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, success), + Q_ARG(QAudio::Mode, QAudio::AudioOutput), + Q_ARG(const QAudioDeviceInfo&, device)); + + if (success) { + onDeviceSelected(QAudio::AudioOutput, device, _outputs._selectedDevice); } } diff --git a/interface/src/scripting/AudioDevices.h b/interface/src/scripting/AudioDevices.h index 8e82ddc4fb..a17c577535 100644 --- a/interface/src/scripting/AudioDevices.h +++ b/interface/src/scripting/AudioDevices.h @@ -37,14 +37,11 @@ public: // get/set devices through a QML ListView QVariant data(const QModelIndex& index, int role) const override; - bool setData(const QModelIndex& index, const QVariant &value, int role) override; // reset device to the last selected device in this context, or the default void resetDevice(bool contextIsHMD, const QString& device); signals: - void deviceSelected(const QAudioDeviceInfo& device = QAudioDeviceInfo(), - const QAudioDeviceInfo& previousDevice = QAudioDeviceInfo()); void deviceChanged(const QAudioDeviceInfo& device); private slots: @@ -54,12 +51,9 @@ private slots: private: friend class AudioDevices; - bool setDevice(int index, bool fromUser); - static QHash _roles; static Qt::ItemFlags _flags; - bool _userSelection { false }; - QAudio::Mode _mode; + const QAudio::Mode _mode; QAudioDeviceInfo _selectedDevice; QList _devices; }; @@ -73,6 +67,8 @@ class AudioDevices : public QObject { public: AudioDevices(bool& contextIsHMD); + void chooseInputDevice(const QAudioDeviceInfo& device); + void chooseOutputDevice(const QAudioDeviceInfo& device); signals: void nop(); diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 41a4ebdf68..87131e4f5c 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -184,12 +184,15 @@ void setupPreferences() { { auto getter = [=]()->float { return myAvatar->getUniformScale(); }; auto setter = [=](float value) { myAvatar->setTargetScale(value); }; - auto preference = new SpinnerPreference(AVATAR_TUNING, "Avatar scale (default is 1.0)", getter, setter); - preference->setMin(0.01f); - preference->setMax(99.9f); + auto preference = new SpinnerSliderPreference(AVATAR_TUNING, "Avatar Scale", getter, setter); + preference->setStep(0.05f); preference->setDecimals(2); - preference->setStep(1); preferences->addPreference(preference); + + // When the Interface is first loaded, this section setupPreferences(); is loaded - + // causing the myAvatar->getDomainMinScale() and myAvatar->getDomainMaxScale() to get set to incorrect values + // which can't be changed across domain switches. Having these values loaded up when you load the Dialog each time + // is a way around this, therefore they're not specified here but in the QML. } { auto getter = []()->float { return DependencyManager::get()->getEyeClosingThreshold(); }; diff --git a/interface/src/ui/ResourceImageItem.cpp b/interface/src/ui/ResourceImageItem.cpp new file mode 100644 index 0000000000..7b9592fa4c --- /dev/null +++ b/interface/src/ui/ResourceImageItem.cpp @@ -0,0 +1,114 @@ +// +// ResourceImageItem.cpp +// +// Created by David Kelly and Howard Stearns on 2017/06/08 +// Copyright 2017 High Fidelity, Inc. + +// Distributed under the Apache License, Version 2.0 +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +//#include "Application.h" +#include "ResourceImageItem.h" + +#include +#include +#include +#include + +ResourceImageItem::ResourceImageItem() : QQuickFramebufferObject() { + auto textureCache = DependencyManager::get(); + connect(textureCache.data(), SIGNAL(spectatorCameraFramebufferReset()), this, SLOT(update())); +} + +void ResourceImageItem::setUrl(const QString& url) { + if (url != m_url) { + m_url = url; + update(); + } +} + +void ResourceImageItem::setReady(bool ready) { + if (ready != m_ready) { + m_ready = ready; + update(); + } +} + +void ResourceImageItemRenderer::onUpdateTimer() { + if (_ready) { + if (_networkTexture && _networkTexture->isLoaded()) { + if(_fboMutex.tryLock()) { + invalidateFramebufferObject(); + qApp->getActiveDisplayPlugin()->copyTextureToQuickFramebuffer(_networkTexture, _copyFbo, &_fenceSync); + _fboMutex.unlock(); + } else { + qDebug() << "couldn't get a lock, using last frame"; + } + } else { + _networkTexture = DependencyManager::get()->getTexture(_url); + } + } + update(); +} + +ResourceImageItemRenderer::ResourceImageItemRenderer() : QQuickFramebufferObject::Renderer() { + connect(&_updateTimer, SIGNAL(timeout()), this, SLOT(onUpdateTimer())); + auto textureCache = DependencyManager::get(); +} + +void ResourceImageItemRenderer::synchronize(QQuickFramebufferObject* item) { + ResourceImageItem* resourceImageItem = static_cast(item); + + resourceImageItem->setFlag(QQuickItem::ItemHasContents); + + _url = resourceImageItem->getUrl(); + _ready = resourceImageItem->getReady(); + _visible = resourceImageItem->isVisible(); + _window = resourceImageItem->window(); + + _networkTexture = DependencyManager::get()->getTexture(_url); + static const int UPDATE_TIMER_DELAY_IN_MS = 100; // 100 ms = 10 hz for now + if (_ready && _visible && !_updateTimer.isActive()) { + _updateTimer.start(UPDATE_TIMER_DELAY_IN_MS); + } else if (!(_ready && _visible) && _updateTimer.isActive()) { + _updateTimer.stop(); + } +} + +QOpenGLFramebufferObject* ResourceImageItemRenderer::createFramebufferObject(const QSize& size) { + if (_copyFbo) { + delete _copyFbo; + } + QOpenGLFramebufferObjectFormat format; + format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); + _copyFbo = new QOpenGLFramebufferObject(size, format); + _copyFbo->bind(); + return new QOpenGLFramebufferObject(size, format); +} + +void ResourceImageItemRenderer::render() { + auto f = QOpenGLContext::currentContext()->extraFunctions(); + + if (_fenceSync) { + f->glWaitSync(_fenceSync, 0, GL_TIMEOUT_IGNORED); + f->glDeleteSync(_fenceSync); + _fenceSync = 0; + } + if (_ready) { + _fboMutex.lock(); + _copyFbo->bind(); + QOpenGLFramebufferObject::blitFramebuffer(framebufferObject(), _copyFbo, GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT, GL_NEAREST); + + // this clears the copyFbo texture + // so next frame starts fresh - helps + // when aspect ratio changes + _copyFbo->takeTexture(); + _copyFbo->bind(); + _copyFbo->release(); + + _fboMutex.unlock(); + } + glFlush(); + _window->resetOpenGLState(); +} diff --git a/interface/src/ui/ResourceImageItem.h b/interface/src/ui/ResourceImageItem.h new file mode 100644 index 0000000000..985ab5a66e --- /dev/null +++ b/interface/src/ui/ResourceImageItem.h @@ -0,0 +1,63 @@ +// +// ResourceImageItem.h +// +// Created by David Kelly and Howard Stearns on 2017/06/08 +// Copyright 2017 High Fidelity, Inc. + +// Distributed under the Apache License, Version 2.0 +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once +#ifndef hifi_ResourceImageItem_h +#define hifi_ResourceImageItem_h + +#include "Application.h" + +#include +#include +#include + +#include + +class ResourceImageItemRenderer : public QObject, public QQuickFramebufferObject::Renderer { + Q_OBJECT +public: + ResourceImageItemRenderer(); + QOpenGLFramebufferObject* createFramebufferObject(const QSize& size) override; + void synchronize(QQuickFramebufferObject* item) override; + void render() override; +private: + bool _ready; + QString _url; + bool _visible; + + NetworkTexturePointer _networkTexture; + QQuickWindow* _window; + QMutex _fboMutex; + QOpenGLFramebufferObject* _copyFbo { nullptr }; + GLsync _fenceSync { 0 }; + QTimer _updateTimer; +public slots: + void onUpdateTimer(); +}; + +class ResourceImageItem : public QQuickFramebufferObject { + Q_OBJECT + Q_PROPERTY(QString url READ getUrl WRITE setUrl) + Q_PROPERTY(bool ready READ getReady WRITE setReady) +public: + ResourceImageItem(); + QString getUrl() const { return m_url; } + void setUrl(const QString& url); + bool getReady() const { return m_ready; } + void setReady(bool ready); + QQuickFramebufferObject::Renderer* createRenderer() const override { return new ResourceImageItemRenderer; } + +private: + QString m_url; + bool m_ready { false }; + +}; + +#endif // hifi_ResourceImageItem_h diff --git a/interface/src/ui/overlays/Billboard3DOverlay.cpp b/interface/src/ui/overlays/Billboard3DOverlay.cpp index 182e7da978..f5668caa71 100644 --- a/interface/src/ui/overlays/Billboard3DOverlay.cpp +++ b/interface/src/ui/overlays/Billboard3DOverlay.cpp @@ -37,9 +37,11 @@ QVariant Billboard3DOverlay::getProperty(const QString &property) { return Planar3DOverlay::getProperty(property); } -void Billboard3DOverlay::applyTransformTo(Transform& transform, bool force) { +bool Billboard3DOverlay::applyTransformTo(Transform& transform, bool force) { + bool transformChanged = false; if (force || usecTimestampNow() > _transformExpiry) { - PanelAttachable::applyTransformTo(transform, true); - pointTransformAtCamera(transform, getOffsetRotation()); + transformChanged = PanelAttachable::applyTransformTo(transform, true); + transformChanged |= pointTransformAtCamera(transform, getOffsetRotation()); } + return transformChanged; } diff --git a/interface/src/ui/overlays/Billboard3DOverlay.h b/interface/src/ui/overlays/Billboard3DOverlay.h index d256a92afe..d429537b5b 100644 --- a/interface/src/ui/overlays/Billboard3DOverlay.h +++ b/interface/src/ui/overlays/Billboard3DOverlay.h @@ -27,7 +27,7 @@ public: QVariant getProperty(const QString& property) override; protected: - virtual void applyTransformTo(Transform& transform, bool force = false) override; + virtual bool applyTransformTo(Transform& transform, bool force = false) override; }; #endif // hifi_Billboard3DOverlay_h diff --git a/interface/src/ui/overlays/Billboardable.cpp b/interface/src/ui/overlays/Billboardable.cpp index a01d62bfd1..34a4ef6df5 100644 --- a/interface/src/ui/overlays/Billboardable.cpp +++ b/interface/src/ui/overlays/Billboardable.cpp @@ -28,7 +28,7 @@ QVariant Billboardable::getProperty(const QString &property) { return QVariant(); } -void Billboardable::pointTransformAtCamera(Transform& transform, glm::quat offsetRotation) { +bool Billboardable::pointTransformAtCamera(Transform& transform, glm::quat offsetRotation) { if (isFacingAvatar()) { glm::vec3 billboardPos = transform.getTranslation(); glm::vec3 cameraPos = qApp->getCamera().getPosition(); @@ -38,5 +38,7 @@ void Billboardable::pointTransformAtCamera(Transform& transform, glm::quat offse glm::quat rotation(glm::vec3(elevation, azimuth, 0)); transform.setRotation(rotation); transform.postRotate(offsetRotation); + return true; } + return false; } diff --git a/interface/src/ui/overlays/Billboardable.h b/interface/src/ui/overlays/Billboardable.h index e2d29a2769..46d9ac6479 100644 --- a/interface/src/ui/overlays/Billboardable.h +++ b/interface/src/ui/overlays/Billboardable.h @@ -27,7 +27,7 @@ protected: void setProperties(const QVariantMap& properties); QVariant getProperty(const QString& property); - void pointTransformAtCamera(Transform& transform, glm::quat offsetRotation = {1, 0, 0, 0}); + bool pointTransformAtCamera(Transform& transform, glm::quat offsetRotation = {1, 0, 0, 0}); private: bool _isFacingAvatar = false; diff --git a/interface/src/ui/overlays/Image3DOverlay.cpp b/interface/src/ui/overlays/Image3DOverlay.cpp index c8c9c36a1d..7dfee2c491 100644 --- a/interface/src/ui/overlays/Image3DOverlay.cpp +++ b/interface/src/ui/overlays/Image3DOverlay.cpp @@ -99,10 +99,14 @@ void Image3DOverlay::render(RenderArgs* args) { const float MAX_COLOR = 255.0f; xColor color = getColor(); float alpha = getAlpha(); - + Transform transform = getTransform(); - applyTransformTo(transform, true); - setTransform(transform); + bool transformChanged = applyTransformTo(transform, true); + // If the transform is not modified, setting the transform to + // itself will cause drift over time due to floating point errors. + if (transformChanged) { + setTransform(transform); + } transform.postScale(glm::vec3(getDimensions(), 1.0f)); batch->setModelTransform(transform); diff --git a/interface/src/ui/overlays/PanelAttachable.cpp b/interface/src/ui/overlays/PanelAttachable.cpp index 421155083c..aae8893667 100644 --- a/interface/src/ui/overlays/PanelAttachable.cpp +++ b/interface/src/ui/overlays/PanelAttachable.cpp @@ -61,7 +61,7 @@ void PanelAttachable::setProperties(const QVariantMap& properties) { } } -void PanelAttachable::applyTransformTo(Transform& transform, bool force) { +bool PanelAttachable::applyTransformTo(Transform& transform, bool force) { if (force || usecTimestampNow() > _transformExpiry) { const quint64 TRANSFORM_UPDATE_PERIOD = 100000; // frequency is 10 Hz _transformExpiry = usecTimestampNow() + TRANSFORM_UPDATE_PERIOD; @@ -71,7 +71,9 @@ void PanelAttachable::applyTransformTo(Transform& transform, bool force) { transform.postTranslate(getOffsetPosition()); transform.postRotate(getOffsetRotation()); transform.postScale(getOffsetScale()); + return true; } #endif } + return false; } diff --git a/interface/src/ui/overlays/PanelAttachable.h b/interface/src/ui/overlays/PanelAttachable.h index 4f37cd2258..1598aa4700 100644 --- a/interface/src/ui/overlays/PanelAttachable.h +++ b/interface/src/ui/overlays/PanelAttachable.h @@ -67,7 +67,7 @@ protected: /// set position, rotation and scale on transform based on offsets, and parent panel offsets /// if force is false, only apply transform if it hasn't been applied in the last .1 seconds - virtual void applyTransformTo(Transform& transform, bool force = false); + virtual bool applyTransformTo(Transform& transform, bool force = false); quint64 _transformExpiry = 0; private: diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index d7076a443e..57c00e7183 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -23,20 +23,23 @@ #include "CubicHermiteSpline.h" #include "AnimUtil.h" -static void lookupJointChainInfo(AnimInverseKinematics::JointChainInfo* jointChainInfos, size_t numJointChainInfos, +static const float JOINT_CHAIN_INTERP_TIME = 0.25f; + +static void lookupJointInfo(const AnimInverseKinematics::JointChainInfo& jointChainInfo, int indexA, int indexB, - AnimInverseKinematics::JointChainInfo** jointChainInfoA, - AnimInverseKinematics::JointChainInfo** jointChainInfoB) { - *jointChainInfoA = nullptr; - *jointChainInfoB = nullptr; - for (size_t i = 0; i < numJointChainInfos; i++) { - if (jointChainInfos[i].jointIndex == indexA) { - *jointChainInfoA = jointChainInfos + i; + const AnimInverseKinematics::JointInfo** jointInfoA, + const AnimInverseKinematics::JointInfo** jointInfoB) { + *jointInfoA = nullptr; + *jointInfoB = nullptr; + for (size_t i = 0; i < jointChainInfo.jointInfoVec.size(); i++) { + const AnimInverseKinematics::JointInfo* jointInfo = &jointChainInfo.jointInfoVec[i]; + if (jointInfo->jointIndex == indexA) { + *jointInfoA = jointInfo; } - if (jointChainInfos[i].jointIndex == indexB) { - *jointChainInfoB = jointChainInfos + i; + if (jointInfo->jointIndex == indexB) { + *jointInfoB = jointInfo; } - if (*jointChainInfoA && *jointChainInfoB) { + if (*jointInfoA && *jointInfoB) { break; } } @@ -149,25 +152,28 @@ void AnimInverseKinematics::setTargetVars(const QString& jointName, const QStrin } void AnimInverseKinematics::computeTargets(const AnimVariantMap& animVars, std::vector& targets, const AnimPoseVec& underPoses) { - // build a list of valid targets from _targetVarVec and animVars - _maxTargetIndex = -1; + _hipsTargetIndex = -1; - bool removeUnfoundJoints = false; + + targets.reserve(_targetVarVec.size()); for (auto& targetVar : _targetVarVec) { + + // update targetVar jointIndex cache if (targetVar.jointIndex == -1) { - // this targetVar hasn't been validated yet... int jointIndex = _skeleton->nameToJointIndex(targetVar.jointName); if (jointIndex >= 0) { // this targetVar has a valid joint --> cache the indices targetVar.jointIndex = jointIndex; } else { qCWarning(animation) << "AnimInverseKinematics could not find jointName" << targetVar.jointName << "in skeleton"; - removeUnfoundJoints = true; } - } else { - IKTarget target; + } + + IKTarget target; + if (targetVar.jointIndex != -1) { target.setType(animVars.lookup(targetVar.typeVar, (int)IKTarget::Type::RotationAndPosition)); + target.setIndex(targetVar.jointIndex); if (target.getType() != IKTarget::Type::Unknown) { AnimPose absPose = _skeleton->getAbsolutePose(targetVar.jointIndex, underPoses); glm::quat rotation = animVars.lookupRigToGeometry(targetVar.rotationVar, absPose.rot()); @@ -175,7 +181,6 @@ void AnimInverseKinematics::computeTargets(const AnimVariantMap& animVars, std:: float weight = animVars.lookup(targetVar.weightVar, targetVar.weight); target.setPose(rotation, translation); - target.setIndex(targetVar.jointIndex); target.setWeight(weight); target.setFlexCoefficients(targetVar.numFlexCoefficients, targetVar.flexCoefficients); @@ -188,39 +193,20 @@ void AnimInverseKinematics::computeTargets(const AnimVariantMap& animVars, std:: glm::vec3 poleReferenceVector = animVars.lookupRigToGeometryVector(targetVar.poleReferenceVectorVar, Vectors::UNIT_Z); target.setPoleReferenceVector(glm::normalize(poleReferenceVector)); - targets.push_back(target); - - if (targetVar.jointIndex > _maxTargetIndex) { - _maxTargetIndex = targetVar.jointIndex; - } - // record the index of the hips ik target. if (target.getIndex() == _hipsIndex) { - _hipsTargetIndex = (int)targets.size() - 1; + _hipsTargetIndex = (int)targets.size(); } } + } else { + target.setType((int)IKTarget::Type::Unknown); } - } - if (removeUnfoundJoints) { - int numVars = (int)_targetVarVec.size(); - int i = 0; - while (i < numVars) { - if (_targetVarVec[i].jointIndex == -1) { - if (numVars > 1) { - // swap i for last element - _targetVarVec[i] = _targetVarVec[numVars - 1]; - } - _targetVarVec.pop_back(); - --numVars; - } else { - ++i; - } - } + targets.push_back(target); } } -void AnimInverseKinematics::solve(const AnimContext& context, const std::vector& targets) { +void AnimInverseKinematics::solve(const AnimContext& context, const std::vector& targets, float dt, JointChainInfoVec& jointChainInfoVec) { // compute absolute poses that correspond to relative target poses AnimPoseVec absolutePoses; absolutePoses.resize(_relativePoses.size()); @@ -234,26 +220,75 @@ void AnimInverseKinematics::solve(const AnimContext& context, const std::vector< accumulator.clearAndClean(); } - float maxError = FLT_MAX; + float maxError = 0.0f; int numLoops = 0; const int MAX_IK_LOOPS = 16; - const float MAX_ERROR_TOLERANCE = 0.1f; // cm - while (maxError > MAX_ERROR_TOLERANCE && numLoops < MAX_IK_LOOPS) { + while (numLoops < MAX_IK_LOOPS) { ++numLoops; bool debug = context.getEnableDebugDrawIKChains() && numLoops == MAX_IK_LOOPS; // solve all targets - for (auto& target: targets) { - if (target.getType() == IKTarget::Type::Spline) { - solveTargetWithSpline(context, target, absolutePoses, debug); - } else { - solveTargetWithCCD(context, target, absolutePoses, debug); + for (size_t i = 0; i < targets.size(); i++) { + switch (targets[i].getType()) { + case IKTarget::Type::Unknown: + break; + case IKTarget::Type::Spline: + solveTargetWithSpline(context, targets[i], absolutePoses, debug, jointChainInfoVec[i]); + break; + default: + solveTargetWithCCD(context, targets[i], absolutePoses, debug, jointChainInfoVec[i]); + break; + } + } + + // on last iteration, interpolate jointChains, if necessary + if (numLoops == MAX_IK_LOOPS) { + for (size_t i = 0; i < _prevJointChainInfoVec.size(); i++) { + if (_prevJointChainInfoVec[i].timer > 0.0f) { + float alpha = (JOINT_CHAIN_INTERP_TIME - _prevJointChainInfoVec[i].timer) / JOINT_CHAIN_INTERP_TIME; + size_t chainSize = std::min(_prevJointChainInfoVec[i].jointInfoVec.size(), jointChainInfoVec[i].jointInfoVec.size()); + for (size_t j = 0; j < chainSize; j++) { + jointChainInfoVec[i].jointInfoVec[j].rot = safeMix(_prevJointChainInfoVec[i].jointInfoVec[j].rot, jointChainInfoVec[i].jointInfoVec[j].rot, alpha); + jointChainInfoVec[i].jointInfoVec[j].trans = lerp(_prevJointChainInfoVec[i].jointInfoVec[j].trans, jointChainInfoVec[i].jointInfoVec[j].trans, alpha); + } + + // if joint chain was just disabled, ramp the weight toward zero. + if (_prevJointChainInfoVec[i].target.getType() != IKTarget::Type::Unknown && + jointChainInfoVec[i].target.getType() == IKTarget::Type::Unknown) { + IKTarget newTarget = _prevJointChainInfoVec[i].target; + newTarget.setWeight((1.0f - alpha) * _prevJointChainInfoVec[i].target.getWeight()); + jointChainInfoVec[i].target = newTarget; + } + } + } + } + + // copy jointChainInfoVecs into accumulators + for (size_t i = 0; i < targets.size(); i++) { + const std::vector& jointInfoVec = jointChainInfoVec[i].jointInfoVec; + + // don't accumulate disabled or rotation only ik targets. + IKTarget::Type type = jointChainInfoVec[i].target.getType(); + if (type != IKTarget::Type::Unknown && type != IKTarget::Type::RotationOnly) { + float weight = jointChainInfoVec[i].target.getWeight(); + if (weight > 0.0f) { + for (size_t j = 0; j < jointInfoVec.size(); j++) { + const JointInfo& info = jointInfoVec[j]; + if (info.jointIndex >= 0) { + _rotationAccumulators[info.jointIndex].add(info.rot, weight); + _translationAccumulators[info.jointIndex].add(info.trans, weight); + } + } + } } } // harvest accumulated rotations and apply the average for (int i = 0; i < (int)_relativePoses.size(); ++i) { + if (i == _hipsIndex) { + continue; // don't apply accumulators to hips + } if (_rotationAccumulators[i].size() > 0) { _relativePoses[i].rot() = _rotationAccumulators[i].getAverage(); _rotationAccumulators[i].clear(); @@ -289,7 +324,7 @@ void AnimInverseKinematics::solve(const AnimContext& context, const std::vector< // finally set the relative rotation of each tip to agree with absolute target rotation for (auto& target: targets) { int tipIndex = target.getIndex(); - int parentIndex = _skeleton->getParentIndex(tipIndex); + int parentIndex = (tipIndex >= 0) ? _skeleton->getParentIndex(tipIndex) : -1; // update rotationOnly targets that don't lie on the ik chain of other ik targets. if (parentIndex != -1 && !_rotationAccumulators[tipIndex].isDirty() && target.getType() == IKTarget::Type::RotationOnly) { @@ -308,9 +343,34 @@ void AnimInverseKinematics::solve(const AnimContext& context, const std::vector< absolutePoses[tipIndex].rot() = targetRotation; } } + + // copy jointChainInfoVec into _prevJointChainInfoVec, and update timers + for (size_t i = 0; i < jointChainInfoVec.size(); i++) { + _prevJointChainInfoVec[i].timer = _prevJointChainInfoVec[i].timer - dt; + if (_prevJointChainInfoVec[i].timer <= 0.0f) { + _prevJointChainInfoVec[i] = jointChainInfoVec[i]; + _prevJointChainInfoVec[i].target = targets[i]; + // store relative poses into unknown/rotation only joint chains. + // so we have something to interpolate from if this chain is activated. + IKTarget::Type type = _prevJointChainInfoVec[i].target.getType(); + if (type == IKTarget::Type::Unknown || type == IKTarget::Type::RotationOnly) { + for (size_t j = 0; j < _prevJointChainInfoVec[i].jointInfoVec.size(); j++) { + auto& info = _prevJointChainInfoVec[i].jointInfoVec[j]; + if (info.jointIndex >= 0) { + info.rot = _relativePoses[info.jointIndex].rot(); + info.trans = _relativePoses[info.jointIndex].trans(); + } else { + info.rot = Quaternions::IDENTITY; + info.trans = glm::vec3(0.0f); + } + } + } + } + } } -void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const IKTarget& target, const AnimPoseVec& absolutePoses, bool debug) { +void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const IKTarget& target, const AnimPoseVec& absolutePoses, + bool debug, JointChainInfo& jointChainInfoOut) const { size_t chainDepth = 0; IKTarget::Type targetType = target.getType(); @@ -338,9 +398,6 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const // the tip's parent-relative as we proceed up the chain glm::quat tipParentOrientation = absolutePoses[pivotIndex].rot(); - const size_t MAX_CHAIN_DEPTH = 30; - JointChainInfo jointChainInfos[MAX_CHAIN_DEPTH]; - // NOTE: if this code is removed, the head will remain rigid, causing the spine/hips to thrust forward backward // as the head is nodded. if (targetType == IKTarget::Type::HmdHead || @@ -368,7 +425,7 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const } glm::vec3 tipRelativeTranslation = _relativePoses[target.getIndex()].trans(); - jointChainInfos[chainDepth] = { tipRelativeRotation, tipRelativeTranslation, target.getWeight(), tipIndex, constrained }; + jointChainInfoOut.jointInfoVec[chainDepth] = { tipRelativeRotation, tipRelativeTranslation, tipIndex, constrained }; } // cache tip absolute position @@ -379,7 +436,7 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const // descend toward root, pivoting each joint to get tip closer to target position while (pivotIndex != _hipsIndex && pivotsParentIndex != -1) { - assert(chainDepth < MAX_CHAIN_DEPTH); + assert(chainDepth < jointChainInfoOut.jointInfoVec.size()); // compute the two lines that should be aligned glm::vec3 jointPosition = absolutePoses[pivotIndex].trans(); @@ -444,9 +501,8 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const glm::quat twistPart; glm::vec3 axis = glm::normalize(deltaRotation * leverArm); swingTwistDecomposition(missingRotation, axis, swingPart, twistPart); - float dotSign = copysignf(1.0f, twistPart.w); const float LIMIT_LEAK_FRACTION = 0.1f; - deltaRotation = glm::normalize(glm::lerp(glm::quat(), dotSign * twistPart, LIMIT_LEAK_FRACTION)) * deltaRotation; + deltaRotation = safeLerp(glm::quat(), twistPart, LIMIT_LEAK_FRACTION); } } } @@ -455,9 +511,8 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const // An HmdHead target slaves the orientation of the end-effector by distributing rotation // deltas up the hierarchy. Its target position is enforced later (by shifting the hips). deltaRotation = target.getRotation() * glm::inverse(tipOrientation); - float dotSign = copysignf(1.0f, deltaRotation.w); const float ANGLE_DISTRIBUTION_FACTOR = 0.45f; - deltaRotation = glm::normalize(glm::lerp(glm::quat(), dotSign * deltaRotation, ANGLE_DISTRIBUTION_FACTOR)); + deltaRotation = safeLerp(glm::quat(), deltaRotation, ANGLE_DISTRIBUTION_FACTOR); } // compute joint's new parent-relative rotation after swing @@ -480,7 +535,7 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const } glm::vec3 newTrans = _relativePoses[pivotIndex].trans(); - jointChainInfos[chainDepth] = { newRot, newTrans, target.getWeight(), pivotIndex, constrained }; + jointChainInfoOut.jointInfoVec[chainDepth] = { newRot, newTrans, pivotIndex, constrained }; // keep track of tip's new transform as we descend towards root tipPosition = jointPosition + deltaRotation * (tipPosition - jointPosition); @@ -502,24 +557,25 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const int baseParentJointIndex = _skeleton->getParentIndex(baseJointIndex); AnimPose topPose, midPose, basePose; int topChainIndex = -1, baseChainIndex = -1; + const size_t MAX_CHAIN_DEPTH = 30; AnimPose postAbsPoses[MAX_CHAIN_DEPTH]; AnimPose accum = absolutePoses[_hipsIndex]; AnimPose baseParentPose = absolutePoses[_hipsIndex]; for (int i = (int)chainDepth - 1; i >= 0; i--) { - accum = accum * AnimPose(glm::vec3(1.0f), jointChainInfos[i].relRot, jointChainInfos[i].relTrans); + accum = accum * AnimPose(glm::vec3(1.0f), jointChainInfoOut.jointInfoVec[i].rot, jointChainInfoOut.jointInfoVec[i].trans); postAbsPoses[i] = accum; - if (jointChainInfos[i].jointIndex == topJointIndex) { + if (jointChainInfoOut.jointInfoVec[i].jointIndex == topJointIndex) { topChainIndex = i; topPose = accum; } - if (jointChainInfos[i].jointIndex == midJointIndex) { + if (jointChainInfoOut.jointInfoVec[i].jointIndex == midJointIndex) { midPose = accum; } - if (jointChainInfos[i].jointIndex == baseJointIndex) { + if (jointChainInfoOut.jointInfoVec[i].jointIndex == baseJointIndex) { baseChainIndex = i; basePose = accum; } - if (jointChainInfos[i].jointIndex == baseParentJointIndex) { + if (jointChainInfoOut.jointInfoVec[i].jointIndex == baseParentJointIndex) { baseParentPose = accum; } } @@ -599,21 +655,16 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const } glm::quat newBaseRelRot = glm::inverse(baseParentPose.rot()) * poleRot * basePose.rot(); - jointChainInfos[baseChainIndex].relRot = newBaseRelRot; + jointChainInfoOut.jointInfoVec[baseChainIndex].rot = newBaseRelRot; glm::quat newTopRelRot = glm::inverse(midPose.rot()) * glm::inverse(poleRot) * topPose.rot(); - jointChainInfos[topChainIndex].relRot = newTopRelRot; + jointChainInfoOut.jointInfoVec[topChainIndex].rot = newTopRelRot; } } } - for (size_t i = 0; i < chainDepth; i++) { - _rotationAccumulators[jointChainInfos[i].jointIndex].add(jointChainInfos[i].relRot, jointChainInfos[i].weight); - _translationAccumulators[jointChainInfos[i].jointIndex].add(jointChainInfos[i].relTrans, jointChainInfos[i].weight); - } - if (debug) { - debugDrawIKChain(jointChainInfos, chainDepth, context); + debugDrawIKChain(jointChainInfoOut, context); } } @@ -628,7 +679,7 @@ static CubicHermiteSplineFunctorWithArcLength computeSplineFromTipAndBase(const } // pre-compute information about each joint influeced by this spline IK target. -void AnimInverseKinematics::computeSplineJointInfosForIKTarget(const AnimContext& context, const IKTarget& target) { +void AnimInverseKinematics::computeAndCacheSplineJointInfosForIKTarget(const AnimContext& context, const IKTarget& target) const { std::vector splineJointInfoVec; // build spline between the default poses. @@ -681,13 +732,13 @@ void AnimInverseKinematics::computeSplineJointInfosForIKTarget(const AnimContext _splineJointInfoMap[target.getIndex()] = splineJointInfoVec; } -const std::vector* AnimInverseKinematics::findOrCreateSplineJointInfo(const AnimContext& context, const IKTarget& target) { +const std::vector* AnimInverseKinematics::findOrCreateSplineJointInfo(const AnimContext& context, const IKTarget& target) const { // find or create splineJointInfo for this target auto iter = _splineJointInfoMap.find(target.getIndex()); if (iter != _splineJointInfoMap.end()) { return &(iter->second); } else { - computeSplineJointInfosForIKTarget(context, target); + computeAndCacheSplineJointInfosForIKTarget(context, target); auto iter = _splineJointInfoMap.find(target.getIndex()); if (iter != _splineJointInfoMap.end()) { return &(iter->second); @@ -697,10 +748,8 @@ const std::vector* AnimInverseKinematics return nullptr; } -void AnimInverseKinematics::solveTargetWithSpline(const AnimContext& context, const IKTarget& target, const AnimPoseVec& absolutePoses, bool debug) { - - const size_t MAX_CHAIN_DEPTH = 30; - JointChainInfo jointChainInfos[MAX_CHAIN_DEPTH]; +void AnimInverseKinematics::solveTargetWithSpline(const AnimContext& context, const IKTarget& target, const AnimPoseVec& absolutePoses, + bool debug, JointChainInfo& jointChainInfoOut) const { const int baseIndex = _hipsIndex; @@ -720,7 +769,7 @@ void AnimInverseKinematics::solveTargetWithSpline(const AnimContext& context, co // This prevents the rotation interpolation from rotating the wrong physical way (but correct mathematical way) // when the head is arched backwards very far. - glm::quat halfRot = glm::normalize(glm::lerp(basePose.rot(), tipPose.rot(), 0.5f)); + glm::quat halfRot = safeLerp(basePose.rot(), tipPose.rot(), 0.5f); if (glm::dot(halfRot * Vectors::UNIT_Z, basePose.rot() * Vectors::UNIT_Z) < 0.0f) { tipPose.rot() = -tipPose.rot(); } @@ -743,7 +792,7 @@ void AnimInverseKinematics::solveTargetWithSpline(const AnimContext& context, co if (target.getIndex() == _headIndex) { rotT = t * t; } - glm::quat twistRot = glm::normalize(glm::lerp(basePose.rot(), tipPose.rot(), rotT)); + glm::quat twistRot = safeLerp(basePose.rot(), tipPose.rot(), rotT); // compute the rotation by using the derivative of the spline as the y-axis, and the twistRot x-axis glm::vec3 y = glm::normalize(spline.d(t)); @@ -783,19 +832,14 @@ void AnimInverseKinematics::solveTargetWithSpline(const AnimContext& context, co } } - jointChainInfos[i] = { relPose.rot(), relPose.trans(), target.getWeight(), splineJointInfo.jointIndex, constrained }; + jointChainInfoOut.jointInfoVec[i] = { relPose.rot(), relPose.trans(), splineJointInfo.jointIndex, constrained }; parentAbsPose = flexedAbsPose; } } - for (size_t i = 0; i < splineJointInfoVec->size(); i++) { - _rotationAccumulators[jointChainInfos[i].jointIndex].add(jointChainInfos[i].relRot, jointChainInfos[i].weight); - _translationAccumulators[jointChainInfos[i].jointIndex].add(jointChainInfos[i].relTrans, jointChainInfos[i].weight); - } - if (debug) { - debugDrawIKChain(jointChainInfos, splineJointInfoVec->size(), context); + debugDrawIKChain(jointChainInfoOut, context); } } @@ -806,6 +850,24 @@ const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVar return _relativePoses; } +AnimPose AnimInverseKinematics::applyHipsOffset() const { + glm::vec3 hipsOffset = _hipsOffset; + AnimPose relHipsPose = _relativePoses[_hipsIndex]; + float offsetLength = glm::length(hipsOffset); + const float MIN_HIPS_OFFSET_LENGTH = 0.03f; + if (offsetLength > MIN_HIPS_OFFSET_LENGTH) { + float scaleFactor = ((offsetLength - MIN_HIPS_OFFSET_LENGTH) / offsetLength); + glm::vec3 scaledHipsOffset = scaleFactor * hipsOffset; + if (_hipsParentIndex == -1) { + relHipsPose.trans() = _relativePoses[_hipsIndex].trans() + scaledHipsOffset; + } else { + AnimPose absHipsPose = _skeleton->getAbsolutePose(_hipsIndex, _relativePoses); + absHipsPose.trans() += scaledHipsOffset; + relHipsPose = _skeleton->getAbsolutePose(_hipsParentIndex, _relativePoses).inverse() * absHipsPose; + } + } + return relHipsPose; +} //virtual const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) { @@ -850,33 +912,88 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars _relativePoses = underPoses; } else { + JointChainInfoVec jointChainInfoVec(targets.size()); + { + PROFILE_RANGE_EX(simulation_animation, "ik/jointChainInfo", 0xffff00ff, 0); + + // initialize a new jointChainInfoVec, this will hold the results for solving each ik chain. + JointInfo defaultJointInfo = { glm::quat(), glm::vec3(), -1, false }; + for (size_t i = 0; i < targets.size(); i++) { + size_t chainDepth = (size_t)_skeleton->getChainDepth(targets[i].getIndex()); + jointChainInfoVec[i].jointInfoVec.reserve(chainDepth); + jointChainInfoVec[i].target = targets[i]; + int index = targets[i].getIndex(); + for (size_t j = 0; j < chainDepth; j++) { + jointChainInfoVec[i].jointInfoVec.push_back(defaultJointInfo); + jointChainInfoVec[i].jointInfoVec[j].jointIndex = index; + index = _skeleton->getParentIndex(index); + } + } + + // identify joint chains that have changed types this frame. + _prevJointChainInfoVec.resize(jointChainInfoVec.size()); + for (size_t i = 0; i < _prevJointChainInfoVec.size(); i++) { + if (_prevJointChainInfoVec[i].timer <= 0.0f && + (jointChainInfoVec[i].target.getType() != _prevJointChainInfoVec[i].target.getType() || + jointChainInfoVec[i].target.getPoleVectorEnabled() != _prevJointChainInfoVec[i].target.getPoleVectorEnabled())) { + _prevJointChainInfoVec[i].timer = JOINT_CHAIN_INTERP_TIME; + } + } + } + { PROFILE_RANGE_EX(simulation_animation, "ik/shiftHips", 0xffff00ff, 0); - if (_hipsTargetIndex >= 0 && _hipsTargetIndex < (int)targets.size()) { + if (_hipsTargetIndex >= 0) { + assert(_hipsTargetIndex < (int)targets.size()); + // slam the hips to match the _hipsTarget + AnimPose absPose = targets[_hipsTargetIndex].getPose(); + int parentIndex = _skeleton->getParentIndex(targets[_hipsTargetIndex].getIndex()); - if (parentIndex != -1) { - _relativePoses[_hipsIndex] = _skeleton->getAbsolutePose(parentIndex, _relativePoses).inverse() * absPose; - } else { - _relativePoses[_hipsIndex] = absPose; + AnimPose parentAbsPose = _skeleton->getAbsolutePose(parentIndex, _relativePoses); + + // do smooth interpolation of hips, if necessary. + if (_prevJointChainInfoVec[_hipsTargetIndex].timer > 0.0f && _prevJointChainInfoVec[_hipsTargetIndex].jointInfoVec.size() > 0) { + float alpha = (JOINT_CHAIN_INTERP_TIME - _prevJointChainInfoVec[_hipsTargetIndex].timer) / JOINT_CHAIN_INTERP_TIME; + + auto& info = _prevJointChainInfoVec[_hipsTargetIndex].jointInfoVec[0]; + AnimPose prevHipsRelPose(info.rot, info.trans); + AnimPose prevHipsAbsPose = parentAbsPose * prevHipsRelPose; + ::blend(1, &prevHipsAbsPose, &absPose, alpha, &absPose); } - } else { + + _relativePoses[_hipsIndex] = parentAbsPose.inverse() * absPose; + _relativePoses[_hipsIndex].scale() = glm::vec3(1.0f); + _hipsOffset = Vectors::ZERO; + + } else if (_hipsIndex >= 0) { + // if there is no hips target, shift hips according to the _hipsOffset from the previous frame - float offsetLength = glm::length(_hipsOffset); - const float MIN_HIPS_OFFSET_LENGTH = 0.03f; - if (offsetLength > MIN_HIPS_OFFSET_LENGTH && _hipsIndex >= 0) { - float scaleFactor = ((offsetLength - MIN_HIPS_OFFSET_LENGTH) / offsetLength); - glm::vec3 hipsOffset = scaleFactor * _hipsOffset; - if (_hipsParentIndex == -1) { - _relativePoses[_hipsIndex].trans() = _relativePoses[_hipsIndex].trans() + hipsOffset; - } else { - auto absHipsPose = _skeleton->getAbsolutePose(_hipsIndex, _relativePoses); - absHipsPose.trans() += hipsOffset; - _relativePoses[_hipsIndex] = _skeleton->getAbsolutePose(_hipsParentIndex, _relativePoses).inverse() * absHipsPose; + AnimPose relHipsPose = applyHipsOffset(); + + // determine if we should begin interpolating the hips. + for (size_t i = 0; i < targets.size(); i++) { + if (_prevJointChainInfoVec[i].target.getIndex() == _hipsIndex) { + if (_prevJointChainInfoVec[i].timer > 0.0f) { + // smoothly lerp in hipsOffset + float alpha = (JOINT_CHAIN_INTERP_TIME - _prevJointChainInfoVec[i].timer) / JOINT_CHAIN_INTERP_TIME; + AnimPose prevRelHipsPose(_prevJointChainInfoVec[i].jointInfoVec[0].rot, _prevJointChainInfoVec[i].jointInfoVec[0].trans); + ::blend(1, &prevRelHipsPose, &relHipsPose, alpha, &relHipsPose); + } + break; } } + + _relativePoses[_hipsIndex] = relHipsPose; + } + + // if there is an active jointChainInfo for the hips store the post shifted hips into it. + // This is so we have a valid pose to interplate from when the hips target is disabled. + if (_hipsTargetIndex >= 0) { + jointChainInfoVec[_hipsTargetIndex].jointInfoVec[0].rot = _relativePoses[_hipsIndex].rot(); + jointChainInfoVec[_hipsTargetIndex].jointInfoVec[0].trans = _relativePoses[_hipsIndex].trans(); } // update all HipsRelative targets to account for the hips shift/ik target. @@ -920,15 +1037,14 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars { PROFILE_RANGE_EX(simulation_animation, "ik/ccd", 0xffff00ff, 0); + preconditionRelativePosesToAvoidLimbLock(context, targets); - solve(context, targets); + solve(context, targets, dt, jointChainInfoVec); } if (_hipsTargetIndex < 0) { PROFILE_RANGE_EX(simulation_animation, "ik/measureHipsOffset", 0xffff00ff, 0); - computeHipsOffset(targets, underPoses, dt); - } else { - _hipsOffset = Vectors::ZERO; + _hipsOffset = computeHipsOffset(targets, underPoses, dt, _hipsOffset); } } @@ -937,23 +1053,15 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars } } - if (_leftHandIndex > -1) { - _uncontrolledLeftHandPose = _skeleton->getAbsolutePose(_leftHandIndex, underPoses); - } - if (_rightHandIndex > -1) { - _uncontrolledRightHandPose = _skeleton->getAbsolutePose(_rightHandIndex, underPoses); - } - if (_hipsIndex > -1) { - _uncontrolledHipsPose = _skeleton->getAbsolutePose(_hipsIndex, underPoses); - } - return _relativePoses; } -void AnimInverseKinematics::computeHipsOffset(const std::vector& targets, const AnimPoseVec& underPoses, float dt) { +glm::vec3 AnimInverseKinematics::computeHipsOffset(const std::vector& targets, const AnimPoseVec& underPoses, float dt, glm::vec3 prevHipsOffset) const { + // measure new _hipsOffset for next frame // by looking for discrepancies between where a targeted endEffector is // and where it wants to be (after IK solutions are done) + glm::vec3 hipsOffset = prevHipsOffset; glm::vec3 newHipsOffset = Vectors::ZERO; for (auto& target: targets) { int targetIndex = target.getIndex(); @@ -969,9 +1077,9 @@ void AnimInverseKinematics::computeHipsOffset(const std::vector& targe } else if (target.getType() == IKTarget::Type::HmdHead) { // we want to shift the hips to bring the head to its designated position glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans(); - _hipsOffset += target.getTranslation() - actual; + hipsOffset += target.getTranslation() - actual; // and ignore all other targets - newHipsOffset = _hipsOffset; + newHipsOffset = hipsOffset; break; } else if (target.getType() == IKTarget::Type::RotationAndPosition) { glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans(); @@ -991,16 +1099,18 @@ void AnimInverseKinematics::computeHipsOffset(const std::vector& targe } } - // smooth transitions by relaxing _hipsOffset toward the new value + // smooth transitions by relaxing hipsOffset toward the new value const float HIPS_OFFSET_SLAVE_TIMESCALE = 0.10f; float tau = dt < HIPS_OFFSET_SLAVE_TIMESCALE ? dt / HIPS_OFFSET_SLAVE_TIMESCALE : 1.0f; - _hipsOffset += (newHipsOffset - _hipsOffset) * tau; + hipsOffset += (newHipsOffset - hipsOffset) * tau; // clamp the hips offset - float hipsOffsetLength = glm::length(_hipsOffset); + float hipsOffsetLength = glm::length(hipsOffset); if (hipsOffsetLength > _maxHipsOffsetLength) { - _hipsOffset *= _maxHipsOffsetLength / hipsOffsetLength; + hipsOffset *= _maxHipsOffsetLength / hipsOffsetLength; } + + return hipsOffset; } void AnimInverseKinematics::setMaxHipsOffsetLength(float maxLength) { @@ -1414,8 +1524,6 @@ void AnimInverseKinematics::setSkeletonInternal(AnimSkeleton::ConstPointer skele targetVar.jointIndex = -1; } - _maxTargetIndex = -1; - for (auto& accumulator: _rotationAccumulators) { accumulator.clearAndClean(); } @@ -1446,10 +1554,6 @@ void AnimInverseKinematics::setSkeletonInternal(AnimSkeleton::ConstPointer skele _leftHandIndex = -1; _rightHandIndex = -1; } - - _uncontrolledLeftHandPose = AnimPose(); - _uncontrolledRightHandPose = AnimPose(); - _uncontrolledHipsPose = AnimPose(); } static glm::vec3 sphericalToCartesian(float phi, float theta) { @@ -1495,14 +1599,14 @@ void AnimInverseKinematics::debugDrawRelativePoses(const AnimContext& context) c } } -void AnimInverseKinematics::debugDrawIKChain(JointChainInfo* jointChainInfos, size_t numJointChainInfos, const AnimContext& context) const { +void AnimInverseKinematics::debugDrawIKChain(const JointChainInfo& jointChainInfo, const AnimContext& context) const { AnimPoseVec poses = _relativePoses; // copy debug joint rotations into the relative poses - for (size_t i = 0; i < numJointChainInfos; i++) { - const JointChainInfo& info = jointChainInfos[i]; - poses[info.jointIndex].rot() = info.relRot; - poses[info.jointIndex].trans() = info.relTrans; + for (size_t i = 0; i < jointChainInfo.jointInfoVec.size(); i++) { + const JointInfo& info = jointChainInfo.jointInfoVec[i]; + poses[info.jointIndex].rot() = info.rot; + poses[info.jointIndex].trans() = info.trans; } // convert relative poses to absolute @@ -1519,9 +1623,9 @@ void AnimInverseKinematics::debugDrawIKChain(JointChainInfo* jointChainInfos, si // draw each pose for (int i = 0; i < (int)poses.size(); i++) { int parentIndex = _skeleton->getParentIndex(i); - JointChainInfo* jointInfo = nullptr; - JointChainInfo* parentJointInfo = nullptr; - lookupJointChainInfo(jointChainInfos, numJointChainInfos, i, parentIndex, &jointInfo, &parentJointInfo); + const JointInfo* jointInfo = nullptr; + const JointInfo* parentJointInfo = nullptr; + lookupJointInfo(jointChainInfo, i, parentIndex, &jointInfo, &parentJointInfo); if (jointInfo && parentJointInfo) { // transform local axes into world space. @@ -1608,7 +1712,7 @@ void AnimInverseKinematics::debugDrawConstraints(const AnimContext& context) con const int NUM_SWING_STEPS = 10; for (int i = 0; i < NUM_SWING_STEPS + 1; i++) { - glm::quat rot = glm::normalize(glm::lerp(minRot, maxRot, i * (1.0f / NUM_SWING_STEPS))); + glm::quat rot = safeLerp(minRot, maxRot, i * (1.0f / NUM_SWING_STEPS)); glm::vec3 axis = transformVectorFast(geomToWorldMatrix, parentAbsRot * rot * refRot * Vectors::UNIT_Y); DebugDraw::getInstance().drawRay(pos, pos + TWIST_LENGTH * axis, CYAN); } @@ -1626,7 +1730,7 @@ void AnimInverseKinematics::debugDrawConstraints(const AnimContext& context) con const int NUM_SWING_STEPS = 10; for (int i = 0; i < NUM_SWING_STEPS + 1; i++) { - glm::quat rot = glm::normalize(glm::lerp(minRot, maxRot, i * (1.0f / NUM_SWING_STEPS))); + glm::quat rot = safeLerp(minRot, maxRot, i * (1.0f / NUM_SWING_STEPS)); glm::vec3 axis = transformVectorFast(geomToWorldMatrix, parentAbsRot * rot * refRot * Vectors::UNIT_X); DebugDraw::getInstance().drawRay(pos, pos + TWIST_LENGTH * axis, CYAN); } @@ -1666,10 +1770,9 @@ void AnimInverseKinematics::blendToPoses(const AnimPoseVec& targetPoses, const A // relax toward poses int numJoints = (int)_relativePoses.size(); for (int i = 0; i < numJoints; ++i) { - float dotSign = copysignf(1.0f, glm::dot(_relativePoses[i].rot(), targetPoses[i].rot())); if (_rotationAccumulators[i].isDirty()) { // this joint is affected by IK --> blend toward the targetPoses rotation - _relativePoses[i].rot() = glm::normalize(glm::lerp(_relativePoses[i].rot(), dotSign * targetPoses[i].rot(), blendFactor)); + _relativePoses[i].rot() = safeLerp(_relativePoses[i].rot(), targetPoses[i].rot(), blendFactor); } else { // this joint is NOT affected by IK --> slam to underPoses rotation _relativePoses[i].rot() = underPoses[i].rot(); diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h index d473ae3698..7f7640aa24 100644 --- a/libraries/animation/src/AnimInverseKinematics.h +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -26,14 +26,21 @@ class RotationConstraint; class AnimInverseKinematics : public AnimNode { public: - struct JointChainInfo { - glm::quat relRot; - glm::vec3 relTrans; - float weight; + struct JointInfo { + glm::quat rot; + glm::vec3 trans; int jointIndex; bool constrained; }; + struct JointChainInfo { + std::vector jointInfoVec; + IKTarget target; + float timer { 0.0f }; + }; + + using JointChainInfoVec = std::vector; + explicit AnimInverseKinematics(const QString& id); virtual ~AnimInverseKinematics() override; @@ -66,23 +73,22 @@ public: void setSolutionSource(SolutionSource solutionSource) { _solutionSource = solutionSource; } void setSolutionSourceVar(const QString& solutionSourceVar) { _solutionSourceVar = solutionSourceVar; } - const AnimPose& getUncontrolledLeftHandPose() { return _uncontrolledLeftHandPose; } - const AnimPose& getUncontrolledRightHandPose() { return _uncontrolledRightHandPose; } - const AnimPose& getUncontrolledHipPose() { return _uncontrolledHipsPose; } - protected: void computeTargets(const AnimVariantMap& animVars, std::vector& targets, const AnimPoseVec& underPoses); - void solve(const AnimContext& context, const std::vector& targets); - void solveTargetWithCCD(const AnimContext& context, const IKTarget& target, const AnimPoseVec& absolutePoses, bool debug); - void solveTargetWithSpline(const AnimContext& context, const IKTarget& target, const AnimPoseVec& absolutePoses, bool debug); + void solve(const AnimContext& context, const std::vector& targets, float dt, JointChainInfoVec& jointChainInfoVec); + void solveTargetWithCCD(const AnimContext& context, const IKTarget& target, const AnimPoseVec& absolutePoses, + bool debug, JointChainInfo& jointChainInfoOut) const; + void solveTargetWithSpline(const AnimContext& context, const IKTarget& target, const AnimPoseVec& absolutePoses, + bool debug, JointChainInfo& jointChainInfoOut) const; virtual void setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) override; - void debugDrawIKChain(JointChainInfo* jointChainInfos, size_t numJointChainInfos, const AnimContext& context) const; + void debugDrawIKChain(const JointChainInfo& jointChainInfo, const AnimContext& context) const; void debugDrawRelativePoses(const AnimContext& context) const; void debugDrawConstraints(const AnimContext& context) const; void debugDrawSpineSplines(const AnimContext& context, const std::vector& targets) const; void initRelativePosesFromSolutionSource(SolutionSource solutionSource, const AnimPoseVec& underPose); void blendToPoses(const AnimPoseVec& targetPoses, const AnimPoseVec& underPose, float blendFactor); void preconditionRelativePosesToAvoidLimbLock(const AnimContext& context, const std::vector& targets); + AnimPose applyHipsOffset() const; // used to pre-compute information about each joint influeced by a spline IK target. struct SplineJointInfo { @@ -91,8 +97,8 @@ protected: AnimPose offsetPose; // local offset from the spline to the joint. }; - void computeSplineJointInfosForIKTarget(const AnimContext& context, const IKTarget& target); - const std::vector* findOrCreateSplineJointInfo(const AnimContext& context, const IKTarget& target); + void computeAndCacheSplineJointInfosForIKTarget(const AnimContext& context, const IKTarget& target) const; + const std::vector* findOrCreateSplineJointInfo(const AnimContext& context, const IKTarget& target) const; // for AnimDebugDraw rendering virtual const AnimPoseVec& getPosesInternal() const override { return _relativePoses; } @@ -101,7 +107,7 @@ protected: void clearConstraints(); void initConstraints(); void initLimitCenterPoses(); - void computeHipsOffset(const std::vector& targets, const AnimPoseVec& underPoses, float dt); + glm::vec3 computeHipsOffset(const std::vector& targets, const AnimPoseVec& underPoses, float dt, glm::vec3 prevHipsOffset) const; // no copies AnimInverseKinematics(const AnimInverseKinematics&) = delete; @@ -136,7 +142,7 @@ protected: AnimPoseVec _relativePoses; // current relative poses AnimPoseVec _limitCenterPoses; // relative - std::map> _splineJointInfoMap; + mutable std::map> _splineJointInfoMap; // experimental data for moving hips during IK glm::vec3 _hipsOffset { Vectors::ZERO }; @@ -148,18 +154,12 @@ protected: int _leftHandIndex { -1 }; int _rightHandIndex { -1 }; - // _maxTargetIndex is tracked to help optimize the recalculation of absolute poses - // during the the cyclic coordinate descent algorithm - int _maxTargetIndex { 0 }; - float _maxErrorOnLastSolve { FLT_MAX }; bool _previousEnableDebugIKTargets { false }; SolutionSource _solutionSource { SolutionSource::RelaxToUnderPoses }; QString _solutionSourceVar; - AnimPose _uncontrolledLeftHandPose { AnimPose() }; - AnimPose _uncontrolledRightHandPose { AnimPose() }; - AnimPose _uncontrolledHipsPose { AnimPose() }; + JointChainInfoVec _prevJointChainInfoVec; }; #endif // hifi_AnimInverseKinematics_h diff --git a/libraries/animation/src/AnimSkeleton.cpp b/libraries/animation/src/AnimSkeleton.cpp index 062e016660..804ffb0583 100644 --- a/libraries/animation/src/AnimSkeleton.cpp +++ b/libraries/animation/src/AnimSkeleton.cpp @@ -42,6 +42,20 @@ int AnimSkeleton::getNumJoints() const { return _jointsSize; } +int AnimSkeleton::getChainDepth(int jointIndex) const { + if (jointIndex >= 0) { + int chainDepth = 0; + int index = jointIndex; + do { + chainDepth++; + index = _joints[index].parentIndex; + } while (index != -1); + return chainDepth; + } else { + return 0; + } +} + const AnimPose& AnimSkeleton::getAbsoluteBindPose(int jointIndex) const { return _absoluteBindPoses[jointIndex]; } diff --git a/libraries/animation/src/AnimSkeleton.h b/libraries/animation/src/AnimSkeleton.h index 6315f2d62b..99c9a148f7 100644 --- a/libraries/animation/src/AnimSkeleton.h +++ b/libraries/animation/src/AnimSkeleton.h @@ -28,6 +28,7 @@ public: int nameToJointIndex(const QString& jointName) const; const QString& getJointName(int jointIndex) const; int getNumJoints() const; + int getChainDepth(int jointIndex) const; // absolute pose, not relative to parent const AnimPose& getAbsoluteBindPose(int jointIndex) const; diff --git a/libraries/animation/src/AnimUtil.cpp b/libraries/animation/src/AnimUtil.cpp index a4659f1e76..bcf30642e8 100644 --- a/libraries/animation/src/AnimUtil.cpp +++ b/libraries/animation/src/AnimUtil.cpp @@ -28,7 +28,7 @@ void blend(size_t numPoses, const AnimPose* a, const AnimPose* b, float alpha, A } result[i].scale() = lerp(aPose.scale(), bPose.scale(), alpha); - result[i].rot() = glm::normalize(glm::lerp(aPose.rot(), q2, alpha)); + result[i].rot() = safeLerp(aPose.rot(), bPose.rot(), alpha); result[i].trans() = lerp(aPose.trans(), bPose.trans(), alpha); } } diff --git a/libraries/animation/src/AnimUtil.h b/libraries/animation/src/AnimUtil.h index 055fd630eb..d215fdc654 100644 --- a/libraries/animation/src/AnimUtil.h +++ b/libraries/animation/src/AnimUtil.h @@ -21,4 +21,14 @@ glm::quat averageQuats(size_t numQuats, const glm::quat* quats); float accumulateTime(float startFrame, float endFrame, float timeScale, float currentFrame, float dt, bool loopFlag, const QString& id, AnimNode::Triggers& triggersOut); +inline glm::quat safeLerp(const glm::quat& a, const glm::quat& b, float alpha) { + // adjust signs if necessary + glm::quat bTemp = b; + float dot = glm::dot(a, bTemp); + if (dot < 0.0f) { + bTemp = -bTemp; + } + return glm::normalize(glm::lerp(a, bTemp, alpha)); +} + #endif diff --git a/libraries/animation/src/IKTarget.h b/libraries/animation/src/IKTarget.h index 5567539659..325a1b40b6 100644 --- a/libraries/animation/src/IKTarget.h +++ b/libraries/animation/src/IKTarget.h @@ -56,8 +56,8 @@ private: glm::vec3 _poleReferenceVector; bool _poleVectorEnabled { false }; int _index { -1 }; - Type _type { Type::RotationAndPosition }; - float _weight; + Type _type { Type::Unknown }; + float _weight { 0.0f }; float _flexCoefficients[MAX_FLEX_COEFFICIENTS]; size_t _numFlexCoefficients; }; diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 6ebb68773f..fc0ca73c96 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1115,36 +1115,13 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab const glm::vec3 bodyCapsuleEnd = bodyCapsuleCenter + glm::vec3(0, bodyCapsuleHalfHeight, 0); const float HAND_RADIUS = 0.05f; - - const float RELAX_DURATION = 0.6f; - const float CONTROL_DURATION = 0.4f; - const bool TO_CONTROLLED = true; - const bool FROM_CONTROLLED = false; - const bool LEFT_HAND = true; - const bool RIGHT_HAND = false; - const float ELBOW_POLE_VECTOR_BLEND_FACTOR = 0.95f; if (leftHandEnabled) { - if (!_isLeftHandControlled) { - _leftHandControlTimeRemaining = CONTROL_DURATION; - _isLeftHandControlled = true; - } glm::vec3 handPosition = leftHandPose.trans(); glm::quat handRotation = leftHandPose.rot(); - if (_leftHandControlTimeRemaining > 0.0f) { - // Move hand from non-controlled position to controlled position. - _leftHandControlTimeRemaining = std::max(_leftHandControlTimeRemaining - dt, 0.0f); - AnimPose handPose(Vectors::ONE, handRotation, handPosition); - if (transitionHandPose(_leftHandControlTimeRemaining, CONTROL_DURATION, handPose, - LEFT_HAND, TO_CONTROLLED, handPose)) { - handPosition = handPose.trans(); - handRotation = handPose.rot(); - } - } - if (!hipsEnabled) { // prevent the hand IK targets from intersecting the body capsule glm::vec3 displacement; @@ -1157,9 +1134,6 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab _animVars.set("leftHandRotation", handRotation); _animVars.set("leftHandType", (int)IKTarget::Type::RotationAndPosition); - _lastLeftHandControlledPose = AnimPose(Vectors::ONE, handRotation, handPosition); - _isLeftHandControlled = true; - // compute pole vector int handJointIndex = _animSkeleton->nameToJointIndex("LeftHand"); int armJointIndex = _animSkeleton->nameToJointIndex("LeftArm"); @@ -1187,47 +1161,17 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab _prevLeftHandPoleVectorValid = false; _animVars.set("leftHandPoleVectorEnabled", false); - if (_isLeftHandControlled) { - _leftHandRelaxTimeRemaining = RELAX_DURATION; - _isLeftHandControlled = false; - } + _animVars.unset("leftHandPosition"); + _animVars.unset("leftHandRotation"); + _animVars.set("leftHandType", (int)IKTarget::Type::HipsRelativeRotationAndPosition); - if (_leftHandRelaxTimeRemaining > 0.0f) { - // Move hand from controlled position to non-controlled position. - _leftHandRelaxTimeRemaining = std::max(_leftHandRelaxTimeRemaining - dt, 0.0f); - AnimPose handPose; - if (transitionHandPose(_leftHandRelaxTimeRemaining, RELAX_DURATION, _lastLeftHandControlledPose, - LEFT_HAND, FROM_CONTROLLED, handPose)) { - _animVars.set("leftHandPosition", handPose.trans()); - _animVars.set("leftHandRotation", handPose.rot()); - _animVars.set("leftHandType", (int)IKTarget::Type::RotationAndPosition); - } - } else { - _animVars.unset("leftHandPosition"); - _animVars.unset("leftHandRotation"); - _animVars.set("leftHandType", (int)IKTarget::Type::HipsRelativeRotationAndPosition); - } } if (rightHandEnabled) { - if (!_isRightHandControlled) { - _rightHandControlTimeRemaining = CONTROL_DURATION; - _isRightHandControlled = true; - } glm::vec3 handPosition = rightHandPose.trans(); glm::quat handRotation = rightHandPose.rot(); - if (_rightHandControlTimeRemaining > 0.0f) { - // Move hand from non-controlled position to controlled position. - _rightHandControlTimeRemaining = std::max(_rightHandControlTimeRemaining - dt, 0.0f); - AnimPose handPose(Vectors::ONE, handRotation, handPosition); - if (transitionHandPose(_rightHandControlTimeRemaining, CONTROL_DURATION, handPose, RIGHT_HAND, TO_CONTROLLED, handPose)) { - handPosition = handPose.trans(); - handRotation = handPose.rot(); - } - } - if (!hipsEnabled) { // prevent the hand IK targets from intersecting the body capsule glm::vec3 displacement; @@ -1240,9 +1184,6 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab _animVars.set("rightHandRotation", handRotation); _animVars.set("rightHandType", (int)IKTarget::Type::RotationAndPosition); - _lastRightHandControlledPose = AnimPose(Vectors::ONE, handRotation, handPosition); - _isRightHandControlled = true; - // compute pole vector int handJointIndex = _animSkeleton->nameToJointIndex("RightHand"); int armJointIndex = _animSkeleton->nameToJointIndex("RightArm"); @@ -1270,25 +1211,9 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab _prevRightHandPoleVectorValid = false; _animVars.set("rightHandPoleVectorEnabled", false); - if (_isRightHandControlled) { - _rightHandRelaxTimeRemaining = RELAX_DURATION; - _isRightHandControlled = false; - } - - if (_rightHandRelaxTimeRemaining > 0.0f) { - // Move hand from controlled position to non-controlled position. - _rightHandRelaxTimeRemaining = std::max(_rightHandRelaxTimeRemaining - dt, 0.0f); - AnimPose handPose; - if (transitionHandPose(_rightHandRelaxTimeRemaining, RELAX_DURATION, _lastRightHandControlledPose, RIGHT_HAND, FROM_CONTROLLED, handPose)) { - _animVars.set("rightHandPosition", handPose.trans()); - _animVars.set("rightHandRotation", handPose.rot()); - _animVars.set("rightHandType", (int)IKTarget::Type::RotationAndPosition); - } - } else { - _animVars.unset("rightHandPosition"); - _animVars.unset("rightHandRotation"); - _animVars.set("rightHandType", (int)IKTarget::Type::HipsRelativeRotationAndPosition); - } + _animVars.unset("rightHandPosition"); + _animVars.unset("rightHandRotation"); + _animVars.set("rightHandType", (int)IKTarget::Type::HipsRelativeRotationAndPosition); } } @@ -1737,39 +1662,38 @@ void Rig::computeAvatarBoundingCapsule( ikNode.setTargetVars("RightFoot", "rightFootPosition", "rightFootRotation", "rightFootType", "rightFootWeight", 1.0f, {}, QString(), QString(), QString()); - - AnimPose geometryToRig = _modelOffset * _geometryOffset; - - AnimPose hips(glm::vec3(1), glm::quat(), glm::vec3()); + glm::vec3 hipsPosition(0.0f); int hipsIndex = indexOfJoint("Hips"); if (hipsIndex >= 0) { - hips = geometryToRig * _animSkeleton->getAbsoluteBindPose(hipsIndex); + hipsPosition = transformPoint(_geometryToRigTransform, _animSkeleton->getAbsoluteDefaultPose(hipsIndex).trans()); } AnimVariantMap animVars; + animVars.setRigToGeometryTransform(_rigToGeometryTransform); glm::quat handRotation = glm::angleAxis(PI, Vectors::UNIT_X); - animVars.set("leftHandPosition", hips.trans()); + animVars.set("leftHandPosition", hipsPosition); animVars.set("leftHandRotation", handRotation); animVars.set("leftHandType", (int)IKTarget::Type::RotationAndPosition); - animVars.set("rightHandPosition", hips.trans()); + animVars.set("rightHandPosition", hipsPosition); animVars.set("rightHandRotation", handRotation); animVars.set("rightHandType", (int)IKTarget::Type::RotationAndPosition); int rightFootIndex = indexOfJoint("RightFoot"); int leftFootIndex = indexOfJoint("LeftFoot"); if (rightFootIndex != -1 && leftFootIndex != -1) { - glm::vec3 foot = Vectors::ZERO; + glm::vec3 geomFootPosition = glm::vec3(0.0f, _animSkeleton->getAbsoluteDefaultPose(rightFootIndex).trans().y, 0.0f); + glm::vec3 footPosition = transformPoint(_geometryToRigTransform, geomFootPosition); glm::quat footRotation = glm::angleAxis(0.5f * PI, Vectors::UNIT_X); - animVars.set("leftFootPosition", foot); + animVars.set("leftFootPosition", footPosition); animVars.set("leftFootRotation", footRotation); animVars.set("leftFootType", (int)IKTarget::Type::RotationAndPosition); - animVars.set("rightFootPosition", foot); + animVars.set("rightFootPosition", footPosition); animVars.set("rightFootRotation", footRotation); animVars.set("rightFootType", (int)IKTarget::Type::RotationAndPosition); } // call overlay twice: once to verify AnimPoseVec joints and again to do the IK AnimNode::Triggers triggersOut; - AnimContext context(false, false, false, glm::mat4(), glm::mat4()); + AnimContext context(false, false, false, _geometryToRigTransform, _rigToGeometryTransform); float dt = 1.0f; // the value of this does not matter ikNode.overlay(animVars, context, dt, triggersOut, _animSkeleton->getRelativeBindPoses()); AnimPoseVec finalPoses = ikNode.overlay(animVars, context, dt, triggersOut, _animSkeleton->getRelativeBindPoses()); @@ -1802,34 +1726,13 @@ void Rig::computeAvatarBoundingCapsule( // compute bounding shape parameters // NOTE: we assume that the longest side of totalExtents is the yAxis... - glm::vec3 diagonal = (geometryToRig * totalExtents.maximum) - (geometryToRig * totalExtents.minimum); + glm::vec3 diagonal = (transformPoint(_geometryToRigTransform, totalExtents.maximum) - + transformPoint(_geometryToRigTransform, totalExtents.minimum)); // ... and assume the radiusOut is half the RMS of the X and Z sides: radiusOut = 0.5f * sqrtf(0.5f * (diagonal.x * diagonal.x + diagonal.z * diagonal.z)); heightOut = diagonal.y - 2.0f * radiusOut; glm::vec3 rootPosition = finalPoses[geometry.rootJointIndex].trans(); - glm::vec3 rigCenter = (geometryToRig * (0.5f * (totalExtents.maximum + totalExtents.minimum))); - localOffsetOut = rigCenter - (geometryToRig * rootPosition); -} - -bool Rig::transitionHandPose(float deltaTime, float totalDuration, AnimPose& controlledHandPose, bool isLeftHand, - bool isToControlled, AnimPose& returnHandPose) { - auto ikNode = getAnimInverseKinematicsNode(); - if (ikNode) { - float alpha = 1.0f - deltaTime / totalDuration; - const AnimPose geometryToRigTransform(_geometryToRigTransform); - AnimPose uncontrolledHandPose; - if (isLeftHand) { - uncontrolledHandPose = geometryToRigTransform * ikNode->getUncontrolledLeftHandPose(); - } else { - uncontrolledHandPose = geometryToRigTransform * ikNode->getUncontrolledRightHandPose(); - } - if (isToControlled) { - ::blend(1, &uncontrolledHandPose, &controlledHandPose, alpha, &returnHandPose); - } else { - ::blend(1, &controlledHandPose, &uncontrolledHandPose, alpha, &returnHandPose); - } - return true; - } - return false; + glm::vec3 rigCenter = transformPoint(_geometryToRigTransform, (0.5f * (totalExtents.maximum + totalExtents.minimum))); + localOffsetOut = rigCenter - transformPoint(_geometryToRigTransform, rootPosition); } diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index c17a7b9c8f..5293fa1fe7 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -340,18 +340,6 @@ protected: int _nextStateHandlerId { 0 }; QMutex _stateMutex; - bool transitionHandPose(float deltaTime, float totalDuration, AnimPose& controlledHandPose, bool isLeftHand, - bool isToControlled, AnimPose& returnHandPose); - - bool _isLeftHandControlled { false }; - bool _isRightHandControlled { false }; - float _leftHandControlTimeRemaining { 0.0f }; - float _rightHandControlTimeRemaining { 0.0f }; - float _leftHandRelaxTimeRemaining { 0.0f }; - float _rightHandRelaxTimeRemaining { 0.0f }; - AnimPose _lastLeftHandControlledPose; - AnimPose _lastRightHandControlledPose; - glm::vec3 _prevRightFootPoleVector { Vectors::UNIT_Z }; bool _prevRightFootPoleVectorValid { false }; diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index c630fe09e4..bc02da1cc4 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -92,6 +92,7 @@ void AudioClient::checkDevices() { auto inputDevices = getAvailableDevices(QAudio::AudioInput); auto outputDevices = getAvailableDevices(QAudio::AudioOutput); + Lock lock(_deviceMutex); if (inputDevices != _inputDevices) { _inputDevices.swap(inputDevices); emit devicesChanged(QAudio::AudioInput, _inputDevices); @@ -210,9 +211,9 @@ AudioClient::AudioClient() : connect(&_receivedAudioStream, &MixedProcessedAudioStream::processSamples, this, &AudioClient::processReceivedSamples, Qt::DirectConnection); - connect(this, &AudioClient::changeDevice, this, [=](const QAudioDeviceInfo& outputDeviceInfo) { + connect(this, &AudioClient::changeDevice, this, [=](const QAudioDeviceInfo& outputDeviceInfo) { qCDebug(audioclient) << "got AudioClient::changeDevice signal, about to call switchOutputToAudioDevice() outputDeviceInfo: [" << outputDeviceInfo.deviceName() << "]"; - switchOutputToAudioDevice(outputDeviceInfo); + switchOutputToAudioDevice(outputDeviceInfo); }); connect(&_receivedAudioStream, &InboundAudioStream::mismatchedAudioCodec, this, &AudioClient::handleMismatchAudioFormat); @@ -261,10 +262,10 @@ void AudioClient::cleanupBeforeQuit() { // so this must be explicitly, synchronously stopped static ConditionalGuard guard; if (QThread::currentThread() != thread()) { - // This will likely be called from the main thread, but we don't want to do blocking queued calls - // from the main thread, so we use a normal auto-connection invoke, and then use a conditional to wait + // This will likely be called from the main thread, but we don't want to do blocking queued calls + // from the main thread, so we use a normal auto-connection invoke, and then use a conditional to wait // for completion - // The effect is the same, yes, but we actually want to avoid the use of Qt::BlockingQueuedConnection + // The effect is the same, yes, but we actually want to avoid the use of Qt::BlockingQueuedConnection // in the code QMetaObject::invokeMethod(this, "cleanupBeforeQuit"); guard.wait(); @@ -630,7 +631,7 @@ void AudioClient::handleAudioEnvironmentDataPacket(QSharedPointerreadPrimitive(&bitset); bool hasReverb = oneAtBit(bitset, HAS_REVERB_BIT); - + if (hasReverb) { float reverbTime, wetLevel; message->readPrimitive(&reverbTime); @@ -728,7 +729,7 @@ void AudioClient::Gate::flush() { void AudioClient::handleNoisyMutePacket(QSharedPointer message) { if (!_muted) { toggleMute(); - + // have the audio scripting interface emit a signal to say we were muted by the mixer emit mutedByMixer(); } @@ -737,7 +738,7 @@ void AudioClient::handleNoisyMutePacket(QSharedPointer message) void AudioClient::handleMuteEnvironmentPacket(QSharedPointer message) { glm::vec3 position; float radius; - + message->readPrimitive(&position); message->readPrimitive(&radius); @@ -770,7 +771,7 @@ void AudioClient::handleSelectedAudioFormat(QSharedPointer mess } void AudioClient::selectAudioFormat(const QString& selectedCodecName) { - + _selectedCodecName = selectedCodecName; qCDebug(audioclient) << "Selected Codec:" << _selectedCodecName; @@ -787,7 +788,7 @@ void AudioClient::selectAudioFormat(const QString& selectedCodecName) { for (auto& plugin : codecPlugins) { if (_selectedCodecName == plugin->getName()) { _codec = plugin; - _receivedAudioStream.setupCodec(plugin, _selectedCodecName, AudioConstants::STEREO); + _receivedAudioStream.setupCodec(plugin, _selectedCodecName, AudioConstants::STEREO); _encoder = plugin->createEncoder(AudioConstants::SAMPLE_RATE, AudioConstants::MONO); qCDebug(audioclient) << "Selected Codec Plugin:" << _codec.get(); break; @@ -795,7 +796,7 @@ void AudioClient::selectAudioFormat(const QString& selectedCodecName) { } } - + bool AudioClient::switchAudioDevice(QAudio::Mode mode, const QAudioDeviceInfo& deviceInfo) { auto device = deviceInfo; @@ -1203,11 +1204,11 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { // lock the injectors Lock lock(_injectorsMutex); - QVector injectorsToRemove; + QVector injectorsToRemove; memset(mixBuffer, 0, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO * sizeof(float)); - for (AudioInjector* injector : _activeLocalAudioInjectors) { + for (const AudioInjectorPointer& injector : _activeLocalAudioInjectors) { // the lock guarantees that injectorBuffer, if found, is invariant AudioInjectorLocalBuffer* injectorBuffer = injector->getLocalBuffer(); if (injectorBuffer) { @@ -1220,7 +1221,7 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { // get one frame from the injector memset(_localScratchBuffer, 0, bytesToRead); if (0 < injectorBuffer->readData((char*)_localScratchBuffer, bytesToRead)) { - + if (injector->isAmbisonic()) { // no distance attenuation @@ -1249,36 +1250,36 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) { mixBuffer[i] += convertToFloat(_localScratchBuffer[i]) * gain; } - + } else { // calculate distance, gain and azimuth for hrtf glm::vec3 relativePosition = injector->getPosition() - _positionGetter(); float distance = glm::max(glm::length(relativePosition), EPSILON); - float gain = gainForSource(distance, injector->getVolume()); + float gain = gainForSource(distance, injector->getVolume()); float azimuth = azimuthForSource(relativePosition); - + // mono gets spatialized into mixBuffer - injector->getLocalHRTF().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX, + injector->getLocalHRTF().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX, azimuth, distance, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } - + } else { - + qCDebug(audioclient) << "injector has no more data, marking finished for removal"; injector->finishLocalInjection(); injectorsToRemove.append(injector); } } else { - + qCDebug(audioclient) << "injector has no local buffer, marking as finished for removal"; injector->finishLocalInjection(); injectorsToRemove.append(injector); } } - - for (AudioInjector* injector : injectorsToRemove) { + + for (const AudioInjectorPointer& injector : injectorsToRemove) { qCDebug(audioclient) << "removing injector"; _activeLocalAudioInjectors.removeOne(injector); } @@ -1369,7 +1370,7 @@ void AudioClient::setIsStereoInput(bool isStereoInput) { } } -bool AudioClient::outputLocalInjector(AudioInjector* injector) { +bool AudioClient::outputLocalInjector(const AudioInjectorPointer& injector) { AudioInjectorLocalBuffer* injectorBuffer = injector->getLocalBuffer(); if (injectorBuffer) { // local injectors are on the AudioInjectorsThread, so we must guard access @@ -1711,9 +1712,9 @@ int AudioClient::calculateNumberOfFrameSamples(int numBytes) const { float AudioClient::azimuthForSource(const glm::vec3& relativePosition) { glm::quat inverseOrientation = glm::inverse(_orientationGetter()); - + glm::vec3 rotatedSourcePosition = inverseOrientation * relativePosition; - + // project the rotated source position vector onto the XZ plane rotatedSourcePosition.y = 0.0f; @@ -1721,15 +1722,15 @@ float AudioClient::azimuthForSource(const glm::vec3& relativePosition) { float rotatedSourcePositionLength2 = glm::length2(rotatedSourcePosition); if (rotatedSourcePositionLength2 > SOURCE_DISTANCE_THRESHOLD) { - + // produce an oriented angle about the y-axis glm::vec3 direction = rotatedSourcePosition * (1.0f / fastSqrtf(rotatedSourcePositionLength2)); float angle = fastAcosf(glm::clamp(-direction.z, -1.0f, 1.0f)); // UNIT_NEG_Z is "forward" return (direction.x < 0.0f) ? -angle : angle; - } else { + } else { // no azimuth if they are in same spot - return 0.0f; + return 0.0f; } } @@ -1869,9 +1870,9 @@ void AudioClient::startThread() { moveToNewNamedThread(this, "Audio Thread", [this] { start(); }); } -void AudioClient::setInputVolume(float volume) { - if (_audioInput && volume != (float)_audioInput->volume()) { - _audioInput->setVolume(volume); +void AudioClient::setInputVolume(float volume) { + if (_audioInput && volume != (float)_audioInput->volume()) { + _audioInput->setVolume(volume); emit inputVolumeChanged(_audioInput->volume()); } } diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 62b99d2443..31e36671c7 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -143,7 +143,7 @@ public: Q_INVOKABLE void setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 scale); - bool outputLocalInjector(AudioInjector* injector) override; + bool outputLocalInjector(const AudioInjectorPointer& injector) override; QAudioDeviceInfo getActiveAudioDevice(QAudio::Mode mode) const; QList getAudioDevices(QAudio::Mode mode) const; @@ -380,7 +380,7 @@ private: bool _hasReceivedFirstPacket { false }; - QVector _activeLocalAudioInjectors; + QVector _activeLocalAudioInjectors; bool _isPlayingBackRecording { false }; diff --git a/libraries/audio/src/AbstractAudioInterface.h b/libraries/audio/src/AbstractAudioInterface.h index 2e14b9956b..8b48b55206 100644 --- a/libraries/audio/src/AbstractAudioInterface.h +++ b/libraries/audio/src/AbstractAudioInterface.h @@ -18,6 +18,7 @@ #include #include "AudioInjectorOptions.h" +#include "AudioInjector.h" class AudioInjector; class AudioInjectorLocalBuffer; @@ -35,7 +36,7 @@ public: // threadsafe // moves injector->getLocalBuffer() to another thread (so removes its parent) // take care to delete it when ~AudioInjector, as parenting Qt semantics will not work - virtual bool outputLocalInjector(AudioInjector* injector) = 0; + virtual bool outputLocalInjector(const AudioInjectorPointer& injector) = 0; public slots: virtual bool shouldLoopbackInjectors() { return false; } diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index 47e6c98144..9f32372a8e 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -27,8 +27,6 @@ #include "AudioSRC.h" #include "AudioHelpers.h" -int audioInjectorPtrMetaTypeId = qRegisterMetaType(); - AbstractAudioInterface* AudioInjector::_localAudioInterface{ nullptr }; AudioInjectorState operator& (AudioInjectorState lhs, AudioInjectorState rhs) { @@ -92,11 +90,6 @@ void AudioInjector::finish() { emit finished(); deleteLocalBuffer(); - - if (stateHas(AudioInjectorState::PendingDelete)) { - // we've been asked to delete after finishing, trigger a deleteLater here - deleteLater(); - } } void AudioInjector::restart() { @@ -132,7 +125,7 @@ void AudioInjector::restart() { } } -bool AudioInjector::inject(bool(AudioInjectorManager::*injection)(AudioInjector*)) { +bool AudioInjector::inject(bool(AudioInjectorManager::*injection)(const AudioInjectorPointer&)) { _state = AudioInjectorState::NotFinished; int byteOffset = 0; @@ -150,7 +143,7 @@ bool AudioInjector::inject(bool(AudioInjectorManager::*injection)(AudioInjector* bool success = true; if (!_options.localOnly) { auto injectorManager = DependencyManager::get(); - if (!(*injectorManager.*injection)(this)) { + if (!(*injectorManager.*injection)(sharedFromThis())) { success = false; finishNetworkInjection(); } @@ -173,7 +166,7 @@ bool AudioInjector::injectLocally() { // call this function on the AudioClient's thread // this will move the local buffer's thread to the LocalInjectorThread - success = _localAudioInterface->outputLocalInjector(this); + success = _localAudioInterface->outputLocalInjector(sharedFromThis()); if (!success) { qCDebug(audio) << "AudioInjector::injectLocally could not output locally via _localAudioInterface"; @@ -418,20 +411,16 @@ void AudioInjector::triggerDeleteAfterFinish() { } if (stateHas(AudioInjectorState::Finished)) { - stopAndDeleteLater(); + stop(); } else { _state |= AudioInjectorState::PendingDelete; } } -void AudioInjector::stopAndDeleteLater() { - stop(); - QMetaObject::invokeMethod(this, "deleteLater", Qt::QueuedConnection); -} - -AudioInjector* AudioInjector::playSound(SharedSoundPointer sound, const float volume, const float stretchFactor, const glm::vec3 position) { +AudioInjectorPointer AudioInjector::playSound(SharedSoundPointer sound, const float volume, + const float stretchFactor, const glm::vec3 position) { if (!sound || !sound->isReady()) { - return nullptr; + return AudioInjectorPointer(); } AudioInjectorOptions options; @@ -462,8 +451,8 @@ AudioInjector* AudioInjector::playSound(SharedSoundPointer sound, const float vo return playSoundAndDelete(resampled, options); } -AudioInjector* AudioInjector::playSoundAndDelete(const QByteArray& buffer, const AudioInjectorOptions options) { - AudioInjector* sound = playSound(buffer, options); +AudioInjectorPointer AudioInjector::playSoundAndDelete(const QByteArray& buffer, const AudioInjectorOptions options) { + AudioInjectorPointer sound = playSound(buffer, options); if (sound) { sound->_state |= AudioInjectorState::PendingDelete; @@ -473,8 +462,9 @@ AudioInjector* AudioInjector::playSoundAndDelete(const QByteArray& buffer, const } -AudioInjector* AudioInjector::playSound(const QByteArray& buffer, const AudioInjectorOptions options) { - AudioInjector* injector = new AudioInjector(buffer, options); +AudioInjectorPointer AudioInjector::playSound(const QByteArray& buffer, const AudioInjectorOptions options) { + AudioInjectorPointer injector = AudioInjectorPointer::create(buffer, options); + if (!injector->inject(&AudioInjectorManager::threadInjector)) { qWarning() << "AudioInjector::playSound failed to thread injector"; } diff --git a/libraries/audio/src/AudioInjector.h b/libraries/audio/src/AudioInjector.h index a901c2520f..fc197f7ba0 100644 --- a/libraries/audio/src/AudioInjector.h +++ b/libraries/audio/src/AudioInjector.h @@ -32,6 +32,8 @@ class AbstractAudioInterface; class AudioInjectorManager; +class AudioInjector; +using AudioInjectorPointer = QSharedPointer; enum class AudioInjectorState : uint8_t { @@ -46,19 +48,19 @@ AudioInjectorState operator& (AudioInjectorState lhs, AudioInjectorState rhs); AudioInjectorState& operator|= (AudioInjectorState& lhs, AudioInjectorState rhs); // In order to make scripting cleaner for the AudioInjector, the script now holds on to the AudioInjector object -// until it dies. -class AudioInjector : public QObject { +// until it dies. +class AudioInjector : public QObject, public QEnableSharedFromThis { Q_OBJECT public: AudioInjector(const Sound& sound, const AudioInjectorOptions& injectorOptions); AudioInjector(const QByteArray& audioData, const AudioInjectorOptions& injectorOptions); ~AudioInjector(); - + bool isFinished() const { return (stateHas(AudioInjectorState::Finished)); } - + int getCurrentSendOffset() const { return _currentSendOffset; } void setCurrentSendOffset(int currentSendOffset) { _currentSendOffset = currentSendOffset; } - + AudioInjectorLocalBuffer* getLocalBuffer() const { return _localBuffer; } AudioHRTF& getLocalHRTF() { return _localHRTF; } AudioFOA& getLocalFOA() { return _localFOA; } @@ -72,36 +74,36 @@ public: bool stateHas(AudioInjectorState state) const ; static void setLocalAudioInterface(AbstractAudioInterface* audioInterface) { _localAudioInterface = audioInterface; } - static AudioInjector* playSoundAndDelete(const QByteArray& buffer, const AudioInjectorOptions options); - static AudioInjector* playSound(const QByteArray& buffer, const AudioInjectorOptions options); - static AudioInjector* playSound(SharedSoundPointer sound, const float volume, const float stretchFactor, const glm::vec3 position); + static AudioInjectorPointer playSoundAndDelete(const QByteArray& buffer, const AudioInjectorOptions options); + static AudioInjectorPointer playSound(const QByteArray& buffer, const AudioInjectorOptions options); + static AudioInjectorPointer playSound(SharedSoundPointer sound, const float volume, + const float stretchFactor, const glm::vec3 position); public slots: void restart(); - + void stop(); void triggerDeleteAfterFinish(); - void stopAndDeleteLater(); - + const AudioInjectorOptions& getOptions() const { return _options; } void setOptions(const AudioInjectorOptions& options); - + float getLoudness() const { return _loudness; } bool isPlaying() const { return !stateHas(AudioInjectorState::Finished); } void finish(); void finishLocalInjection(); void finishNetworkInjection(); - + signals: void finished(); void restarting(); - + private: int64_t injectNextFrame(); - bool inject(bool(AudioInjectorManager::*injection)(AudioInjector*)); + bool inject(bool(AudioInjectorManager::*injection)(const AudioInjectorPointer&)); bool injectLocally(); void deleteLocalBuffer(); - + static AbstractAudioInterface* _localAudioInterface; QByteArray _audioData; @@ -112,17 +114,15 @@ private: int _currentSendOffset { 0 }; std::unique_ptr _currentPacket { nullptr }; AudioInjectorLocalBuffer* _localBuffer { nullptr }; - + int64_t _nextFrame { 0 }; std::unique_ptr _frameTimer { nullptr }; quint16 _outgoingSequenceNumber { 0 }; - + // when the injector is local, we need this AudioHRTF _localHRTF; AudioFOA _localFOA; friend class AudioInjectorManager; }; -Q_DECLARE_METATYPE(AudioInjector*) - #endif // hifi_AudioInjector_h diff --git a/libraries/audio/src/AudioInjectorManager.cpp b/libraries/audio/src/AudioInjectorManager.cpp index c66e209ea9..f30d3093ec 100644 --- a/libraries/audio/src/AudioInjectorManager.cpp +++ b/libraries/audio/src/AudioInjectorManager.cpp @@ -21,26 +21,26 @@ AudioInjectorManager::~AudioInjectorManager() { _shouldStop = true; - + Lock lock(_injectorsMutex); - + // make sure any still living injectors are stopped and deleted while (!_injectors.empty()) { // grab the injector at the front auto& timePointerPair = _injectors.top(); - + // ask it to stop and be deleted - timePointerPair.second->stopAndDeleteLater(); - + timePointerPair.second->stop(); + _injectors.pop(); } - + // get rid of the lock now that we've stopped all living injectors lock.unlock(); - + // in case the thread is waiting for injectors wake it up now _injectorReady.notify_one(); - + // quit and wait on the manager thread, if we ever created it if (_thread) { _thread->quit(); @@ -51,10 +51,10 @@ AudioInjectorManager::~AudioInjectorManager() { void AudioInjectorManager::createThread() { _thread = new QThread; _thread->setObjectName("Audio Injector Thread"); - + // when the thread is started, have it call our run to handle injection of audio connect(_thread, &QThread::started, this, &AudioInjectorManager::run, Qt::DirectConnection); - + // start the thread _thread->start(); } @@ -63,20 +63,20 @@ void AudioInjectorManager::run() { while (!_shouldStop) { // wait until the next injector is ready, or until we get a new injector given to us Lock lock(_injectorsMutex); - + if (_injectors.size() > 0) { // when does the next injector need to send a frame? // do we get to wait or should we just go for it now? - + auto timeInjectorPair = _injectors.top(); - + auto nextTimestamp = timeInjectorPair.first; int64_t difference = int64_t(nextTimestamp - usecTimestampNow()); - + if (difference > 0) { _injectorReady.wait_for(lock, std::chrono::microseconds(difference)); } - + if (_injectors.size() > 0) { // loop through the injectors in the map and send whatever frames need to go out auto front = _injectors.top(); @@ -90,7 +90,7 @@ void AudioInjectorManager::run() { // either way we're popping this injector off - get a copy first auto injector = front.second; _injectors.pop(); - + if (!injector.isNull()) { // this is an injector that's ready to go, have it send a frame now auto nextCallDelta = injector->injectNextFrame(); @@ -100,7 +100,7 @@ void AudioInjectorManager::run() { heldInjectors.emplace(heldInjectors.end(), usecTimestampNow() + nextCallDelta, injector); } } - + if (_injectors.size() > 0) { front = _injectors.top(); } else { @@ -120,10 +120,10 @@ void AudioInjectorManager::run() { // we have no current injectors, wait until we get at least one before we do anything _injectorReady.wait(lock); } - + // unlock the lock in case something in process events needs to modify the queue lock.unlock(); - + QCoreApplication::processEvents(); } } @@ -139,36 +139,36 @@ bool AudioInjectorManager::wouldExceedLimits() { // Should be called inside of a return false; } -bool AudioInjectorManager::threadInjector(AudioInjector* injector) { +bool AudioInjectorManager::threadInjector(const AudioInjectorPointer& injector) { if (_shouldStop) { qCDebug(audio) << "AudioInjectorManager::threadInjector asked to thread injector but is shutting down."; return false; } - + // guard the injectors vector with a mutex Lock lock(_injectorsMutex); - + if (wouldExceedLimits()) { return false; } else { if (!_thread) { createThread(); } - + // move the injector to the QThread injector->moveToThread(_thread); - + // add the injector to the queue with a send timestamp of now - _injectors.emplace(usecTimestampNow(), InjectorQPointer { injector }); - + _injectors.emplace(usecTimestampNow(), injector); + // notify our wait condition so we can inject two frames for this injector immediately _injectorReady.notify_one(); - + return true; } } -bool AudioInjectorManager::restartFinishedInjector(AudioInjector* injector) { +bool AudioInjectorManager::restartFinishedInjector(const AudioInjectorPointer& injector) { if (_shouldStop) { qCDebug(audio) << "AudioInjectorManager::threadInjector asked to thread injector but is shutting down."; return false; @@ -181,8 +181,8 @@ bool AudioInjectorManager::restartFinishedInjector(AudioInjector* injector) { return false; } else { // add the injector to the queue with a send timestamp of now - _injectors.emplace(usecTimestampNow(), InjectorQPointer { injector }); - + _injectors.emplace(usecTimestampNow(), injector); + // notify our wait condition so we can inject two frames for this injector immediately _injectorReady.notify_one(); } diff --git a/libraries/audio/src/AudioInjectorManager.h b/libraries/audio/src/AudioInjectorManager.h index de5537856e..9aca3014e3 100644 --- a/libraries/audio/src/AudioInjectorManager.h +++ b/libraries/audio/src/AudioInjectorManager.h @@ -23,7 +23,7 @@ #include -class AudioInjector; +#include "AudioInjector.h" class AudioInjectorManager : public QObject, public Dependency { Q_OBJECT @@ -33,39 +33,38 @@ public: private slots: void run(); private: - - using InjectorQPointer = QPointer; - using TimeInjectorPointerPair = std::pair; - + + using TimeInjectorPointerPair = std::pair; + struct greaterTime { bool operator() (const TimeInjectorPointerPair& x, const TimeInjectorPointerPair& y) const { return x.first > y.first; } }; - + using InjectorQueue = std::priority_queue, greaterTime>; using Mutex = std::mutex; using Lock = std::unique_lock; - - bool threadInjector(AudioInjector* injector); - bool restartFinishedInjector(AudioInjector* injector); + + bool threadInjector(const AudioInjectorPointer& injector); + bool restartFinishedInjector(const AudioInjectorPointer& injector); void notifyInjectorReadyCondition() { _injectorReady.notify_one(); } bool wouldExceedLimits(); - + AudioInjectorManager() {}; AudioInjectorManager(const AudioInjectorManager&) = delete; AudioInjectorManager& operator=(const AudioInjectorManager&) = delete; - + void createThread(); - + QThread* _thread { nullptr }; bool _shouldStop { false }; InjectorQueue _injectors; Mutex _injectorsMutex; std::condition_variable _injectorReady; - + friend class AudioInjector; }; diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 2a223a1eaf..4016592d0a 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -1050,7 +1050,34 @@ int Avatar::getJointIndex(const QString& name) const { QStringList Avatar::getJointNames() const { QStringList result; withValidJointIndicesCache([&]() { - result = _modelJointIndicesCache.keys(); + // find out how large the vector needs to be + int maxJointIndex = -1; + QHashIterator k(_modelJointIndicesCache); + while (k.hasNext()) { + k.next(); + int index = k.value(); + if (index > maxJointIndex) { + maxJointIndex = index; + } + } + // iterate through the hash and put joint names + // into the vector at their indices + QVector resultVector(maxJointIndex+1); + QHashIterator i(_modelJointIndicesCache); + while (i.hasNext()) { + i.next(); + int index = i.value(); + resultVector[index] = i.key(); + } + // convert to QList and drop out blanks + result = resultVector.toList(); + QMutableListIterator j(result); + while (j.hasNext()) { + QString jointName = j.next(); + if (jointName.isEmpty()) { + j.remove(); + } + } }); return result; } @@ -1453,8 +1480,7 @@ void Avatar::addToScene(AvatarSharedPointer myHandle, const render::ScenePointer if (scene) { auto nodelist = DependencyManager::get(); if (showAvatars - && !nodelist->isIgnoringNode(getSessionUUID()) - && !nodelist->isRadiusIgnoringNode(getSessionUUID())) { + && !nodelist->isIgnoringNode(getSessionUUID())) { render::Transaction transaction; addToScene(myHandle, scene, transaction); scene->enqueueTransaction(transaction); diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 5e63619c41..6fdb4d1ef6 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -2366,6 +2366,12 @@ AvatarEntityMap AvatarData::getAvatarEntityData() const { return result; } +void AvatarData::insertDetachedEntityID(const QUuid entityID) { + _avatarEntitiesLock.withWriteLock([&] { + _avatarEntityDetached.insert(entityID); + }); +} + void AvatarData::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) { if (avatarEntityData.size() > MAX_NUM_AVATAR_ENTITIES) { // the data is suspect diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index f64d9dc2d1..16768ec62a 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -615,6 +615,7 @@ public: Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const; Q_INVOKABLE void setAvatarEntityData(const AvatarEntityMap& avatarEntityData); void setAvatarEntityDataChanged(bool value) { _avatarEntityDataChanged = value; } + void insertDetachedEntityID(const QUuid entityID); AvatarEntityIDs getAndClearRecentlyDetachedIDs(); // thread safe diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index e8c37bdaa8..3712080cdb 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -170,13 +170,6 @@ void AvatarHashMap::processKillAvatar(QSharedPointer message, S removeAvatar(sessionUUID, reason); } -void AvatarHashMap::processExitingSpaceBubble(QSharedPointer message, SharedNodePointer sendingNode) { - // read the node id - QUuid sessionUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); - auto nodeList = DependencyManager::get(); - nodeList->radiusIgnoreNodeBySessionID(sessionUUID, false); -} - void AvatarHashMap::removeAvatar(const QUuid& sessionUUID, KillAvatarReason removalReason) { QWriteLocker locker(&_hashLock); diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index 21ea8081c7..68fc232685 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -60,7 +60,6 @@ protected slots: void processAvatarDataPacket(QSharedPointer message, SharedNodePointer sendingNode); void processAvatarIdentityPacket(QSharedPointer message, SharedNodePointer sendingNode); void processKillAvatar(QSharedPointer message, SharedNodePointer sendingNode); - void processExitingSpaceBubble(QSharedPointer message, SharedNodePointer sendingNode); protected: AvatarHashMap(); diff --git a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h index 1da0441d8f..8d01539e8a 100644 --- a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h @@ -19,6 +19,7 @@ public: bool hasFocus() const override; void submitFrame(const gpu::FramePointer& newFrame) override; QImage getScreenshot(float aspectRatio = 0.0f) const override; + void copyTextureToQuickFramebuffer(NetworkTexturePointer source, QOpenGLFramebufferObject* target, GLsync* fenceSync) override {}; private: static const QString NAME; }; diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 67bbb452ca..e1259fc5fc 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -16,6 +16,7 @@ #include #include +#include #if defined(Q_OS_MAC) #include #endif @@ -41,7 +42,7 @@ #include #include #include - +#include #include "CompositorHelper.h" #include "Logging.h" @@ -55,7 +56,7 @@ out vec4 outFragColor; float sRGBFloatToLinear(float value) { const float SRGB_ELBOW = 0.04045; - + return (value <= SRGB_ELBOW) ? value / 12.92 : pow((value + 0.055) / 1.055, 2.4); } @@ -121,10 +122,10 @@ public: PROFILE_SET_THREAD_NAME("Present Thread"); // FIXME determine the best priority balance between this and the main thread... - // It may be dependent on the display plugin being used, since VR plugins should + // It may be dependent on the display plugin being used, since VR plugins should // have higher priority on rendering (although we could say that the Oculus plugin // doesn't need that since it has async timewarp). - // A higher priority here + // A higher priority here setPriority(QThread::HighPriority); OpenGLDisplayPlugin* currentPlugin{ nullptr }; Q_ASSERT(_context); @@ -233,7 +234,7 @@ public: // Move the context back to the presentation thread _context->moveToThread(this); - // restore control of the context to the presentation thread and signal + // restore control of the context to the presentation thread and signal // the end of the operation _finishedMainThreadOperation = true; lock.unlock(); @@ -291,7 +292,7 @@ bool OpenGLDisplayPlugin::activate() { if (!RENDER_THREAD) { RENDER_THREAD = _presentThread; } - + // Child classes may override this in order to do things like initialize // libraries, etc if (!internalActivate()) { @@ -411,7 +412,7 @@ void OpenGLDisplayPlugin::customizeContext() { gpu::Shader::makeProgram(*program); gpu::StatePointer state = gpu::StatePointer(new gpu::State()); state->setDepthTest(gpu::State::DepthTest(false)); - state->setBlendFunction(true, + state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); _overlayPipeline = gpu::Pipeline::create(program, state); @@ -496,16 +497,48 @@ void OpenGLDisplayPlugin::submitFrame(const gpu::FramePointer& newFrame) { _newFrameQueue.push(newFrame); }); } + void OpenGLDisplayPlugin::renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer texture, glm::ivec4 viewport, const glm::ivec4 scissor) { + renderFromTexture(batch, texture, viewport, scissor, gpu::FramebufferPointer()); +} + +void OpenGLDisplayPlugin::renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer texture, glm::ivec4 viewport, const glm::ivec4 scissor, gpu::FramebufferPointer copyFbo /*=gpu::FramebufferPointer()*/) { + auto fbo = gpu::FramebufferPointer(); batch.enableStereo(false); batch.resetViewTransform(); - batch.setFramebuffer(gpu::FramebufferPointer()); + batch.setFramebuffer(fbo); batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); batch.setStateScissorRect(scissor); batch.setViewportTransform(viewport); batch.setResourceTexture(0, texture); batch.setPipeline(_presentPipeline); batch.draw(gpu::TRIANGLE_STRIP, 4); + if (copyFbo) { + gpu::Vec4i copyFboRect(0, 0, copyFbo->getWidth(), copyFbo->getHeight()); + gpu::Vec4i sourceRect(scissor.x, scissor.y, scissor.x + scissor.z, scissor.y + scissor.w); + float aspectRatio = (float)scissor.w / (float) scissor.z; // height/width + // scale width first + int xOffset = 0; + int yOffset = 0; + int newWidth = copyFbo->getWidth(); + int newHeight = std::round(aspectRatio * (float) copyFbo->getWidth()); + if (newHeight > copyFbo->getHeight()) { + // ok, so now fill height instead + newHeight = copyFbo->getHeight(); + newWidth = std::round((float)copyFbo->getHeight() / aspectRatio); + xOffset = (copyFbo->getWidth() - newWidth) / 2; + } else { + yOffset = (copyFbo->getHeight() - newHeight) / 2; + } + gpu::Vec4i copyRect(xOffset, yOffset, xOffset + newWidth, yOffset + newHeight); + batch.setFramebuffer(copyFbo); + + batch.resetViewTransform(); + batch.setViewportTransform(copyFboRect); + batch.setStateScissorRect(copyFboRect); + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, {0.0f, 0.0f, 0.0f, 1.0f}); + batch.blit(fbo, sourceRect, copyFbo, copyRect); + } } void OpenGLDisplayPlugin::updateFrameData() { @@ -686,7 +719,7 @@ void OpenGLDisplayPlugin::resetPresentRate() { // _presentRate = RateCounter<100>(); } -float OpenGLDisplayPlugin::renderRate() const { +float OpenGLDisplayPlugin::renderRate() const { return _renderRate.rate(); } @@ -821,3 +854,53 @@ void OpenGLDisplayPlugin::updateCompositeFramebuffer() { _compositeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("OpenGLDisplayPlugin::composite", gpu::Element::COLOR_RGBA_32, renderSize.x, renderSize.y)); } } + +void OpenGLDisplayPlugin::copyTextureToQuickFramebuffer(NetworkTexturePointer networkTexture, QOpenGLFramebufferObject* target, GLsync* fenceSync) { + auto glBackend = const_cast(*this).getGLBackend(); + withMainThreadContext([&] { + GLuint sourceTexture = glBackend->getTextureID(networkTexture->getGPUTexture()); + GLuint targetTexture = target->texture(); + GLuint fbo[2] {0, 0}; + + // need mipmaps for blitting texture + glGenerateTextureMipmap(sourceTexture); + + // create 2 fbos (one for initial texture, second for scaled one) + glCreateFramebuffers(2, fbo); + + // setup source fbo + glBindFramebuffer(GL_FRAMEBUFFER, fbo[0]); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, sourceTexture, 0); + + GLint texWidth = networkTexture->getWidth(); + GLint texHeight = networkTexture->getHeight(); + + // setup destination fbo + glBindFramebuffer(GL_FRAMEBUFFER, fbo[1]); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, targetTexture, 0); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT); + + + // maintain aspect ratio, filling the width first if possible. If that makes the height too + // much, fill height instead. TODO: only do this when texture changes + GLint newX = 0; + GLint newY = 0; + float aspectRatio = (float)texHeight / (float)texWidth; + GLint newWidth = target->width(); + GLint newHeight = std::round(aspectRatio * (float) target->width()); + if (newHeight > target->height()) { + newHeight = target->height(); + newWidth = std::round((float)target->height() / aspectRatio); + newX = (target->width() - newWidth) / 2; + } else { + newY = (target->height() - newHeight) / 2; + } + glBlitNamedFramebuffer(fbo[0], fbo[1], 0, 0, texWidth, texHeight, newX, newY, newX + newWidth, newY + newHeight, GL_DEPTH_BUFFER_BIT|GL_COLOR_BUFFER_BIT, GL_NEAREST); + + // don't delete the textures! + glDeleteFramebuffers(2, fbo); + *fenceSync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + }); +} + diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index 7e7889ff47..2f93fa630d 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -38,7 +38,7 @@ protected: using Condition = std::condition_variable; public: ~OpenGLDisplayPlugin(); - // These must be final to ensure proper ordering of operations + // These must be final to ensure proper ordering of operations // between the main thread and the presentation thread bool activate() override final; void deactivate() override final; @@ -79,6 +79,8 @@ public: // Three threads, one for rendering, one for texture transfers, one reserved for the GL driver int getRequiredThreadCount() const override { return 3; } + void copyTextureToQuickFramebuffer(NetworkTexturePointer source, QOpenGLFramebufferObject* target, GLsync* fenceSync) override; + protected: friend class PresentThread; @@ -103,7 +105,7 @@ protected: // Returns true on successful activation virtual bool internalActivate() { return true; } virtual void internalDeactivate() {} - + // Returns true on successful activation of standby session virtual bool activateStandBySession() { return true; } virtual void deactivateSession() {} @@ -111,6 +113,7 @@ protected: // Plugin specific functionality to send the composed scene to the output window or device virtual void internalPresent(); + void renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer texture, glm::ivec4 viewport, const glm::ivec4 scissor, gpu::FramebufferPointer fbo); void renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer texture, glm::ivec4 viewport, const glm::ivec4 scissor); virtual void updateFrameData(); diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index ea91890f33..b183850e7f 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -134,7 +134,7 @@ void HmdDisplayPlugin::customizeContext() { state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - + gpu::Shader::BindingSet bindings; bindings.insert({ "lineData", LINE_DATA_SLOT });; gpu::Shader::makeProgram(*program, bindings); @@ -243,6 +243,8 @@ void HmdDisplayPlugin::internalPresent() { glm::ivec4 viewport = getViewportForSourceSize(sourceSize); glm::ivec4 scissor = viewport; + auto fbo = gpu::FramebufferPointer(); + render([&](gpu::Batch& batch) { if (_monoPreview) { @@ -285,11 +287,15 @@ void HmdDisplayPlugin::internalPresent() { viewport = ivec4(scissorOffset - scaledShiftLeftBy, viewportOffset, viewportSizeX, viewportSizeY); } + // TODO: only bother getting and passing in the hmdPreviewFramebuffer if the camera is on + fbo = DependencyManager::get()->getHmdPreviewFramebuffer(windowSize.x, windowSize.y); + viewport.z *= 2; } - renderFromTexture(batch, _compositeFramebuffer->getRenderBuffer(0), viewport, scissor); + renderFromTexture(batch, _compositeFramebuffer->getRenderBuffer(0), viewport, scissor, fbo); }); swapBuffers(); + } else if (_clearPreviewFlag) { QImage image; if (_vsyncEnabled) { @@ -312,7 +318,7 @@ void HmdDisplayPlugin::internalPresent() { _previewTexture->assignStoredMip(0, image.byteCount(), image.constBits()); _previewTexture->setAutoGenerateMips(true); } - + auto viewport = getViewportForSourceSize(uvec2(_previewTexture->getDimensions())); render([&](gpu::Batch& batch) { @@ -323,7 +329,7 @@ void HmdDisplayPlugin::internalPresent() { } postPreview(); - // If preview is disabled, we need to check to see if the window size has changed + // If preview is disabled, we need to check to see if the window size has changed // and re-render the no-preview message if (_disablePreview) { auto window = _container->getPrimaryWidget(); @@ -510,7 +516,7 @@ void HmdDisplayPlugin::OverlayRenderer::build() { indices = std::make_shared(); //UV mapping source: http://www.mvps.org/directx/articles/spheremap.htm - + static const float fov = CompositorHelper::VIRTUAL_UI_TARGET_FOV.y; static const float aspectRatio = CompositorHelper::VIRTUAL_UI_ASPECT_RATIO; static const uint16_t stacks = 128; @@ -672,7 +678,7 @@ bool HmdDisplayPlugin::setHandLaser(uint32_t hands, HandLaserMode mode, const ve _handLasers[1] = info; } }); - // FIXME defer to a child class plugin to determine if hand lasers are actually + // FIXME defer to a child class plugin to determine if hand lasers are actually // available based on the presence or absence of hand controllers return true; } @@ -687,7 +693,7 @@ bool HmdDisplayPlugin::setExtraLaser(HandLaserMode mode, const vec4& color, cons _extraLaserStart = sensorSpaceStart; }); - // FIXME defer to a child class plugin to determine if hand lasers are actually + // FIXME defer to a child class plugin to determine if hand lasers are actually // available based on the presence or absence of hand controllers return true; } @@ -702,7 +708,7 @@ void HmdDisplayPlugin::compositeExtra() { if (_presentHandPoses[0] == IDENTITY_MATRIX && _presentHandPoses[1] == IDENTITY_MATRIX && !_presentExtraLaser.valid()) { return; } - + render([&](gpu::Batch& batch) { batch.setFramebuffer(_compositeFramebuffer); batch.setModelTransform(Transform()); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index a8eca41077..987d3118f7 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include "RenderableEntityItem.h" @@ -452,6 +453,8 @@ RayToEntityIntersectionResult EntityTreeRenderer::findRayIntersectionWorker(cons void EntityTreeRenderer::connectSignalsToSlots(EntityScriptingInterface* entityScriptingInterface) { + auto hoverOverlayInterface = DependencyManager::get().data(); + connect(this, &EntityTreeRenderer::mousePressOnEntity, entityScriptingInterface, &EntityScriptingInterface::mousePressOnEntity); connect(this, &EntityTreeRenderer::mouseMoveOnEntity, entityScriptingInterface, &EntityScriptingInterface::mouseMoveOnEntity); connect(this, &EntityTreeRenderer::mouseReleaseOnEntity, entityScriptingInterface, &EntityScriptingInterface::mouseReleaseOnEntity); @@ -461,8 +464,12 @@ void EntityTreeRenderer::connectSignalsToSlots(EntityScriptingInterface* entityS connect(this, &EntityTreeRenderer::clickReleaseOnEntity, entityScriptingInterface, &EntityScriptingInterface::clickReleaseOnEntity); connect(this, &EntityTreeRenderer::hoverEnterEntity, entityScriptingInterface, &EntityScriptingInterface::hoverEnterEntity); + connect(this, SIGNAL(hoverEnterEntity(const EntityItemID&, const PointerEvent&)), hoverOverlayInterface, SLOT(createHoverOverlay(const EntityItemID&, const PointerEvent&))); + connect(this, &EntityTreeRenderer::hoverOverEntity, entityScriptingInterface, &EntityScriptingInterface::hoverOverEntity); + connect(this, &EntityTreeRenderer::hoverLeaveEntity, entityScriptingInterface, &EntityScriptingInterface::hoverLeaveEntity); + connect(this, SIGNAL(hoverLeaveEntity(const EntityItemID&, const PointerEvent&)), hoverOverlayInterface, SLOT(destroyHoverOverlay(const EntityItemID&, const PointerEvent&))); connect(this, &EntityTreeRenderer::enterEntity, entityScriptingInterface, &EntityScriptingInterface::enterEntity); connect(this, &EntityTreeRenderer::leaveEntity, entityScriptingInterface, &EntityScriptingInterface::leaveEntity); diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp index 10bd70be13..a3c0d9877e 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp @@ -13,14 +13,13 @@ #include #include #include +#include #include #include #include "EntitiesRendererLogging.h" #include "RenderableParticleEffectEntityItem.h" -#include "untextured_particle_vert.h" -#include "untextured_particle_frag.h" #include "textured_particle_vert.h" #include "textured_particle_frag.h" @@ -29,6 +28,16 @@ class ParticlePayloadData { public: static const size_t VERTEX_PER_PARTICLE = 4; + static uint8_t CUSTOM_PIPELINE_NUMBER; + static render::ShapePipelinePointer shapePipelineFactory(const render::ShapePlumber& plumber, const render::ShapeKey& key); + static void registerShapePipeline() { + if (!CUSTOM_PIPELINE_NUMBER) { + CUSTOM_PIPELINE_NUMBER = render::ShapePipeline::registerCustomShapePipelineFactory(shapePipelineFactory); + } + } + + static std::weak_ptr _texturedPipeline; + template struct InterpolationData { T start; @@ -70,9 +79,6 @@ public: offsetof(ParticlePrimitive, uv), gpu::Stream::PER_INSTANCE); } - void setPipeline(PipelinePointer pipeline) { _pipeline = pipeline; } - const PipelinePointer& getPipeline() const { return _pipeline; } - const Transform& getModelTransform() const { return _modelTransform; } void setModelTransform(const Transform& modelTransform) { _modelTransform = modelTransform; } @@ -90,15 +96,15 @@ public: bool getVisibleFlag() const { return _visibleFlag; } void setVisibleFlag(bool visibleFlag) { _visibleFlag = visibleFlag; } - + void render(RenderArgs* args) const { - assert(_pipeline); gpu::Batch& batch = *args->_batch; - batch.setPipeline(_pipeline); if (_texture) { batch.setResourceTexture(0, _texture); + } else { + batch.setResourceTexture(0, DependencyManager::get()->getWhiteTexture()); } batch.setModelTransform(_modelTransform); @@ -113,7 +119,6 @@ public: protected: Transform _modelTransform; AABox _bound; - PipelinePointer _pipeline; FormatPointer _vertexFormat { std::make_shared() }; BufferPointer _particleBuffer { std::make_shared() }; BufferView _uniformBuffer; @@ -142,23 +147,49 @@ namespace render { payload->render(args); } } + template <> + const ShapeKey shapeGetShapeKey(const ParticlePayloadData::Pointer& payload) { + return render::ShapeKey::Builder().withCustom(ParticlePayloadData::CUSTOM_PIPELINE_NUMBER).withTranslucent().build(); + } } +uint8_t ParticlePayloadData::CUSTOM_PIPELINE_NUMBER = 0; +std::weak_ptr ParticlePayloadData::_texturedPipeline; + +render::ShapePipelinePointer ParticlePayloadData::shapePipelineFactory(const render::ShapePlumber& plumber, const render::ShapeKey& key) { + auto texturedPipeline = _texturedPipeline.lock(); + if (!texturedPipeline) { + auto state = std::make_shared(); + state->setCullMode(gpu::State::CULL_BACK); + state->setDepthTest(true, false, gpu::LESS_EQUAL); + state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + PrepareStencil::testMask(*state); + + auto vertShader = gpu::Shader::createVertex(std::string(textured_particle_vert)); + auto fragShader = gpu::Shader::createPixel(std::string(textured_particle_frag)); + + auto program = gpu::Shader::createProgram(vertShader, fragShader); + _texturedPipeline = texturedPipeline = gpu::Pipeline::create(program, state); + } + + return std::make_shared(texturedPipeline, nullptr, nullptr, nullptr); +} EntityItemPointer RenderableParticleEffectEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { auto entity = std::make_shared(entityID); entity->setProperties(properties); + + // As we create the first ParticuleSystem entity, let s register its special shapePIpeline factory: + ParticlePayloadData::registerShapePipeline(); + return entity; } RenderableParticleEffectEntityItem::RenderableParticleEffectEntityItem(const EntityItemID& entityItemID) : ParticleEffectEntityItem(entityItemID) { - // lazy creation of particle system pipeline - if (!_untexturedPipeline || !_texturedPipeline) { - createPipelines(); - } } bool RenderableParticleEffectEntityItem::addToScene(const EntityItemPointer& self, @@ -167,7 +198,6 @@ bool RenderableParticleEffectEntityItem::addToScene(const EntityItemPointer& sel _scene = scene; _renderItemId = _scene->allocateID(); auto particlePayloadData = std::make_shared(); - particlePayloadData->setPipeline(_untexturedPipeline); auto renderPayload = std::make_shared(particlePayloadData); render::Item::Status::Getters statusGetters; makeEntityItemStatusGetters(getThisPointer(), statusGetters); @@ -276,47 +306,14 @@ void RenderableParticleEffectEntityItem::updateRenderItem() { if (_texture && _texture->isLoaded()) { payload.setTexture(_texture->getGPUTexture()); - payload.setPipeline(_texturedPipeline); } else { payload.setTexture(nullptr); - payload.setPipeline(_untexturedPipeline); } }); _scene->enqueueTransaction(transaction); } -void RenderableParticleEffectEntityItem::createPipelines() { - if (!_untexturedPipeline) { - auto state = std::make_shared(); - state->setCullMode(gpu::State::CULL_BACK); - state->setDepthTest(true, false, gpu::LESS_EQUAL); - state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE, - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - PrepareStencil::testMask(*state); - - auto vertShader = gpu::Shader::createVertex(std::string(untextured_particle_vert)); - auto fragShader = gpu::Shader::createPixel(std::string(untextured_particle_frag)); - - auto program = gpu::Shader::createProgram(vertShader, fragShader); - _untexturedPipeline = gpu::Pipeline::create(program, state); - } - if (!_texturedPipeline) { - auto state = std::make_shared(); - state->setCullMode(gpu::State::CULL_BACK); - state->setDepthTest(true, false, gpu::LESS_EQUAL); - state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE, - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - PrepareStencil::testMask(*state); - - auto vertShader = gpu::Shader::createVertex(std::string(textured_particle_vert)); - auto fragShader = gpu::Shader::createPixel(std::string(textured_particle_frag)); - - auto program = gpu::Shader::createProgram(vertShader, fragShader); - _texturedPipeline = gpu::Pipeline::create(program, state); - } -} - void RenderableParticleEffectEntityItem::notifyBoundChanged() { if (!render::Item::isValidID(_renderItemId)) { return; diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h index 678b582b41..b0d7e1c920 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h @@ -34,16 +34,13 @@ protected: virtual void locationChanged(bool tellPhysics = true) override { EntityItem::locationChanged(tellPhysics); notifyBoundChanged(); } virtual void dimensionsChanged() override { EntityItem::dimensionsChanged(); notifyBoundChanged(); } - void notifyBoundChanged(); + void notifyBoundChanged(); - void createPipelines(); - render::ScenePointer _scene; render::ItemID _renderItemId{ render::Item::INVALID_ITEM_ID }; NetworkTexturePointer _texture; - gpu::PipelinePointer _untexturedPipeline; - gpu::PipelinePointer _texturedPipeline; + }; diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index 88a5d2b873..1d309a8e14 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -59,12 +59,8 @@ #include "EntityEditPacketSender.h" #include "PhysicalEntitySimulation.h" -gpu::PipelinePointer RenderablePolyVoxEntityItem::_pipeline = nullptr; -gpu::PipelinePointer RenderablePolyVoxEntityItem::_wireframePipeline = nullptr; - const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5; - /* A PolyVoxEntity has several interdependent parts: @@ -116,6 +112,10 @@ EntityItemPointer RenderablePolyVoxEntityItem::factory(const EntityItemID& entit EntityItemPointer entity{ new RenderablePolyVoxEntityItem(entityID) }; entity->setProperties(properties); std::static_pointer_cast(entity)->initializePolyVox(); + + // As we create the first Polyvox entity, let's register its special shapePipeline factory: + PolyVoxPayload::registerShapePipeline(); + return entity; } @@ -732,35 +732,6 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { !mesh->getIndexBuffer()._buffer) { return; } - - if (!_pipeline) { - gpu::ShaderPointer vertexShader = gpu::Shader::createVertex(std::string(polyvox_vert)); - gpu::ShaderPointer pixelShader = gpu::Shader::createPixel(std::string(polyvox_frag)); - - gpu::Shader::BindingSet slotBindings; - slotBindings.insert(gpu::Shader::Binding(std::string("materialBuffer"), MATERIAL_GPU_SLOT)); - slotBindings.insert(gpu::Shader::Binding(std::string("xMap"), 0)); - slotBindings.insert(gpu::Shader::Binding(std::string("yMap"), 1)); - slotBindings.insert(gpu::Shader::Binding(std::string("zMap"), 2)); - - gpu::ShaderPointer program = gpu::Shader::createProgram(vertexShader, pixelShader); - gpu::Shader::makeProgram(*program, slotBindings); - - auto state = std::make_shared(); - state->setCullMode(gpu::State::CULL_BACK); - state->setDepthTest(true, true, gpu::LESS_EQUAL); - PrepareStencil::testMaskDrawShape(*state); - - _pipeline = gpu::Pipeline::create(program, state); - - auto wireframeState = std::make_shared(); - wireframeState->setCullMode(gpu::State::CULL_BACK); - wireframeState->setDepthTest(true, true, gpu::LESS_EQUAL); - wireframeState->setFillMode(gpu::State::FILL_LINE); - PrepareStencil::testMaskDrawShape(*wireframeState); - - _wireframePipeline = gpu::Pipeline::create(program, wireframeState); - } if (!_vertexFormat) { auto vf = std::make_shared(); @@ -771,11 +742,6 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { gpu::Batch& batch = *args->_batch; - // Pick correct Pipeline - bool wireframe = (render::ShapeKey(args->_globalShapeKey).isWireframe()); - auto pipeline = (wireframe ? _wireframePipeline : _pipeline); - batch.setPipeline(pipeline); - Transform transform(voxelToWorldMatrix()); batch.setModelTransform(transform); batch.setInputFormat(_vertexFormat); @@ -817,7 +783,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { batch.setResourceTexture(2, DependencyManager::get()->getWhiteTexture()); } - int voxelVolumeSizeLocation = pipeline->getProgram()->getUniforms().findLocation("voxelVolumeSize"); + int voxelVolumeSizeLocation = args->_shapePipeline->pipeline->getProgram()->getUniforms().findLocation("voxelVolumeSize"); batch._glUniform3f(voxelVolumeSizeLocation, voxelVolumeSize.x, voxelVolumeSize.y, voxelVolumeSize.z); batch.drawIndexed(gpu::TRIANGLES, (gpu::uint32)mesh->getNumIndices(), 0); @@ -848,6 +814,48 @@ void RenderablePolyVoxEntityItem::removeFromScene(const EntityItemPointer& self, render::Item::clearID(_myItem); } +uint8_t PolyVoxPayload::CUSTOM_PIPELINE_NUMBER = 0; + +std::shared_ptr PolyVoxPayload::_pipeline; +std::shared_ptr PolyVoxPayload::_wireframePipeline; + +render::ShapePipelinePointer PolyVoxPayload::shapePipelineFactory(const render::ShapePlumber& plumber, const render::ShapeKey& key) { + if (!_pipeline) { + gpu::ShaderPointer vertexShader = gpu::Shader::createVertex(std::string(polyvox_vert)); + gpu::ShaderPointer pixelShader = gpu::Shader::createPixel(std::string(polyvox_frag)); + + gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("materialBuffer"), PolyVoxPayload::MATERIAL_GPU_SLOT)); + slotBindings.insert(gpu::Shader::Binding(std::string("xMap"), 0)); + slotBindings.insert(gpu::Shader::Binding(std::string("yMap"), 1)); + slotBindings.insert(gpu::Shader::Binding(std::string("zMap"), 2)); + + gpu::ShaderPointer program = gpu::Shader::createProgram(vertexShader, pixelShader); + gpu::Shader::makeProgram(*program, slotBindings); + + auto state = std::make_shared(); + state->setCullMode(gpu::State::CULL_BACK); + state->setDepthTest(true, true, gpu::LESS_EQUAL); + PrepareStencil::testMaskDrawShape(*state); + + _pipeline = gpu::Pipeline::create(program, state); + + auto wireframeState = std::make_shared(); + wireframeState->setCullMode(gpu::State::CULL_BACK); + wireframeState->setDepthTest(true, true, gpu::LESS_EQUAL); + wireframeState->setFillMode(gpu::State::FILL_LINE); + PrepareStencil::testMaskDrawShape(*wireframeState); + + _wireframePipeline = gpu::Pipeline::create(program, wireframeState); + } + + if (key.isWireframe()) { + return std::make_shared(_wireframePipeline, nullptr, nullptr, nullptr); + } else { + return std::make_shared(_pipeline, nullptr, nullptr, nullptr); + } +} + namespace render { template <> const ItemKey payloadGetKey(const PolyVoxPayload::Pointer& payload) { return ItemKey::Builder::opaqueShape(); @@ -871,6 +879,10 @@ namespace render { payload->_owner->getRenderableInterface()->render(args); } } + + template <> const ShapeKey shapeGetShapeKey(const PolyVoxPayload::Pointer& payload) { + return ShapeKey::Builder().withCustom(PolyVoxPayload::CUSTOM_PIPELINE_NUMBER).build(); + } } @@ -1619,7 +1631,7 @@ void RenderablePolyVoxEntityItem::bonkNeighbors() { void RenderablePolyVoxEntityItem::locationChanged(bool tellPhysics) { EntityItem::locationChanged(tellPhysics); - if (!_pipeline || !render::Item::isValidID(_myItem)) { + if (!render::Item::isValidID(_myItem)) { return; } render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h index 45625ada6d..8f20a7a298 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h @@ -28,6 +28,19 @@ class PolyVoxPayload { public: + + static uint8_t CUSTOM_PIPELINE_NUMBER; + static render::ShapePipelinePointer shapePipelineFactory(const render::ShapePlumber& plumber, const render::ShapeKey& key); + static void registerShapePipeline() { + if (!CUSTOM_PIPELINE_NUMBER) { + CUSTOM_PIPELINE_NUMBER = render::ShapePipeline::registerCustomShapePipelineFactory(shapePipelineFactory); + } + } + + static const int MATERIAL_GPU_SLOT = 3; + static std::shared_ptr _pipeline; + static std::shared_ptr _wireframePipeline; + PolyVoxPayload(EntityItemPointer owner) : _owner(owner), _bounds(AABox()) { } typedef render::Payload Payload; typedef Payload::DataPointer Pointer; @@ -40,6 +53,7 @@ namespace render { template <> const ItemKey payloadGetKey(const PolyVoxPayload::Pointer& payload); template <> const Item::Bound payloadGetBound(const PolyVoxPayload::Pointer& payload); template <> void payloadRender(const PolyVoxPayload::Pointer& payload, RenderArgs* args); + template <> const ShapeKey shapeGetShapeKey(const PolyVoxPayload::Pointer& payload); } @@ -168,10 +182,7 @@ private: NetworkTexturePointer _yTexture; NetworkTexturePointer _zTexture; - const int MATERIAL_GPU_SLOT = 3; render::ItemID _myItem{ render::Item::INVALID_ITEM_ID }; - static gpu::PipelinePointer _pipeline; - static gpu::PipelinePointer _wireframePipeline; ShapeInfo _shapeInfo; diff --git a/libraries/entities-renderer/src/untextured_particle.slf b/libraries/entities-renderer/src/untextured_particle.slf deleted file mode 100644 index 11f25bb693..0000000000 --- a/libraries/entities-renderer/src/untextured_particle.slf +++ /dev/null @@ -1,18 +0,0 @@ -<@include gpu/Config.slh@> -<$VERSION_HEADER$> -// Generated on <$_SCRIBE_DATE$> -// fragment shader -// -// Copyright 2015 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 -// - -in vec4 _color; - -out vec4 outFragColor; - -void main(void) { - outFragColor = _color; -} diff --git a/libraries/entities-renderer/src/untextured_particle.slv b/libraries/entities-renderer/src/untextured_particle.slv deleted file mode 100644 index 85f9d438bf..0000000000 --- a/libraries/entities-renderer/src/untextured_particle.slv +++ /dev/null @@ -1,25 +0,0 @@ -<@include gpu/Config.slh@> -<$VERSION_HEADER$> -// Generated on <$_SCRIBE_DATE$> -// -// particle vertex shader -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -<@include gpu/Inputs.slh@> -<@include gpu/Color.slh@> -<@include gpu/Transform.slh@> -<$declareStandardTransform()$> - -out vec4 _color; - -void main(void) { - // pass along the color - _color = colorToLinearRGBA(inColor); - - TransformCamera cam = getTransformCamera(); - TransformObject obj = getTransformObject(); - <$transformModelToClipPos(cam, obj, inPosition, gl_Position)$> -} \ No newline at end of file diff --git a/libraries/entities/src/AddEntityOperator.cpp b/libraries/entities/src/AddEntityOperator.cpp index e86e70dd80..78d986f538 100644 --- a/libraries/entities/src/AddEntityOperator.cpp +++ b/libraries/entities/src/AddEntityOperator.cpp @@ -46,10 +46,8 @@ bool AddEntityOperator::preRecursion(const OctreeElementPointer& element) { // If this element is the best fit for the new entity properties, then add/or update it if (entityTreeElement->bestFitBounds(_newEntityBox)) { - + _tree->addEntityMapEntry(_newEntity); entityTreeElement->addEntityItem(_newEntity); - _tree->setContainingElement(_newEntity->getEntityItemID(), entityTreeElement); - _foundNew = true; keepSearching = false; } else { diff --git a/libraries/entities/src/DeleteEntityOperator.cpp b/libraries/entities/src/DeleteEntityOperator.cpp index 709c281341..cbd0ad7839 100644 --- a/libraries/entities/src/DeleteEntityOperator.cpp +++ b/libraries/entities/src/DeleteEntityOperator.cpp @@ -96,7 +96,7 @@ bool DeleteEntityOperator::preRecursion(const OctreeElementPointer& element) { bool entityDeleted = entityTreeElement->removeEntityItem(theEntity); // remove it from the element assert(entityDeleted); (void)entityDeleted; // quite warning - _tree->setContainingElement(details.entity->getEntityItemID(), NULL); // update or id to element lookup + _tree->clearEntityMapEntry(details.entity->getEntityItemID()); _foundCount++; } } diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 23ce097cc2..5996327e87 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -89,7 +89,8 @@ EntityItem::EntityItem(const EntityItemID& entityItemID) : EntityItem::~EntityItem() { // clear out any left-over actions - EntityTreePointer entityTree = _element ? _element->getTree() : nullptr; + EntityTreeElementPointer element = _element; // use local copy of _element for logic below + EntityTreePointer entityTree = element ? element->getTree() : nullptr; EntitySimulationPointer simulation = entityTree ? entityTree->getSimulation() : nullptr; if (simulation) { clearActions(simulation); @@ -880,8 +881,9 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef // Tracking for editing roundtrips here. We will tell our EntityTree that we just got incoming data about // and entity that was edited at some time in the past. The tree will determine how it wants to track this // information. - if (_element && _element->getTree()) { - _element->getTree()->trackIncomingEntityLastEdited(lastEditedFromBufferAdjusted, bytesRead); + EntityTreeElementPointer element = _element; // use local copy of _element for logic below + if (element && element->getTree()) { + element->getTree()->trackIncomingEntityLastEdited(lastEditedFromBufferAdjusted, bytesRead); } @@ -2056,7 +2058,8 @@ bool EntityItem::removeActionInternal(const QUuid& actionID, EntitySimulationPoi _previouslyDeletedActions.insert(actionID, usecTimestampNow()); if (_objectActions.contains(actionID)) { if (!simulation) { - EntityTreePointer entityTree = _element ? _element->getTree() : nullptr; + EntityTreeElementPointer element = _element; // use local copy of _element for logic below + EntityTreePointer entityTree = element ? element->getTree() : nullptr; simulation = entityTree ? entityTree->getSimulation() : nullptr; } diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index f22631d363..2cefd647cb 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include "EntityItemID.h" #include "EntitiesLogging.h" @@ -499,6 +500,11 @@ void EntityScriptingInterface::deleteEntity(QUuid id) { const QUuid myNodeID = nodeList->getSessionUUID(); if (entity->getClientOnly() && entity->getOwningAvatarID() != myNodeID) { // don't delete other avatar's avatarEntities + // If you actually own the entity but the onwership property is not set because of a domain switch + // The lines below makes sure the entity is deleted once its properties are set. + auto avatarHashMap = DependencyManager::get(); + AvatarSharedPointer myAvatar = avatarHashMap->getAvatarBySessionID(myNodeID); + myAvatar->insertDetachedEntityID(id); shouldDelete = false; return; } diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 4773f45af7..5e58736477 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -90,13 +90,17 @@ void EntityTree::eraseAllOctreeElements(bool createNewRoot) { if (_simulation) { _simulation->clearEntities(); } - { - QWriteLocker locker(&_entityToElementLock); - foreach(EntityTreeElementPointer element, _entityToElementMap) { - element->cleanupEntities(); + QHash localMap; + localMap.swap(_entityMap); + this->withWriteLock([&] { + foreach(EntityItemPointer entity, localMap) { + EntityTreeElementPointer element = entity->getElement(); + if (element) { + element->cleanupEntities(); + } } - _entityToElementMap.clear(); - } + }); + localMap.clear(); Octree::eraseAllOctreeElements(createNewRoot); resetClientEditStats(); @@ -136,29 +140,24 @@ void EntityTree::postAddEntity(EntityItemPointer entity) { } bool EntityTree::updateEntity(const EntityItemID& entityID, const EntityItemProperties& properties, const SharedNodePointer& senderNode) { - EntityTreeElementPointer containingElement = getContainingElement(entityID); + EntityItemPointer entity; + { + QReadLocker locker(&_entityMapLock); + entity = _entityMap.value(entityID); + } + if (!entity) { + return false; + } + return updateEntity(entity, properties, senderNode); +} + +bool EntityTree::updateEntity(EntityItemPointer entity, const EntityItemProperties& origProperties, + const SharedNodePointer& senderNode) { + EntityTreeElementPointer containingElement = entity->getElement(); if (!containingElement) { return false; } - EntityItemPointer existingEntity = containingElement->getEntityWithEntityItemID(entityID); - if (!existingEntity) { - return false; - } - - return updateEntityWithElement(existingEntity, properties, containingElement, senderNode); -} - -bool EntityTree::updateEntity(EntityItemPointer entity, const EntityItemProperties& properties, const SharedNodePointer& senderNode) { - EntityTreeElementPointer containingElement = getContainingElement(entity->getEntityItemID()); - if (!containingElement) { - return false; - } - return updateEntityWithElement(entity, properties, containingElement, senderNode); -} - -bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityItemProperties& origProperties, - EntityTreeElementPointer containingElement, const SharedNodePointer& senderNode) { EntityItemProperties properties = origProperties; bool allowLockChange; @@ -190,7 +189,7 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI bool success; AACube queryCube = entity->getQueryAACube(success); if (!success) { - qCDebug(entities) << "Warning -- failed to get query-cube for" << entity->getID(); + qCWarning(entities) << "failed to get query-cube for" << entity->getID(); } UpdateEntityOperator theOperator(getThisPointer(), containingElement, entity, queryCube); recurseTreeWithOperator(&theOperator); @@ -331,9 +330,8 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI } // TODO: this final containingElement check should eventually be removed (or wrapped in an #ifdef DEBUG). - containingElement = getContainingElement(entity->getEntityItemID()); - if (!containingElement) { - qCDebug(entities) << "UNEXPECTED!!!! after updateEntity() we no longer have a containing element??? entityID=" + if (!entity->getElement()) { + qCWarning(entities) << "EntityTree::updateEntity() we no longer have a containing element for entityID=" << entity->getEntityItemID(); return false; } @@ -366,7 +364,7 @@ EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const Enti // You should not call this on existing entities that are already part of the tree! Call updateEntity() EntityTreeElementPointer containingElement = getContainingElement(entityID); if (containingElement) { - qCDebug(entities) << "UNEXPECTED!!! ----- don't call addEntity() on existing entity items. entityID=" << entityID + qCWarning(entities) << "EntityTree::addEntity() on existing entity item with entityID=" << entityID << "containingElement=" << containingElement.get(); return result; } @@ -422,7 +420,7 @@ void EntityTree::deleteEntity(const EntityItemID& entityID, bool force, bool ign EntityTreeElementPointer containingElement = getContainingElement(entityID); if (!containingElement) { if (!ignoreWarnings) { - qCDebug(entities) << "UNEXPECTED!!!! EntityTree::deleteEntity() entityID doesn't exist!!! entityID=" << entityID; + qCWarning(entities) << "EntityTree::deleteEntity() on non-existent entityID=" << entityID; } return; } @@ -430,8 +428,7 @@ void EntityTree::deleteEntity(const EntityItemID& entityID, bool force, bool ign EntityItemPointer existingEntity = containingElement->getEntityWithEntityItemID(entityID); if (!existingEntity) { if (!ignoreWarnings) { - qCDebug(entities) << "UNEXPECTED!!!! don't call EntityTree::deleteEntity() on entity items that don't exist. " - "entityID=" << entityID; + qCWarning(entities) << "EntityTree::deleteEntity() on non-existant entity item with entityID=" << entityID; } return; } @@ -478,7 +475,7 @@ void EntityTree::deleteEntities(QSet entityIDs, bool force, bool i EntityTreeElementPointer containingElement = getContainingElement(entityID); if (!containingElement) { if (!ignoreWarnings) { - qCDebug(entities) << "UNEXPECTED!!!! EntityTree::deleteEntities() entityID doesn't exist!!! entityID=" << entityID; + qCWarning(entities) << "EntityTree::deleteEntities() on non-existent entityID=" << entityID; } continue; } @@ -486,8 +483,7 @@ void EntityTree::deleteEntities(QSet entityIDs, bool force, bool i EntityItemPointer existingEntity = containingElement->getEntityWithEntityItemID(entityID); if (!existingEntity) { if (!ignoreWarnings) { - qCDebug(entities) << "UNEXPECTED!!!! don't call EntityTree::deleteEntities() on entity items that don't exist. " - "entityID=" << entityID; + qCWarning(entities) << "EntityTree::deleteEntities() on non-existent entity item with entityID=" << entityID; } continue; } @@ -975,7 +971,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c const SharedNodePointer& senderNode) { if (!getIsServer()) { - qCDebug(entities) << "UNEXPECTED!!! processEditPacketData() should only be called on a server tree."; + qCWarning(entities) << "EntityTree::processEditPacketData() should only be called on a server tree."; return 0; } @@ -1502,27 +1498,43 @@ int EntityTree::processEraseMessageDetails(const QByteArray& dataByteArray, cons } EntityTreeElementPointer EntityTree::getContainingElement(const EntityItemID& entityItemID) /*const*/ { - QReadLocker locker(&_entityToElementLock); - EntityTreeElementPointer element = _entityToElementMap.value(entityItemID); - return element; + EntityItemPointer entity; + { + QReadLocker locker(&_entityMapLock); + entity = _entityMap.value(entityItemID); + } + if (entity) { + return entity->getElement(); + } + return EntityTreeElementPointer(nullptr); } -void EntityTree::setContainingElement(const EntityItemID& entityItemID, EntityTreeElementPointer element) { - QWriteLocker locker(&_entityToElementLock); - if (element) { - _entityToElementMap[entityItemID] = element; - } else { - _entityToElementMap.remove(entityItemID); +void EntityTree::addEntityMapEntry(EntityItemPointer entity) { + EntityItemID id = entity->getEntityItemID(); + QWriteLocker locker(&_entityMapLock); + EntityItemPointer otherEntity = _entityMap.value(id); + if (otherEntity) { + qCWarning(entities) << "EntityTree::addEntityMapEntry() found pre-existing id " << id; + assert(false); + return; } + _entityMap.insert(id, entity); +} + +void EntityTree::clearEntityMapEntry(const EntityItemID& id) { + QWriteLocker locker(&_entityMapLock); + _entityMap.remove(id); } void EntityTree::debugDumpMap() { + // QHash's are implicitly shared, so we make a shared copy and use that instead. + // This way we might be able to avoid both a lock and a true copy. + QHash localMap(_entityMap); qCDebug(entities) << "EntityTree::debugDumpMap() --------------------------"; - QReadLocker locker(&_entityToElementLock); - QHashIterator i(_entityToElementMap); + QHashIterator i(localMap); while (i.hasNext()) { i.next(); - qCDebug(entities) << i.key() << ": " << i.value().get(); + qCDebug(entities) << i.key() << ": " << i.value()->getElement().get(); } qCDebug(entities) << "-----------------------------------------------------"; } diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 24e6c364b1..efb8cf81ba 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -119,9 +119,6 @@ public: // use this method if you only know the entityID bool updateEntity(const EntityItemID& entityID, const EntityItemProperties& properties, const SharedNodePointer& senderNode = SharedNodePointer(nullptr)); - // use this method if you have a pointer to the entity (avoid an extra entity lookup) - bool updateEntity(EntityItemPointer entity, const EntityItemProperties& properties, const SharedNodePointer& senderNode = SharedNodePointer(nullptr)); - // check if the avatar is a child of this entity, If so set the avatar parentID to null void unhookChildAvatar(const EntityItemID entityID); void deleteEntity(const EntityItemID& entityID, bool force = false, bool ignoreWarnings = true); @@ -183,7 +180,8 @@ public: int processEraseMessageDetails(const QByteArray& buffer, const SharedNodePointer& sourceNode); EntityTreeElementPointer getContainingElement(const EntityItemID& entityItemID) /*const*/; - void setContainingElement(const EntityItemID& entityItemID, EntityTreeElementPointer element); + void addEntityMapEntry(EntityItemPointer entity); + void clearEntityMapEntry(const EntityItemID& id); void debugDumpMap(); virtual void dumpTree() override; virtual void pruneTree() override; @@ -275,9 +273,8 @@ signals: protected: void processRemovedEntities(const DeleteEntityOperator& theOperator); - bool updateEntityWithElement(EntityItemPointer entity, const EntityItemProperties& properties, - EntityTreeElementPointer containingElement, - const SharedNodePointer& senderNode = SharedNodePointer(nullptr)); + bool updateEntity(EntityItemPointer entity, const EntityItemProperties& properties, + const SharedNodePointer& senderNode = SharedNodePointer(nullptr)); static bool findNearPointOperation(const OctreeElementPointer& element, void* extraData); static bool findInSphereOperation(const OctreeElementPointer& element, void* extraData); static bool findInCubeOperation(const OctreeElementPointer& element, void* extraData); @@ -309,8 +306,8 @@ protected: _deletedEntityItemIDs << id; } - mutable QReadWriteLock _entityToElementLock; - QHash _entityToElementMap; + mutable QReadWriteLock _entityMapLock; + QHash _entityMap; EntitySimulationPointer _simulation; diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp index cce7ee006f..108cb39222 100644 --- a/libraries/entities/src/EntityTreeElement.cpp +++ b/libraries/entities/src/EntityTreeElement.cpp @@ -885,10 +885,10 @@ EntityItemPointer EntityTreeElement::getEntityWithEntityItemID(const EntityItemI void EntityTreeElement::cleanupEntities() { withWriteLock([&] { foreach(EntityItemPointer entity, _entityItems) { + // NOTE: only EntityTreeElement should ever be changing the value of entity->_element // NOTE: We explicitly don't delete the EntityItem here because since we only // access it by smart pointers, when we remove it from the _entityItems // we know that it will be deleted. - //delete entity; entity->_element = NULL; } _entityItems.clear(); @@ -903,6 +903,7 @@ bool EntityTreeElement::removeEntityWithEntityItemID(const EntityItemID& id) { EntityItemPointer& entity = _entityItems[i]; if (entity->getEntityItemID() == id) { foundEntity = true; + // NOTE: only EntityTreeElement should ever be changing the value of entity->_element entity->_element = NULL; _entityItems.removeAt(i); break; @@ -918,6 +919,7 @@ bool EntityTreeElement::removeEntityItem(EntityItemPointer entity) { numEntries = _entityItems.removeAll(entity); }); if (numEntries > 0) { + // NOTE: only EntityTreeElement should ever be changing the value of entity->_element assert(entity->_element.get() == this); entity->_element = NULL; return true; @@ -1001,7 +1003,6 @@ int EntityTreeElement::readElementDataFromBuffer(const unsigned char* data, int if (currentContainingElement.get() != this) { currentContainingElement->removeEntityItem(entityItem); addEntityItem(entityItem); - _myTree->setContainingElement(entityItemID, getThisPointer()); } } } @@ -1032,9 +1033,9 @@ int EntityTreeElement::readElementDataFromBuffer(const unsigned char* data, int // don't add if we've recently deleted.... if (!_myTree->isDeletedEntity(entityItem->getID())) { + _myTree->addEntityMapEntry(entityItem); addEntityItem(entityItem); // add this new entity to this elements entities entityItemID = entityItem->getEntityItemID(); - _myTree->setContainingElement(entityItemID, getThisPointer()); _myTree->postAddEntity(entityItem); if (entityItem->getCreated() == UNKNOWN_CREATED_TIME) { entityItem->recordCreationTime(); diff --git a/libraries/entities/src/HoverOverlayInterface.cpp b/libraries/entities/src/HoverOverlayInterface.cpp new file mode 100644 index 0000000000..dcfde41e39 --- /dev/null +++ b/libraries/entities/src/HoverOverlayInterface.cpp @@ -0,0 +1,38 @@ +// +// HoverOverlayInterface.cpp +// libraries/entities/src +// +// Created by Zach Fox on 2017-07-14. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "HoverOverlayInterface.h" + +HoverOverlayInterface::HoverOverlayInterface() { + // "hover_overlay" debug log category disabled by default. + // Create your own "qtlogging.ini" file and set your "QT_LOGGING_CONF" environment variable + // if you'd like to enable/disable certain categories. + // More details: http://doc.qt.io/qt-5/qloggingcategory.html#configuring-categories + QLoggingCategory::setFilterRules(QStringLiteral("hifi.hover_overlay.debug=false")); +} + +void HoverOverlayInterface::createHoverOverlay(const EntityItemID& entityItemID, const PointerEvent& event) { + qCDebug(hover_overlay) << "Creating Hover Overlay on top of entity with ID: " << entityItemID; + setCurrentHoveredEntity(entityItemID); +} + +void HoverOverlayInterface::createHoverOverlay(const EntityItemID& entityItemID) { + HoverOverlayInterface::createHoverOverlay(entityItemID, PointerEvent()); +} + +void HoverOverlayInterface::destroyHoverOverlay(const EntityItemID& entityItemID, const PointerEvent& event) { + qCDebug(hover_overlay) << "Destroying Hover Overlay on top of entity with ID: " << entityItemID; + setCurrentHoveredEntity(QUuid()); +} + +void HoverOverlayInterface::destroyHoverOverlay(const EntityItemID& entityItemID) { + HoverOverlayInterface::destroyHoverOverlay(entityItemID, PointerEvent()); +} diff --git a/libraries/entities/src/HoverOverlayInterface.h b/libraries/entities/src/HoverOverlayInterface.h new file mode 100644 index 0000000000..a39faab819 --- /dev/null +++ b/libraries/entities/src/HoverOverlayInterface.h @@ -0,0 +1,49 @@ +// +// HoverOverlayInterface.h +// libraries/entities/src +// +// Created by Zach Fox on 2017-07-14. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once +#ifndef hifi_HoverOverlayInterface_h +#define hifi_HoverOverlayInterface_h + +#include +#include + +#include +#include + +#include "EntityTree.h" +#include "HoverOverlayLogging.h" + +/**jsdoc +* @namespace HoverOverlay +*/ +class HoverOverlayInterface : public QObject, public Dependency { + Q_OBJECT + + Q_PROPERTY(QUuid currentHoveredEntity READ getCurrentHoveredEntity WRITE setCurrentHoveredEntity) +public: + HoverOverlayInterface(); + + Q_INVOKABLE QUuid getCurrentHoveredEntity() { return _currentHoveredEntity; } + void setCurrentHoveredEntity(const QUuid& entityID) { _currentHoveredEntity = entityID; } + +public slots: + void createHoverOverlay(const EntityItemID& entityItemID, const PointerEvent& event); + void createHoverOverlay(const EntityItemID& entityItemID); + void destroyHoverOverlay(const EntityItemID& entityItemID, const PointerEvent& event); + void destroyHoverOverlay(const EntityItemID& entityItemID); + +private: + bool _verboseLogging { true }; + QUuid _currentHoveredEntity{}; +}; + +#endif // hifi_HoverOverlayInterface_h diff --git a/libraries/entities/src/HoverOverlayLogging.cpp b/libraries/entities/src/HoverOverlayLogging.cpp new file mode 100644 index 0000000000..99a2dff782 --- /dev/null +++ b/libraries/entities/src/HoverOverlayLogging.cpp @@ -0,0 +1,14 @@ +// +// HoverOverlayLogging.cpp +// libraries/entities/src +// +// Created by Zach Fox on 2017-07-17 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "HoverOverlayLogging.h" + +Q_LOGGING_CATEGORY(hover_overlay, "hifi.hover_overlay") diff --git a/libraries/entities/src/HoverOverlayLogging.h b/libraries/entities/src/HoverOverlayLogging.h new file mode 100644 index 0000000000..f0a024bba9 --- /dev/null +++ b/libraries/entities/src/HoverOverlayLogging.h @@ -0,0 +1,20 @@ +// +// HoverOverlayLogging.h +// libraries/entities/src +// +// Created by Zach Fox on 2017-07-17 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once +#ifndef hifi_HoverOverlayLogging_h +#define hifi_HoverOverlayLogging_h + +#include + +Q_DECLARE_LOGGING_CATEGORY(hover_overlay) + +#endif // hifi_HoverOverlayLogging_h diff --git a/libraries/entities/src/MovingEntitiesOperator.cpp b/libraries/entities/src/MovingEntitiesOperator.cpp index ab97c67aa2..42e5a2ece5 100644 --- a/libraries/entities/src/MovingEntitiesOperator.cpp +++ b/libraries/entities/src/MovingEntitiesOperator.cpp @@ -51,7 +51,7 @@ MovingEntitiesOperator::~MovingEntitiesOperator() { void MovingEntitiesOperator::addEntityToMoveList(EntityItemPointer entity, const AACube& newCube) { - EntityTreeElementPointer oldContainingElement = _tree->getContainingElement(entity->getEntityItemID()); + EntityTreeElementPointer oldContainingElement = entity->getElement(); AABox newCubeClamped = newCube.clamp((float)-HALF_TREE_SCALE, (float)HALF_TREE_SCALE); if (_wantDebug) { @@ -193,7 +193,6 @@ bool MovingEntitiesOperator::preRecursion(const OctreeElementPointer& element) { // If this element is the best fit for the new bounds of this entity then add the entity to the element if (!details.newFound && entityTreeElement->bestFitBounds(details.newCube)) { - EntityItemID entityItemID = details.entity->getEntityItemID(); // remove from the old before adding EntityTreeElementPointer oldElement = details.entity->getElement(); if (oldElement != entityTreeElement) { @@ -201,7 +200,6 @@ bool MovingEntitiesOperator::preRecursion(const OctreeElementPointer& element) { oldElement->removeEntityItem(details.entity); } entityTreeElement->addEntityItem(details.entity); - _tree->setContainingElement(entityItemID, entityTreeElement); } _foundNewCount++; //details.newFound = true; // TODO: would be nice to add this optimization diff --git a/libraries/entities/src/UpdateEntityOperator.cpp b/libraries/entities/src/UpdateEntityOperator.cpp index ec6051af04..7a5c87187a 100644 --- a/libraries/entities/src/UpdateEntityOperator.cpp +++ b/libraries/entities/src/UpdateEntityOperator.cpp @@ -138,8 +138,8 @@ bool UpdateEntityOperator::preRecursion(const OctreeElementPointer& element) { qCDebug(entities) << " _foundNew=" << _foundNew; } - // If we haven't yet found the old entity, and this subTreeContains our old - // entity, then we need to keep searching. + // If we haven't yet found the old element, and this subTreeContains our old element, + // then we need to keep searching. if (!_foundOld && subtreeContainsOld) { if (_wantDebug) { @@ -169,7 +169,6 @@ bool UpdateEntityOperator::preRecursion(const OctreeElementPointer& element) { // NOTE: we know we haven't yet added it to its new element because _removeOld is true EntityTreeElementPointer oldElement = _existingEntity->getElement(); oldElement->removeEntityItem(_existingEntity); - _tree->setContainingElement(_entityItemID, NULL); if (oldElement != _containingElement) { qCDebug(entities) << "WARNING entity moved during UpdateEntityOperator recursion"; @@ -187,8 +186,8 @@ bool UpdateEntityOperator::preRecursion(const OctreeElementPointer& element) { } } - // If we haven't yet found the new entity, and this subTreeContains our new - // entity, then we need to keep searching. + // If we haven't yet found the new element, and this subTreeContains our new element, + // then we need to keep searching. if (!_foundNew && subtreeContainsNew) { if (_wantDebug) { @@ -221,7 +220,6 @@ bool UpdateEntityOperator::preRecursion(const OctreeElementPointer& element) { } } entityTreeElement->addEntityItem(_existingEntity); - _tree->setContainingElement(_entityItemID, entityTreeElement); } _foundNew = true; // we found the new element _removeOld = false; // and it has already been removed from the old diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index 417901b9ab..a171f92907 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -123,6 +123,34 @@ glm::vec3 OBJTokenizer::getVec3() { } return v; } +bool OBJTokenizer::getVertex(glm::vec3& vertex, glm::vec3& vertexColor) { + // Used for vertices which may also have a vertex color (RGB [0,1]) to follow. + // NOTE: Returns true if there is a vertex color. + auto x = getFloat(); // N.B.: getFloat() has side-effect + auto y = getFloat(); // And order of arguments is different on Windows/Linux. + auto z = getFloat(); + vertex = glm::vec3(x, y, z); + + auto r = 1.0f, g = 1.0f, b = 1.0f; + bool hasVertexColor = false; + if (isNextTokenFloat()) { + // If there's another float it's one of two things: a W component or an R component. The standard OBJ spec + // doesn't output a W component, so we're making the assumption that if a float follows (unless it's + // only a single value) that it's a vertex color. + r = getFloat(); + if (isNextTokenFloat()) { + // Safe to assume the following values are the green/blue components. + g = getFloat(); + b = getFloat(); + + hasVertexColor = true; + } + + vertexColor = glm::vec3(r, g, b); + } + + return hasVertexColor; +} glm::vec2 OBJTokenizer::getVec2() { float uCoord = getFloat(); float vCoord = 1.0f - getFloat(); @@ -140,7 +168,9 @@ void setMeshPartDefaults(FBXMeshPart& meshPart, QString materialID) { } // OBJFace -bool OBJFace::add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex, const QVector& vertices) { +// NOTE (trent, 7/20/17): The vertexColors vector being passed-in isn't necessary here, but I'm just +// pairing it with the vertices vector for consistency. +bool OBJFace::add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex, const QVector& vertices, const QVector& vertexColors) { bool ok; int index = vertexIndex.toInt(&ok); if (!ok) { @@ -382,7 +412,14 @@ bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mappi #endif } } else if (token == "v") { - vertices.append(tokenizer.getVec3()); + glm::vec3 vertex, vertexColor; + + bool hasVertexColor = tokenizer.getVertex(vertex, vertexColor); + vertices.append(vertex); + + if(hasVertexColor) { + vertexColors.append(vertexColor); + } } else if (token == "vn") { normals.append(tokenizer.getVec3()); } else if (token == "vt") { @@ -410,7 +447,8 @@ bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mappi assert(parts.count() >= 1); assert(parts.count() <= 3); const QByteArray noData {}; - face.add(parts[0], (parts.count() > 1) ? parts[1] : noData, (parts.count() > 2) ? parts[2] : noData, vertices); + face.add(parts[0], (parts.count() > 1) ? parts[1] : noData, (parts.count() > 2) ? parts[2] : noData, + vertices, vertexColors); face.groupName = currentGroup; face.materialName = currentMaterialName; } @@ -540,6 +578,15 @@ FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, glm::vec3 v1 = checked_at(vertices, face.vertexIndices[1]); glm::vec3 v2 = checked_at(vertices, face.vertexIndices[2]); + glm::vec3 vc0, vc1, vc2; + bool hasVertexColors = (vertexColors.size() > 0); + if (hasVertexColors) { + // If there are any vertex colors, it's safe to assume all meshes had them exported. + vc0 = checked_at(vertexColors, face.vertexIndices[0]); + vc1 = checked_at(vertexColors, face.vertexIndices[1]); + vc2 = checked_at(vertexColors, face.vertexIndices[2]); + } + // Scale the vertices if the OBJ file scale is specified as non-one. if (scaleGuess != 1.0f) { v0 *= scaleGuess; @@ -555,6 +602,13 @@ FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, meshPart.triangleIndices.append(mesh.vertices.count()); mesh.vertices << v2; + if (hasVertexColors) { + // Add vertex colors. + mesh.colors << vc0; + mesh.colors << vc1; + mesh.colors << vc2; + } + glm::vec3 n0, n1, n2; if (face.normalIndices.count()) { n0 = checked_at(normals, face.normalIndices[0]); @@ -690,6 +744,7 @@ void fbxDebugDump(const FBXGeometry& fbxgeo) { qCDebug(modelformat) << " meshes.count() =" << fbxgeo.meshes.count(); foreach (FBXMesh mesh, fbxgeo.meshes) { qCDebug(modelformat) << " vertices.count() =" << mesh.vertices.count(); + qCDebug(modelformat) << " colors.count() =" << mesh.colors.count(); qCDebug(modelformat) << " normals.count() =" << mesh.normals.count(); /*if (mesh.normals.count() == mesh.vertices.count()) { for (int i = 0; i < mesh.normals.count(); i++) { diff --git a/libraries/fbx/src/OBJReader.h b/libraries/fbx/src/OBJReader.h index 18a4b89f1e..9a32871590 100644 --- a/libraries/fbx/src/OBJReader.h +++ b/libraries/fbx/src/OBJReader.h @@ -20,6 +20,7 @@ public: void ungetChar(char ch) { _device->ungetChar(ch); } const QString getComment() const { return _comment; } glm::vec3 getVec3(); + bool getVertex(glm::vec3& vertex, glm::vec3& vertexColor); glm::vec2 getVec2(); float getFloat() { return std::stof((nextToken() != OBJTokenizer::DATUM_TOKEN) ? nullptr : getDatum().data()); } @@ -38,7 +39,8 @@ public: QString groupName; // We don't make use of hierarchical structure, but it can be preserved for debugging and future use. QString materialName; // Add one more set of vertex data. Answers true if successful - bool add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex, const QVector& vertices); + bool add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex, + const QVector& vertices, const QVector& vertexColors); // Return a set of one or more OBJFaces from this one, in which each is just a triangle. // Even though FBXMeshPart can handle quads, it would be messy to try to keep track of mixed-size faces, so we treat everything as triangles. QVector triangulate(); @@ -65,7 +67,8 @@ class OBJReader: public QObject { // QObject so we can make network requests. Q_OBJECT public: typedef QVector FaceGroup; - QVector vertices; // all that we ever encounter while reading + QVector vertices; + QVector vertexColors; QVector textureUVs; QVector normals; QVector faceGroups; diff --git a/libraries/gl/src/gl/GLShaders.cpp b/libraries/gl/src/gl/GLShaders.cpp index fd0c6788cb..8ef0198676 100644 --- a/libraries/gl/src/gl/GLShaders.cpp +++ b/libraries/gl/src/gl/GLShaders.cpp @@ -182,7 +182,6 @@ GLuint compileProgram(const std::vector& glshaders, std::string& error) filestream.close(); } */ - delete[] temp; glDeleteProgram(glprogram); return 0; diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp index 20749cd567..85bde4c2f1 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.cpp +++ b/libraries/model-networking/src/model-networking/TextureCache.cpp @@ -54,6 +54,7 @@ const std::string TextureCache::KTX_EXT { "ktx" }; static const QString RESOURCE_SCHEME = "resource"; static const QUrl SPECTATOR_CAMERA_FRAME_URL("resource://spectatorCameraFrame"); +static const QUrl HMD_PREVIEW_FRAME_URL("resource://hmdPreviewFrame"); static const float SKYBOX_LOAD_PRIORITY { 10.0f }; // Make sure skybox loads first static const float HIGH_MIPS_LOAD_PRIORITY { 9.0f }; // Make sure high mips loads after skybox but before models @@ -219,7 +220,7 @@ gpu::TexturePointer TextureCache::cacheTextureByHash(const std::string& hash, co gpu::TexturePointer getFallbackTextureForType(image::TextureUsage::Type type) { gpu::TexturePointer result; auto textureCache = DependencyManager::get(); - // Since this can be called on a background thread, there's a chance that the cache + // Since this can be called on a background thread, there's a chance that the cache // will be destroyed by the time we request it if (!textureCache) { return result; @@ -369,7 +370,7 @@ void NetworkTexture::makeRequest() { if (!_sourceIsKTX) { Resource::makeRequest(); return; - } + } // We special-handle ktx requests to run 2 concurrent requests right off the bat PROFILE_ASYNC_BEGIN(resource, "Resource:" + getType(), QString::number(_requestID), { { "url", _url.toString() }, { "activeURL", _activeUrl.toString() } }); @@ -908,7 +909,7 @@ void ImageReader::read() { } } - // If we found the texture either because it's in use or via KTX deserialization, + // If we found the texture either because it's in use or via KTX deserialization, // set the image and return immediately. if (texture) { QMetaObject::invokeMethod(resource.data(), "setImage", @@ -957,7 +958,7 @@ void ImageReader::read() { qCWarning(modelnetworking) << "Unable to serialize texture to KTX " << _url; } - // We replace the texture with the one stored in the cache. This deals with the possible race condition of two different + // We replace the texture with the one stored in the cache. This deals with the possible race condition of two different // images with the same hash being loaded concurrently. Only one of them will make it into the cache by hash first and will // be the winner texture = textureCache->cacheTextureByHash(hash, texture); @@ -969,23 +970,44 @@ void ImageReader::read() { Q_ARG(int, texture->getHeight())); } - NetworkTexturePointer TextureCache::getResourceTexture(QUrl resourceTextureUrl) { gpu::TexturePointer texture; if (resourceTextureUrl == SPECTATOR_CAMERA_FRAME_URL) { if (!_spectatorCameraNetworkTexture) { _spectatorCameraNetworkTexture.reset(new NetworkTexture(resourceTextureUrl)); } - texture = _spectatorCameraFramebuffer->getRenderBuffer(0); - if (texture) { - _spectatorCameraNetworkTexture->setImage(texture, texture->getWidth(), texture->getHeight()); - return _spectatorCameraNetworkTexture; + if (_spectatorCameraFramebuffer) { + texture = _spectatorCameraFramebuffer->getRenderBuffer(0); + if (texture) { + _spectatorCameraNetworkTexture->setImage(texture, texture->getWidth(), texture->getHeight()); + return _spectatorCameraNetworkTexture; + } + } + } + // FIXME: Generalize this, DRY up this code + if (resourceTextureUrl == HMD_PREVIEW_FRAME_URL) { + if (!_hmdPreviewNetworkTexture) { + _hmdPreviewNetworkTexture.reset(new NetworkTexture(resourceTextureUrl)); + } + if (_hmdPreviewFramebuffer) { + texture = _hmdPreviewFramebuffer->getRenderBuffer(0); + if (texture) { + _hmdPreviewNetworkTexture->setImage(texture, texture->getWidth(), texture->getHeight()); + return _hmdPreviewNetworkTexture; + } } } return NetworkTexturePointer(); } +const gpu::FramebufferPointer& TextureCache::getHmdPreviewFramebuffer(int width, int height) { + if (!_hmdPreviewFramebuffer || _hmdPreviewFramebuffer->getWidth() != width || _hmdPreviewFramebuffer->getHeight() != height) { + _hmdPreviewFramebuffer.reset(gpu::Framebuffer::create("hmdPreview",gpu::Element::COLOR_SRGBA_32, width, height)); + } + return _hmdPreviewFramebuffer; +} + const gpu::FramebufferPointer& TextureCache::getSpectatorCameraFramebuffer() { if (!_spectatorCameraFramebuffer) { resetSpectatorCameraFramebuffer(2048, 1024); @@ -996,4 +1018,5 @@ const gpu::FramebufferPointer& TextureCache::getSpectatorCameraFramebuffer() { void TextureCache::resetSpectatorCameraFramebuffer(int width, int height) { _spectatorCameraFramebuffer.reset(gpu::Framebuffer::create("spectatorCamera", gpu::Element::COLOR_SRGBA_32, width, height)); _spectatorCameraNetworkTexture.reset(); + emit spectatorCameraFramebufferReset(); } diff --git a/libraries/model-networking/src/model-networking/TextureCache.h b/libraries/model-networking/src/model-networking/TextureCache.h index 43edc3593d..f5a0ec5215 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.h +++ b/libraries/model-networking/src/model-networking/TextureCache.h @@ -74,7 +74,7 @@ protected: virtual bool isCacheable() const override { return _loaded; } virtual void downloadFinished(const QByteArray& data) override; - + Q_INVOKABLE void loadContent(const QByteArray& content); Q_INVOKABLE void setImage(gpu::TexturePointer texture, int originalWidth, int originalHeight); @@ -170,6 +170,10 @@ public: NetworkTexturePointer getResourceTexture(QUrl resourceTextureUrl); const gpu::FramebufferPointer& getSpectatorCameraFramebuffer(); void resetSpectatorCameraFramebuffer(int width, int height); + const gpu::FramebufferPointer& getHmdPreviewFramebuffer(int width, int height); + +signals: + void spectatorCameraFramebufferReset(); protected: // Overload ResourceCache::prefetch to allow specifying texture type for loads @@ -202,6 +206,9 @@ private: NetworkTexturePointer _spectatorCameraNetworkTexture; gpu::FramebufferPointer _spectatorCameraFramebuffer; + + NetworkTexturePointer _hmdPreviewNetworkTexture; + gpu::FramebufferPointer _hmdPreviewFramebuffer; }; #endif // hifi_TextureCache_h diff --git a/libraries/networking/src/AssetResourceRequest.cpp b/libraries/networking/src/AssetResourceRequest.cpp index f4a3b20fd5..a41283cc0d 100644 --- a/libraries/networking/src/AssetResourceRequest.cpp +++ b/libraries/networking/src/AssetResourceRequest.cpp @@ -187,6 +187,9 @@ void AssetResourceRequest::onDownloadProgress(qint64 bytesReceived, qint64 bytes emit progress(bytesReceived, bytesTotal); auto now = p_high_resolution_clock::now(); + + // Recording ATP bytes downloaded in stats + DependencyManager::get()->updateStat(STAT_ATP_RESOURCE_TOTAL_BYTES, bytesReceived); // if we haven't received the full asset check if it is time to output progress to log // we do so every X seconds to assist with ATP download tracking @@ -201,6 +204,5 @@ void AssetResourceRequest::onDownloadProgress(qint64 bytesReceived, qint64 bytes _lastProgressDebug = now; } - } diff --git a/libraries/networking/src/FileResourceRequest.cpp b/libraries/networking/src/FileResourceRequest.cpp index d0e2721679..26857716e1 100644 --- a/libraries/networking/src/FileResourceRequest.cpp +++ b/libraries/networking/src/FileResourceRequest.cpp @@ -20,7 +20,7 @@ void FileResourceRequest::doSend() { auto statTracker = DependencyManager::get(); statTracker->incrementStat(STAT_FILE_REQUEST_STARTED); - + int fileSize = 0; QString filename = _url.toLocalFile(); // sometimes on windows, we see the toLocalFile() return null, @@ -53,6 +53,7 @@ void FileResourceRequest::doSend() { } _result = ResourceRequest::Success; + fileSize = file.size(); } } else { @@ -68,6 +69,8 @@ void FileResourceRequest::doSend() { if (_result == ResourceRequest::Success) { statTracker->incrementStat(STAT_FILE_REQUEST_SUCCESS); + // Recording FILE bytes downloaded in stats + statTracker->updateStat(STAT_FILE_RESOURCE_TOTAL_BYTES,fileSize); } else { statTracker->incrementStat(STAT_FILE_REQUEST_FAILED); } diff --git a/libraries/networking/src/HTTPResourceRequest.cpp b/libraries/networking/src/HTTPResourceRequest.cpp index 266ea429a0..c6d0370a70 100644 --- a/libraries/networking/src/HTTPResourceRequest.cpp +++ b/libraries/networking/src/HTTPResourceRequest.cpp @@ -201,6 +201,11 @@ void HTTPResourceRequest::onDownloadProgress(qint64 bytesReceived, qint64 bytesT _sendTimer->start(); emit progress(bytesReceived, bytesTotal); + + // Recording HTTP bytes downloaded in stats + DependencyManager::get()->updateStat(STAT_HTTP_RESOURCE_TOTAL_BYTES, bytesReceived); + + } void HTTPResourceRequest::onTimeout() { diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 262f0318b6..75c97cc205 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -241,10 +241,6 @@ void NodeList::reset() { _numNoReplyDomainCheckIns = 0; - // lock and clear our set of radius ignored IDs - _radiusIgnoredSetLock.lockForWrite(); - _radiusIgnoredNodeIDs.clear(); - _radiusIgnoredSetLock.unlock(); // lock and clear our set of ignored IDs _ignoredSetLock.lockForWrite(); _ignoredNodeIDs.clear(); @@ -810,22 +806,6 @@ void NodeList::sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationN sendPacket(std::move(ignorePacket), *destinationNode); } -void NodeList::radiusIgnoreNodeBySessionID(const QUuid& nodeID, bool radiusIgnoreEnabled) { - if (radiusIgnoreEnabled) { - QReadLocker radiusIgnoredSetLocker{ &_radiusIgnoredSetLock }; // read lock for insert - // add this nodeID to our set of ignored IDs - _radiusIgnoredNodeIDs.insert(nodeID); - } else { - QWriteLocker radiusIgnoredSetLocker{ &_radiusIgnoredSetLock }; // write lock for unsafe_erase - _radiusIgnoredNodeIDs.unsafe_erase(nodeID); - } -} - -bool NodeList::isRadiusIgnoringNode(const QUuid& nodeID) const { - QReadLocker radiusIgnoredSetLocker{ &_radiusIgnoredSetLock }; // read lock for reading - return _radiusIgnoredNodeIDs.find(nodeID) != _radiusIgnoredNodeIDs.cend(); -} - void NodeList::ignoreNodeBySessionID(const QUuid& nodeID, bool ignoreEnabled) { // enumerate the nodes to send a reliable ignore packet to each that can leverage it if (!nodeID.isNull() && _sessionUUID != nodeID) { @@ -950,7 +930,7 @@ void NodeList::maybeSendIgnoreSetToNode(SharedNodePointer newNode) { if (_personalMutedNodeIDs.size() > 0) { // setup a packet list so we can send the stream of ignore IDs - auto personalMutePacketList = NLPacketList::create(PacketType::NodeIgnoreRequest, QByteArray(), true); + auto personalMutePacketList = NLPacketList::create(PacketType::NodeIgnoreRequest, QByteArray(), true, true); // Force the "enabled" flag in this packet to true personalMutePacketList->writePrimitive(true); @@ -977,7 +957,7 @@ void NodeList::maybeSendIgnoreSetToNode(SharedNodePointer newNode) { if (_ignoredNodeIDs.size() > 0) { // setup a packet list so we can send the stream of ignore IDs - auto ignorePacketList = NLPacketList::create(PacketType::NodeIgnoreRequest, QByteArray(), true); + auto ignorePacketList = NLPacketList::create(PacketType::NodeIgnoreRequest, QByteArray(), true, true); // Force the "enabled" flag in this packet to true ignorePacketList->writePrimitive(true); diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index 6db760b3ca..b3a12153e5 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -77,8 +77,6 @@ public: void toggleIgnoreRadius() { ignoreNodesInRadius(!getIgnoreRadiusEnabled()); } void enableIgnoreRadius() { ignoreNodesInRadius(true); } void disableIgnoreRadius() { ignoreNodesInRadius(false); } - void radiusIgnoreNodeBySessionID(const QUuid& nodeID, bool radiusIgnoreEnabled); - bool isRadiusIgnoringNode(const QUuid& other) const; void ignoreNodeBySessionID(const QUuid& nodeID, bool ignoreEnabled); bool isIgnoringNode(const QUuid& nodeID) const; void personalMuteNodeBySessionID(const QUuid& nodeID, bool muteEnabled); @@ -166,8 +164,6 @@ private: QTimer _keepAlivePingTimer; bool _requestsDomainListData; - mutable QReadWriteLock _radiusIgnoredSetLock; - tbb::concurrent_unordered_set _radiusIgnoredNodeIDs; mutable QReadWriteLock _ignoredSetLock; tbb::concurrent_unordered_set _ignoredNodeIDs; mutable QReadWriteLock _personalMutedSetLock; diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index 43f0e9335d..fbdfa4b87a 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -102,6 +102,8 @@ QSharedPointer ResourceCacheSharedItems::getHighestPendingRequest() { QSharedPointer highestResource; Lock lock(_mutex); + bool currentHighestIsFile = false; + for (int i = 0; i < _pendingRequests.size();) { // Clear any freed resources auto resource = _pendingRequests.at(i).lock(); @@ -112,10 +114,12 @@ QSharedPointer ResourceCacheSharedItems::getHighestPendingRequest() { // Check load priority float priority = resource->getLoadPriority(); - if (priority >= highestPriority) { + bool isFile = resource->getURL().scheme() == URL_SCHEME_FILE; + if (priority >= highestPriority && (isFile || !currentHighestIsFile)) { highestPriority = priority; highestIndex = i; highestResource = resource; + currentHighestIsFile = isFile; } i++; } diff --git a/libraries/networking/src/ResourceRequest.h b/libraries/networking/src/ResourceRequest.h index 39bcb3fe93..3ee86025a2 100644 --- a/libraries/networking/src/ResourceRequest.h +++ b/libraries/networking/src/ResourceRequest.h @@ -33,6 +33,9 @@ const QString STAT_HTTP_REQUEST_CACHE = "CacheHTTPRequest"; const QString STAT_ATP_MAPPING_REQUEST_STARTED = "StartedATPMappingRequest"; const QString STAT_ATP_MAPPING_REQUEST_FAILED = "FailedATPMappingRequest"; const QString STAT_ATP_MAPPING_REQUEST_SUCCESS = "SuccessfulATPMappingRequest"; +const QString STAT_HTTP_RESOURCE_TOTAL_BYTES = "HTTPBytesDownloaded"; +const QString STAT_ATP_RESOURCE_TOTAL_BYTES = "ATPBytesDownloaded"; +const QString STAT_FILE_RESOURCE_TOTAL_BYTES = "FILEBytesDownloaded"; class ResourceRequest : public QObject { Q_OBJECT diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 6c42193e11..848bfd97cf 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -105,7 +105,6 @@ public: UsernameFromIDReply, ViewFrustum, RequestsDomainListData, - ExitingSpaceBubble, PerAvatarGainSet, EntityScriptGetStatus, EntityScriptGetStatusReply, diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp index 2e93f3515f..cceecaeed9 100644 --- a/libraries/octree/src/Octree.cpp +++ b/libraries/octree/src/Octree.cpp @@ -29,8 +29,12 @@ #include #include #include +#include +#include #include #include +#include +#include #include #include @@ -1667,7 +1671,28 @@ bool Octree::readJSONFromGzippedFile(QString qFileName) { return readJSONFromStream(-1, jsonStream); } +// hack to get the marketplace id into the entities. We will create a way to get this from a hash of +// the entity later, but this helps us move things along for now +QString getMarketplaceID(const QString& urlString) { + // the url should be http://mpassets.highfidelity.com/-v1/.extension + // a regex for the this is a PITA as there are several valid versions of uuids, and so + // lets strip out the uuid (if any) and try to create a UUID from the string, relying on + // QT to parse it + static const QRegularExpression re("^http:\\/\\/mpassets.highfidelity.com\\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})-v[\\d]+\\/.*"); + QRegularExpressionMatch match = re.match(urlString); + if (match.hasMatch()) { + QString matched = match.captured(1); + if (QUuid(matched).isNull()) { + qDebug() << "invalid uuid for marketplaceID"; + } else { + return matched; + } + } + return QString(); +} + bool Octree::readFromURL(const QString& urlString) { + QString marketplaceID = getMarketplaceID(urlString); auto request = std::unique_ptr(DependencyManager::get()->createResourceRequest(this, urlString)); @@ -1686,11 +1711,11 @@ bool Octree::readFromURL(const QString& urlString) { auto data = request->getData(); QDataStream inputStream(data); - return readFromStream(data.size(), inputStream); + return readFromStream(data.size(), inputStream, marketplaceID); } -bool Octree::readFromStream(unsigned long streamLength, QDataStream& inputStream) { +bool Octree::readFromStream(unsigned long streamLength, QDataStream& inputStream, const QString& marketplaceID) { // decide if this is binary SVO or JSON-formatted SVO QIODevice *device = inputStream.device(); char firstChar; @@ -1702,7 +1727,7 @@ bool Octree::readFromStream(unsigned long streamLength, QDataStream& inputStream return readSVOFromStream(streamLength, inputStream); } else { qCDebug(octree) << "Reading from JSON SVO Stream length:" << streamLength; - return readJSONFromStream(streamLength, inputStream); + return readJSONFromStream(streamLength, inputStream, marketplaceID); } } @@ -1838,9 +1863,31 @@ bool Octree::readSVOFromStream(unsigned long streamLength, QDataStream& inputStr return fileOk; } +// hack to get the marketplace id into the entities. We will create a way to get this from a hash of +// the entity later, but this helps us move things along for now +QJsonDocument addMarketplaceIDToDocumentEntities(QJsonDocument& doc, const QString& marketplaceID) { + if (!marketplaceID.isEmpty()) { + QJsonDocument newDoc; + QJsonObject rootObj = doc.object(); + QJsonArray newEntitiesArray; + + // build a new entities array + auto entitiesArray = rootObj["Entities"].toArray(); + for(auto it = entitiesArray.begin(); it != entitiesArray.end(); it++) { + auto entity = (*it).toObject(); + entity["marketplaceID"] = marketplaceID; + newEntitiesArray.append(entity); + } + rootObj["Entities"] = newEntitiesArray; + newDoc.setObject(rootObj); + return newDoc; + } + return doc; +} + const int READ_JSON_BUFFER_SIZE = 2048; -bool Octree::readJSONFromStream(unsigned long streamLength, QDataStream& inputStream) { +bool Octree::readJSONFromStream(unsigned long streamLength, QDataStream& inputStream, const QString& marketplaceID /*=""*/) { // if the data is gzipped we may not have a useful bytesAvailable() result, so just keep reading until // we get an eof. Leave streamLength parameter for consistency. @@ -1860,6 +1907,9 @@ bool Octree::readJSONFromStream(unsigned long streamLength, QDataStream& inputSt } QJsonDocument asDocument = QJsonDocument::fromJson(jsonBuffer); + if (!marketplaceID.isEmpty()) { + asDocument = addMarketplaceIDToDocumentEntities(asDocument, marketplaceID); + } QVariant asVariant = asDocument.toVariant(); QVariantMap asMap = asVariant.toMap(); bool success = readFromMap(asMap); diff --git a/libraries/octree/src/Octree.h b/libraries/octree/src/Octree.h index 512a0ab64e..38454d20b5 100644 --- a/libraries/octree/src/Octree.h +++ b/libraries/octree/src/Octree.h @@ -212,14 +212,14 @@ public: virtual bool handlesEditPacketType(PacketType packetType) const { return false; } virtual int processEditPacketData(ReceivedMessage& message, const unsigned char* editData, int maxLength, const SharedNodePointer& sourceNode) { return 0; } - + virtual bool recurseChildrenWithData() const { return true; } virtual bool rootElementHasData() const { return false; } virtual int minimumRequiredRootDataBytes() const { return 0; } virtual bool suppressEmptySubtrees() const { return true; } virtual void releaseSceneEncodeData(OctreeElementExtraEncodeData* extraEncodeData) const { } virtual bool mustIncludeAllChildData() const { return true; } - + /// some versions of the SVO file will include breaks with buffer lengths between each buffer chunk in the SVO /// file. If the Octree subclass expects this for this particular version of the file, it should override this /// method and return true. @@ -236,15 +236,15 @@ public: void reaverageOctreeElements(OctreeElementPointer startElement = OctreeElementPointer()); void deleteOctreeElementAt(float x, float y, float z, float s); - + /// Find the voxel at position x,y,z,s /// \return pointer to the OctreeElement or NULL if none at x,y,z,s. OctreeElementPointer getOctreeElementAt(float x, float y, float z, float s) const; - + /// Find the voxel at position x,y,z,s /// \return pointer to the OctreeElement or to the smallest enclosing parent if none at x,y,z,s. OctreeElementPointer getOctreeEnclosingElementAt(float x, float y, float z, float s) const; - + OctreeElementPointer getOrCreateChildElementAt(float x, float y, float z, float s); OctreeElementPointer getOrCreateChildElementContaining(const AACube& box); @@ -261,7 +261,7 @@ public: int encodeTreeBitstream(const OctreeElementPointer& element, OctreePacketData* packetData, OctreeElementBag& bag, EncodeBitstreamParams& params) ; - + bool isDirty() const { return _isDirty; } void clearDirtyBit() { _isDirty = false; } void setDirtyBit() { _isDirty = true; } @@ -301,9 +301,9 @@ public: // Octree importers bool readFromFile(const char* filename); bool readFromURL(const QString& url); // will support file urls as well... - bool readFromStream(unsigned long streamLength, QDataStream& inputStream); + bool readFromStream(unsigned long streamLength, QDataStream& inputStream, const QString& marketplaceID=""); bool readSVOFromStream(unsigned long streamLength, QDataStream& inputStream); - bool readJSONFromStream(unsigned long streamLength, QDataStream& inputStream); + bool readJSONFromStream(unsigned long streamLength, QDataStream& inputStream, const QString& marketplaceID=""); bool readJSONFromGzippedFile(QString qFileName); virtual bool readFromMap(QVariantMap& entityDescription) = 0; diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index 7bfdbddbc5..481a2609fc 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -22,9 +22,10 @@ #include #include #include - #include "Plugin.h" +class QOpenGLFramebufferObject; + class QImage; enum Eye { @@ -60,8 +61,12 @@ namespace gpu { using TexturePointer = std::shared_ptr; } +class NetworkTexture; +using NetworkTexturePointer = QSharedPointer; +typedef struct __GLsync *GLsync; + // Stereo display functionality -// TODO move out of this file don't derive DisplayPlugin from this. Instead use dynamic casting when +// TODO move out of this file don't derive DisplayPlugin from this. Instead use dynamic casting when // displayPlugin->isStereo returns true class StereoDisplay { public: @@ -78,7 +83,7 @@ public: }; // HMD display functionality -// TODO move out of this file don't derive DisplayPlugin from this. Instead use dynamic casting when +// TODO move out of this file don't derive DisplayPlugin from this. Instead use dynamic casting when // displayPlugin->isHmd returns true class HmdDisplay : public StereoDisplay { public: @@ -142,7 +147,7 @@ public: virtual float getTargetFrameRate() const { return 1.0f; } virtual bool hasAsyncReprojection() const { return false; } - /// Returns a boolean value indicating whether the display is currently visible + /// Returns a boolean value indicating whether the display is currently visible /// to the user. For monitor displays, false might indicate that a screensaver, /// or power-save mode is active. For HMDs it may reflect a sensor indicating /// whether the HMD is being worn @@ -204,10 +209,12 @@ public: // Rate at which rendered frames are being skipped virtual float droppedFrameRate() const { return -1.0f; } virtual bool getSupportsAutoSwitch() { return false; } - + // Hardware specific stats virtual QJsonObject getHardwareStats() const { return QJsonObject(); } + virtual void copyTextureToQuickFramebuffer(NetworkTexturePointer source, QOpenGLFramebufferObject* target, GLsync* fenceSync) = 0; + uint32_t presentCount() const { return _presentedFrameIndex; } // Time since last call to incrementPresentCount (only valid if DEBUG_PAINT_DELAY is defined) int64_t getPaintDelayUsecs() const; diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 49b844efb0..5f2acff16f 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -29,6 +29,7 @@ #include "gpu/StandardShaderLib.h" #include "model/TextureMap.h" +#include "render/Args.h" #include "standardTransformPNTC_vert.h" #include "standardDrawTexture_frag.h" @@ -459,7 +460,8 @@ _nextID(0) { // Set the defaults needed for a simple program batch.setResourceTexture(render::ShapePipeline::Slot::MAP::ALBEDO, DependencyManager::get()->getWhiteTexture()); - } + }, + nullptr ); GeometryCache::_simpleTransparentPipeline = std::make_shared(getSimplePipeline(false, true, true, false), nullptr, @@ -467,11 +469,12 @@ _nextID(0) { // Set the defaults needed for a simple program batch.setResourceTexture(render::ShapePipeline::Slot::MAP::ALBEDO, DependencyManager::get()->getWhiteTexture()); - } + }, + nullptr ); GeometryCache::_simpleWirePipeline = std::make_shared(getSimplePipeline(false, false, true, true), nullptr, - [](const render::ShapePipeline&, gpu::Batch& batch, RenderArgs* args) {}); + [](const render::ShapePipeline&, gpu::Batch& batch, RenderArgs* args) {}, nullptr); } GeometryCache::~GeometryCache() { diff --git a/libraries/render-utils/src/SoftAttachmentModel.cpp b/libraries/render-utils/src/SoftAttachmentModel.cpp index f15b696006..63b18d49b8 100644 --- a/libraries/render-utils/src/SoftAttachmentModel.cpp +++ b/libraries/render-utils/src/SoftAttachmentModel.cpp @@ -35,6 +35,10 @@ void SoftAttachmentModel::updateClusterMatrices() { if (!_needsUpdateClusterMatrices) { return; } + if (!isLoaded()) { + return; + } + _needsUpdateClusterMatrices = false; const FBXGeometry& geometry = getFBXGeometry(); diff --git a/libraries/render/src/render/DrawTask.cpp b/libraries/render/src/render/DrawTask.cpp index 1153a737a1..a3175ffdec 100755 --- a/libraries/render/src/render/DrawTask.cpp +++ b/libraries/render/src/render/DrawTask.cpp @@ -45,8 +45,9 @@ void renderShape(RenderArgs* args, const ShapePlumberPointer& shapeContext, cons if (key.isValid() && !key.hasOwnPipeline()) { args->_shapePipeline = shapeContext->pickPipeline(args, key); if (args->_shapePipeline) { + args->_shapePipeline->prepareShapeItem(args, key, item); item.render(args); - } + } args->_shapePipeline = nullptr; } else if (key.hasOwnPipeline()) { item.render(args); @@ -110,10 +111,11 @@ void render::renderStateSortShapes(const RenderContextPointer& renderContext, for (auto& pipelineKey : sortedPipelines) { auto& bucket = sortedShapes[pipelineKey]; args->_shapePipeline = shapeContext->pickPipeline(args, pipelineKey); - if (!args->_shapePipeline) { + if (!args->_shapePipeline) { continue; } for (auto& item : bucket) { + args->_shapePipeline->prepareShapeItem(args, pipelineKey, item); item.render(args); } } diff --git a/libraries/render/src/render/ShapePipeline.cpp b/libraries/render/src/render/ShapePipeline.cpp index 79ac5c0990..c83c0b44fc 100644 --- a/libraries/render/src/render/ShapePipeline.cpp +++ b/libraries/render/src/render/ShapePipeline.cpp @@ -17,18 +17,34 @@ using namespace render; +ShapePipeline::CustomFactoryMap ShapePipeline::_globalCustomFactoryMap; + +ShapePipeline::CustomKey ShapePipeline::registerCustomShapePipelineFactory(CustomFactory factory) { + ShapePipeline::CustomKey custom = (ShapePipeline::CustomKey) _globalCustomFactoryMap.size() + 1; + _globalCustomFactoryMap[custom] = factory; + return custom; +} + + void ShapePipeline::prepare(gpu::Batch& batch, RenderArgs* args) { - if (batchSetter) { - batchSetter(*this, batch, args); + if (_batchSetter) { + _batchSetter(*this, batch, args); } } +void ShapePipeline::prepareShapeItem(RenderArgs* args, const ShapeKey& key, const Item& shape) { + if (_itemSetter) { + _itemSetter(*this, args, shape); + } +} + + ShapeKey::Filter::Builder::Builder() { _mask.set(OWN_PIPELINE); _mask.set(INVALID); } -void ShapePlumber::addPipelineHelper(const Filter& filter, ShapeKey key, int bit, const PipelinePointer& pipeline) { +void ShapePlumber::addPipelineHelper(const Filter& filter, ShapeKey key, int bit, const PipelinePointer& pipeline) const { // Iterate over all keys if (bit < (int)ShapeKey::FlagBit::NUM_FLAGS) { addPipelineHelper(filter, key, bit + 1, pipeline); @@ -48,12 +64,12 @@ void ShapePlumber::addPipelineHelper(const Filter& filter, ShapeKey key, int bit } void ShapePlumber::addPipeline(const Key& key, const gpu::ShaderPointer& program, const gpu::StatePointer& state, - BatchSetter batchSetter) { - addPipeline(Filter{key}, program, state, batchSetter); + BatchSetter batchSetter, ItemSetter itemSetter) { + addPipeline(Filter{key}, program, state, batchSetter, itemSetter); } void ShapePlumber::addPipeline(const Filter& filter, const gpu::ShaderPointer& program, const gpu::StatePointer& state, - BatchSetter batchSetter) { + BatchSetter batchSetter, ItemSetter itemSetter) { gpu::Shader::BindingSet slotBindings; slotBindings.insert(gpu::Shader::Binding(std::string("lightingModelBuffer"), Slot::BUFFER::LIGHTING_MODEL)); slotBindings.insert(gpu::Shader::Binding(std::string("skinClusterBuffer"), Slot::BUFFER::SKINNING)); @@ -90,7 +106,7 @@ void ShapePlumber::addPipeline(const Filter& filter, const gpu::ShaderPointer& p ShapeKey key{filter._flags}; auto gpuPipeline = gpu::Pipeline::create(program, state); - auto shapePipeline = std::make_shared(gpuPipeline, locations, batchSetter); + auto shapePipeline = std::make_shared(gpuPipeline, locations, batchSetter, itemSetter); addPipelineHelper(filter, key, 0, shapePipeline); } @@ -103,23 +119,34 @@ const ShapePipelinePointer ShapePlumber::pickPipeline(RenderArgs* args, const Ke const auto& pipelineIterator = _pipelineMap.find(key); if (pipelineIterator == _pipelineMap.end()) { - // The first time we can't find a pipeline, we should log it + // The first time we can't find a pipeline, we should try things to solve that if (_missingKeys.find(key) == _missingKeys.end()) { - _missingKeys.insert(key); - qCDebug(renderlogging) << "Couldn't find a pipeline for" << key; + if (key.isCustom()) { + auto factoryIt = ShapePipeline::_globalCustomFactoryMap.find(key.getCustom()); + if ((factoryIt != ShapePipeline::_globalCustomFactoryMap.end()) && (factoryIt)->second) { + // found a factory for the custom key, can now generate a shape pipeline for this case: + addPipelineHelper(Filter(key), key, 0, (factoryIt)->second(*this, key)); + + return pickPipeline(args, key); + } else { + qCDebug(renderlogging) << "ShapePlumber::Couldn't find a custom pipeline factory for " << key.getCustom() << " key is: " << key; + } + } + + _missingKeys.insert(key); + qCDebug(renderlogging) << "ShapePlumber::Couldn't find a pipeline for" << key; } return PipelinePointer(nullptr); } PipelinePointer shapePipeline(pipelineIterator->second); - auto& batch = args->_batch; // Setup the one pipeline (to rule them all) - batch->setPipeline(shapePipeline->pipeline); + args->_batch->setPipeline(shapePipeline->pipeline); // Run the pipeline's BatchSetter on the passed in batch - if (shapePipeline->batchSetter) { - shapePipeline->batchSetter(*shapePipeline, *batch, args); + if (shapePipeline->_batchSetter) { + shapePipeline->_batchSetter(*shapePipeline, *(args->_batch), args); } return shapePipeline; diff --git a/libraries/render/src/render/ShapePipeline.h b/libraries/render/src/render/ShapePipeline.h index 1b97baae7c..13daac2db7 100644 --- a/libraries/render/src/render/ShapePipeline.h +++ b/libraries/render/src/render/ShapePipeline.h @@ -19,6 +19,8 @@ #include "Args.h" namespace render { +class Item; +class ShapePlumber; class ShapeKey { public: @@ -38,7 +40,19 @@ public: OWN_PIPELINE, INVALID, + CUSTOM_0, + CUSTOM_1, + CUSTOM_2, + CUSTOM_3, + CUSTOM_4, + CUSTOM_5, + CUSTOM_6, + CUSTOM_7, + NUM_FLAGS, // Not a valid flag + + CUSTOM_MASK = (0xFF << CUSTOM_0), + }; using Flags = std::bitset; @@ -73,6 +87,8 @@ public: Builder& withOwnPipeline() { _flags.set(OWN_PIPELINE); return (*this); } Builder& invalidate() { _flags.set(INVALID); return (*this); } + Builder& withCustom(uint8_t custom) { _flags &= (~CUSTOM_MASK); _flags |= (custom << CUSTOM_0); return (*this); } + static const ShapeKey ownPipeline() { return Builder().withOwnPipeline(); } static const ShapeKey invalid() { return Builder().invalidate(); } @@ -127,6 +143,9 @@ public: Builder& withCullFace() { _flags.reset(NO_CULL_FACE); _mask.set(NO_CULL_FACE); return (*this); } Builder& withoutCullFace() { _flags.set(NO_CULL_FACE); _mask.set(NO_CULL_FACE); return (*this); } + Builder& withCustom(uint8_t custom) { _flags &= (~CUSTOM_MASK); _flags |= (custom << CUSTOM_0); _mask |= (CUSTOM_MASK); return (*this); } + Builder& withoutCustom() { _flags &= (~CUSTOM_MASK); _mask |= (CUSTOM_MASK); return (*this); } + protected: friend class Filter; Flags _flags{0}; @@ -155,6 +174,9 @@ public: bool hasOwnPipeline() const { return _flags[OWN_PIPELINE]; } bool isValid() const { return !_flags[INVALID]; } + uint8_t getCustom() const { return (_flags.to_ulong() & CUSTOM_MASK) >> CUSTOM_0; } + bool isCustom() const { return (_flags.to_ulong() & CUSTOM_MASK); } + // Comparator for use in stl containers class Hash { public: @@ -240,22 +262,39 @@ public: }; using LocationsPointer = std::shared_ptr; - using BatchSetter = std::function; + using BatchSetter = std::function; - ShapePipeline(gpu::PipelinePointer pipeline, LocationsPointer locations, BatchSetter batchSetter) : - pipeline(pipeline), locations(locations), batchSetter(batchSetter) {} + using ItemSetter = std::function; - // Normally, a pipeline is accessed thorugh pickPipeline. If it needs to be set manually, + ShapePipeline(gpu::PipelinePointer pipeline, LocationsPointer locations, BatchSetter batchSetter, ItemSetter itemSetter) : + pipeline(pipeline), + locations(locations), + _batchSetter(batchSetter), + _itemSetter(itemSetter) {} + + // Normally, a pipeline is accessed through pickPipeline. If it needs to be set manually, // after calling setPipeline this method should be called to prepare the pipeline with default buffers. - void prepare(gpu::Batch& batch, RenderArgs* args); + void prepare(gpu::Batch& batch, Args* args); gpu::PipelinePointer pipeline; std::shared_ptr locations; + void prepareShapeItem(Args* args, const ShapeKey& key, const Item& shape); + protected: friend class ShapePlumber; - BatchSetter batchSetter; + BatchSetter _batchSetter; + ItemSetter _itemSetter; +public: + using CustomKey = uint8_t; + using CustomFactory = std::function (const ShapePlumber& plumber, const ShapeKey& key)>; + using CustomFactoryMap = std::map; + + static CustomFactoryMap _globalCustomFactoryMap; + + static CustomKey registerCustomShapePipelineFactory(CustomFactory factory); + }; using ShapePipelinePointer = std::shared_ptr; @@ -270,22 +309,24 @@ public: using Locations = Pipeline::Locations; using LocationsPointer = Pipeline::LocationsPointer; using BatchSetter = Pipeline::BatchSetter; + using ItemSetter = Pipeline::ItemSetter; void addPipeline(const Key& key, const gpu::ShaderPointer& program, const gpu::StatePointer& state, - BatchSetter batchSetter = nullptr); + BatchSetter batchSetter = nullptr, ItemSetter itemSetter = nullptr); void addPipeline(const Filter& filter, const gpu::ShaderPointer& program, const gpu::StatePointer& state, - BatchSetter batchSetter = nullptr); + BatchSetter batchSetter = nullptr, ItemSetter itemSetter = nullptr); const PipelinePointer pickPipeline(RenderArgs* args, const Key& key) const; protected: - void addPipelineHelper(const Filter& filter, Key key, int bit, const PipelinePointer& pipeline); - PipelineMap _pipelineMap; + void addPipelineHelper(const Filter& filter, Key key, int bit, const PipelinePointer& pipeline) const; + mutable PipelineMap _pipelineMap; private: mutable std::unordered_set _missingKeys; }; + using ShapePlumberPointer = std::shared_ptr; } diff --git a/libraries/script-engine/src/ScriptAudioInjector.cpp b/libraries/script-engine/src/ScriptAudioInjector.cpp index c0ad2debd9..516f62401f 100644 --- a/libraries/script-engine/src/ScriptAudioInjector.cpp +++ b/libraries/script-engine/src/ScriptAudioInjector.cpp @@ -21,7 +21,7 @@ QScriptValue injectorToScriptValue(QScriptEngine* engine, ScriptAudioInjector* c // when the script goes down we want to cleanup the injector QObject::connect(engine, &QScriptEngine::destroyed, in, &ScriptAudioInjector::stopInjectorImmediately, Qt::DirectConnection); - + return engine->newQObject(in, QScriptEngine::ScriptOwnership); } @@ -29,10 +29,10 @@ void injectorFromScriptValue(const QScriptValue& object, ScriptAudioInjector*& o out = qobject_cast(object.toQObject()); } -ScriptAudioInjector::ScriptAudioInjector(AudioInjector* injector) : +ScriptAudioInjector::ScriptAudioInjector(const AudioInjectorPointer& injector) : _injector(injector) { - QObject::connect(injector, &AudioInjector::finished, this, &ScriptAudioInjector::finished); + QObject::connect(injector.data(), &AudioInjector::finished, this, &ScriptAudioInjector::finished); } ScriptAudioInjector::~ScriptAudioInjector() { @@ -44,5 +44,5 @@ ScriptAudioInjector::~ScriptAudioInjector() { void ScriptAudioInjector::stopInjectorImmediately() { qCDebug(scriptengine) << "ScriptAudioInjector::stopInjectorImmediately called to stop audio injector immediately."; - _injector->stopAndDeleteLater(); + _injector->stop(); } diff --git a/libraries/script-engine/src/ScriptAudioInjector.h b/libraries/script-engine/src/ScriptAudioInjector.h index 4de12af62c..4c2871dd34 100644 --- a/libraries/script-engine/src/ScriptAudioInjector.h +++ b/libraries/script-engine/src/ScriptAudioInjector.h @@ -18,31 +18,31 @@ class ScriptAudioInjector : public QObject { Q_OBJECT - + Q_PROPERTY(bool playing READ isPlaying) Q_PROPERTY(float loudness READ getLoudness) Q_PROPERTY(AudioInjectorOptions options WRITE setOptions READ getOptions) public: - ScriptAudioInjector(AudioInjector* injector); + ScriptAudioInjector(const AudioInjectorPointer& injector); ~ScriptAudioInjector(); public slots: void restart() { _injector->restart(); } void stop() { _injector->stop(); } - + const AudioInjectorOptions& getOptions() const { return _injector->getOptions(); } void setOptions(const AudioInjectorOptions& options) { _injector->setOptions(options); } - + float getLoudness() const { return _injector->getLoudness(); } bool isPlaying() const { return _injector->isPlaying(); } - + signals: void finished(); - + protected slots: void stopInjectorImmediately(); private: - QPointer _injector; - + AudioInjectorPointer _injector; + friend QScriptValue injectorToScriptValue(QScriptEngine* engine, ScriptAudioInjector* const& in); }; diff --git a/libraries/shared/src/Preferences.h b/libraries/shared/src/Preferences.h index f1915a9d6a..6093cd3c8a 100644 --- a/libraries/shared/src/Preferences.h +++ b/libraries/shared/src/Preferences.h @@ -52,6 +52,7 @@ public: Browsable, Slider, Spinner, + SpinnerSlider, Checkbox, Button, ComboBox, @@ -254,6 +255,15 @@ public: Type getType() override { return Spinner; } }; +class SpinnerSliderPreference : public FloatPreference { + Q_OBJECT +public: + SpinnerSliderPreference(const QString& category, const QString& name, Getter getter, Setter setter) + : FloatPreference(category, name, getter, setter) { } + + Type getType() override { return SpinnerSlider; } +}; + class IntSpinnerPreference : public IntPreference { Q_OBJECT public: diff --git a/libraries/shared/src/SharedUtil.cpp b/libraries/shared/src/SharedUtil.cpp index 58b8aead45..99bdfc4d90 100644 --- a/libraries/shared/src/SharedUtil.cpp +++ b/libraries/shared/src/SharedUtil.cpp @@ -47,6 +47,7 @@ extern "C" FILE * __cdecl __iob_func(void) { #include #include #include +#include #include #include #include @@ -1077,14 +1078,20 @@ void setMaxCores(uint8_t maxCores) { #endif } -#ifdef Q_OS_WIN -VOID CALLBACK parentDiedCallback(PVOID lpParameter, BOOLEAN timerOrWaitFired) { - if (!timerOrWaitFired && qApp) { +void quitWithParentProcess() { + if (qApp) { qDebug() << "Parent process died, quitting"; qApp->quit(); } } +#ifdef Q_OS_WIN +VOID CALLBACK parentDiedCallback(PVOID lpParameter, BOOLEAN timerOrWaitFired) { + if (!timerOrWaitFired) { + quitWithParentProcess(); + } +} + void watchParentProcess(int parentPID) { DWORD processID = parentPID; HANDLE procHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processID); @@ -1092,8 +1099,17 @@ void watchParentProcess(int parentPID) { HANDLE newHandle; RegisterWaitForSingleObject(&newHandle, procHandle, parentDiedCallback, NULL, INFINITE, WT_EXECUTEONLYONCE); } -#else +#elif defined(Q_OS_MAC) || defined(Q_OS_LINUX) void watchParentProcess(int parentPID) { - qWarning() << "Parent PID option not implemented on this plateform"; + auto timer = new QTimer(qApp); + timer->setInterval(MSECS_PER_SECOND); + QObject::connect(timer, &QTimer::timeout, qApp, [parentPID]() { + auto ppid = getppid(); + if (parentPID != ppid) { + // If the PPID changed, then that means our parent process died. + quitWithParentProcess(); + } + }); + timer->start(); } -#endif // Q_OS_WIN \ No newline at end of file +#endif diff --git a/libraries/shared/src/shared/FileCache.cpp b/libraries/shared/src/shared/FileCache.cpp index 695336847e..9957c6bfc6 100644 --- a/libraries/shared/src/shared/FileCache.cpp +++ b/libraries/shared/src/shared/FileCache.cpp @@ -258,7 +258,8 @@ namespace cache { }; } -void FileCache::eject(const FilePointer& file) { +// Take file pointer by value to insure it doesn't get destructed during the "erase()" calls +void FileCache::eject(FilePointer file) { file->_locked = false; const auto& length = file->getLength(); const auto& key = file->getKey(); diff --git a/libraries/shared/src/shared/FileCache.h b/libraries/shared/src/shared/FileCache.h index 1580674ca0..94fa57457a 100644 --- a/libraries/shared/src/shared/FileCache.h +++ b/libraries/shared/src/shared/FileCache.h @@ -119,7 +119,7 @@ private: void clean(); void clear(); // Remove a file from the cache - void eject(const FilePointer& file); + void eject(FilePointer file); size_t getOverbudgetAmount() const; diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp index 9df727424f..34865ea058 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp @@ -264,9 +264,6 @@ QNetworkAccessManager* QmlNetworkAccessManagerFactory::create(QObject* parent) { return new QmlNetworkAccessManager(parent); } -static QQmlEngine* globalEngine { nullptr }; -static size_t globalEngineRefCount { 0 }; - QString getEventBridgeJavascript() { // FIXME: Refactor with similar code in RenderableWebEntityItem QString javaScriptToInject; @@ -300,9 +297,44 @@ private: }; +#define SINGLE_QML_ENGINE 0 + +#if SINGLE_QML_ENGINE +static QQmlEngine* globalEngine{ nullptr }; +static size_t globalEngineRefCount{ 0 }; +#endif + +void initializeQmlEngine(QQmlEngine* engine, QQuickWindow* window) { + engine->setNetworkAccessManagerFactory(new QmlNetworkAccessManagerFactory); + auto importList = engine->importPathList(); + importList.insert(importList.begin(), PathUtils::resourcesPath()); + engine->setImportPathList(importList); + for (const auto& path : importList) { + qDebug() << path; + } + + if (!engine->incubationController()) { + engine->setIncubationController(window->incubationController()); + } + auto rootContext = engine->rootContext(); + rootContext->setContextProperty("GL", ::getGLContextData()); + rootContext->setContextProperty("urlHandler", new UrlHandler()); + rootContext->setContextProperty("resourceDirectoryUrl", QUrl::fromLocalFile(PathUtils::resourcesPath())); + rootContext->setContextProperty("pathToFonts", "../../"); + rootContext->setContextProperty("ApplicationInterface", qApp); + auto javaScriptToInject = getEventBridgeJavascript(); + if (!javaScriptToInject.isEmpty()) { + rootContext->setContextProperty("eventBridgeJavaScriptToInject", QVariant(javaScriptToInject)); + } + rootContext->setContextProperty("FileTypeProfile", new FileTypeProfile(rootContext)); + rootContext->setContextProperty("HFWebEngineProfile", new HFWebEngineProfile(rootContext)); + rootContext->setContextProperty("HFTabletWebEngineProfile", new HFTabletWebEngineProfile(rootContext)); +} QQmlEngine* acquireEngine(QQuickWindow* window) { Q_ASSERT(QThread::currentThread() == qApp->thread()); + + QQmlEngine* result = nullptr; if (QThread::currentThread() != qApp->thread()) { qCWarning(uiLogging) << "Cannot acquire QML engine on any thread but the main thread"; } @@ -311,47 +343,34 @@ QQmlEngine* acquireEngine(QQuickWindow* window) { qmlRegisterType("Hifi", 1, 0, "SoundEffect"); }); + +#if SINGLE_QML_ENGINE if (!globalEngine) { Q_ASSERT(0 == globalEngineRefCount); globalEngine = new QQmlEngine(); - globalEngine->setNetworkAccessManagerFactory(new QmlNetworkAccessManagerFactory); - - auto importList = globalEngine->importPathList(); - importList.insert(importList.begin(), PathUtils::resourcesPath()); - globalEngine->setImportPathList(importList); - for (const auto& path : importList) { - qDebug() << path; - } - - if (!globalEngine->incubationController()) { - globalEngine->setIncubationController(window->incubationController()); - } - auto rootContext = globalEngine->rootContext(); - rootContext->setContextProperty("GL", ::getGLContextData()); - rootContext->setContextProperty("urlHandler", new UrlHandler()); - rootContext->setContextProperty("resourceDirectoryUrl", QUrl::fromLocalFile(PathUtils::resourcesPath())); - rootContext->setContextProperty("pathToFonts", "../../"); - rootContext->setContextProperty("ApplicationInterface", qApp); - auto javaScriptToInject = getEventBridgeJavascript(); - if (!javaScriptToInject.isEmpty()) { - rootContext->setContextProperty("eventBridgeJavaScriptToInject", QVariant(javaScriptToInject)); - } - rootContext->setContextProperty("FileTypeProfile", new FileTypeProfile(rootContext)); - rootContext->setContextProperty("HFWebEngineProfile", new HFWebEngineProfile(rootContext)); - rootContext->setContextProperty("HFTabletWebEngineProfile", new HFTabletWebEngineProfile(rootContext)); + initializeQmlEngine(result, window); + ++globalEngineRefCount; } + result = globalEngine; +#else + result = new QQmlEngine(); + initializeQmlEngine(result, window); +#endif - ++globalEngineRefCount; - return globalEngine; + return result; } -void releaseEngine() { +void releaseEngine(QQmlEngine* engine) { Q_ASSERT(QThread::currentThread() == qApp->thread()); +#if SINGLE_QML_ENGINE Q_ASSERT(0 != globalEngineRefCount); if (0 == --globalEngineRefCount) { globalEngine->deleteLater(); globalEngine = nullptr; } +#else + engine->deleteLater(); +#endif } void OffscreenQmlSurface::cleanup() { @@ -456,11 +475,11 @@ OffscreenQmlSurface::~OffscreenQmlSurface() { QObject::disconnect(qApp); cleanup(); - + auto engine = _qmlContext->engine(); _canvas->deleteLater(); _rootItem->deleteLater(); _quickWindow->deleteLater(); - releaseEngine(); + releaseEngine(engine); } void OffscreenQmlSurface::onAboutToQuit() { diff --git a/libraries/ui/src/ui/TabletScriptingInterface.cpp b/libraries/ui/src/ui/TabletScriptingInterface.cpp index 857cae15cc..6ff5e46cea 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.cpp +++ b/libraries/ui/src/ui/TabletScriptingInterface.cpp @@ -227,10 +227,8 @@ void TabletProxy::setToolbarMode(bool toolbarMode) { // forward qml surface events to interface js connect(tabletRootWindow, &QmlWindowClass::fromQml, this, &TabletProxy::fromQml); } else { - _state = State::Home; removeButtonsFromToolbar(); addButtonsToHomeScreen(); - emit screenChanged(QVariant("Home"), QVariant(TABLET_SOURCE_URL)); // destroy desktop window if (_desktopWindow) { @@ -238,6 +236,8 @@ void TabletProxy::setToolbarMode(bool toolbarMode) { _desktopWindow = nullptr; } } + loadHomeScreen(true); + emit screenChanged(QVariant("Home"), QVariant(TABLET_SOURCE_URL)); } static void addButtonProxyToQmlTablet(QQuickItem* qmlTablet, TabletButtonProxy* buttonProxy) { diff --git a/libraries/ui/src/ui/types/SoundEffect.h b/libraries/ui/src/ui/types/SoundEffect.h index 656f98dd8d..a7e29d86f9 100644 --- a/libraries/ui/src/ui/types/SoundEffect.h +++ b/libraries/ui/src/ui/types/SoundEffect.h @@ -15,6 +15,7 @@ #include class AudioInjector; +using AudioInjectorPointer = QSharedPointer; // SoundEffect object, exposed to qml only, not interface JavaScript. // This is used to play spatial sound effects on tablets/web entities from within QML. @@ -38,7 +39,7 @@ protected: QUrl _url; float _volume { 1.0f }; SharedSoundPointer _sound; - AudioInjector* _injector { nullptr }; + AudioInjectorPointer _injector; }; #endif // hifi_SoundEffect_h diff --git a/scripts/developer/tests/entityLookupCostMeasurement.js b/scripts/developer/tests/entityLookupCostMeasurement.js new file mode 100644 index 0000000000..15697fe13d --- /dev/null +++ b/scripts/developer/tests/entityLookupCostMeasurement.js @@ -0,0 +1,104 @@ +// Creates a large number of entities on the cardinal planes of the octree (all +// objects will live in the root octree element). Measures how long it takes +// to update the properties of the first and last entity. The difference +// between the two measurements shows how the cost of lookup changes as a +// function of the number of entities. For best results run this in an +// otherwise empty domain. + +var firstId; +var lastId; +var NUM_ENTITIES_ON_SIDE = 25; + +// create the objects +createObjects = function () { + var STRIDE = 0.75; + var WIDTH = 0.5; + var DIMENSIONS = { x: WIDTH, y: WIDTH, z: WIDTH }; + var LIFETIME = 20; + + var properties = { + name: "", + type : "Box", + dimensions : DIMENSIONS, + position : { x: 0, y: 0, z: 0}, + lifetime : LIFETIME, + color : { red:255, green: 64, blue: 255 } + }; + + // xy + var planeName = "xy"; + for (var i = 0; i < NUM_ENTITIES_ON_SIDE; ++i) { + for (var j = 0; j < NUM_ENTITIES_ON_SIDE; ++j) { + properties.name = "Box-" + planeName + "-" + i + "." + j; + properties.position = { x: i * STRIDE, y: j * STRIDE, z: 0 }; + var red = i * 255 / NUM_ENTITIES_ON_SIDE; + var green = j * 255 / NUM_ENTITIES_ON_SIDE; + var blue = 0; + properties.color = { red: red, green: green, blue: blue }; + if (i == 0 && j == 0) { + firstId = Entities.addEntity(properties); + } else { + Entities.addEntity(properties); + } + } + } + + // yz + var planeName = "yz"; + for (var i = 0; i < NUM_ENTITIES_ON_SIDE; ++i) { + for (var j = 0; j < NUM_ENTITIES_ON_SIDE; ++j) { + properties.name = "Box-" + planeName + "-" + i + "." + j; + properties.position = { x: 0, y: i * STRIDE, z: j * STRIDE }; + var red = 0; + var green = i * 255 / NUM_ENTITIES_ON_SIDE; + var blue = j * 255 / NUM_ENTITIES_ON_SIDE; + properties.color = { red: red, green: green, blue: blue }; + Entities.addEntity(properties); + } + } + + // zx + var planeName = "zx"; + for (var i = 0; i < NUM_ENTITIES_ON_SIDE; ++i) { + for (var j = 0; j < NUM_ENTITIES_ON_SIDE; ++j) { + properties.name = "Box-" + planeName + "-" + i + "." + j; + properties.position = { x: j * STRIDE, y: 0, z: i * STRIDE }; + var red = j * 255 / NUM_ENTITIES_ON_SIDE; + var green = 0; + var blue = i * 255 / NUM_ENTITIES_ON_SIDE; + properties.color = { red: red, green: green, blue: blue }; + lastId = Entities.addEntity(properties); + } + } +}; + +createObjects(); + +// measure the time it takes to edit the first and last entities many times +// (requires a lookup by entityId each time) +changeProperties = function (id) { + var newProperties = { color : { red: 255, green: 255, blue: 255 } }; + Entities.editEntity(id, newProperties); +} + +// first +var NUM_CHANGES = 10000; +var firstStart = Date.now(); +for (var k = 0; k < NUM_CHANGES; ++k) { + changeProperties(firstId); +} +var firstEnd = Date.now(); +var firstDt = firstEnd - firstStart; + +// last +var lastStart = Date.now(); +for (var k = 0; k < NUM_CHANGES; ++k) { + changeProperties(lastId); +} +var lastEnd = Date.now(); +var lastDt = lastEnd - lastStart; + +// print the results +var numEntities = 3 * NUM_ENTITIES_ON_SIDE * NUM_ENTITIES_ON_SIDE; +print("numEntities = " + numEntities + " numEdits = " + NUM_CHANGES + " firstDt = " + firstDt + " lastDt = " + lastDt); + diff --git a/scripts/system/chat.js b/scripts/system/chat.js index d03c6aae98..58a1849f1f 100644 --- a/scripts/system/chat.js +++ b/scripts/system/chat.js @@ -370,14 +370,14 @@ // Change the avatar size to bigger. function biggerSize() { //print("biggerSize"); - logMessage("Increasing avatar size bigger!", null); + logMessage("Increasing avatar size", null); MyAvatar.increaseSize(); } // Change the avatar size to smaller. function smallerSize() { //print("smallerSize"); - logMessage("Decreasing avatar size smaler!", null); + logMessage("Decreasing avatar size", null); MyAvatar.decreaseSize(); } @@ -470,14 +470,13 @@ case '?': case 'help': - logMessage('Type "/?" or "/help" for help, which is this!', null); - logMessage('Type "/name " to set your chat name, or "/name" to use your display name, or a random name if that is not defined.', null); - logMessage('Type "/shutup" to shut up your overhead chat message.', null); - logMessage('Type "/say " to say something.', null); - logMessage('Type "/clear" to clear your cha, nullt log.', null); - logMessage('Type "/who" to ask who is h, nullere to chat.', null); - logMessage('Type "/bigger", "/smaller" or "/normal" to change, null your avatar size.', null); - logMessage('(Sorry, that\'s all there is so far!)', null); + logMessage('Type "/?" or "/help" for help', null); + logMessage('Type "/name " to set your chat name, or "/name" to use your display name. If your display name is not defined, a random name will be used.', null); + logMessage('Type "/close" to close your overhead chat message.', null); + logMessage('Type "/say " to display a new message.', null); + logMessage('Type "/clear" to clear your chat log.', null); + logMessage('Type "/who" to ask who is in the chat session.', null); + logMessage('Type "/bigger", "/smaller" or "/normal" to change your avatar size.', null); break; case 'name': @@ -498,9 +497,9 @@ } break; - case 'shutup': + case 'close': popDownSpeechBubble(); - logMessage('Overhead chat message shut up.', null); + logMessage('Overhead chat message closed.', null); break; case 'say': diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 04921fe14d..e2de13d4b1 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -187,6 +187,8 @@ var DEFAULT_GRABBABLE_DATA = { var USE_BLACKLIST = true; var blacklist = []; +var entitiesWithHoverOverlays = []; + var FORBIDDEN_GRAB_NAMES = ["Grab Debug Entity", "grab pointer"]; var FORBIDDEN_GRAB_TYPES = ["Unknown", "Light", "PolyLine", "Zone"]; @@ -1801,9 +1803,14 @@ function MyController(hand) { if (isInEditMode() && !this.isNearStylusTarget && HMD.isHandControllerAvailable()) { // Always showing lasers while in edit mode and hands/stylus is not active. + var rayPickInfo = this.calcRayPickInfo(this.hand); - this.intersectionDistance = (rayPickInfo.entityID || rayPickInfo.overlayID) ? rayPickInfo.distance : 0; - this.searchIndicatorOn(rayPickInfo.searchRay); + if (rayPickInfo.isValid) { + this.intersectionDistance = (rayPickInfo.entityID || rayPickInfo.overlayID) ? rayPickInfo.distance : 0; + this.searchIndicatorOn(rayPickInfo.searchRay); + } else { + this.searchIndicatorOff(); + } } else { this.searchIndicatorOff(); } @@ -1852,12 +1859,14 @@ function MyController(hand) { this.calcRayPickInfo = function(hand, pickRayOverride) { var pickRay; + var valid = true if (pickRayOverride) { pickRay = pickRayOverride; } else { var controllerLocation = getControllerWorldLocation(this.handToController(), true); var worldHandPosition = controllerLocation.position; var worldHandRotation = controllerLocation.orientation; + valid = !(worldHandPosition === undefined); pickRay = { origin: PICK_WITH_HAND_RAY ? worldHandPosition : Camera.position, @@ -1872,7 +1881,8 @@ function MyController(hand) { entityID: null, overlayID: null, searchRay: pickRay, - distance: PICK_MAX_DISTANCE + distance: PICK_MAX_DISTANCE, + isValid: valid }; var now = Date.now(); @@ -2201,6 +2211,15 @@ function MyController(hand) { entityPropertiesCache.addEntity(rayPickInfo.entityID); } + if (rayPickInfo.entityID && entitiesWithHoverOverlays.indexOf(rayPickInfo.entityID) == -1) { + entitiesWithHoverOverlays.forEach(function (element) { + HoverOverlay.destroyHoverOverlay(element); + }); + entitiesWithHoverOverlays = []; + HoverOverlay.createHoverOverlay(rayPickInfo.entityID); + entitiesWithHoverOverlays.push(rayPickInfo.entityID); + } + var candidateHotSpotEntities = Entities.findEntities(handPosition, MAX_EQUIP_HOTSPOT_RADIUS); entityPropertiesCache.addEntities(candidateHotSpotEntities); @@ -3763,6 +3782,11 @@ function MyController(hand) { this.release = function() { this.turnOffVisualizations(); + entitiesWithHoverOverlays.forEach(function (element) { + HoverOverlay.destroyHoverOverlay(element); + }); + entitiesWithHoverOverlays = []; + if (this.grabbedThingID !== null) { Messages.sendMessage('Hifi-Teleport-Ignore-Remove', this.grabbedThingID); diff --git a/scripts/system/edit.js b/scripts/system/edit.js index acea42237b..6bb0675bc8 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -1,5 +1,3 @@ -"use strict"; - // edit.js // // Created by Brad Hefta-Gaub on 10/2/14. @@ -16,6 +14,8 @@ Settings, Entities, Tablet, Toolbars, Messages, Menu, Camera, progressDialog, tooltip, MyAvatar, Quat, Controller, Clipboard, HMD, UndoStack, ParticleExplorerTool */ (function() { // BEGIN LOCAL_SCOPE + +"use strict"; var HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; var EDIT_TOGGLE_BUTTON = "com.highfidelity.interface.system.editButton"; @@ -41,19 +41,19 @@ Script.include([ var selectionDisplay = SelectionDisplay; var selectionManager = SelectionManager; -const PARTICLE_SYSTEM_URL = Script.resolvePath("assets/images/icon-particles.svg"); -const POINT_LIGHT_URL = Script.resolvePath("assets/images/icon-point-light.svg"); -const SPOT_LIGHT_URL = Script.resolvePath("assets/images/icon-spot-light.svg"); +var PARTICLE_SYSTEM_URL = Script.resolvePath("assets/images/icon-particles.svg"); +var POINT_LIGHT_URL = Script.resolvePath("assets/images/icon-point-light.svg"); +var SPOT_LIGHT_URL = Script.resolvePath("assets/images/icon-spot-light.svg"); entityIconOverlayManager = new EntityIconOverlayManager(['Light', 'ParticleEffect'], function(entityID) { var properties = Entities.getEntityProperties(entityID, ['type', 'isSpotlight']); if (properties.type === 'Light') { return { url: properties.isSpotlight ? SPOT_LIGHT_URL : POINT_LIGHT_URL, - } + }; } else { return { url: PARTICLE_SYSTEM_URL, - } + }; } }); @@ -94,7 +94,7 @@ selectionManager.addEventListener(function () { } }); -const KEY_P = 80; //Key code for letter p used for Parenting hotkey. +var KEY_P = 80; //Key code for letter p used for Parenting hotkey. var DEGREES_TO_RADIANS = Math.PI / 180.0; var RADIANS_TO_DEGREES = 180.0 / Math.PI; @@ -123,6 +123,8 @@ var SETTING_EASE_ON_FOCUS = "cameraEaseOnFocus"; var SETTING_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE = "showLightsAndParticlesInEditMode"; var SETTING_SHOW_ZONES_IN_EDIT_MODE = "showZonesInEditMode"; +var CREATE_ENABLED_ICON = "icons/tablet-icons/edit-i.svg"; +var CREATE_DISABLED_ICON = "icons/tablet-icons/edit-disabled.svg"; // marketplace info, etc. not quite ready yet. var SHOULD_SHOW_PROPERTY_MENU = false; @@ -130,6 +132,7 @@ var INSUFFICIENT_PERMISSIONS_ERROR_MSG = "You do not have the necessary permissi var INSUFFICIENT_PERMISSIONS_IMPORT_ERROR_MSG = "You do not have the necessary permissions to place items on this domain."; var isActive = false; +var createButton = null; var IMPORTING_SVO_OVERLAY_WIDTH = 144; var IMPORTING_SVO_OVERLAY_HEIGHT = 30; @@ -397,13 +400,15 @@ var toolBar = (function () { } }); + var createButtonIconRsrc = ((Entities.canRez() || Entities.canRezTmp()) ? CREATE_ENABLED_ICON : CREATE_DISABLED_ICON); tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); activeButton = tablet.addButton({ - icon: "icons/tablet-icons/edit-i.svg", + icon: createButtonIconRsrc, activeIcon: "icons/tablet-icons/edit-a.svg", text: "CREATE", sortOrder: 10 }); + createButton = activeButton; tablet.screenChanged.connect(function (type, url) { if (isActive && (type !== "QML" || url !== "Edit.qml")) { that.toggle(); @@ -411,7 +416,12 @@ var toolBar = (function () { }); tablet.fromQml.connect(fromQml); - activeButton.clicked.connect(function() { + createButton.clicked.connect(function() { + if ( ! (Entities.canRez() || Entities.canRezTmp()) ) { + Window.notifyEditError(INSUFFICIENT_PERMISSIONS_ERROR_MSG); + return; + } + that.toggle(); }); @@ -642,7 +652,7 @@ var toolBar = (function () { grid.setEnabled(true); propertiesTool.setVisible(true); selectionDisplay.triggerMapping.enable(); - print("starting tablet in landscape mode") + print("starting tablet in landscape mode"); tablet.landscape = true; // Not sure what the following was meant to accomplish, but it currently causes // everybody else to think that Interface has lost focus overall. fogbugzid:558 @@ -760,8 +770,38 @@ function handleOverlaySelectionToolUpdates(channel, message, sender) { } } +// Handles any edit mode updates required when domains have switched +function handleDomainChange() { + if ( (createButton === null) || (createButton === undefined) ){ + //--EARLY EXIT--( nothing to safely update ) + return; + } + + var hasRezPermissions = (Entities.canRez() || Entities.canRezTmp()); + createButton.editProperties({ + icon: (hasRezPermissions ? CREATE_ENABLED_ICON : CREATE_DISABLED_ICON), + }); +} + +function handleMessagesReceived(channel, message, sender) { + switch( channel ){ + case 'entityToolUpdates': { + handleOverlaySelectionToolUpdates( channel, message, sender ); + break; + } + case 'Toolbar-DomainChanged': { + handleDomainChange(); + break; + } + default: { + return; + } + } +} + +Messages.subscribe('Toolbar-DomainChanged'); Messages.subscribe("entityToolUpdates"); -Messages.messageReceived.connect(handleOverlaySelectionToolUpdates); +Messages.messageReceived.connect(handleMessagesReceived); var mouseHasMovedSincePress = false; var mousePressStartTime = 0; @@ -1180,6 +1220,8 @@ Script.scriptEnding.connect(function () { Messages.messageReceived.disconnect(handleOverlaySelectionToolUpdates); Messages.unsubscribe("entityToolUpdates"); + Messages.unsubscribe("Toolbar-DomainChanged"); + createButton = null; }); var lastOrientation = null; @@ -1305,7 +1347,7 @@ function unparentSelectedEntities() { if (parentId !== null && parentId.length > 0 && parentId !== "{00000000-0000-0000-0000-000000000000}") { parentCheck = true; } - Entities.editEntity(id, {parentID: null}) + Entities.editEntity(id, {parentID: null}); return true; }); if (parentCheck) { @@ -1340,7 +1382,7 @@ function parentSelectedEntities() { if (parentId !== lastEntityId) { parentCheck = true; } - Entities.editEntity(id, {parentID: lastEntityId}) + Entities.editEntity(id, {parentID: lastEntityId}); } }); @@ -1513,9 +1555,9 @@ function importSVO(importURL) { var entityPositions = []; var entityParentIDs = []; - var properties = Entities.getEntityProperties(pastedEntityIDs[0], ["type"]); + var propType = Entities.getEntityProperties(pastedEntityIDs[0], ["type"]).type; var NO_ADJUST_ENTITY_TYPES = ["Zone", "Light", "ParticleEffect"]; - if (NO_ADJUST_ENTITY_TYPES.indexOf(properties.type) === -1) { + if (NO_ADJUST_ENTITY_TYPES.indexOf(propType) === -1) { var targetDirection; if (Camera.mode === "entity" || Camera.mode === "independent") { targetDirection = Camera.orientation; @@ -1528,36 +1570,36 @@ function importSVO(importURL) { var deltaParallel = HALF_TREE_SCALE; // Distance to move entities parallel to targetDirection. var deltaPerpendicular = Vec3.ZERO; // Distance to move entities perpendicular to targetDirection. for (var i = 0, length = pastedEntityIDs.length; i < length; i++) { - var properties = Entities.getEntityProperties(pastedEntityIDs[i], ["position", "dimensions", + var curLoopEntityProps = Entities.getEntityProperties(pastedEntityIDs[i], ["position", "dimensions", "registrationPoint", "rotation", "parentID"]); var adjustedPosition = adjustPositionPerBoundingBox(targetPosition, targetDirection, - properties.registrationPoint, properties.dimensions, properties.rotation); - var delta = Vec3.subtract(adjustedPosition, properties.position); + curLoopEntityProps.registrationPoint, curLoopEntityProps.dimensions, curLoopEntityProps.rotation); + var delta = Vec3.subtract(adjustedPosition, curLoopEntityProps.position); var distance = Vec3.dot(delta, targetDirection); deltaParallel = Math.min(distance, deltaParallel); deltaPerpendicular = Vec3.sum(Vec3.subtract(delta, Vec3.multiply(distance, targetDirection)), deltaPerpendicular); - entityPositions[i] = properties.position; - entityParentIDs[i] = properties.parentID; + entityPositions[i] = curLoopEntityProps.position; + entityParentIDs[i] = curLoopEntityProps.parentID; } deltaPerpendicular = Vec3.multiply(1 / pastedEntityIDs.length, deltaPerpendicular); deltaPosition = Vec3.sum(Vec3.multiply(deltaParallel, targetDirection), deltaPerpendicular); } if (grid.getSnapToGrid()) { - var properties = Entities.getEntityProperties(pastedEntityIDs[0], ["position", "dimensions", + var firstEntityProps = Entities.getEntityProperties(pastedEntityIDs[0], ["position", "dimensions", "registrationPoint"]); - var position = Vec3.sum(deltaPosition, properties.position); - position = grid.snapToSurface(grid.snapToGrid(position, false, properties.dimensions, - properties.registrationPoint), properties.dimensions, properties.registrationPoint); - deltaPosition = Vec3.subtract(position, properties.position); + var positionPreSnap = Vec3.sum(deltaPosition, firstEntityProps.position); + position = grid.snapToSurface(grid.snapToGrid(positionPreSnap, false, firstEntityProps.dimensions, + firstEntityProps.registrationPoint), firstEntityProps.dimensions, firstEntityProps.registrationPoint); + deltaPosition = Vec3.subtract(position, firstEntityProps.position); } if (!Vec3.equal(deltaPosition, Vec3.ZERO)) { - for (var i = 0, length = pastedEntityIDs.length; i < length; i++) { - if (Uuid.isNull(entityParentIDs[i])) { - Entities.editEntity(pastedEntityIDs[i], { - position: Vec3.sum(deltaPosition, entityPositions[i]) + for (var editEntityIndex = 0, numEntities = pastedEntityIDs.length; editEntityIndex < numEntities; editEntityIndex++) { + if (Uuid.isNull(entityParentIDs[editEntityIndex])) { + Entities.editEntity(pastedEntityIDs[editEntityIndex], { + position: Vec3.sum(deltaPosition, entityPositions[editEntityIndex]) }); } } @@ -2203,7 +2245,7 @@ entityListTool.webView.webEventReceived.connect(function (data) { try { data = JSON.parse(data); } catch(e) { - print("edit.js: Error parsing JSON: " + e.name + " data " + data) + print("edit.js: Error parsing JSON: " + e.name + " data " + data); return; } diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js index 142ed6e7b6..53f88ea62d 100644 --- a/scripts/system/libraries/WebTablet.js +++ b/scripts/system/libraries/WebTablet.js @@ -76,7 +76,7 @@ function calcSpawnInfo(hand, tabletHeight) { var TABLET_RAKE_ANGLE = 30; rotation = Quat.multiply(Quat.angleAxis(TABLET_RAKE_ANGLE, Vec3.multiplyQbyV(lookAt, Vec3.UNIT_X)), lookAt); - var RELATIVE_SPAWN_OFFSET = { x: 0, y: 0.4, z: 0.05 }; + var RELATIVE_SPAWN_OFFSET = { x: 0, y: 0.6, z: 0.1 }; position = Vec3.sum(position, Vec3.multiplyQbyV(rotation, Vec3.multiply(tabletHeight, RELATIVE_SPAWN_OFFSET))); return { diff --git a/scripts/tutorials/entity_scripts/sit.js b/scripts/tutorials/entity_scripts/sit.js index 93e765910b..70456ea493 100644 --- a/scripts/tutorials/entity_scripts/sit.js +++ b/scripts/tutorials/entity_scripts/sit.js @@ -12,9 +12,9 @@ Script.include("/~/system/libraries/utils.js"); if (!String.prototype.startsWith) { String.prototype.startsWith = function(searchString, position){ - position = position || 0; - return this.substr(position, searchString.length) === searchString; - }; + position = position || 0; + return this.substr(position, searchString.length) === searchString; + }; } var SETTING_KEY = "com.highfidelity.avatar.isSitting"; @@ -122,7 +122,7 @@ this.rolesToOverride = function() { return MyAvatar.getAnimationRoles().filter(function(role) { - return role === "fly" || role.startsWith("inAir"); + return !(role.startsWith("right") || role.startsWith("left")); }); } @@ -343,7 +343,7 @@ } this.cleanupOverlay(); } - + this.clickDownOnEntity = function (id, event) { if (isInEditMode()) { return; diff --git a/tests/shaders/src/main.cpp b/tests/shaders/src/main.cpp index d10ab1ddbe..9847e9f7b9 100644 --- a/tests/shaders/src/main.cpp +++ b/tests/shaders/src/main.cpp @@ -57,8 +57,6 @@ #include #include -#include -#include #include #include @@ -172,7 +170,6 @@ void QTestWindow::draw() { testShaderBuild(skin_model_normal_map_vert, model_translucent_frag); testShaderBuild(model_shadow_vert, model_shadow_frag); - testShaderBuild(untextured_particle_vert, untextured_particle_frag); testShaderBuild(textured_particle_vert, textured_particle_frag); /* FIXME: Bring back the ssao shader tests testShaderBuild(gaussian_blur_vertical_vert, gaussian_blur_frag); diff --git a/unpublishedScripts/marketplace/camera-move/Eye-Camera.svg b/unpublishedScripts/marketplace/camera-move/Eye-Camera.svg new file mode 100644 index 0000000000..c58700823d --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/Eye-Camera.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + diff --git a/unpublishedScripts/marketplace/camera-move/_debug.js b/unpublishedScripts/marketplace/camera-move/_debug.js new file mode 100644 index 0000000000..d8c05d646e --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/_debug.js @@ -0,0 +1,133 @@ +/* eslint-env jquery, browser */ +/* eslint-disable comma-dangle, no-empty */ +/* global EventBridge: true, PARAMS, signal, assert, log, debugPrint, + bridgedSettings, _utils, jquerySettings, */ + +// helper functions for debugging and testing the UI in an external web brower +var _debug = { + handleUncaughtException: function onerror(message, fileName, lineNumber, colNumber, err) { + if (message === onerror.lastMessage) { + return; + } + onerror.lastMessage = message; + var error = (err || Error.lastError); + // var stack = error && error.stack; + var output = _utils.normalizeStackTrace(error || { message: message }); + window.console.error(['window.onerror: ', output, message]); // eslint-disable-line no-console + var errorNode = document.querySelector('#errors'), + textNode = errorNode && errorNode.querySelector('.output'); + if (textNode) { + textNode.innerText = output; + } + if (errorNode) { + errorNode.style.display = 'block'; + } + if (error){ + error.onerrored = true; + } + }, + loadScriptNodes: function loadScriptNodes(selector) { + // scripts are loaded this way to ensure that when the client script refreshes, so are the app's dependencies + [].forEach.call(document.querySelectorAll(selector), function(script) { + script.parentNode.removeChild(script); + if (script.src) { + script.src += location.search; + } + script.type = 'application/javascript'; + document.write(script.outerHTML); + }); + }, + + // TESTING MOCKs + openEventBridgeMock: function openEventBridgeMock(onEventBridgeOpened) { + var updatedValues = openEventBridgeMock.updatedValues = {}; + // emulate EventBridge's API + EventBridge = { + emitWebEvent: signal(function emitWebEvent(message){}), + scriptEventReceived: signal(function scriptEventReceived(message){}), + }; + EventBridge.emitWebEvent.connect(onEmitWebEvent); + onEventBridgeOpened(EventBridge); + assert(!bridgedSettings.onUnhandledMessage); + bridgedSettings.onUnhandledMessage = function(msg) { + log('bridgedSettings.onUnhandledMessage', msg); + return true; + }; + // manually trigger initial bootstrapping responses (that the client script would normally send) + bridgedSettings.handleExtraParams({uuid: PARAMS.uuid, ns: PARAMS.ns, extraParams: { + mock: true, + appVersion: 'browsermock', + toggleKey: { text: 'SPACE', isShifted: true }, + mode: { + toolbar: true, + browser: true, + desktop: true, + tablet: /tablet/.test(location) || /android|ipad|iphone/i.test(navigator.userAgent), + hmd: /hmd/.test(location), + }, + } }); + bridgedSettings.setValue('ui-show-advanced-options', true); + + function log(msg) { + // eslint-disable-next-line no-console + console.log.apply(console, ['[mock] ' + msg].concat([].slice.call(arguments,1))); + } + + // generate mock data in response to outgoing web page events + function onEmitWebEvent(message) { + try { + var obj = JSON.parse(message); + } catch (e) {} + if (!obj) { + // message isn't JSON so just log it and bail early + log('consuming non-callback web event', message); + return; + } + switch (obj.method) { + case 'valueUpdated': { + log('valueUpdated',obj.params); + updatedValues[obj.params[0]] = obj.params[1]; + return; + } + case 'Settings.getValue': { + var key = obj.params[0]; + var node = jquerySettings.findNodeByKey(key, true); + // log('Settings.getValue.findNodeByKey', key, node); + var type = node && (node.dataset.hifiType || node.dataset.type || node.type); + switch (type) { + case 'hifiButton': + case 'hifiCheckbox': { + obj.result = /tooltip|advanced-options/i.test(key) || PARAMS.tooltiptest ? true : Math.random() > 0.5; + } break; + case 'hifiRadioGroup': { + var radios = $(node).find('input[type=radio]').toArray(); + while (Math.random() < 0.9) { + radios.push(radios.shift()); + } + obj.result = radios[0].value; + } break; + case 'hifiSpinner': + case 'hifiSlider': { + var step = node.step || 1, precision = (1/step).toString().length - 1; + var magnitude = node.max || (precision >=1 ? Math.pow(10, precision-1) : 10); + obj.result = parseFloat((Math.random() * magnitude).toFixed(precision||1)); + } break; + default: { + log('unhandled node type for making dummy data: ' + [key, node && node.type, type, node && node.type] + ' @ ' + (node && node.id)); + obj.result = updatedValues[key] || false; + } break; + } + debugPrint('mock getValue data %c%s = %c%s', 'color:blue', + JSON.stringify(key), 'color:green', JSON.stringify(obj.result)); + } break; + default: { + log('ignoring outbound method call', obj); + } break; + } + setTimeout(function() { + EventBridge.scriptEventReceived(JSON.stringify(obj)); + }, 100); + } + }, +}; + diff --git a/unpublishedScripts/marketplace/camera-move/_json-persist.js b/unpublishedScripts/marketplace/camera-move/_json-persist.js new file mode 100644 index 0000000000..5915842d1b --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/_json-persist.js @@ -0,0 +1,130 @@ +/* eslint-env jquery, browser */ +/* eslint-disable comma-dangle, no-empty */ +/* global _utils, PARAMS, VERSION, signal, assert, log, debugPrint, + bridgedSettings, POPUP */ + +// JSON export / import helpers proto module +var SettingsJSON = (function() { + _utils.exists; + assert.exists; + + return { + setPath: setPath, + rollupPaths: rollupPaths, + encodeNodes: encodeNodes, + exportAll: exportAll, + showSettings: showSettings, + applyJSON: applyJSON, + promptJSON: promptJSON, + popupJSON: popupJSON, + }; + + function encodeNodes(resolver) { + return resolver.getAllNodes().reduce((function(out, input, i) { + //debugPrint('input['+i+']', input.id); + var id = input.id, + key = resolver.getKey(id); + //debugPrint('toJSON', id, key, input.id); + setPath(out, key.split('/'), resolver.getValue(key)); + return out; + }).bind(this), {}); + } + + function setPath(obj, path, value) { + var key = path.pop(); + obj = path.reduce(function(obj, subkey) { + return obj[subkey] = obj[subkey] || {}; + }, obj); + //debugPrint('setPath', key, Object.keys(obj)); + obj[key] = value; + } + + function rollupPaths(obj, output, path) { + path = path || []; + output = output || {}; + // log('rollupPaths', Object.keys(obj||{}), Object.keys(output), path); + for (var p in obj) { + path.push(p); + var value = obj[p]; + if (value && typeof value === 'object') { + rollupPaths(obj[p], output, path); + } else { + output[path.join('/')] = value; + } + path.pop(); + } + return output; + } + + function exportAll(resolver, name) { + var settings = encodeNodes(resolver); + Object.keys(settings).forEach(function(prop) { + if (typeof settings[prop] === 'object') { + _utils.sortedAssign(settings[prop]); + } + }); + return { + version: VERSION, + name: name || undefined, + settings: settings, + _metadata: { timestamp: new Date(), PARAMS: PARAMS, url: location.href, } + }; + } + + function showSettings(resolver, saveName) { + popupJSON(saveName || '(current settings)', Object.assign(exportAll(resolver, saveName), { + extraParams: bridgedSettings.extraParams, + })); + } + + function popupJSON(title, tmp) { + var HTML = document.getElementById('POPUP').innerHTML + .replace(/\bxx-script\b/g, 'script') + .replace('JSON', JSON.stringify(tmp, 0, 2).replace(/\n/g, '
')); + if (/WebWindowEx/.test(navigator.userAgent) ) { + bridgedSettings.sendEvent({ + method: 'overlayWebWindow', + userAgent: navigator.userAgent, + options: { + title: 'app-camera-move-export' + (title ? '::'+title : ''), + content: HTML, + }, + }); + } else { + // append a footer to the data URI so it displays cleaner in the built-in browser window that opens + var footer = '<\!-- #' + HTML.substr(0,256).replace(/./g,' ') + (title || 'Camera Move Settings'); + window.open("data:text/html;escape," + encodeURIComponent(HTML) + footer,"app-camera-move-export"); + } + } + + function applyJSON(resolver, name, tmp) { + assert(tmp && 'version' in tmp && 'settings' in tmp, 'invalid settings record: ' + JSON.stringify(tmp)); + var settings = rollupPaths(tmp.settings); + for (var p in settings) { + if (/^[.]/.test(p)) { + continue; + } + var key = resolver.getId(p, true); + if (!key) { + log('$applySettings -- skipping unregistered Settings key: ', p); + } else { + resolver.setValue(p, settings[p], name+'.settings.'+p); + } + } + } + + function promptJSON() { + var json = window.prompt('(paste JSON here)', ''); + if (!json) { + return; + } + try { + log('parsing json', json); + json = JSON.parse(json); + } catch (e) { + throw new Error('Could not parse pasted JSON: ' + e + '\n\n' + (json||'').replace(/").children(0).text(hash); + + // patch less with absolute line number reporting + options.errorReporting && options.errorReporting.$patch && options.errorReporting.$patch(); + + // to recompile inline styles (in response to onresize or when developing), + // a fresh copy of the source nodes gets swapped-in + var newNodes = lessManager.elements.clone().appendTo(document.body); + // note: refresh(reload, modifyVars, clearFileCache) + less.refresh(false, globalVars).then(function(result) { + debugPrint('less.refresh completed OK', result); + })['catch'](function(err) { + log('less ERROR:', err); + }); + var oldNodes = onViewportUpdated.lastNodes; + oldNodes && oldNodes.remove(); + onViewportUpdated.lastNodes = newNodes; + } + onViewportUpdated.lastHash = hash; + } +} diff --git a/unpublishedScripts/marketplace/camera-move/_tooltips.js b/unpublishedScripts/marketplace/camera-move/_tooltips.js new file mode 100644 index 0000000000..94206b2ef9 --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/_tooltips.js @@ -0,0 +1,169 @@ +/* eslint-env jquery, browser */ +/* eslint-disable comma-dangle, no-empty */ +/* global PARAMS, signal, assert, log, debugPrint */ + +// ---------------------------------------------------------------------------- +// manage jquery-tooltipster hover tooltips +var TooltipManager = (function(global) { + Object.assign(TooltipManager, { + BASECONFIG: { + theme: ['tooltipster-noir'], + side: ['right','top','bottom', 'left'], + updateAnimation: 'scale', + delay: [750, 1000], + distance: { right: 24, left: 8, top: 8, bottom: 8 }, + contentAsHTML: true, + }, + }); + + function TooltipManager(options) { + assert(options.elements && options.tooltips, 'TooltipManager constructor expects .elements and .tooltips'); + Object.assign(this, { + instances: [], + options: options, + config: Object.assign({}, TooltipManager.BASECONFIG, { + trigger: !options.testmode ? 'hover' : 'click', + interactive: options.testmode, + minWidth: options.viewport && options.viewport.min.width, + maxWidth: options.viewport && options.viewport.max.width, + }), + }); + options.enabled && this.initialize(); + } + + TooltipManager.prototype = { + constructor: TooltipManager, + initialize: function() { + var options = this.options, + _config = this.config, + _self = this, + candidates = $(options.elements); + + candidates.add($('button')).each(function() { + var id = this.id, + input = $(this), + tip = options.tooltips[id] || options.tooltips[input.data('for')]; + + var alreadyTipped = input.is('.tooltipstered') || input.closest('.tooltipstered').get(0); + if (alreadyTipped || !tip) { + return !tip && _debugPrint('!tooltippable -- missing tooltip for ' + (id || input.data('for') || input.text())); + } + var config = Object.assign({ content: tip }, _config); + + function mergeConfig() { + var attr = $(this).attr('data-tooltipster'), + object = $(this).data('tooltipster'); + typeof object === 'object' && Object.assign(config, object); + attr && Object.assign(config, JSON.parse(attr)); + } + try { + input.parents(':data(tooltipster),[data-tooltipster]').each(mergeConfig); + input.each(mergeConfig); // prioritize own settings + } catch(e) { + console.error('error extracting tooltipster data:' + [e, id]); + } + + var target = $(input.closest('.tooltip-target').get(0) || + (input.is('input') && input) || null); + + assert(target && target[0] && tip); + debugPrint('binding tooltip', config, target[0].nodeName, id || target[0]); + var instance = target.tooltipster(config) + .tooltipster('instance'); + + instance.on('close', function(event) { + if (options.keepopen === target) { + debugPrint(event.type, 'canceling close keepopen === target', id); + event.stop(); + options.keepopen = null; + } + }); + instance.on('before', function(event) { + debugPrint(event.type, 'before', event); + !options.testmode && _self.closeAll(); + !options.enabled && event.stop(); + }); + target.find(':focusable, input, [tabindex], button, .control') + .add(target).add(input) + .add(input.closest(':focusable, input, [tabindex]')) + .on({ + click: function(evt) { + if (input.is('button')) { + return setTimeout(instance.close.bind(instance,null),50); + } + options.keepopen = target; + }, + focus: instance.open.bind(instance, null), + blur: function(evt) { + instance.close(); _self.openFocusedTooltip(); + }, + }); + _self.instances.push(instance); + }); + return this.instances; + }, + openFocusedTooltip: function() { + if (!this.options.enabled) { + return; + } + setTimeout(function() { + if (!document.activeElement || document.activeElement === document.body || + !$(document.activeElement).closest('section')) { + return; + } + var tip = $([]) + .add($(document.activeElement)) + .add($(document.activeElement).find('.tooltipstered')) + .add($(document.activeElement).closest('.tooltipstered')) + .filter('.tooltipstered'); + if (tip.is('.tooltipstered')) { + // log('opening focused tooltip', tip.length, tip[0].id); + tip.tooltipster('open'); + } + }, 1); + }, + rapidClose: function(instance, reopen) { + if (!instance.status().open) { + return; + } + instance.elementTooltip() && $(instance.elementTooltip()).hide(); + instance.close(function() { + reopen && instance.open(); + }); + }, + openAll: function() { + $('.tooltipstered').tooltipster('open'); + }, + closeAll: function() { + $.tooltipster.instances().forEach(function(instance) { + this.rapidClose(instance); + }.bind(this)); + }, + updateViewport: function(viewport) { + var options = { + minWidth: viewport.min.width, + maxWidth: viewport.max.width, + }; + Object.assign(this.config, options); + $.tooltipster.setDefaults(options); + debugPrint('updating tooltipster options', JSON.stringify(options)); + $.tooltipster.instances().forEach(function(instance) { + instance.option('minWidth', options.minWidth); + instance.option('maxWidth', options.maxWidth); + this.rapidClose(instance, instance.status().open); + }.bind(this)); + }, + enable: function() { + this.options.enabled = true; + if (this.options.testmode) { + this.openAll(); + } + }, + disable: function() { + this.options.enabled = false; + this.closeAll(); + }, + };// prototype + + return TooltipManager; +})(this); diff --git a/unpublishedScripts/marketplace/camera-move/app-camera-move.js b/unpublishedScripts/marketplace/camera-move/app-camera-move.js new file mode 100644 index 0000000000..f58dd3d3bd --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/app-camera-move.js @@ -0,0 +1,644 @@ +// app-camera-move.js +// +// Created by Timothy Dedischew on 05/05/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +// This Client script sets up the Camera Control Tablet App, which can be used to configure and +// drive your avatar with easing/smoothing movement constraints for a less jittery filming experience. + +/* eslint-disable comma-dangle, no-empty */ +"use strict"; + +var VERSION = '0.0.1', + NAMESPACE = 'app-camera-move', + APP_HTML_URL = Script.resolvePath('app.html'), + BUTTON_CONFIG = { + text: '\nCam Drive', + icon: Script.resolvePath('Eye-Camera.svg'), + }, + DEFAULT_TOGGLE_KEY = { text: 'SPACE' }; + +var MINIMAL_CURSOR_SCALE = 0.5, + FILENAME = Script.resolvePath(''), + WANT_DEBUG = Settings.getValue(NAMESPACE + '/debug', false) + EPSILON = 1e-6; + +function log() { + print( NAMESPACE + ' | ' + [].slice.call(arguments).join(' ') ); +} + +var require = Script.require, + debugPrint = function(){}, + _debugChannel = NAMESPACE + '.stats', + overlayDebugOutput = function(){}; + +if (WANT_DEBUG) { + log('WANT_DEBUG is true; instrumenting debug support', WANT_DEBUG); + _instrumentDebug(); +} + +var _utils = require('./modules/_utils.js'), + assert = _utils.assert, + CustomSettingsApp = require('./modules/custom-settings-app/CustomSettingsApp.js'), + movementUtils = require('./modules/movement-utils.js?'+ +new Date), + configUtils = require('./modules/config-utils.js'), + AvatarUpdater = require('./avatar-updater.js'); + + +Object.assign = Object.assign || _utils.assign; + +var cameraControls, eventMapper, cameraConfig, applicationConfig; + +var DEFAULTS = { + 'namespace': NAMESPACE, + 'debug': WANT_DEBUG, + 'jitter-test': false, + 'camera-move-enabled': false, + 'thread-update-mode': movementUtils.CameraControls.SCRIPT_UPDATE, + 'fps': 90, + 'drive-mode': movementUtils.DriveModes.MOTOR, + 'use-head': true, + 'stay-grounded': true, + 'prevent-roll': true, + 'constant-delta-time': false, + 'minimal-cursor': false, + 'normalize-inputs': false, + 'enable-mouse-smooth': true, + 'translation-max-velocity': 5.50, + 'translation-ease-in': 1.25, + 'translation-ease-out': 5.50, + 'rotation-max-velocity': 90.00, + 'rotation-ease-in': 1.00, + 'rotation-ease-out': 5.50, + 'rotation-x-speed': 45, + 'rotation-y-speed': 60, + 'rotation-z-speed': 1, + 'mouse-multiplier': 1.0, + 'keyboard-multiplier': 1.0, + + 'ui-enable-tooltips': true, + 'ui-show-advanced-options': false, + + 'Avatar/Draw Mesh': true, + 'Scene/shouldRenderEntities': true, + 'Scene/shouldRenderAvatars': true, + 'Avatar/Show My Eye Vectors': false, + 'Avatar/Show Other Eye Vectors': false, +}; + +// map setting names to/from corresponding Menu and API properties +var APPLICATION_SETTINGS = { + 'Avatar/Enable Avatar Collisions': { + menu: 'Avatar > Enable Avatar Collisions', + object: [ MyAvatar, 'collisionsEnabled' ], + }, + 'Avatar/Draw Mesh': { + menu: 'Developer > Draw Mesh', + object: [ MyAvatar, 'getEnableMeshVisible', 'setEnableMeshVisible' ], + }, + 'Avatar/Show My Eye Vectors': { menu: 'Developer > Show My Eye Vectors' }, + 'Avatar/Show Other Eye Vectors': { menu: 'Developer > Show Other Eye Vectors' }, + 'Avatar/useSnapTurn': { object: [ MyAvatar, 'getSnapTurn', 'setSnapTurn' ] }, + 'Avatar/lookAtSnappingEnabled': 'lookAtSnappingEnabled' in MyAvatar && { + menu: 'Developer > Enable LookAt Snapping', + object: [ MyAvatar, 'lookAtSnappingEnabled' ] + }, + 'Scene/shouldRenderEntities': { object: [ Scene, 'shouldRenderEntities' ] }, + 'Scene/shouldRenderAvatars': { object: [ Scene, 'shouldRenderAvatars' ] }, + 'camera-move-enabled': { + get: function() { + return cameraControls && cameraControls.enabled; + }, + set: function(nv) { + cameraControls.setEnabled(!!nv); + }, + }, +}; + +var DEBUG_INFO = { + // these values are also sent to the tablet app after EventBridge initialization + appVersion: VERSION, + utilsVersion: _utils.version, + movementVersion: movementUtils.version, + configVersion: configUtils.version, + clientScript: Script.resolvePath(''), + MyAvatar: { + supportsPitchSpeed: 'pitchSpeed' in MyAvatar, + supportsYawSpeed: 'yawSpeed' in MyAvatar, + supportsLookAtSnappingEnabled: 'lookAtSnappingEnabled' in MyAvatar, + }, + Reticle: { + supportsScale: 'scale' in Reticle, + }, + protocolVersion: location.protocolVersion, +}; + +var globalState = { + // cached values from the last animation frame + previousValues: { + reset: function() { + this.pitchYawRoll = Vec3.ZERO; + this.thrust = Vec3.ZERO; + }, + }, + + // batch updates to MyAvatar/Camera properties (submitting together seems to help reduce jitter) + pendingChanges: _utils.DeferredUpdater.createGroup({ + Camera: Camera, + MyAvatar: MyAvatar, + }, { dedupe: false }), + + // current input controls' effective velocities + currentVelocities: new movementUtils.VelocityTracker({ + translation: Vec3.ZERO, + rotation: Vec3.ZERO, + zoom: Vec3.ZERO, + }), +}; + +function main() { + log('initializing...', VERSION); + + var tablet = Tablet.getTablet('com.highfidelity.interface.tablet.system'), + button = tablet.addButton(BUTTON_CONFIG); + + Script.scriptEnding.connect(function() { + tablet.removeButton(button); + button = null; + }); + + // track runtime state (applicationConfig) and Settings state (cameraConfig) + applicationConfig = new configUtils.ApplicationConfig({ + namespace: DEFAULTS.namespace, + config: APPLICATION_SETTINGS, + }); + cameraConfig = new configUtils.SettingsConfig({ + namespace: DEFAULTS.namespace, + defaultValues: DEFAULTS, + }); + + var toggleKey = DEFAULT_TOGGLE_KEY; + if (cameraConfig.getValue('toggle-key')) { + try { + toggleKey = JSON.parse(cameraConfig.getValue('toggle-key')); + } catch (e) {} + } + // monitor configuration changes / keep tablet app up-to-date + var MONITOR_INTERVAL_MS = 1000; + _startConfigationMonitor(applicationConfig, cameraConfig, MONITOR_INTERVAL_MS); + + // ---------------------------------------------------------------------------- + // set up the tablet app + log('APP_HTML_URL', APP_HTML_URL); + var settingsApp = new CustomSettingsApp({ + namespace: cameraConfig.namespace, + uuid: cameraConfig.uuid, + settingsAPI: cameraConfig, + url: APP_HTML_URL, + tablet: tablet, + extraParams: Object.assign({ + toggleKey: toggleKey, + }, getSystemMetadata(), DEBUG_INFO), + debug: WANT_DEBUG > 1, + }); + Script.scriptEnding.connect(settingsApp, 'cleanup'); + settingsApp.valueUpdated.connect(function(key, value, oldValue, origin) { + log('settingsApp.valueUpdated: '+ key + ' = ' + JSON.stringify(value) + ' (was: ' + JSON.stringify(oldValue) + ')'); + if (/tablet/i.test(origin)) { + // apply relevant settings immediately if changed from the tablet UI + if (applicationConfig.applyValue(key, value, origin)) { + log('settingsApp applied immediate setting', key, value); + } + } + }); + + // process custom eventbridge messages + settingsApp.onUnhandledMessage = function(msg) { + switch (msg.method) { + case 'window.close': { + this.toggle(false); + } break; + case 'reloadClientScript': { + log('reloadClientScript...'); + _utils.reloadClientScript(FILENAME); + } break; + case 'resetSensors': { + Menu.triggerOption('Reset Sensors'); + Script.setTimeout(function() { + MyAvatar.bodyPitch = 0; + MyAvatar.bodyRoll = 0; + MyAvatar.orientation = Quat.cancelOutRollAndPitch(MyAvatar.orientation); + }, 500); + } break; + case 'reset': { + var resetValues = {}; + // maintain current value of 'show advanced' so user can observe any advanced settings being reset + var showAdvancedKey = cameraConfig.resolve('ui-show-advanced-options'); + resetValues[showAdvancedKey] = cameraConfig.getValue(showAdvancedKey); + Object.keys(DEFAULTS).reduce(function(out, key) { + var resolved = cameraConfig.resolve(key); + out[resolved] = resolved in out ? out[resolved] : DEFAULTS[key]; + return out; + }, resetValues); + Object.keys(applicationConfig.config).reduce(function(out, key) { + var resolved = applicationConfig.resolve(key); + out[resolved] = resolved in out ? out[resolved] : applicationConfig.getValue(key); + return out; + }, resetValues); + log('restting to system defaults:', JSON.stringify(resetValues, 0, 2)); + for (var p in resetValues) { + var value = resetValues[p]; + applicationConfig.applyValue(p, value, 'reset'); + cameraConfig.setValue(p, value); + } + } break; + default: { + log('onUnhandledMessage', JSON.stringify(msg,0,2)); + } break; + } + }; + + // ---------------------------------------------------------------------------- + // set up the keyboard/mouse/controller input state manager + eventMapper = new movementUtils.MovementEventMapper({ + namespace: DEFAULTS.namespace, + mouseSmooth: cameraConfig.getValue('enable-mouse-smooth'), + mouseMultiplier: cameraConfig.getValue('mouse-multiplier'), + keyboardMultiplier: cameraConfig.getValue('keyboard-multiplier'), + eventFilter: function eventFilter(from, event, defaultFilter) { + var result = defaultFilter(from, event), + driveKeyName = event.driveKeyName; + if (!result || !driveKeyName) { + if (from === 'Keyboard.RightMouseButton') { + // let the app know when the user is mouse looking + settingsApp.syncValue('Keyboard.RightMouseButton', event.actionValue, 'eventFilter'); + } + return 0; + } + if (cameraConfig.getValue('normalize-inputs')) { + result = _utils.sign(result); + } + if (from === 'Actions.Pitch') { + result *= cameraConfig.getFloat('rotation-x-speed'); + } else if (from === 'Actions.Yaw') { + result *= cameraConfig.getFloat('rotation-y-speed'); + } + return result; + }, + }); + Script.scriptEnding.connect(eventMapper, 'disable'); + // keep track of these changes live so the controller mapping can be kept in sync + applicationConfig.register({ + 'enable-mouse-smooth': { object: [ eventMapper.options, 'mouseSmooth' ] }, + 'keyboard-multiplier': { object: [ eventMapper.options, 'keyboardMultiplier' ] }, + 'mouse-multiplier': { object: [ eventMapper.options, 'mouseMultiplier' ] }, + }); + + // ---------------------------------------------------------------------------- + // set up the top-level camera controls manager / animator + var avatarUpdater = new AvatarUpdater({ + debugChannel: _debugChannel, + globalState: globalState, + getCameraMovementSettings: getCameraMovementSettings, + getMovementState: _utils.bind(eventMapper, 'getState'), + }); + cameraControls = new movementUtils.CameraControls({ + namespace: DEFAULTS.namespace, + update: avatarUpdater, + threadMode: cameraConfig.getValue('thread-update-mode'), + fps: cameraConfig.getValue('fps'), + getRuntimeSeconds: _utils.getRuntimeSeconds, + }); + Script.scriptEnding.connect(cameraControls, 'disable'); + applicationConfig.register({ + 'thread-update-mode': { object: [ cameraControls, 'threadMode' ] }, + 'fps': { object: [ cameraControls, 'fps' ] }, + }); + + // ---------------------------------------------------------------------------- + // set up SPACEBAR for toggling camera movement mode + var spacebar = new _utils.KeyListener(Object.assign(toggleKey, { + onKeyPressEvent: function(event) { + cameraControls.setEnabled(!cameraControls.enabled); + }, + })); + Script.scriptEnding.connect(spacebar, 'disconnect'); + + // ---------------------------------------------------------------------------- + // set up ESC for resetting all drive key states + Script.scriptEnding.connect(new _utils.KeyListener({ + text: 'ESC', + onKeyPressEvent: function(event) { + if (cameraControls.enabled) { + log('ESC pressed -- resetting drive keys:', JSON.stringify({ + virtualDriveKeys: eventMapper.states, + movementState: eventMapper.getState(), + }, 0, 2)); + eventMapper.states.reset(); + MyAvatar.velocity = Vec3.ZERO; + MyAvatar.angularVelocity = Vec3.ZERO; + } + }, + }), 'disconnect'); + + // set up the tablet button to toggle the UI display + button.clicked.connect(settingsApp, function(enable) { + Object.assign(this.extraParams, getSystemMetadata()); + button.editProperties({ text: '(opening)' + BUTTON_CONFIG.text, isActive: true }); + this.toggle(enable); + }); + + settingsApp.isActiveChanged.connect(function(isActive) { + updateButtonText(); + if (Overlays.getOverlayType(HMD.tabletScreenID)) { + var fromMode = Overlays.getProperty(HMD.tabletScreenID, 'inputMode'), + inputMode = isActive ? "Mouse" : "Touch"; + log('switching HMD.tabletScreenID from inputMode', fromMode, 'to', inputMode); + Overlays.editOverlay(HMD.tabletScreenID, { inputMode: inputMode }); + } + }); + + cameraControls.modeChanged.connect(onCameraModeChanged); + + function updateButtonText() { + var lines = [ + settingsApp.isActive ? '(app open)' : '', + cameraControls.enabled ? (avatarUpdater.update.momentaryFPS||0).toFixed(2) + 'fps' : BUTTON_CONFIG.text.trim() + ]; + button && button.editProperties({ text: lines.join('\n') }); + } + + var fpsTimeout = 0; + cameraControls.enabledChanged.connect(function(enabled) { + log('enabledChanged', enabled); + button && button.editProperties({ isActive: enabled }); + if (enabled) { + onCameraControlsEnabled(); + fpsTimeout = Script.setInterval(updateButtonText, 1000); + } else { + if (fpsTimeout) { + Script.clearInterval(fpsTimeout); + fpsTimeout = 0; + } + eventMapper.disable(); + avatarUpdater._resetMyAvatarMotor({ MyAvatar: MyAvatar }); + updateButtonText(); + if (settingsApp.isActive) { + settingsApp.syncValue('Keyboard.RightMouseButton', false, 'cameraControls.disabled'); + } + } + overlayDebugOutput.overlayID && Overlays.editOverlay(overlayDebugOutput.overlayID, { visible: enabled }); + }); + + // when certain settings change we need to reset the drive systems + var resetIfChanged = [ + 'minimal-cursor', 'drive-mode', 'fps', 'thread-update-mode', + 'mouse-multiplier', 'keyboard-multiplier', + 'enable-mouse-smooth', 'constant-delta-time', + ].filter(Boolean).map(_utils.bind(cameraConfig, 'resolve')); + + cameraConfig.valueUpdated.connect(function(key, value, oldValue, origin) { + var triggerReset = !!~resetIfChanged.indexOf(key); + log('cameraConfig.valueUpdated: ' + key + ' = ' + JSON.stringify(value), '(was:' + JSON.stringify(oldValue) + ')', + 'triggerReset: ' + triggerReset); + + if (/tablet/i.test(origin)) { + if (applicationConfig.applyValue(key, value, origin)) { + log('cameraConfig applied immediate setting', key, value); + } + + } + triggerReset && cameraControls.reset(); + }); + + if (cameraConfig.getValue('camera-move-enabled')) { + cameraControls.enable(); + } + + log('DEFAULTS', JSON.stringify(DEFAULTS, 0, 2)); +} // main() + +function onCameraControlsEnabled() { + log('onCameraControlsEnabled'); + globalState.previousValues.reset(); + globalState.currentVelocities.reset(); + globalState.pendingChanges.reset(); + eventMapper.enable(); + if (cameraConfig.getValue('minimal-cursor')) { + Reticle.scale = MINIMAL_CURSOR_SCALE; + } + log('cameraConfig', JSON.stringify({ + cameraConfig: getCameraMovementSettings(), + })); +} + +// reset orientation-related values when the Camera.mode changes +function onCameraModeChanged(mode, oldMode) { + globalState.pendingChanges.reset(); + globalState.previousValues.reset(); + eventMapper.reset(); + var preventRoll = cameraConfig.getValue('prevent-roll'); + var avatarOrientation = cameraConfig.getValue('use-head') ? MyAvatar.headOrientation : MyAvatar.orientation; + if (preventRoll) { + avatarOrientation = Quat.cancelOutRollAndPitch(avatarOrientation); + } + switch (Camera.mode) { + case 'mirror': + case 'entity': + case 'independent': + globalState.currentVelocities.reset(); + break; + default: + Camera.position = MyAvatar.position; + Camera.orientation = avatarOrientation; + break; + } + MyAvatar.orientation = avatarOrientation; + if (preventRoll) { + MyAvatar.headPitch = MyAvatar.headRoll = 0; + } +} + +// consolidate and normalize cameraConfig settings +function getCameraMovementSettings() { + return { + epsilon: EPSILON, + debug: cameraConfig.getValue('debug'), + jitterTest: cameraConfig.getValue('jitter-test'), + driveMode: cameraConfig.getValue('drive-mode'), + threadMode: cameraConfig.getValue('thread-update-mode'), + fps: cameraConfig.getValue('fps'), + useHead: cameraConfig.getValue('use-head'), + stayGrounded: cameraConfig.getValue('stay-grounded'), + preventRoll: cameraConfig.getValue('prevent-roll'), + useConstantDeltaTime: cameraConfig.getValue('constant-delta-time'), + + collisionsEnabled: applicationConfig.getValue('Avatar/Enable Avatar Collisions'), + mouseSmooth: cameraConfig.getValue('enable-mouse-smooth'), + mouseMultiplier: cameraConfig.getValue('mouse-multiplier'), + keyboardMultiplier: cameraConfig.getValue('keyboard-multiplier'), + + rotation: _getEasingGroup(cameraConfig, 'rotation'), + translation: _getEasingGroup(cameraConfig, 'translation'), + zoom: _getEasingGroup(cameraConfig, 'zoom'), + }; + + // extract a single easing group (translation, rotation, or zoom) from cameraConfig + function _getEasingGroup(cameraConfig, group) { + var multiplier = 1.0; + if (group === 'zoom') { + // BoomIn / TranslateCameraZ support is only partially plumbed -- for now use scaled translation easings + group = 'translation'; + multiplier = 0.001; + } + return { + easeIn: cameraConfig.getFloat(group + '-ease-in'), + easeOut: cameraConfig.getFloat(group + '-ease-out'), + maxVelocity: multiplier * cameraConfig.getFloat(group + '-max-velocity'), + speed: Vec3.multiply(multiplier, { + x: cameraConfig.getFloat(group + '-x-speed'), + y: cameraConfig.getFloat(group + '-y-speed'), + z: cameraConfig.getFloat(group + '-z-speed') + }), + }; + } +} + +// monitor and sync Application state -> Settings values +function _startConfigationMonitor(applicationConfig, cameraConfig, interval) { + return Script.setInterval(function monitor() { + var settingNames = Object.keys(applicationConfig.config); + settingNames.forEach(function(key) { + applicationConfig.resyncValue(key); // align Menus <=> APIs + var value = cameraConfig.getValue(key), + appValue = applicationConfig.getValue(key); + if (appValue !== undefined && String(appValue) !== String(value)) { + log('applicationConfig -> cameraConfig', + key, [typeof appValue, appValue], '(was:'+[typeof value, value]+')'); + cameraConfig.setValue(key, appValue); // align Application <=> Settings + } + }); + }, interval); +} + +// ---------------------------------------------------------------------------- +// DEBUG overlay support (enable by setting app-camera-move/debug = true in settings +// ---------------------------------------------------------------------------- +function _instrumentDebug() { + debugPrint = log; + var cacheBuster = '?' + new Date().getTime().toString(36); + require = Script.require(Script.resolvePath('./modules/_utils.js') + cacheBuster).makeDebugRequire(Script.resolvePath('.')); + APP_HTML_URL += cacheBuster; + overlayDebugOutput = _createOverlayDebugOutput({ + lineHeight: 12, + font: { size: 12 }, + width: 250, height: 800 }); + // auto-disable camera move mode when debugging + Script.scriptEnding.connect(function() { + cameraConfig && cameraConfig.setValue('camera-move-enabled', false); + }); +} + +function _fixedPrecisionStringifiyFilter(key, value, object) { + if (typeof value === 'object' && value && 'w' in value) { + return Quat.safeEulerAngles(value); + } else if (typeof value === 'number') { + return value.toFixed(4)*1; + } + return value; +} + +function _createOverlayDebugOutput(options) { + options = require('./modules/_utils.js').assign({ + x: 0, y: 0, width: 500, height: 800, visible: false + }, options || {}); + options.lineHeight = options.lineHeight || Math.round(options.height / 36); + options.font = options.font || { size: Math.floor(options.height / 36) }; + overlayDebugOutput.overlayID = Overlays.addOverlay('text', options); + + Messages.subscribe(_debugChannel); + Messages.messageReceived.connect(onMessageReceived); + + Script.scriptEnding.connect(function() { + Overlays.deleteOverlay(overlayDebugOutput.overlayID); + Messages.unsubscribe(_debugChannel); + Messages.messageReceived.disconnect(onMessageReceived); + }); + function overlayDebugOutput(output) { + var text = JSON.stringify(output, _fixedPrecisionStringifiyFilter, 2); + if (text !== overlayDebugOutput.lastText) { + overlayDebugOutput.lastText = text; + Overlays.editOverlay(overlayDebugOutput.overlayID, { text: text }); + } + } + function onMessageReceived(channel, message, ssend, local) { + if (local && channel === _debugChannel) { + overlayDebugOutput(JSON.parse(message)); + } + } + return overlayDebugOutput; +} + +// ---------------------------------------------------------------------------- +_patchCameraModeSetting(); +function _patchCameraModeSetting() { + // FIXME: looks like the Camera API suffered a regression where Camera.mode = 'first person' or 'third person' + // no longer works from the API; setting via Menu items still seems to work though. + Camera.$setModeString = Camera.$setModeString || function(mode) { + // 'independent' => "Independent Mode", 'first person' => 'First Person', etc. + var cameraMenuItem = (mode+'') + .replace(/^(independent|entity)$/, '$1 mode') + .replace(/\b[a-z]/g, function(ch) { + return ch.toUpperCase(); + }); + + log('working around Camera.mode bug by enabling the menuItem:', cameraMenuItem); + Menu.setIsOptionChecked(cameraMenuItem, true); + }; +} + +function getSystemMetadata() { + var tablet = Tablet.getTablet('com.highfidelity.interface.tablet.system'); + return { + mode: { + hmd: HMD.active, + desktop: !HMD.active, + toolbar: Uuid.isNull(HMD.tabletID), + tablet: !Uuid.isNull(HMD.tabletID), + }, + tablet: { + toolbarMode: tablet.toolbarMode, + desktopScale: Settings.getValue('desktopTabletScale'), + hmdScale: Settings.getValue('hmdTabletScale'), + }, + window: { + width: Window.innerWidth, + height: Window.innerHeight, + }, + desktop: { + width: Desktop.width, + height: Desktop.height, + }, + }; +} + +// ---------------------------------------------------------------------------- +main(); + +if (typeof module !== 'object') { + // if uncaught exceptions occur, show the first in an alert with option to stop the script + Script.unhandledException.connect(function onUnhandledException(error) { + Script.unhandledException.disconnect(onUnhandledException); + log('UNHANDLED EXCEPTION', error, error && error.stack); + try { + cameraControls.disable(); + } catch (e) {} + // show the error message and first two stack entries + var trace = _utils.normalizeStackTrace(error); + var message = [ error ].concat(trace.split('\n').slice(0,2)).concat('stop script?').join('\n'); + Window.confirm('app-camera-move error: ' + message.substr(0,256)) && Script.stop(); + }); +} diff --git a/unpublishedScripts/marketplace/camera-move/app.html b/unpublishedScripts/marketplace/camera-move/app.html new file mode 100644 index 0000000000..100a8237b5 --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/app.html @@ -0,0 +1,1340 @@ + + + + Camera Move + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Camera Move

+ +
...
+
+
+ +
+
+

Translation

+
+ + + + avatar walking/flying speed limit (ie: the value eased-into) + +
+
+ +
+ +
    +
  • lower values gently ramp-into moving
  • +
  • higher values rapidly accelerate to top speed
  • +
+
+
+ +
+ +
    +
  • lower values bring movement to a rolling stop
  • +
  • higher values stop movements more immediately
  • +
+
+
+ +
+ +
+

Rotation

+
+ + + look up/down (pitch) and turn left/right (yaw) speed limit +
+
+ +
+ +
    +
  • lower values gently start turning or looking up/down
  • +
  • higher values quickly turn and look around
  • +
+
+
+ +
+ +
    +
  • lower values bring turning/looking to a rolling stop
  • +
  • higher values stop turning/looking more immediately
  • +
+
+
+ +
+ +
+

Options

+
+ + + + +
+
+ + + +
+ +
+
+ +
+
+
keybinding:  
+
+
+ +
+
+
+
+ + + + + + + + + + + +
+ + diff --git a/unpublishedScripts/marketplace/camera-move/app.js b/unpublishedScripts/marketplace/camera-move/app.js new file mode 100644 index 0000000000..0546ead02a --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/app.js @@ -0,0 +1,613 @@ +// app.js -- support functions + +/* eslint-env console, jquery, browser, shared-node-browser */ +/* eslint-disable comma-dangle */ +/* global Mousetrap, TooltipManager, SettingsJSON, PARAMS, signal, assert, log, debugPrint */ +// ---------------------------------------------------------------------------- + +var viewportUpdated, bridgedSettings, jquerySettings, tooltipManager, lessManager; + +function setupUI() { + $('#debug-menu button, footer button') + .hifiButton({ + create: function() { + $(this).addClass('tooltip-target') + .data('tooltipster', { side: ['top','bottom'] }); + } + }); + + var $json = SettingsJSON; + + window.buttonHandlers = { + 'test-event-bridge': function() { + log('bridgedSettings.eventBridge === Window.EventBridge', bridgedSettings.eventBridge === window.EventBridge); + bridgedSettings.sendEvent({ method: 'test-event-bridge' }); + EventBridge.emitWebEvent('EventBridge.emitWebEvent: testing 1..2..3..'); + }, + 'page-reload': function() { + log('triggering location.reload'); + location.reload(); + }, + 'script-reload': function() { + log('triggering script.reload'); + bridgedSettings.sendEvent({ method: 'reloadClientScript' }); + }, + 'reset-sensors': function() { + log('resetting avatar orientation'); + bridgedSettings.sendEvent({ method: 'resetSensors' }); + }, + 'reset-to-defaults': function() { + tooltipManager && tooltipManager.closeAll(); + document.activeElement && document.activeElement.blur(); + document.body.focus(); + setTimeout(function() { + bridgedSettings.sendEvent({ method: 'reset' }); + },1); + }, + 'copy-json': $json.showSettings.bind($json, jquerySettings, null), + 'paste-json': function() { + $json.applyJSON( + jquerySettings, + 'pasted', + $json.promptJSON() + ); + }, + 'toggle-advanced-options': function(evt) { + var checkbox = $(this).hifiButton('instance').checkbox; + var on = checkbox.value(!checkbox.value()); + $('body').toggleClass('ui-show-advanced-options', on); + evt.stopPropagation(); + evt.preventDefault(); + if ($(this).is('.tooltipstered')) { + $(this).tooltipster('instance').content((on ? 'hide' : 'show') + ' advanced options'); + } + if (checkbox.value()) { + $('.scrollable').delay(100).animate({ + scrollTop: innerHeight - $('header').innerHeight() - 24 + }, 1500); + } + }, + 'appVersion': function(evt) { + evt.shiftKey && $json.showSettings(jquerySettings); + }, + 'errors': function(evt) { + $(evt.target).is('button') && $(this).find('.output').text('').end().hide(); + }, + }; + buttonHandlers['button-toggle-advanced-options'] = + buttonHandlers['toggle-advanced-options']; + Object.keys(window.buttonHandlers).forEach(function(p) { + $('#' + p).on('click', window.buttonHandlers[p]); + }); + + // ---------------------------------------------------------------- + // trim whitespace in labels + $('label').contents().filter(function() { + if (this.nodeType !== window.Node.TEXT_NODE) { + return false; + } + this.textContent = this.textContent.trim(); + return !this.textContent.length; + }).remove(); + + var settingsNodes = $('fieldset,button[data-for],input:not([type=radio])'); + settingsNodes.each(function() { + // set up the bidirectional mapping between DOM and Settings + jquerySettings.registerNode(this); + }); + + var spinnerOptions = { + disabled: true, + create: function() { + var input = $(this), + key = assert(jquerySettings.getKey(input.data('for'))); + + var options = input.hifiSpinner('instance').options; + options.min = options.min || 0.0; + + bridgedSettings.getValueAsync(key, function(err, result) { + input.filter(':not([data-autoenable=false])').hifiSpinner('enable'); + jquerySettings.setValue(key, result); + }); + }, + }; + + $( ".rows > label" ).each(function() { + var label = $(this), + input = label.find('input'), + type = input.data('type') || input.attr('type'); + label.wrap('
').parent().addClass(['hifi-'+type, type, 'row'].join(' ')) + .on('focus.row, click.row, hover.row', function() { + $(this).find('.tooltipstered').tooltipster('open'); + }); + }); + + debugPrint('initializing hifiSpinners'); + // numeric settings + $( ".number.row" ) + .find( "input[data-type=number]" ) + .addClass('setting') + .hifiSpinner(spinnerOptions); + + // radio groups settings + $( ".radio.rows" ) + .find('label').addClass('tooltip-target').end() + .addClass('setting') + .hifiRadioGroup({ + direction: 'vertical', + disabled: true, + create: function() { + assert(this !== window); + var group = $(this), id = this.id; + var key = assert(jquerySettings.getKey(group.data('for'))); + + bridgedSettings.getValueAsync(key, function(err, result) { + debugPrint('> GOT RADIO', key, id, result); + group.filter(':not([data-autoenable=false])').hifiRadioGroup('enable'); + jquerySettings.setValue(key, result); + group.change(); + }); + }, + }) + + // checkbox settings + $( "input[type=checkbox]" ) + .addClass('setting') + .hifiCheckbox({ + disabled: true, + create: function() { + var key = assert(jquerySettings.getKey(this.id)), + input = $(this); + input.closest('label').addClass('tooltip-target'); + bridgedSettings.getValueAsync(key, function(err, result) { + input.filter(':not([data-autoenable=false])').hifiCheckbox('enable'); + jquerySettings.setValue(key, result); + }); + }, + }); + + // slider + numeric settings + // use the whole row as a tooltip target + $( ".slider.row" ).addClass('tooltip-target').data('tooltipster', { + distance: -20, + side: ['top', 'bottom'], + }).each(function(ent) { + var element = $(this).find( ".control" ), + input = $(this).find('input'), + id = input.prop('id'), + key = assert(jquerySettings.getKey(id)); + + var commonOptions = { + disabled: true, + min: parseFloat(input.prop('min') || 0), + max: parseFloat(input.prop('max') || 10), + step: parseFloat(input.prop('step') || 0.01), + autoenable: input.data('autoenable') !== 'false', + }; + debugPrint('commonOptions', commonOptions); + + // see: https://api.jqueryui.com/slider/ for more options + var slider = element.hifiSlider(Object.assign({ + orientation: "horizontal", + range: "min", + animate: 'fast', + value: 0.0 + }, commonOptions)).hifiSlider('instance'); + + debugPrint('initializing hifiSlider->hifiSpinner'); + // setup chrome up/down arrow steps and propagate input field -> slider + var spinner = input.on('change', function() { + var value = spinner.value(); + if (isFinite(value) && slider.value() !== value) { + slider.value(value); + } + }).addClass('setting') + .hifiSpinner( + Object.assign({}, commonOptions, { max: 1e4 }) + ).hifiSpinner('instance'); + + bridgedSettings.getValueAsync(key, function(err, result) { + slider.options.autoenable !== false && slider.enable(); + spinner.options.autoenable !== false && spinner.enable(); + spinner.value(result); + }); + }); + + $('#fps').hifiSpinner(spinnerOptions).closest('.row').css('pointer-events', 'all').on('click.subinput', function(evt) { + jquerySettings.setValue('thread-update-mode', 'requestAnimationFrame'); + evt.target.focus(); + }); + // detect invalid numbers entered into spinner fields + $(':ui-hifiSpinner').on('change.validation', function(evt) { + var spinner = $(this).hifiSpinner('instance'); + $(this).closest('.row').toggleClass('invalid', !spinner.isValid()); + }); + + // ---------------------------------------------------------------------------- + // allow tabbing between checkboxes using the container row + $(':ui-hifiCheckbox,:ui-hifiRadioButton').prop('tabindex', -1).closest('.row').prop('tabindex', 0); + + + // select the input field text when first focused + $('input').not('input[type=radio],input[type=checkbox]').on('focus', function () { + var dt = (new Date - this.blurredAt); + if (!(dt < 5)) { // debounce + this.blurredAt = +new Date; + $(this).one('mouseup.selectit', function() { + $(this).select(); + return false; + }).select(); + } + }).on('blur', function(e) { + this.blurredAt = new Date; + }); + + // monitor changes to specific settings that affect the UI + var monitors = { + // advanced options toggle + 'ui-show-advanced-options': function onChange(value) { + function handle(err, result) { + log('** ui-show-advanced-options updated', result+''); + $('body').toggleClass('ui-show-advanced-options', !!result); + jquerySettings.setValue('ui-show-advanced-options', result) + } + if (!onChange.fetched) { + bridgedSettings.getValueAsync('ui-show-advanced-options', handle); + return onChange.fetched = true; + } + handle(null, value); + }, + + // UI tooltips toggle + 'ui-enable-tooltips': function(value) { + if (!tooltipManager) return; + if (value) { + tooltipManager.enable(); + tooltipManager.openFocusedTooltip(); + } else { + tooltipManager.disable(); + } + }, + + // enable/disable fps field (based on whether thread mode is requestAnimationFrame) + 'thread-update-mode': function(value) { + var enabled = (value === 'requestAnimationFrame'), fps = $('#fps'); + fps.hifiSpinner(enabled ? 'enable' : 'disable'); + fps.closest('.row').toggleClass('disabled', !enabled); + }, + + // flag BODY with CSS class to indicate active camera move mode + 'camera-move-enabled': function(value) { + $('body').toggleClass('camera-move-enabled', value); + }, + + // update the "keybinding" and #appVersion extraParams displays + '.extraParams': function extraParams(value, other) { + value = bridgedSettings.extraParams; + if (value.mode) { + for (var p in value.mode) { + // tablet-mode, hmd-mode, etc. + $('body').toggleClass(p + '-mode', value.mode[p]); + } + document.oncontextmenu = value.mode.tablet ? function(evt) { return evt.preventDefault(),false; } : null; + $('[data-type=number]').prop('type', value.mode.tablet ? 'number' : 'text'); + } + var versionDisplay = [ + value.appVersion || '(unknown appVersion)', + PARAMS.debug && '(debug)', + value.mode && value.mode.tablet ? '(tablet)' : '', + ].filter(Boolean).join(' | '); + $('#appVersion').find('.output').text(versionDisplay).end().show(); + + if (value.toggleKey) { + $('#toggleKey').find('.binding').empty() + .append(getKeysHTML(value.toggleKey)).end().show(); + } + + var activateLookAtOption = value.MyAvatar && value.MyAvatar.supportsLookAtSnappingEnabled; + $(jquerySettings.findNodeByKey('Avatar/lookAtSnappingEnabled')) + .hifiCheckbox(activateLookAtOption ? 'enable' : 'disable') + .closest('.row').toggleClass('disabled', !activateLookAtOption) + .css('pointer-events', 'all') // so tooltips display regardless + + var activateCursorOption = value.Reticle && value.Reticle.supportsScale; + $('#minimal-cursor:ui-hifiCheckbox') + .hifiCheckbox(activateCursorOption ? 'enable' : 'disable') + .closest('.row').toggleClass('disabled', !activateCursorOption) + .css('pointer-events', 'all') // so tooltips display regardless + }, + + // gray out / ungray out page content if user is mouse looking around in Interface + // (otherwise the cursor still interacts with web content...) + 'Keyboard.RightMouseButton': function(localValue, key, value) { + debugPrint(localValue, '... Keyboard.RightMouseButton:' + value); + window.active = !value; + }, + }; + monitors['toggle-advanced-options'] = monitors['ui-toggle-advanced-options']; + monitorSettings(monitors); + // disable selection + // $('input').css('-webkit-user-select', 'none'); + + viewportUpdated.connect(lessManager, 'onViewportUpdated'); + + setupTooltips(); + + $(window).trigger('resize'); // populate viewport + + // set up DOM MutationObservers + settingsNodes.each(function tcobo() { + if (this.dataset.hifiType === 'hifiButton') { + return; + } + var id = assert(this.dataset['for'] || this.id, 'could not id for node: ' + this.outerHTML); + assert(!tcobo[id]); // detect dupes + tcobo[id] = true; + debugPrint('OBSERVING NODE', id, this.id || this.getAttribute('for')); + jquerySettings.observeNode(this); + }); + + // set up key bindings + setupMousetrapKeys(); + + function getKeysHTML(binding) { + var text = binding.text || ('(#' + binding.key + ')'); + // translate hifi's proprietary key scheme into human-friendly KBDs + return [ 'Control', 'Meta', 'Alt', 'Super', 'Menu', 'Shifted' ] + .map(function(flag) { + return binding['is' + flag] && flag; + }) + .concat(text) + .filter(Boolean) + .map(function(key) { + return '' + key.replace('Shifted','Shift') + ''; + }) + .join('-'); + } +} // setupUI + +function setupTooltips() { + // extract the tooltip captions + var tooltips = window.tooltips = {}; + var target = '[id], [data-for], [for]'; + $('.tooltip') + .removeClass('tooltip').addClass('x-tooltip') + .each(function() { + var element = $(this), + input = $(element.parent().find('input').get(0) || + element.closest('button').get(0)); + id = element.prop('id') || element.data('for') || + input.prop('id') || input.data('for'); + assert(id); + tooltips[id] = this.outerHTML; + }).hide(); + tooltipManager = new TooltipManager({ + enabled: false, + testmode: PARAMS.tooltiptest, + viewport: PARAMS.viewport, + tooltips: tooltips, + elements: '#reset-to-defaults, button, input', + }); + viewportUpdated.connect(tooltipManager, 'updateViewport'); + // tooltips aren't needed right away, so defer initializing for better page load times + window.setTimeout(tooltipManager.initialize.bind(tooltipManager), 1000); +} + +// helper for instrumenting local jquery onchange handlers +function monitorSettings(options) { + return Object.keys(options).reduce(function(out, id) { + var key = bridgedSettings.resolve(id), + domId = jquerySettings.getId(key, true); + + if (!domId) { + var placeholder = { + id: id, + type: 'placeholder', + toString: function() { + return this.id; + }, + value: undefined, + }; + jquerySettings.registerSetting(placeholder, key); + debugPrint('registered placeholder value for setting', id, key); + assert(jquerySettings.findNodeByKey(key) === placeholder); + } + + // if (domId === 'toggle-advanced-options') alert([key,id,domId, jquerySettings.findNodeByKey(key)]) + assert(function assertion(){ + return typeof key === 'string'; + }, 'monitorSettings -- received invalid key type'); + + var context = { + id: id, + key: key, + domId: domId, + options: options, + lastValue: undefined, + initializer: function(hint) { + var key = this.key, + lastValue = this.lastValue; + if (lastValue !== undefined) { + return log('skipping repeat initializer', key, hint); + } + this.lastValue = lastValue = jquerySettings.getValue(key); + this._onChange.call(jquerySettings, key, lastValue, undefined, hint); + }, + _onChange: function _onChange(key, value) { + var currentValue = this.getValue(context.id), + jsonCurrentValue = JSON.stringify(currentValue); + + if (jsonCurrentValue === context.jsonLastValue) { + if (jsonCurrentValue !== undefined) { + debugPrint([context.key, '_onChange', this, 'not triggering _onChange for duplicated value']); + } + return; + } + context.jsonLastValue = jsonCurrentValue; + var args = [].slice.call(arguments, 0); + debugPrint('monitorSetting._onChange', context.key, value, [].concat(args).pop()); + context.options[context.id].apply(this, [ currentValue ].concat(args)); + }, + + onValueReceived: function(key) { + if (key === this.key) { + this._onChange.apply(bridgedSettings, arguments); + } + }, + onMutationEvent: function(event) { + if (event.key === this.key) { + context._onChange.call(jquerySettings, event.key, event.value, event.oldValue, event.hifiType+':mutation'); + } + }, + onPendingRequestsFinished: function onPendingRequestsFinished() { + bridgedSettings.pendingRequestsFinished.disconnect(this, 'onPendingRequestsFinished'); + this.initializer('pendingRequestsFinished'); + }, + }; + + bridgedSettings.valueReceived.connect(context, 'onValueReceived'); + jquerySettings.mutationEvent.connect(context, 'onMutationEvent'); + + if (bridgedSettings.pendingRequestCount()) { + bridgedSettings.pendingRequestsFinished.connect(context, 'onPendingRequestsFinished'); + } else { + window.setTimeout(context.initializer.bind(context, 'monitorSettings init'), 1); + } + return context; + }, {}); +} + +function initializeDOM() { + + Object.defineProperty(window, 'active', { + get: function() { + return window._active; + }, + set: function(nv) { + nv = !!nv; + window._active = nv; + debugPrint('window.active == ' + nv); + if (!nv) { + document.activeElement && document.activeElement.blur(); + document.body.focus(); + tooltipManager && tooltipManager.disable(); + debugPrint('TOOLTIPS DISABLED'); + } else if (tooltipManager && bridgedSettings) { + if (bridgedSettings.getValue('ui-enable-tooltips')){ + tooltipManager.enable(); + debugPrint('TOOLTIPS RE-ENABLED'); + } + } + $('body').toggleClass('active', window._active); + }, + }); + $('body').toggleClass('active', window._active = true); + + function checkAnim(evt) { + if (!checkAnim.disabled) { + if ($('.scrollable').is(':animated')) { + $('.scrollable').stop(); + log(evt.type, 'stop animation'); + } + } + } + viewportUpdated = signal(function viewportUpdated(viewport) {}); + function triggerViewportUpdate() { + var viewport = { + inner: { width: innerWidth, height: innerHeight }, + client: { + width: document.body.clientWidth || window.innerWidth, + height: document.body.clientHeight || window.innerHeight, + }, + min: { width: window.innerWidth / 3, height: 32 }, + max: { width: window.innerWidth * 7/8, height: window.innerHeight * 7/8 }, + }; + debugPrint('viewportUpdated', viewport); + PARAMS.viewport = Object.assign(PARAMS.viewport||{}, viewport); + viewportUpdated(viewport, triggerViewportUpdate.lastViewport); + triggerViewportUpdate.lastViewport = viewport; + } + $(window).on({ + resize: function resize() { + window.clearTimeout(resize.to); + resize.to = window.setTimeout(triggerViewportUpdate, 100); + }, + mousedown: checkAnim, mouseup: checkAnim, scroll: checkAnim, wheel: checkAnim, + blur: function() { + log('** BLUR ** '); + $('body').addClass('window-blurred'); + document.body.focus(); + document.activeElement && document.activeElement.blur(); + // tooltipManager.closeAll(); + }, + focus: function() { + log('** FOCUS **'); + $('body').removeClass('window-blurred'); + }, + }); +} + +function setupMousetrapKeys() { + if (!window.Mousetrap) { + return log('WARNING: window.Mousetrap not found; not configurating keybindings'); + } + mousetrapMultiBind({ + 'ctrl+a, option+a': function global(evt, combo) { + $(document.activeElement).filter('input').select(); + }, + 'enter': function global(evt, combo) { + var node = document.activeElement; + if ($(node).is('input')) { + log('enter on input element'); + tooltipManager.closeAll(); + node.blur(); + var nexts = $('[tabindex],input,:focusable').not('[tabindex=-1],.ui-slider-handle'); + nexts.add(nexts.find('input')); + nexts = nexts.toArray(); + if (~nexts.indexOf(node)) { + var nextActive = nexts[nexts.indexOf(node)+1]; + log('setting focus to', nextActive); + $(nextActive).focus(); + } else { + log('could not deduce next tabbable element', nexts.length, this); + } + } + return true; + }, + 'ctrl+w': bridgedSettings.sendEvent.bind(bridgedSettings, { method: 'window.close' }), + 'r': location.reload.bind(location), + 'space': function global(evt, combo) { + log('SPACE', evt.target, document.activeElement); + $(document.activeElement).filter('.row').find(':ui-hifiCheckbox,:ui-hifiRadioButton').click(); + if (!$(document.activeElement).is('input,.ui-widget')) { + return false; + } + return true; + }, + }); + // $('input').addClass('mousetrap'); + function mousetrapMultiBind(a, b) { + var obj = typeof a === 'object' ? a : + Object.defineProperty({}, a, {enumerable: true, value: b }); + Object.keys(obj).forEach(function(key) { + var method = obj[key].name === 'global' ? 'bindGlobal' : 'bind'; + key.split(/\s*,\s*/).forEach(function(combo) { + debugPrint('Mousetrap', method, combo, typeof obj[key]); + Mousetrap[method](combo, function(evt, combo) { + debugPrint('Mousetrap', method, combo); + return obj[key].apply(this, arguments); + }); + }); + }); + } +} + +// support the URL having a #node-id (or #debug=1&node-id) hash fragment jumping to that element +function jumpToAnchor(id) { + id = JSON.stringify(id); + $('[id='+id+'],[name='+id+']').first().each(function() { + log('jumpToAnchor', id); + $(this).show(); + this.scrollIntoView({ behavior: 'smooth' }); + }); +}; diff --git a/unpublishedScripts/marketplace/camera-move/avatar-updater.js b/unpublishedScripts/marketplace/camera-move/avatar-updater.js new file mode 100644 index 0000000000..5a91a0e8a8 --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/avatar-updater.js @@ -0,0 +1,268 @@ +/* eslint-env commonjs */ +// ---------------------------------------------------------------------------- + +// helper module that performs the avatar movement update calculations + +module.exports = AvatarUpdater; + +var _utils = require('./modules/_utils.js'), + assert = _utils.assert; + +var movementUtils = require('./modules/movement-utils.js'); + +function AvatarUpdater(options) { + options = options || {}; + assert(function assertion() { + return typeof options.getCameraMovementSettings === 'function' && + typeof options.getMovementState === 'function' && + options.globalState; + }); + + var DEFAULT_MOTOR_TIMESCALE = 1e6; // a large value that matches Interface's default + var EASED_MOTOR_TIMESCALE = 0.01; // a small value to make Interface quickly apply MyAvatar.motorVelocity + var EASED_MOTOR_THRESHOLD = 0.1; // above this speed (m/s) EASED_MOTOR_TIMESCALE is used + var ACCELERATION_MULTIPLIERS = { translation: 1, rotation: 1, zoom: 1 }; + var STAYGROUNDED_PITCH_THRESHOLD = 45.0; // degrees; ground level is maintained when pitch is within this threshold + var MIN_DELTA_TIME = 0.0001; // to avoid math overflow, never consider dt less than this value + var DEG_TO_RAD = Math.PI / 180.0; + update.frameCount = 0; + update.endTime = update.windowStartTime = _utils.getRuntimeSeconds(); + update.windowFrame = update.windowStartFrame= 0; + + this.update = update; + this.options = options; + this._resetMyAvatarMotor = _resetMyAvatarMotor; + this._applyDirectPitchYaw = _applyDirectPitchYaw; + + var globalState = options.globalState; + var getCameraMovementSettings = options.getCameraMovementSettings; + var getMovementState = options.getMovementState; + var _debugChannel = options.debugChannel; + function update(dt) { + update.frameCount++; + var startTime = _utils.getRuntimeSeconds(); + var settings = getCameraMovementSettings(), + EPSILON = settings.epsilon; + + var independentCamera = Camera.mode === 'independent', + headPitch = MyAvatar.headPitch; + + var actualDeltaTime = startTime - update.endTime, + practicalDeltaTime = Math.max(MIN_DELTA_TIME, actualDeltaTime), + deltaTime; + + if (settings.useConstantDeltaTime) { + deltaTime = settings.threadMode === movementUtils.CameraControls.ANIMATION_FRAME ? + (1 / settings.fps) : (1 / 90); + } else if (settings.threadMode === movementUtils.CameraControls.SCRIPT_UPDATE) { + deltaTime = dt; + } else { + deltaTime = practicalDeltaTime; + } + + var orientationProperty = settings.useHead ? 'headOrientation' : 'orientation', + currentOrientation = independentCamera ? Camera.orientation : MyAvatar[orientationProperty], + currentPosition = MyAvatar.position; + + var previousValues = globalState.previousValues, + pendingChanges = globalState.pendingChanges, + currentVelocities = globalState.currentVelocities; + + var movementState = getMovementState({ update: deltaTime }), + targetState = movementUtils.applyEasing(deltaTime, 'easeIn', settings, movementState, ACCELERATION_MULTIPLIERS), + dragState = movementUtils.applyEasing(deltaTime, 'easeOut', settings, currentVelocities, ACCELERATION_MULTIPLIERS); + + currentVelocities.integrate(targetState, currentVelocities, dragState, settings); + + var currentSpeed = Vec3.length(currentVelocities.translation), + targetSpeed = Vec3.length(movementState.translation), + verticalHold = movementState.isGrounded && settings.stayGrounded && Math.abs(headPitch) < STAYGROUNDED_PITCH_THRESHOLD; + + var deltaOrientation = Quat.fromVec3Degrees(Vec3.multiply(deltaTime, currentVelocities.rotation)), + targetOrientation = Quat.normalize(Quat.multiply(currentOrientation, deltaOrientation)); + + var targetVelocity = Vec3.multiplyQbyV(targetOrientation, currentVelocities.translation); + + if (verticalHold) { + targetVelocity.y = 0; + } + + var deltaPosition = Vec3.multiply(deltaTime, targetVelocity); + + _resetMyAvatarMotor(pendingChanges); + + if (!independentCamera) { + var DriveModes = movementUtils.DriveModes; + switch (settings.driveMode) { + case DriveModes.MOTOR: { + if (currentSpeed > EPSILON || targetSpeed > EPSILON) { + var motorTimescale = (currentSpeed > EASED_MOTOR_THRESHOLD ? EASED_MOTOR_TIMESCALE : DEFAULT_MOTOR_TIMESCALE); + var motorPitch = Quat.fromPitchYawRollDegrees(headPitch, 180, 0), + motorVelocity = Vec3.multiplyQbyV(motorPitch, currentVelocities.translation); + if (verticalHold) { + motorVelocity.y = 0; + } + Object.assign(pendingChanges.MyAvatar, { + motorVelocity: motorVelocity, + motorTimescale: motorTimescale + }); + } + break; + } + case DriveModes.THRUST: { + var thrustVector = currentVelocities.translation, + maxThrust = settings.translation.maxVelocity, + thrust; + if (targetSpeed > EPSILON) { + thrust = movementUtils.calculateThrust(maxThrust * 5, thrustVector, previousValues.thrust); + } else if (currentSpeed > 1 && Vec3.length(previousValues.thrust) > 1) { + thrust = Vec3.multiply(-currentSpeed / 10.0, thrustVector); + } else { + thrust = Vec3.ZERO; + } + if (thrust) { + thrust = Vec3.multiplyQbyV(MyAvatar[orientationProperty], thrust); + if (verticalHold) { + thrust.y = 0; + } + } + previousValues.thrust = pendingChanges.MyAvatar.setThrust = thrust; + break; + } + case DriveModes.POSITION: { + pendingChanges.MyAvatar.position = Vec3.sum(currentPosition, deltaPosition); + break; + } + default: { + throw new Error('unknown driveMode: ' + settings.driveMode); + } + } + } + + var finalOrientation; + switch (Camera.mode) { + case 'mirror': // fall through + case 'independent': + targetOrientation = settings.preventRoll ? Quat.cancelOutRoll(targetOrientation) : targetOrientation; + var boomVector = Vec3.multiply(-currentVelocities.zoom.z, Quat.getFront(targetOrientation)), + deltaCameraPosition = Vec3.sum(boomVector, deltaPosition); + Object.assign(pendingChanges.Camera, { + position: Vec3.sum(Camera.position, deltaCameraPosition), + orientation: targetOrientation + }); + break; + case 'entity': + finalOrientation = targetOrientation; + break; + default: // 'first person', 'third person' + finalOrientation = targetOrientation; + break; + } + + if (settings.jitterTest) { + finalOrientation = Quat.multiply(MyAvatar[orientationProperty], Quat.fromPitchYawRollDegrees(0, 60 * deltaTime, 0)); + // Quat.fromPitchYawRollDegrees(0, _utils.getRuntimeSeconds() * 60, 0) + } + + if (finalOrientation) { + if (settings.preventRoll) { + finalOrientation = Quat.cancelOutRoll(finalOrientation); + } + previousValues.finalOrientation = pendingChanges.MyAvatar[orientationProperty] = Quat.normalize(finalOrientation); + } + + if (!movementState.mouseSmooth && movementState.isRightMouseButton) { + // directly apply mouse pitch and yaw when mouse smoothing is disabled + _applyDirectPitchYaw(deltaTime, movementState, settings); + } + + var endTime = _utils.getRuntimeSeconds(); + var cycleTime = endTime - update.endTime; + update.endTime = endTime; + + pendingChanges.submit(); + + if ((endTime - update.windowStartTime) > 3) { + update.momentaryFPS = (update.frameCount - update.windowStartFrame) / + (endTime - update.windowStartTime); + update.windowStartFrame = update.frameCount; + update.windowStartTime = endTime; + } + + if (_debugChannel && update.windowStartFrame === update.frameCount) { + Messages.sendLocalMessage(_debugChannel, JSON.stringify({ + threadFrames: update.threadFrames, + frame: update.frameCount, + threadMode: settings.threadMode, + driveMode: settings.driveMode, + orientationProperty: orientationProperty, + isGrounded: movementState.isGrounded, + targetAnimationFPS: settings.threadMode === movementUtils.CameraControls.ANIMATION_FRAME ? settings.fps : undefined, + actualFPS: 1 / actualDeltaTime, + effectiveAnimationFPS: 1 / deltaTime, + seconds: { + startTime: startTime, + endTime: endTime + }, + milliseconds: { + actualDeltaTime: actualDeltaTime * 1000, + deltaTime: deltaTime * 1000, + cycleTime: cycleTime * 1000, + calculationTime: (endTime - startTime) * 1000 + }, + finalOrientation: finalOrientation, + thrust: thrust, + maxVelocity: settings.translation, + targetVelocity: targetVelocity, + currentSpeed: currentSpeed, + targetSpeed: targetSpeed + }, 0, 2)); + } + } + + function _resetMyAvatarMotor(targetObject) { + if (MyAvatar.motorTimescale !== DEFAULT_MOTOR_TIMESCALE) { + targetObject.MyAvatar.motorTimescale = DEFAULT_MOTOR_TIMESCALE; + } + if (MyAvatar.motorReferenceFrame !== 'avatar') { + targetObject.MyAvatar.motorReferenceFrame = 'avatar'; + } + if (Vec3.length(MyAvatar.motorVelocity)) { + targetObject.MyAvatar.motorVelocity = Vec3.ZERO; + } + } + + function _applyDirectPitchYaw(deltaTime, movementState, settings) { + var orientationProperty = settings.useHead ? 'headOrientation' : 'orientation', + rotation = movementState.rotation, + speed = Vec3.multiply(-DEG_TO_RAD / 2.0, settings.rotation.speed); + + var previousValues = globalState.previousValues, + pendingChanges = globalState.pendingChanges, + currentVelocities = globalState.currentVelocities; + + var previous = previousValues.pitchYawRoll, + target = Vec3.multiply(deltaTime, Vec3.multiplyVbyV(rotation, speed)), + pitchYawRoll = Vec3.mix(previous, target, 0.5), + orientation = Quat.fromVec3Degrees(pitchYawRoll); + + previousValues.pitchYawRoll = pitchYawRoll; + + if (pendingChanges.MyAvatar.headOrientation || pendingChanges.MyAvatar.orientation) { + var newOrientation = Quat.multiply(MyAvatar[orientationProperty], orientation); + delete pendingChanges.MyAvatar.headOrientation; + delete pendingChanges.MyAvatar.orientation; + if (settings.preventRoll) { + newOrientation = Quat.cancelOutRoll(newOrientation); + } + MyAvatar[orientationProperty] = newOrientation; + } else if (pendingChanges.Camera.orientation) { + var cameraOrientation = Quat.multiply(Camera.orientation, orientation); + if (settings.preventRoll) { + cameraOrientation = Quat.cancelOutRoll(cameraOrientation); + } + Camera.orientation = cameraOrientation; + } + currentVelocities.rotation = Vec3.ZERO; + } +} diff --git a/unpublishedScripts/marketplace/camera-move/hifi-jquery-ui.js b/unpublishedScripts/marketplace/camera-move/hifi-jquery-ui.js new file mode 100644 index 0000000000..31f2186ee1 --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/hifi-jquery-ui.js @@ -0,0 +1,311 @@ +// extended jQuery UI controls + +/* eslint-env console, jquery, browser */ +/* eslint-disable comma-dangle, no-empty */ +/* global assert, log, debugPrint */ +// ---------------------------------------------------------------------------- + +// WIDGET BASE +Object.assign($.Widget.prototype, { + // common bootstrapping across widget types + initHifiControl: function initHifiControl(hifiType) { + initHifiControl.widgetCount = (initHifiControl.widgetCount || 0) + 1; + hifiType = hifiType || this.widgetName; + + var element = this.element, options = this.options, node = element.get(0), dataset = node.dataset; + assert(!this.element.is('.initialized')); + this.element.addClass('initialized'); + var attributes = [].reduce.call(node.attributes, function(out, attribute) { + out[attribute.name] = attribute.value; + return out; + }, {}); + + var searchOrder = [ options, dataset, attributes, node ]; + function setData(key, fallback) { + var value = searchOrder.map(function(obj) { + return obj[key]; + }).concat(fallback).filter(function(value) { + return value !== undefined; + })[0]; + return value === undefined ? null : (dataset[key] = value); + } + options.hifiWidgetId = hifiType + '-' + initHifiControl.widgetCount; + node.id = node.id || options.hifiWidgetId; + dataset.hifiType = hifiType; + setData('type'); + setData('for', node.id); + setData('checked'); + if (setData('value', null) !== null) { + element.attr('value', dataset.value); + } + + return node.id; + }, + hifiFindWidget: function(hifiType, quiet) { + var selector = ':ui-'+hifiType; + var _for = JSON.stringify(this.element.data('for')||undefined), + element = _for && $('[id='+_for+']').filter(selector); + if (!element.is(selector)) { + element = this.element.closest(selector); + } + var instance = element.filter(selector)[hifiType]('instance'); + + if (!instance && !quiet) { + // eslint-disable-next-line no-console + console.error([ + instance, 'could not find target instance ' + selector + + ' for ' + this.element.data('hifi-type') + + ' #' + this.element.prop('id') + ' for=' + this.element.data('for') + ]); + } + return instance; + }, +}); + +// CHECKBOX +$.widget('ui.hifiCheckbox', $.ui.checkboxradio, { + value: function value(nv) { + if (arguments.length) { + var currentValue = this.element.prop('checked'); + if (nv !== currentValue){ + this.element.prop('checked', nv); + this.element.change(); + } + } + return this.element.prop('checked'); + }, + _create: function() { + var id = this.initHifiControl(); + this.element.attr('value', id); + // add an implicit label if missing + var forId = 'for=' + JSON.stringify(id); + var label = $(this.element.get(0)).closest('label').add($('label[' + forId + ']')); + if (!label.get(0)) { + $('').appendTo(this.element); + } + this._super(); + this.element.on('change._hifiCheckbox, click._hifiCheckbox', function() { + var checked = this.value(), + attr = this.element.attr('checked'); + if (checked && !attr) { + this.element.attr('checked', 'checked'); + } else if (!checked && attr) { + this.element.removeAttr('checked'); + } + this.refresh(); + }.bind(this)); + }, +}); + +// BUTTON +$.widget('ui.hifiButton', $.ui.button, { + value: function(nv) { + var dataset = this.element[0].dataset; + if (arguments.length) { + var checked = (dataset.checked === 'true'); + nv = (nv === 'true' || !!nv); + if (nv !== checked) { + debugPrint('hifibutton checked changed', nv, checked); + dataset.checked = nv; + this.element.change(); + } else { + debugPrint('hifibutton value same', nv, checked); + } + } + return dataset.checked === 'true'; + }, + _create: function() { + this.element.data('type', 'checkbox'); + this.initHifiControl(); + this._super(); + this.element[0].dataset.checked = !!this.element.attr('checked'); + var _for = this.element.data('for') || undefined; + if (_for && _for !== this.element[0].id) { + _for = JSON.stringify(_for); + var checkbox = this.hifiFindWidget('hifiCheckbox', true); + if (!checkbox) { + var input = $('').hide(); + input.appendTo(this.element); + checkbox = input.find('input') + .hifiCheckbox() + .hifiCheckbox('instance'); + } + this.element.find('.tooltip-target').removeClass('tooltip-target'); + this.element.prop('id', 'button-'+this.element.prop('id')); + checkbox.element.on('change._hifiButton', function() { + debugPrint('checkbox -> button'); + this.value(checkbox.value()); + }.bind(this)); + this.element.on('change', function() { + debugPrint('button -> checkbox'); + checkbox.value(this.value()); + }.bind(this)); + this.checkbox = checkbox; + } + }, +}); + +// RADIO BUTTON +$.widget('ui.hifiRadioButton', $.ui.checkboxradio, { + value: function value(nv) { + if (arguments.length) { + this.element.prop('checked', !!nv); + this.element.change(); + } + return this.element.prop('checked'); + }, + _create: function() { + var id = this.initHifiControl(); + this.element.attr('value', this.element.data('value') || id); + // console.log(this.element[0]); + assert(this.element.data('for')); + this._super(); + + this.element.on('change._hifiRadioButton, click._hifiRadioButton', function() { + var group = this.hifiFindWidget('hifiRadioGroup'), + checked = !!this.element.attr('checked'), + dotchecked = this.element.prop('checked'), + value = this.element.attr('value'); + + if (dotchecked !== checked || group.value() !== value) { + if (dotchecked && group.value() !== value) { + log(value, 'UPDATING GRUOP', group.element[0].id); + group.value(value); + } + } + }.bind(this)); + }, +}); + +// RADIO GROUP +$.widget('ui.hifiRadioGroup', $.ui.controlgroup, { + radio: function(selector) { + return this.element.find(':ui-hifiRadioButton' + selector).hifiRadioButton('instance'); + }, + refresh: function() { + var value = this.value(); + this.element.find(':ui-hifiRadioButton').each(function() { + $(this).prop('checked', $(this).attr('value') === value).hifiRadioButton('refresh'); + }); + this._super(); + }, + value: function value(nv) { + if (arguments.length) { + var id = this.element[0].id, + previous = this.value(); + debugPrint('RADIOBUTTON GROUP value', id + ' = ' + nv + '(was: ' + previous + ')'); + this.element.attr('value', nv); + this.refresh(); + } + return this.element.attr('value'); + }, + _create: function(x) { + debugPrint('ui.hifiRadioGroup._create', this.element[0]); + this.initHifiControl(); + this.options.items = { + hifiRadioButton: 'input[type=radio]', + }; + this._super(); + // allow setting correct radio button by assign to .value property (or $.fn.val() etc.) + Object.defineProperty(this.element[0], 'value', { + set: function(nv) { + try { + this.radio('#' + nv).value(true); + } catch (e) {} + return this.value(); + }.bind(this), + get: function() { + return this.element.attr('value'); + }.bind(this), + }); + }, +}); + +// SPINNER (numeric input + up/down buttons) +$.widget('ui.hifiSpinner', $.ui.spinner, { + value: function value(nv) { + if (arguments.length) { + var num = parseFloat(nv); + debugPrint('ui.hifiSpinner.value set', this.element[0].id, num, '(was: ' + this.value() + ')', 'raw:'+nv); + this._value(num); + this.element.change(); + } + return parseFloat(this.element.val()); + }, + _value: function(value, allowAny) { + debugPrint('ui.hifiSpinner._value', value, allowAny); + return this._super(value, allowAny); + }, + _create: function() { + this.initHifiControl(); + var step = this.options.step = this.options.step || 1.0; + // allow step=".01" for precision and data-step=".1" for default increment amount + this.options.prescale = parseFloat(this.element.data('step') || step) / (step); + this._super(); + this.previous = null; + this.element.on('change._hifiSpinner', function() { + var value = this.value(), + invalid = !this.isValid(); + debugPrint('hifiSpinner.changed', value, invalid ? '!!!invalid' : 'valid'); + !invalid && this.element.attr('value', value); + }.bind(this)); + }, + _spin: function( step, event ) { + step = step * this.options.prescale * ( + event.shiftKey ? 0.1 : event.ctrlKey ? 10 : 1 + ); + return this._super( step, event ); + }, + _stop: function( event, ui ) { + try { + return this._super(event, ui); + } finally { + if (/mouse/.test(event && event.type)) { + var value = this.value(); + if ((value || value === 0) && !isNaN(value) && this.previous !== null && this.previous !== value) { + this.value(this.value()); + } + this.previous = value; + } + } + }, + _format: function(n) { + var precision = this._precision(); + return parseFloat(n).toFixed(precision); + }, + _events: { + mousewheel: function(event, delta) { + if (document.activeElement === this.element[0]) { + // fix broken mousewheel on Chrome / embedded webkit + delta = delta === undefined ? -(event.originalEvent.deltaY+event.originalEvent.deltaX) : delta; + $.ui.spinner.prototype._events.mousewheel.call(this, event, delta); + } + } + } +}); + +// SLIDER +$.widget('ui.hifiSlider', $.ui.slider, { + value: function value(nv) { + if (arguments.length) { + var num = this._trimAlignValue(nv); + debugPrint('hifiSlider.value', nv, num); + if (this.options.value !== num) { + this.options.value = num; + this.element.change(); + } + } + return this.options.value; + }, + _create: function() { + this.initHifiControl(); + this._super(); + this.element + .attr('type', this.element.attr('type') || 'slider') + .find('.ui-slider-handle').html('
').end() + .on('change', function() { + this.hifiFindWidget('hifiSpinner').value(this.value()); + this._refresh(); + }.bind(this)); + }, +}); diff --git a/unpublishedScripts/marketplace/camera-move/modules/EnumMeta.js b/unpublishedScripts/marketplace/camera-move/modules/EnumMeta.js new file mode 100644 index 0000000000..e463442c05 --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/modules/EnumMeta.js @@ -0,0 +1,238 @@ +// EnumMeta.js -- helper module that maps related enum values to printable names and ids + +/* eslint-env commonjs */ +/* global DriveKeys, console */ + +var VERSION = '0.0.1'; +var WANT_DEBUG = false; + +function _debugPrint() { + // eslint-disable-next-line no-console + (typeof Script === 'object' ? print : console.log)('EnumMeta | ' + [].slice.call(arguments).join(' ')); +} + +var debugPrint = WANT_DEBUG ? _debugPrint : function(){}; + +try { + /* global process */ + if (process.title === 'node') { + _defineNodeJSMocks(); + } +} catch (e) { /* eslint-disable-line empty-block */ } + +_debugPrint(VERSION); + +// FIXME: C++ emits this action event, but doesn't expose it yet to scripting +// (ie: as Actions.ZOOM or Actions.TranslateCameraZ) +var ACTION_TRANSLATE_CAMERA_Z = { + actionName: 'TranslateCameraZ', + actionID: 12, + driveKeyName: 'ZOOM' +}; + +module.exports = { + version: VERSION, + DriveKeyNames: invertKeys(DriveKeys), + Controller: { + Hardware: Object.keys(Controller.Hardware).reduce(function(names, prop) { + names[prop+'Names'] = invertKeys(Controller.Hardware[prop]); + return names; + }, {}), + ActionNames: _getActionNames(), + StandardNames: invertKeys(Controller.Standard) + }, + getDriveKeyNameFromActionName: getDriveKeyNameFromActionName, + getActionNameFromDriveKeyName: getActionNameFromDriveKeyName, + eventKeyText2KeyboardName: eventKeyText2KeyboardName, + keyboardName2eventKeyText: keyboardName2eventKeyText, + ACTION_TRANSLATE_CAMERA_Z: ACTION_TRANSLATE_CAMERA_Z, + INVALID_ACTION_ID: Controller.findAction('INVALID_ACTION_ID_FOO') +}; + +_debugPrint('///'+VERSION, Object.keys(module.exports)); + +var actionsMapping = {}, driveKeyMapping = {}; + +initializeMappings(actionsMapping, driveKeyMapping); + +function invertKeys(object) { + if (!object) { + return object; + } + return Object.keys(object).reduce(function(out, key) { + out[object[key]] = key; + return out; + }, {}); +} + +function _getActionNames() { + var ActionNames = invertKeys(Controller.Hardware.Actions); + ActionNames[ACTION_TRANSLATE_CAMERA_Z.actionID] = ACTION_TRANSLATE_CAMERA_Z.actionName; + function mapActionName(actionName) { + var actionKey = Controller.Hardware.Actions[actionName], + actionID = Controller.findAction(actionName), + similarName = eventKeyText2KeyboardName(actionName), + existingName = ActionNames[actionID]; + + var keyName = actionName; + + if (actionID === module.exports.INVALID_ACTION_ID) { + _debugPrint('actionID === INVALID_ACTION_ID', actionName); + } + switch (actionName) { + case 'StepTranslateX': actionName = 'StepTranslate'; break; + case 'StepTranslateY': actionName = 'StepTranslate'; break; + case 'StepTranslateZ': actionName = 'StepTranslate'; break; + case 'ACTION1': actionName = 'PrimaryAction'; break; + case 'ACTION2': actionName = 'SecondaryAction'; break; + } + debugPrint(keyName, actionName, actionKey, actionID); + + similarName = similarName.replace('Lateral','Strafe').replace(/^(?:Longitudinal|Vertical)/, ''); + if (actionID in ActionNames) { + // check if overlap is just BoomIn <=> BOOM_IN + if (similarName !== existingName && actionName !== existingName) { + throw new Error('assumption failed: overlapping actionID:'+JSON.stringify({ + actionID: actionID, + actionKey: actionKey, + actionName: actionName, + similarName: similarName, + keyName: keyName, + existingName: existingName + },0,2)); + } + } else { + ActionNames[actionID] = actionName; + ActionNames[actionKey] = keyName; + } + } + // first map non-legacy (eg: Up and not VERTICAL_UP) actions + Object.keys(Controller.Hardware.Actions).filter(function(name) { + return /[a-z]/.test(name); + }).sort().reverse().forEach(mapActionName); + // now legacy actions + Object.keys(Controller.Hardware.Actions).filter(function(name) { + return !/[a-z]/.test(name); + }).sort().reverse().forEach(mapActionName); + + return ActionNames; +} + +// attempts to brute-force translate an Action name into a DriveKey name +// eg: _translateActionName('TranslateX') === 'TRANSLATE_X' +// eg: _translateActionName('Yaw') === 'YAW' +function _translateActionName(name, _index) { + name = name || ''; + var key = name; + var re = new RegExp('[A-Z][a-z0-9]+', 'g'); + key = key.replace(re, function(Word) { + return Word.toUpperCase()+'_'; + }) + .replace(/_$/, ''); + + if (key in DriveKeys) { + debugPrint('getDriveKeyFromEventName', _index, name, key, DriveKeys[key]); + return key; + } +} + +function getActionNameFromDriveKeyName(driveKeyName) { + return driveKeyMapping[driveKeyName]; +} +// maps an action lookup value to a DriveKey name +// eg: actionName: 'Yaw' === 'YAW' +// actionKey: Controller.Actions.Yaw => 'YAW' +// actionID: Controller.findAction('Yaw') => 'YAW' +function getDriveKeyNameFromActionName(lookupValue) { + if (lookupValue === ACTION_TRANSLATE_CAMERA_Z.actionName || + lookupValue === ACTION_TRANSLATE_CAMERA_Z.actionID) { + return ACTION_TRANSLATE_CAMERA_Z.driveKeyName; + } + if (typeof lookupValue === 'string') { + lookupValue = Controller.findAction(lookupValue); + } + return actionsMapping[lookupValue]; +} + +// maps a Controller.key*Event event.text -> Controller.Hardware.Keyboard[name] +// eg: ('Page Up') === 'PgUp' +// eg: ('LEFT') === 'Left' +function eventKeyText2KeyboardName(text) { + if (eventKeyText2KeyboardName[text]) { + // use memoized value + return eventKeyText2KeyboardName[text]; + } + var keyboardName = (text||'').toUpperCase().split(/[ _]/).map(function(WORD) { + return WORD.replace(/([A-Z])(\w*)/g, function(_, A, b) { + return (A.toUpperCase() + b.toLowerCase()); + }); + }).join('').replace('Page','Pg'); + return eventKeyText2KeyboardName[text] = eventKeyText2KeyboardName[keyboardName] = keyboardName; +} + +// maps a Controller.Hardware.Keyboard[name] -> Controller.key*Event event.text +// eg: ('PgUp') === 'PAGE UP' +// eg: ('Shift') === 'SHIFT' +function keyboardName2eventKeyText(keyName) { + if (keyboardName2eventKeyText[keyName]) { + // use memoized value + return keyboardName2eventKeyText[keyName]; + } + var text = keyName.replace('Pg', 'Page'); + var caseWords = text.match(/[A-Z][a-z0-9]+/g) || [ text ]; + var eventText = caseWords.map(function(str) { + return str.toUpperCase(); + }).join('_'); + return keyboardName2eventKeyText[keyName] = eventText; +} + +function initializeMappings(actionMap, driveKeyMap) { + _debugPrint('creating mapping'); + var ref = ACTION_TRANSLATE_CAMERA_Z; + actionMap[ref.actionName] = actionMap[ref.actionID] = ref.driveKeyName; + actionMap.BoomIn = 'ZOOM'; + actionMap.BoomOut = 'ZOOM'; + + Controller.getActionNames().sort().reduce( + function(out, name, index, arr) { + var actionKey = arr[index]; + var actionID = Controller.findAction(name); + var value = actionID in out ? out[actionID] : _translateActionName(name, index); + if (value !== undefined) { + var prefix = (actionID in out ? '+++' : '---'); + debugPrint(prefix + ' Action2DriveKeyName['+name+'('+actionID+')] = ' + value); + driveKeyMap[value] = driveKeyMap[value] || name; + } + out[name] = out[actionID] = out[actionKey] = value; + return out; + }, actionMap); +} + +// ---------------------------------------------------------------------------- +// mocks for litmus testing using Node.js command line tools +function _defineNodeJSMocks() { + /* eslint-disable no-global-assign */ + DriveKeys = { + TRANSLATE_X: 12345 + }; + Controller = { + getActionNames: function() { + return Object.keys(this.Hardware.Actions); + }, + findAction: function(name) { + return this.Hardware.Actions[name] || 4095; + }, + Hardware: { + Actions: { + TranslateX: 54321 + }, + Application: { + Grounded: 1 + }, + Keyboard: { + A: 65 + } + } + }; + /* eslint-enable no-global-assign */ +} diff --git a/unpublishedScripts/marketplace/camera-move/modules/_utils.js b/unpublishedScripts/marketplace/camera-move/modules/_utils.js new file mode 100644 index 0000000000..bf0f09aac8 --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/modules/_utils.js @@ -0,0 +1,457 @@ +// _utils.js -- misc. helper classes/functions + +"use strict"; +/* eslint-env commonjs, hifi */ +/* eslint-disable comma-dangle, no-empty */ +/* global HIRES_CLOCK, Desktop, OverlayWebWindow */ +// var HIRES_CLOCK = (typeof Window === 'object' && Window && Window.performance) && Window.performance.now; +var USE_HIRES_CLOCK = typeof HIRES_CLOCK === 'function'; + +var exports = { + version: '0.0.1c' + (USE_HIRES_CLOCK ? '-hires' : ''), + bind: bind, + signal: signal, + assign: assign, + sortedAssign: sortedAssign, + sign: sign, + assert: assert, + makeDebugRequire: makeDebugRequire, + DeferredUpdater: DeferredUpdater, + KeyListener: KeyListener, + getRuntimeSeconds: getRuntimeSeconds, + createAnimationStepper: createAnimationStepper, + reloadClientScript: reloadClientScript, + + normalizeStackTrace: normalizeStackTrace, + BrowserUtils: BrowserUtils, +}; +try { + module.exports = exports; // Interface / Node.js +} catch (e) { + this._utils = assign(this._utils || {}, exports); // browser +} + +// ---------------------------------------------------------------------------- +function makeDebugRequire(relativeTo) { + return function boundDebugRequire(id) { + return debugRequire(id, relativeTo); + }; +} +function debugRequire(id, relativeTo) { + if (typeof Script === 'object') { + relativeTo = (relativeTo||Script.resolvePath('.')).replace(/\/+$/, ''); + // hack-around for use during local development / testing that forces every require to re-fetch the script from the server + var modulePath = Script._requireResolve(id, relativeTo+'/') + '?' + new Date().getTime().toString(36); + print('========== DEBUGREQUIRE:' + modulePath); + Script.require.cache[modulePath] = Script.require.cache[id] = undefined; + Script.require.__qt_data__[modulePath] = Script.require.__qt_data__[id] = true; + return Script.require(modulePath); + } else { + return require(id); + } +} + +// examples: +// assert(function assertion() { return (conditions === true) }, 'assertion failed!') +// var neededValue = assert(idString, 'idString not specified!'); +// assert(false, 'unexpected state'); +function assert(truthy, message) { + message = message || 'Assertion Failed:'; + + if (typeof truthy === 'function' && truthy.name === 'assertion') { + // extract function body to display with the assertion message + var assertion = (truthy+'').replace(/[\r\n]/g, ' ') + .replace(/^[^{]+\{|\}$|^\s*|\s*$/g, '').trim() + .replace(/^return /,'').replace(/\s[\r\n\t\s]+/g, ' '); + message += ' ' + JSON.stringify(assertion); + try { + truthy = truthy(); + } catch (e) { + message += '(exception: ' + e +')'; + } + } + if (!truthy) { + message += ' ('+truthy+')'; + throw new Error(message); + } + return truthy; +} + + +// ---------------------------------------------------------------------------- +function sign(x) { + x = +x; + if (x === 0 || isNaN(x)) { + return Number(x); + } + return x > 0 ? 1 : -1; +} +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill +/* eslint-disable */ +function assign(target, varArgs) { // .length of function is 2 + 'use strict'; + if (target == null) { // TypeError if undefined or null + throw new TypeError('Cannot convert undefined or null to object'); + } + + var to = Object(target); + + for (var index = 1; index < arguments.length; index++) { + var nextSource = arguments[index]; + + if (nextSource != null) { // Skip over if undefined or null + for (var nextKey in nextSource) { + // Avoid bugs when hasOwnProperty is shadowed + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; +} +/* eslint-enable */ +// //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill + +// hack to sort keys in v8 for prettier JSON exports +function sortedAssign(target, sources) { + var allParams = assign.apply(this, [{}].concat([].slice.call(arguments))); + for (var p in target) { + delete target[p]; + } + Object.keys(allParams).sort(function(a,b) { + function swapCase(ch) { + return /[A-Z]/.test(ch) ? ch.toLowerCase() : ch.toUpperCase(); + } + a = a.replace(/^./, swapCase); + b = b.replace(/^./, swapCase); + a = /Version/.test(a) ? 'AAAA'+a : a; + b = /Version/.test(b) ? 'AAAA'+b : b; + return a < b ? -1 : a > b ? 1 : 0; + }).forEach(function(key) { + target[key] = allParams[key]; + }); + return target; +} + +// ---------------------------------------------------------------------------- +// @function - bind a function to a `this` context +// @param {Object} - the `this` context +// @param {Function|String} - function or method name +bind.debug = true; +function bind(thiz, method) { + var methodName = typeof method === 'string' ? method : method.name; + method = thiz[method] || method; + if (bind.debug && methodName) { + methodName = methodName.replace(/[^A-Za-z0-9_$]/g, '_'); + var debug = {}; + debug[methodName] = method; + return eval('1,function bound'+methodName+'() { return debug.'+methodName+'.apply(thiz, arguments); }'); + } + return function() { + return method.apply(thiz, arguments); + }; +} + +// @function - Qt signal polyfill +function signal(template) { + var callbacks = []; + return Object.defineProperties(function() { + var args = [].slice.call(arguments); + callbacks.forEach(function(obj) { + obj.handler.apply(obj.scope, args); + }); + }, { + connect: { value: function(scope, handler) { + callbacks.push({scope: scope, handler: scope[handler] || handler || scope}); + }}, + disconnect: { value: function(scope, handler) { + var match = {scope: scope, handler: scope[handler] || handler || scope}; + callbacks = callbacks.filter(function(obj) { + return !(obj.scope === match.scope && obj.handler === match.handler); + }); + }} + }); +} +// ---------------------------------------------------------------------------- +function normalizeStackTrace(err, options) { + options = options || {}; + // * Chromium: " at (file://.../filename.js:45:65)" + // * Interface: " at filename.js:45" + var output = err.stack ? err.stack.replace( + /((?:https?|file):[/][/].*?):(\d+)(?::\d+)?([)]|\s|$)/g, + function(_, url, lineNumber, suffix) { + var fileref = url.split(/[?#]/)[0].split('/').pop(); + if (Array.isArray(options.wrapFilesWith)) { + fileref = options.wrapFilesWith[0] + fileref + options.wrapFilesWith[1]; + } + if (Array.isArray(options.wrapLineNumbersWith)) { + lineNumber = options.wrapLineNumbersWith[0] + lineNumber + options.wrapLineNumbersWith[1]; + } + return fileref + ':' + lineNumber + suffix; + } + ).replace(/[(]([-\w.%:]+[.](?:html|js))[)]/g, '$1') : err.message; + return ' '+output; +} + +// utilities specific to use from web browsers / embedded Interface web windows +function BrowserUtils(global) { + global = global || (1,eval)('this'); + return { + global: global, + console: global.console, + log: function(msg) { return this.console.log('browserUtils | ' + [].slice.call(arguments).join(' ')); }, + makeConsoleWorkRight: function(console, forcePatching) { + if (console.$patched || !(forcePatching || global.qt)) { + return console; + } + var patched = ['log','debug','info','warn','error'].reduce(function(output, method) { + output[method] = function() { + return console[method]([].slice.call(arguments).join(' ')); + }; + return output; + }, { $patched: console }); + for (var p in console) { + if (typeof console[p] === 'function' && !(p in patched)) { + patched[p] = console[p].bind(console); + } + } + patched.__proto__ = console; // let scope chain find constants and other non-function values + return patched; + }, + parseQueryParams: function(querystring) { + return this.extendWithQueryParams({}, querystring); + }, + extendWithQueryParams: function(obj, querystring) { + querystring = querystring || global.location.href; + querystring.replace(/\b(\w+)=([^&?#]+)/g, function(_, key, value) { + value = unescape(value); + obj[key] = value; + }); + return obj; + }, + // openEventBridge handles the cluster of scenarios Interface has imposed on webviews for making EventBridge connections + openEventBridge: function openEventBridge(callback) { + this.log('openEventBridge |', 'typeof global.EventBridge == ' + [typeof global.EventBridge, global.EventBridge ]); + var error; + try { + global.EventBridge.toString = function() { return '[global.EventBridge at startup]'; }; + global.EventBridge.scriptEventReceived.connect.exists; + // this.log('openEventBridge| EventBridge already exists... -- invoking callback', 'typeof EventBridge == ' + typeof global.EventBridge); + try { + return callback(global.EventBridge); + } catch(e) { + error = e; + } + } catch (e) { + this.log('EventBridge not found in a usable state -- attempting to instrument via qt.webChannelTransport', + Object.keys(global.EventBridge||{})); + var QWebChannel = assert(global.QWebChannel, 'expected global.QWebChannel to exist'), + qt = assert(global.qt, 'expected global.qt to exist'); + assert(qt.webChannelTransport, 'expected global.qt.webChannelTransport to exist'); + new QWebChannel(qt.webChannelTransport, bind(this, function (channel) { + var objects = channel.objects; + if (global.EventBridge) { + log('>>> global.EventBridge was unavailable at page load, but has spontaneously materialized; ' + + [ typeof global.EventBridge, global.EventBridge ]); + } + var eventBridge = objects.eventBridge || (objects.eventBridgeWrapper && objects.eventBridgeWrapper.eventBridge); + eventBridge.toString = function() { return '[window.EventBridge per QWebChannel]'; }; + assert(!global.EventBridge || global.EventBridge === eventBridge, 'global.EventBridge !== QWebChannel eventBridge\n' + + [global.EventBridge, eventBridge]); + global.EventBridge = eventBridge; + global.EventBridge.$WebChannel = channel; + this.log('openEventBridge opened -- invoking callback', 'typeof EventBridge === ' + typeof global.EventBridge); + callback(global.EventBridge); + })); + } + if (error) { + throw error; + } + }, + }; +} +// ---------------------------------------------------------------------------- +// queue property/method updates to target so that they can be applied all-at-once +function DeferredUpdater(target, options) { + options = options || {}; + // define _meta as a non-enumerable (so it doesn't show up in for (var p in ...) loops) + Object.defineProperty(this, '_meta', { enumerable: false, value: { + target: target, + lastValue: {}, + dedupe: options.dedupe, + }}); +} +DeferredUpdater.prototype = { + reset: function() { + var self = this; + Object.keys(this).forEach(function(property) { + delete self[property]; + }); + this._meta.lastValue = {}; + }, + submit: function() { + var meta = this._meta, + target = meta.target, + self = this, + submitted = {}; + self.submit = getRuntimeSeconds(); + Object.keys(self).forEach(function(property) { + var newValue = self[property]; + submitted[property] = newValue; + if (typeof target[property] === 'function') { + target[property](newValue); + } else { + target[property] = newValue; + } + delete self[property]; + }); + return submitted; + } +}; +// create a group of deferred updaters eg: DeferredUpdater.createGroup({ MyAvatar: MyAvatar, Camera: Camera }) +DeferredUpdater.createGroup = function(items, options) { + var result = { + __proto__: { + reset: function() { + Object.keys(this).forEach(bind(this, function(item) { + this[item].reset(); + })); + }, + submit: function() { + var submitted = {}; + Object.keys(this).forEach(bind(this, function(item) { + submitted[item] = this[item].submit(); + })); + return submitted; + } + } + }; + Object.keys(items).forEach(function(item) { + result[item] = new DeferredUpdater(items[item], options); + }); + return result; +}; + +// ---------------------------------------------------------------------------- + +// session runtime in seconds +getRuntimeSeconds.EPOCH = getRuntimeSeconds(0); +function getRuntimeSeconds(since) { + since = since === undefined ? getRuntimeSeconds.EPOCH : since; + var now = USE_HIRES_CLOCK ? new HIRES_CLOCK() : +new Date; + return ((now / 1000.0) - since); +} + +// requestAnimationFrame emulation +function createAnimationStepper(options) { + options = options || {}; + var fps = options.fps || 30, + waitMs = 1000 / fps, + getTime = options.getRuntimeSeconds || getRuntimeSeconds, + lastUpdateTime = -1e-6, + timeout = 0; + + requestAnimationFrame.fps = fps; + requestAnimationFrame.reset = function() { + if (timeout) { + Script.clearTimeout(timeout); + timeout = 0; + } + }; + + function requestAnimationFrame(update) { + requestAnimationFrame.reset(); + timeout = Script.setTimeout(function() { + timeout = 0; + update(getTime(lastUpdateTime)); + lastUpdateTime = getTime(); + }, waitMs ); + } + + return requestAnimationFrame; +} + +// ---------------------------------------------------------------------------- +// KeyListener provides a scoped wrapper where options.onKeyPressEvent gets +// called when a key event matches the specified event.text / key spec +// example: var listener = new KeyListener({ text: 'SPACE', isShifted: false, onKeyPressEvent: function(event) { ... } }); +// Script.scriptEnding.connect(listener, 'disconnect'); +function KeyListener(options) { + assert(typeof options === 'object' && 'text' in options && 'onKeyPressEvent' in options); + + this._options = options; + assign(this, { + modifiers: this._getEventModifiers(options, true) + }, options); + this.log = options.log || function log() { + print('KeyListener | ', [].slice.call(arguments).join(' ')); + }; + this.log('created KeyListener', JSON.stringify(this.text), this.modifiers); + this.connect(); +} +KeyListener.prototype = { + _getEventModifiers: function(event, trueOnly) { + return '(' + [ 'Control', 'Meta', 'Alt', 'Super', 'Menu', 'Shifted' ].map(function(mod) { + var isMod = 'is' + mod, + value = event[isMod], + found = (trueOnly ? value : typeof value === 'boolean'); + return found && isMod + ' == ' + value; + }).filter(Boolean).sort().join(' | ') + ')'; + }, + handleEvent: function(event, target) { + if (event.text === this.text) { + var modifiers = this._getEventModifiers(event, true); + if (modifiers !== this.modifiers) { + return; + } + return this[target](event); + } + }, + connect: function() { + return this.$bindEvents(true); + }, + disconnect: function() { + return this.$bindEvents(false); + }, + $onKeyPressEvent: function(event) { + return this.handleEvent(event, 'onKeyPressEvent'); + }, + $onKeyReleaseEvent: function(event) { + return this.handleEvent(event, 'onKeyReleaseEvent'); + }, + $bindEvents: function(connect) { + if (this.onKeyPressEvent) { + Controller.keyPressEvent[connect ? 'connect' : 'disconnect'](this, '$onKeyPressEvent'); + } + if (this.onKeyReleaseEvent) { + Controller.keyReleaseEvent[connect ? 'connect' : 'disconnect'](this, '$onKeyReleaseEvent'); + } + Controller[(connect ? 'capture' : 'release') + 'KeyEvents'](this._options); + } +}; + +// helper to reload a client script +reloadClientScript._findRunning = function(filename) { + return ScriptDiscoveryService.getRunning().filter(function(script) { + return 0 === script.path.indexOf(filename); + }); +}; +function reloadClientScript(filename) { + function log() { + print('reloadClientScript | ', [].slice.call(arguments).join(' ')); + } + log('attempting to reload using stopScript(..., true):', filename); + var result = ScriptDiscoveryService.stopScript(filename, true); + if (!result) { + var matches = reloadClientScript._findRunning(filename), + path = matches[0] && matches[0].path; + if (path) { + log('attempting to reload using matched getRunning path: ' + path); + result = ScriptDiscoveryService.stopScript(path, true); + } + } + log('///result:' + result); + return result; +} diff --git a/unpublishedScripts/marketplace/camera-move/modules/config-utils.js b/unpublishedScripts/marketplace/camera-move/modules/config-utils.js new file mode 100644 index 0000000000..3b037edbea --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/modules/config-utils.js @@ -0,0 +1,252 @@ +// config-utils.js -- helpers for coordinating Application runtime vs. Settings configuration +// +// * ApplicationConfig -- Menu items and API values. +// * SettingsConfig -- scoped Settings values. + +"use strict"; +/* eslint-env commonjs */ +/* global log */ +module.exports = { + version: '0.0.1a', + ApplicationConfig: ApplicationConfig, + SettingsConfig: SettingsConfig +}; + +var _utils = require('./_utils.js'), + assert = _utils.assert; +Object.assign = Object.assign || _utils.assign; + +function _debugPrint() { + print('config-utils | ' + [].slice.call(arguments).join(' ')); +} + +var debugPrint = function() {}; + +// ---------------------------------------------------------------------------- +// Application-specific configuration values using runtime state / API props +// +// options.config[] supports the following item formats: +// 'settingsName': { menu: 'Menu > MenuItem'}, // assumes MenuItem is a checkbox / checkable value +// 'settingsName': { object: [ MyAvatar, 'property' ] }, +// 'settingsName': { object: [ MyAvatar, 'getterMethod', 'setterMethod' ] }, +// 'settingsName': { menu: 'Menu > MenuItem', object: [ MyAvatar, 'property' ] }, +// 'settingsName': { get: function getter() { ...}, set: function(nv) { ... } }, + +function ApplicationConfig(options) { + options = options || {}; + assert('namespace' in options && 'config' in options); + if (options.debug) { + debugPrint = _debugPrint; + debugPrint('debugPrinting enabled'); + } + this.namespace = options.namespace; + this.valueUpdated = _utils.signal(function valueUpdated(key, newValue, oldValue, origin){}); + + this.config = {}; + this.register(options.config); +} +ApplicationConfig.prototype = { + resolve: function resolve(key) { + assert(typeof key === 'string', 'ApplicationConfig.resolve error: key is not a string: ' + key); + if (0 !== key.indexOf('.') && !~key.indexOf('/')) { + key = [ this.namespace, key ].join('/'); + } + return (key in this.config) ? key : (debugPrint('ApplicationConfig -- could not resolve key: ' + key),undefined); + }, + registerItem: function(settingName, item) { + item._settingName = settingName; + item.settingName = ~settingName.indexOf('/') ? settingName : [ this.namespace, settingName ].join('/'); + return this.config[item.settingName] = this.config[settingName] = new ApplicationConfigItem(item); + }, + // process items into fully-qualfied ApplicationConfigItem instances + register: function(items) { + for (var p in items) { + var item = items[p]; + item && this.registerItem(p, item); + } + }, + _getItem: function(key) { + return this.config[this.resolve(key)]; + }, + getValue: function(key, defaultValue) { + var item = this._getItem(key); + if (!item) { + return defaultValue; + } + return item.get(); + }, + setValue: function setValue(key, value) { + key = this.resolve(key); + var lastValue = this.getValue(key, value); + var ret = this._getItem(key).set(value); + if (lastValue !== value) { + this.valueUpdated(key, value, lastValue, 'ApplicationConfig.setValue'); + } + return ret; + }, + // sync dual-source (ie: Menu + API) items + resyncValue: function(key) { + var item = this._getItem(key); + return item && item.resync(); + }, + // sync Settings values -> Application state + applyValue: function applyValue(key, value, origin) { + if (this.resolve(key)) { + var appValue = this.getValue(key, value); + debugPrint('applyValue', key, value, origin ? '['+origin+']' : '', appValue); + if (appValue !== value) { + this.setValue(key, value); + debugPrint('applied new setting', key, value, '(was:'+appValue+')'); + return true; + } + } + } +}; + +// ApplicationConfigItem represents a single API/Menu item accessor +function ApplicationConfigItem(item) { + Object.assign(this, item); + Object.assign(this, { + _item: item.get && item, + _object: this._parseObjectConfig(this.object), + _menu: this._parseMenuConfig(this.menu) + }); + this.authority = this._item ? 'item' : this._object ? 'object' : this._menu ? 'menu' : null; + this._authority = this['_'+this.authority]; + debugPrint('_authority', this.authority, this._authority, Object.keys(this._authority)); + assert(this._authority, 'expected item.get, .object or .menu definition; ' + this.settingName); +} +ApplicationConfigItem.prototype = { + resync: function resync() { + var authoritativeValue = this._authority.get(); + if (this._menu && this._menu.get() !== authoritativeValue) { + _debugPrint(this.settingName, this._menu.menuItem, + '... menu value ('+this._menu.get()+') out of sync;', + 'setting to authoritativeValue ('+authoritativeValue+')'); + this._menu.set(authoritativeValue); + } + if (this._object && this._object.get() !== authoritativeValue) { + _debugPrint(this.settingName, this._object.getter || this._object.property, + '... object value ('+this._object.get()+') out of sync;', + 'setting to authoritativeValue ('+authoritativeValue+')'); + this._object.set(authoritativeValue); + } + }, + toString: function() { + return '[ApplicationConfigItem ' + [ + 'setting:' + JSON.stringify(this.settingName), + 'authority:' + JSON.stringify(this.authority), + this._object && 'object:' + JSON.stringify(this._object.property || this._object.getter), + this._menu && 'menu:' + JSON.stringify(this._menu.menu) + ].filter(Boolean).join(' ') + ']'; + }, + get: function get() { + return this._authority.get(); + }, + set: function set(nv) { + this._object && this._object.set(nv); + this._menu && this._menu.set(nv); + return nv; + }, + _raiseError: function(errorMessage) { + if (this.debug) { + throw new Error(errorMessage); + } else { + _debugPrint('ERROR: ' + errorMessage); + } + }, + _parseObjectConfig: function(parts) { + if (!Array.isArray(parts) || parts.length < 2) { + return null; + } + var object = parts[0], getter = parts[1], setter = parts[2]; + if (typeof object[getter] === 'function' && typeof object[setter] === 'function') { + // [ API, 'getter', 'setter' ] + return { + object: object, getter: getter, setter: setter, + get: function getObjectValue() { + return this.object[this.getter](); + }, + set: function setObjectValue(nv) { + return this.object[this.setter](nv), nv; + } + }; + } else if (getter in object) { + // [ API, 'property' ] + return { + object: object, property: getter, + get: function() { + return this.object[this.property]; + }, + set: function(nv) { + return this.object[this.property] = nv; + } + }; + } + this._raiseError('{ object: [ Object, getterOrPropertyName, setterName ] } -- invalid params or does not exist: ' + + [ this.settingName, this.object, getter, setter ].join(' | ')); + }, + _parseMenuConfig: function(menu) { + if (!menu || typeof menu !== 'string') { + return null; + } + var parts = menu.split(/\s*>\s*/), menuItemName = parts.pop(), menuName = parts.join(' > '); + if (menuItemName && Menu.menuItemExists(menuName, menuItemName)) { + return { + menu: menu, menuName: menuName, menuItemName: menuItemName, + get: function() { + return Menu.isOptionChecked(this.menuItemName); + }, + set: function(nv) { + return Menu.setIsOptionChecked(this.menuItemName, nv), nv; + } + }; + } + this._raiseError('{ menu: "Menu > Item" } structure -- invalid params or does not exist: ' + + [ this.settingName, this.menu, menuName, menuItemName ].join(' | ')); + } +}; // ApplicationConfigItem.prototype + +// ---------------------------------------------------------------------------- +// grouped configuration using the Settings.* API +function SettingsConfig(options) { + options = options || {}; + assert('namespace' in options); + this.namespace = options.namespace; + this.defaultValues = {}; + this.valueUpdated = _utils.signal(function valueUpdated(key, newValue, oldValue, origin){}); + if (options.defaultValues) { + Object.keys(options.defaultValues) + .forEach(_utils.bind(this, function(key) { + var fullSettingsKey = this.resolve(key); + this.defaultValues[fullSettingsKey] = options.defaultValues[key]; + })); + } +} +SettingsConfig.prototype = { + resolve: function(key) { + assert(typeof key === 'string', 'SettingsConfig.resolve error: key is not a string: ' + key); + return (0 !== key.indexOf('.') && !~key.indexOf('/')) ? + [ this.namespace, key ].join('/') : key; + }, + getValue: function(key, defaultValue) { + key = this.resolve(key); + defaultValue = defaultValue === undefined ? this.defaultValues[key] : defaultValue; + return Settings.getValue(key, defaultValue); + }, + setValue: function setValue(key, value) { + key = this.resolve(key); + var lastValue = this.getValue(key); + var ret = Settings.setValue(key, value); + if (lastValue !== value) { + this.valueUpdated(key, value, lastValue, 'SettingsConfig.setValue'); + } + return ret; + }, + getFloat: function getFloat(key, defaultValue) { + key = this.resolve(key); + defaultValue = defaultValue === undefined ? this.defaultValues[key] : defaultValue; + var value = parseFloat(this.getValue(key, defaultValue)); + return isFinite(value) ? value : isFinite(defaultValue) ? defaultValue : 0.0; + } +}; diff --git a/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/CustomSettingsApp.js b/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/CustomSettingsApp.js new file mode 100644 index 0000000000..c76adea940 --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/CustomSettingsApp.js @@ -0,0 +1,324 @@ +// CustomSettingsApp.js -- manages Settings between a Client script and connected "settings" tablet WebView page +// see browser/BridgedSettings.js for the webView side + +// example: +// var button = tablet.addButton({ text: 'My Settings' }); +// var mySettingsApp = new CustomSettingsApp({ +// namespace: 'mySettingsGroup', +// url: Script.resolvePath('myapp.html'), +// uuid: button.uuid, +// tablet: tablet +// }); +// +// // settings are automatically sync'd from the web page back to Interface; to be notified when that happens, use: +// myAppSettings.valueUpdated.connect(function(name, value, oldValue, origin) { +// print('setting updated from web page', name, value, oldValue, origin); +// }); +// +// // settings are also automatically sync'd from Interface back to the web page; to manually sync a value, use: +// myAppSettings.syncValue(fullSettingsKey, value); + +/* eslint-env commonjs */ +"use strict"; + +CustomSettingsApp.version = '0.0.0'; +module.exports = CustomSettingsApp; + +var _utils = require('../_utils.js'); +Object.assign = Object.assign || _utils.assign; + +function assert(truthy, message) { + return _utils.assert.call(this, truthy, 'CustomSettingsApp | ' + message); +} + +function _debugPrint() { + print('CustomSettingsApp | ' + [].slice.call(arguments).join(' ')); +} +var debugPrint = function() {}; + +function CustomSettingsApp(options) { + assert('url' in options, 'expected options.url'); + if (options.debug) { + debugPrint = _debugPrint; + } + + this.url = options.url; + this.namespace = options.namespace || 'BridgedSettings'; + this.uuid = options.uuid || Uuid.generate(); + this.recheckInterval = options.recheckInterval || 1000; + + this.settingsScreenVisible = false; + this.isActive = false; + + this.extraParams = Object.assign(options.extraParams || {}, { + customSettingsVersion: CustomSettingsApp.version+'', + protocolVersion: location.protocolVersion && location.protocolVersion() + }); + + var params = { + namespace: this.namespace, + uuid: this.uuid, + debug: options.debug || undefined + }; + + // encode PARAMS into '?key=value&...' + var query = Object.keys(params).map(function encodeValue(key) { + var value = encodeURIComponent(params[key] === undefined ? '' : params[key]); + return [ key, value ].join('='); + }).join('&'); + this.url += '?&' + query; + + this.isActiveChanged = _utils.signal(function(isActive) {}); + this.valueUpdated = _utils.signal(function(key, value, oldValue, origin) {}); + + this.settingsAPI = options.settingsAPI || Settings; + + // keep track of accessed settings so they can be kept in sync if changed externally + this._activeSettings = { sent: {}, received: {}, remote: {} }; + + if (options.tablet) { + this._initialize(options.tablet); + } +} + +CustomSettingsApp.prototype = { + tablet: null, + resolve: function(key) { + if (0 === key.indexOf('.') || ~key.indexOf('/')) { + // key is already qualified under a group; return as-is + return key; + } + // nest under the current namespace + return [ this.namespace, key ].join('/'); + }, + sendEvent: function(msg) { + assert(this.tablet, '!this.tablet'); + msg.ns = msg.ns || this.namespace; + msg.uuid = msg.uuid || this.uuid; + this.tablet.emitScriptEvent(JSON.stringify(msg)); + }, + getValue: function(key, defaultValue) { + key = this.resolve(key); + return key in this._activeSettings.remote ? this._activeSettings.remote[key] : defaultValue; + }, + setValue: function(key, value) { + key = this.resolve(key); + var current = this.getValue(key); + if (current !== value) { + return this.syncValue(key, value, 'CustomSettingsApp.setValue'); + } + return false; + }, + syncValue: function(key, value, origin) { + key = this.resolve(key); + var oldValue = this._activeSettings.remote[key]; + assert(value !== null, 'CustomSettingsApp.syncValue value is null'); + this.sendEvent({ id: 'valueUpdated', params: [key, value, oldValue, origin] }); + this._activeSettings.sent[key] = value; + this._activeSettings.remote[key] = value; + this.valueUpdated(key, value, oldValue, (origin ? origin+':' : '') + 'CustomSettingsApp.syncValue'); + }, + onScreenChanged: function onScreenChanged(type, url) { + this.settingsScreenVisible = (url === this.url); + debugPrint('===> onScreenChanged', type, url, 'settingsScreenVisible: ' + this.settingsScreenVisible); + if (this.isActive && !this.settingsScreenVisible) { + this.isActiveChanged(this.isActive = false); + } + }, + + _apiGetValue: function(key, defaultValue) { + // trim rooted keys like "/desktopTabletBecomesToolbar" => "desktopTabletBecomesToolbar" + key = key.replace(/^\//,''); + return this.settingsAPI.getValue(key, defaultValue); + }, + _apiSetValue: function(key, value) { + key = key.replace(/^\//,''); + return this.settingsAPI.setValue(key, value); + }, + _setValue: function(key, value, oldValue, origin) { + var current = this._apiGetValue(key), + lastRemoteValue = this._activeSettings.remote[key]; + debugPrint('.setValue(' + JSON.stringify({key: key, value: value, current: current, lastRemoteValue: lastRemoteValue })+')'); + this._activeSettings.received[key] = value; + this._activeSettings.remote[key] = value; + var result; + if (lastRemoteValue !== value) { + this.valueUpdated(key, value, lastRemoteValue, 'CustomSettingsApp.tablet'); + } + if (current !== value) { + result = this._apiSetValue(key, value); + } + return result; + }, + + _handleValidatedMessage: function(obj, msg) { + var tablet = this.tablet; + if (!tablet) { + throw new Error('_handleValidatedMessage called when not connected to tablet...'); + } + var params = Array.isArray(obj.params) ? obj.params : [obj.params]; + var parts = (obj.method||'').split('.'), api = parts[0], method = parts[1]; + switch(api) { + case 'valueUpdated': obj.result = this._setValue.apply(this, params); break; + case 'Settings': + if (method && params[0]) { + var key = this.resolve(params[0]), value = params[1]; + debugPrint('>>>>', method, key, value); + switch (method) { + case 'getValue': + obj.result = this._apiGetValue(key, value); + this._activeSettings.sent[key] = obj.result; + this._activeSettings.remote[key] = obj.result; + break; + case 'setValue': + obj.result = this._setValue(key, value, params[2], params[3]); + break; + default: + obj.error = 'unmapped Settings method: ' + method; + throw new Error(obj.error); + } + break; + } + default: if (this.onUnhandledMessage) { + this.onUnhandledMessage(obj, msg); + } else { + obj.error = 'unmapped method call: ' + msg; + } + } + if (obj.id) { + // if message has an id, reply with the same message obj which now has a .result or .error field + // note: a small delay is needed because of an apparent race condition between ScriptEngine and Tablet WebViews + Script.setTimeout(_utils.bind(this, function() { + this.sendEvent(obj); + }), 100); + } else if (obj.error) { + throw new Error(obj.error); + } + }, + onWebEventReceived: function onWebEventReceived(msg) { + debugPrint('onWebEventReceived', msg); + var tablet = this.tablet; + if (!tablet) { + throw new Error('onWebEventReceived called when not connected to tablet...'); + } + if (msg === this.url) { + if (this.isActive) { + // user (or page) refreshed the web view; trigger !isActive so client script can perform cleanup + this.isActiveChanged(this.isActive = false); + } + this.isActiveChanged(this.isActive = true); + // reply to initial HTML page ACK with any extraParams that were specified + this.sendEvent({ id: 'extraParams', extraParams: this.extraParams }); + return; + } + try { + var obj = assert(JSON.parse(msg)); + } catch (e) { + return; + } + if (obj.ns === this.namespace && obj.uuid === this.uuid) { + debugPrint('valid onWebEventReceived', msg); + this._handleValidatedMessage(obj, msg); + } + }, + + _initialize: function(tablet) { + if (this.tablet) { + throw new Error('CustomSettingsApp._initialize called but this.tablet already has a value'); + } + this.tablet = tablet; + tablet.webEventReceived.connect(this, 'onWebEventReceived'); + tablet.screenChanged.connect(this, 'onScreenChanged'); + + this.onAPIValueUpdated = function(key, newValue, oldValue, origin) { + if (this._activeSettings.remote[key] !== newValue) { + _debugPrint('onAPIValueUpdated: ' + key + ' = ' + JSON.stringify(newValue), + '(was: ' + JSON.stringify(oldValue) +')'); + this.syncValue(key, newValue, (origin ? origin+':' : '') + 'CustomSettingsApp.onAPIValueUpdated'); + } + }; + this.isActiveChanged.connect(this, function(isActive) { + this._activeSettings.remote = {}; // reset assumptions about remote values + isActive ? this.$startMonitor() : this.$stopMonitor(); + }); + debugPrint('CustomSettingsApp...initialized', this.namespace); + }, + + $syncSettings: function() { + for (var p in this._activeSettings.sent) { + var value = this._apiGetValue(p), + lastValue = this._activeSettings.remote[p]; + if (value !== undefined && value !== null && value !== '' && value !== lastValue) { + _debugPrint('CustomSettingsApp... detected external settings change', p, value); + this.syncValue(p, value, 'Settings'); + this.valueUpdated(p, value, lastValue, 'CustomSettingsApp.$syncSettings'); + } + } + }, + $startMonitor: function() { + if (!(this.recheckInterval > 0)) { + _debugPrint('$startMonitor -- recheckInterval <= 0; not starting settings monitor thread'); + return false; + } + if (this.interval) { + this.$stopMonitor(); + } + if (this.settingsAPI.valueUpdated) { + _debugPrint('settingsAPI supports valueUpdated -- binding to detect settings changes', this.settingsAPI); + this.settingsAPI.valueUpdated.connect(this, 'onAPIValueUpdated'); + } + this.interval = Script.setInterval(_utils.bind(this, '$syncSettings'), this.recheckInterval); + _debugPrint('STARTED MONITORING THREAD'); + }, + $stopMonitor: function() { + if (this.interval) { + Script.clearInterval(this.interval); + this.interval = 0; + if (this.settingsAPI.valueUpdated) { + this.settingsAPI.valueUpdated.disconnect(this, 'onAPIValueUpdated'); + } + _debugPrint('stopped monitoring thread'); + return true; + } + }, + + cleanup: function() { + if (!this.tablet) { + return _debugPrint('CustomSettingsApp...cleanup called when not initialized'); + } + var tablet = this.tablet; + tablet.webEventReceived.disconnect(this, 'onWebEventReceived'); + tablet.screenChanged.disconnect(this, 'onScreenChanged'); + this.$stopMonitor(); + if (this.isActive) { + try { + this.isActiveChanged(this.isActive = false); + } catch (e) { + _debugPrint('ERROR: cleanup error during isActiveChanged(false)', e); + } + } + this.toggle(false); + this.settingsScreenVisible = false; + this.tablet = null; + debugPrint('cleanup completed', this.namespace); + }, + + toggle: function(show) { + if (!this.tablet) { + return _debugPrint('CustomSettingsApp...cleanup called when not initialized'); + } + if (typeof show !== 'boolean') { + show = !this.settingsScreenVisible; + } + + if (this.settingsScreenVisible && !show) { + this.tablet.gotoHomeScreen(); + } else if (!this.settingsScreenVisible && show) { + Script.setTimeout(_utils.bind(this, function() { + // Interface sometimes crashes if not for adding a small timeout here :( + this.tablet.gotoWebScreen(this.url); + }), 1); + } + } +}; + diff --git a/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/browser/BridgedSettings.js b/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/browser/BridgedSettings.js new file mode 100644 index 0000000000..9b4735689f --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/browser/BridgedSettings.js @@ -0,0 +1,197 @@ +// BridgedSettings.js -- HTML-side implementation of bridged/async Settings +// see ../CustomSettingsApp.js for the corresponding Interface script + +/* eslint-env commonjs, browser */ +/* eslint-disable comma-dangle, no-empty */ + +(function(global) { + "use strict"; + + BridgedSettings.version = '0.0.2'; + + try { + module.exports = BridgedSettings; + } catch (e) { + global.BridgedSettings = BridgedSettings; + } + + var _utils = global._utils || (typeof require === 'function' && require('../../_utils.js')); + if (!_utils || !_utils.signal) { + throw new Error('html.BridgedSettings.js -- expected _utils to be available on the global object (ie: window._utils)'); + } + var signal = _utils.signal; + + function log() { + console.info('bridgedSettings | ' + [].slice.call(arguments).join(' ')); // eslint-disable-line no-console + } + log('version', BridgedSettings.version); + + var debugPrint = function() {}; // = log + + function BridgedSettings(options) { + options = options || {}; + // Note: Interface changed how window.EventBridge behaves again; it now arbitrarily replaces the global value + // sometime after the initial page load, invaliding any held references to it. + // As a workaround this proxies the local property to the current global value. + var _lastEventBridge = global.EventBridge; + Object.defineProperty(this, 'eventBridge', { enumerable: true, get: function() { + if (_lastEventBridge !== global.EventBridge) { + log('>>> EventBridge changed in-flight', '(was: ' + _lastEventBridge + ' | is: ' + global.EventBridge + ')'); + _lastEventBridge = global.EventBridge; + } + return global.EventBridge; + }}); + Object.assign(this, { + //eventBridge: options.eventBridge || global.EventBridge, + namespace: options.namespace || 'BridgedSettings', + uuid: options.uuid || undefined, + valueReceived: signal(function valueReceived(key, newValue, oldValue, origin){}), + callbackError: signal(function callbackError(error, message){}), + pendingRequestsFinished: signal(function pendingRequestsFinished(){}), + extraParams: options.extraParams || {}, + _hifiValues: {}, + + debug: options.debug, + log: log.bind({}, options.namespace + ' |'), + debugPrint: function() { + return this.debug && this.log.apply(this, arguments); + }, + _boundScriptEventReceived: this.onScriptEventReceived.bind(this), + callbacks: Object.defineProperties(options.callbacks || {}, { + extraParams: { value: this.handleExtraParams }, + valueUpdated: { value: this.handleValueUpdated }, + }) + }); + this.log('connecting to EventBridge.scriptEventReceived'); + this.eventBridge.scriptEventReceived.connect(this._boundScriptEventReceived); + } + + BridgedSettings.prototype = { + _callbackId: 1, + toString: function() { + return '[BridgedSettings namespace='+this.namespace+']'; + }, + resolve: function(key) { + if (0 !== key.indexOf('.') && !~key.indexOf('/')) { + return [ this.namespace, key ].join('/'); + } else { + return key; + } + }, + handleValueUpdated: function(msg) { + // client script notified us that a value was updated on that side + var key = this.resolve(msg.params[0]), + value = msg.params[1], + oldValue = msg.params[2], + origin = msg.params[3]; + log('callbacks.valueUpdated', key, value, oldValue, origin); + this._hifiValues[key] = value; + this.valueReceived(key, value, oldValue, (origin?origin+':':'') + 'callbacks.valueUpdated'); + }, + handleExtraParams: function(msg) { + // client script sent us extraParams + var extraParams = msg.extraParams; + var previousParams = JSON.parse(JSON.stringify(this.extraParams)); + + _utils.sortedAssign(this.extraParams, extraParams); + + this._hifiValues['.extraParams'] = this.extraParams; + this.debugPrint('received .extraParams', JSON.stringify(extraParams,0,2)); + this.valueReceived('.extraParams', this.extraParams, previousParams, 'html.bridgedSettings.handleExtraParams'); + }, + cleanup: function() { + try { + this.eventBridge.scriptEventReceived.disconnect(this._boundScriptEventReceived); + } catch (e) { + this.log('error disconnecting from scriptEventReceived:', e); + } + }, + pendingRequestCount: function() { + return Object.keys(this.callbacks).length; + }, + _handleValidatedMessage: function(obj, msg) { + var callback = this.callbacks[obj.id]; + if (callback) { + try { + return callback.call(this, obj) || true; + } catch (e) { + this.log('CALLBACK ERROR', this.namespace, obj.id, '_onScriptEventReceived', e); + this.callbackError(e, obj); + if (this.debug) { + throw e; + } + } + } else if (this.onUnhandledMessage) { + return this.onUnhandledMessage(obj, msg); + } + }, + onScriptEventReceived: function(msg) { + this.debugPrint(this.namespace, '_onScriptEventReceived......' + msg); + try { + var obj = JSON.parse(msg); + var validSender = obj.ns === this.namespace && obj.uuid === this.uuid; + if (validSender) { + return this._handleValidatedMessage(obj, msg); + } else { + debugPrint('xskipping', JSON.stringify([obj.ns, obj.uuid]), JSON.stringify(this), msg); + } + } catch (e) { + log('rpc error:', e, msg); + return e; + } + }, + sendEvent: function(msg) { + msg.ns = msg.ns || this.namespace; + msg.uuid = msg.uuid || this.uuid; + debugPrint('sendEvent', JSON.stringify(msg)); + this.eventBridge.emitWebEvent(JSON.stringify(msg)); + }, + getValue: function(key, defaultValue) { + key = this.resolve(key); + return key in this._hifiValues ? this._hifiValues[key] : defaultValue; + }, + setValue: function(key, value) { + key = this.resolve(key); + var current = this.getValue(key); + if (current !== value) { + debugPrint('SET VALUE : ' + JSON.stringify({ key: key, current: current, value: value })); + return this.syncValue(key, value, 'setValue'); + } + this._hifiValues[key] = value; + return false; + }, + syncValue: function(key, value, origin) { + return this.sendEvent({ method: 'valueUpdated', params: [key, value, this.getValue(key), origin] }); + }, + getValueAsync: function(key, defaultValue, callback) { + key = this.resolve(key); + if (typeof defaultValue === 'function') { + callback = defaultValue; + defaultValue = undefined; + } + var params = defaultValue !== undefined ? [ key, defaultValue ] : [ key ], + event = { method: 'Settings.getValue', params: params }; + + this.debugPrint('< getValueAsync...', key, params); + if (callback) { + event.id = this._callbackId++; + this.callbacks[event.id] = function(obj) { + try { + callback(obj.error, obj.result); + if (!obj.error) { + this._hifiValues[key] = obj.result; + } + } finally { + delete this.callbacks[event.id]; + } + if (this.pendingRequestCount() === 0) { + setTimeout(function() { + this.pendingRequestsFinished(); + }.bind(this), 1); + } + }; + } + this.sendEvent(event); + }, + }; +})(this); diff --git a/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/browser/JQuerySettings.js b/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/browser/JQuerySettings.js new file mode 100644 index 0000000000..66b1aad53a --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/browser/JQuerySettings.js @@ -0,0 +1,195 @@ +// JQuerySettings.js -- HTML-side helper class for managing settings-linked jQuery UI elements + +/* eslint-env jquery, commonjs, browser */ +/* eslint-disable comma-dangle, no-empty */ +/* global assert, log */ +// ---------------------------------------------------------------------------- +(function(global) { + "use strict"; + + JQuerySettings.version = '0.0.0'; + + try { + module.exports = JQuerySettings; + } catch (e) { + global.JQuerySettings= JQuerySettings; + } + + var _utils = global._utils || (typeof require === 'function' && require('../../_utils.js')); + + if (!_utils || !_utils.signal) { + throw new Error('html.JQuerySettings.js -- expected _utils to be available on the global object (ie: window._utils)'+module); + } + var signal = _utils.signal, + assert = _utils.assert; + + function log() { + console.info('jquerySettings | ' + [].slice.call(arguments).join(' ')); // eslint-disable-line no-console + } + log('version', JQuerySettings.version); + + var debugPrint = function() {}; // = log + + function JQuerySettings(options) { + assert('namespace' in options); + + Object.assign(this, { + id2Setting: {}, // DOM id -> qualified Settings key + Setting2id: {}, // qualified Settings key -> DOM id + observers: {}, // DOM MutationObservers + mutationEvent: signal(function mutationEvent(event) {}), + boundOnDOMMutation: this._onDOMMutation.bind(this), + }, options); + } + JQuerySettings.idCounter = 0; + JQuerySettings.prototype = { + toString: function() { + return '[JQuerySettings namespace='+this.namespace+']'; + }, + mutationConfig: { + attributes: true, + attributeOldValue: true, + attributeFilter: [ 'value', 'checked', 'data-checked', 'data-value' ] + }, + _onDOMMutation: function(mutations, observer) { + mutations.forEach(function(mutation, index) { + var target = mutation.target, + targetId = target.dataset['for'] || target.id, + domId = target.id, + attrValue = target.getAttribute(mutation.attributeName), + hifiType = target.dataset.hifiType, + value = hifiType ? $(target)[hifiType]('instance').value() : attrValue, + oldValue = mutation.oldValue; + var event = { + key: this.getKey(targetId, true) || this.getKey(domId), + value: value, + oldValue: oldValue, + hifiType: hifiType, + domId: domId, + domType: target.getAttribute('type') || target.type, + targetId: targetId, + attrValue: attrValue, + domName: target.name, + type: mutation.type, + }; + + switch (typeof value) { + case 'boolean': event.oldValue = !!event.oldValue; break; + case 'number': + var tmp = parseFloat(oldValue); + if (isFinite(tmp)) { + event.oldValue = tmp; + } + break; + } + + return (event.oldValue === event.value) ? + debugPrint('SKIP NON-MUTATION', event.key, event.hifiType) : + this.mutationEvent(event); + }.bind(this)); + }, + observeNode: function(node) { + assert(node.id); + var observer = this.observers[node.id]; + if (!observer) { + observer = new MutationObserver(this.boundOnDOMMutation); + observer.observe(node, this.mutationConfig); + this.observers[node.id] = observer; + } + debugPrint('observeNode', node.id, node.dataset.hifiType, node.name); + return observer; + }, + resolve: function(key) { + assert(typeof key === 'string'); + if (0 !== key.indexOf('.') && !~key.indexOf('/')) { + return [ this.namespace, key ].join('/'); + } else { + return key; + } + }, + registerSetting: function(id, key) { + assert(id, 'registerSetting -- invalid id: ' + id + ' for key:' + key); + this.id2Setting[id] = key; + if (!(key in this.Setting2id)) { + this.Setting2id[key] = id; + } else { + key = null; + } + debugPrint('JQuerySettings.registerSetting -- registered: ' + JSON.stringify({ id: id, key: key })); + }, + registerNode: function(node) { + var element = $(node), + target = element.data('for') || element.attr('for') || element.prop('id'); + assert(target, 'registerNode could determine settings target: ' + node.outerHTML); + if (!node.id) { + node.id = ['id', target.replace(/[^-\w]/g,'-'), JQuerySettings.idCounter++ ].join('-'); + } + var key = node.dataset['key'] = this.resolve(target); + this.registerSetting(node.id, key); + + debugPrint('registerNode', node.id, target, key); + // return this.observeNode(node); + }, + // lookup the DOM id for a given Settings key + getId: function(key, missingOk) { + key = this.resolve(key); + assert(missingOk || function assertion(){ + return typeof key === 'string'; + }); + if (key in this.Setting2id || missingOk) { + return this.Setting2id[key]; + } + log('WARNING: jquerySettings.getId: !Setting2id['+key+'] ' + this.Setting2id[key], key in this.Setting2id); + }, + getAllNodes: function() { + return Object.keys(this.Setting2id) + .map(function(key) { + return this.findNodeByKey(key); + }.bind(this)) + .filter(function(node) { + return node.type !== 'placeholder'; + }).filter(Boolean); + }, + // lookup the Settings key for a given DOM id + getKey: function(id, missingOk) { + if ((id in this.id2Setting) || missingOk) { + return this.id2Setting[id]; + } + log('WARNING: jquerySettings.getKey: !id2Setting['+id+']'); + }, + // lookup the DOM node for a given Settings key + findNodeByKey: function(key, missingOk) { + key = this.resolve(key); + var id = this.getId(key, missingOk); + var node = typeof id === 'object' ? id : document.getElementById(id); + if (node || missingOk) { + return node; + } + log('WARNING: jquerySettings.findNodeByKey -- node not found:', 'key=='+key, 'id=='+id); + }, + getValue: function(key, defaultValue) { + return this.getNodeValue(this.findNodeByKey(key)); + }, + setValue: function(key, value, origin) { + return this.setNodeValue(this.findNodeByKey(key), value, origin || 'setValue'); + }, + getNodeValue: function(node) { + assert(node && typeof node === 'object', 'getNodeValue expects a DOM node'); + node = node.jquery ? node.get(0) : node; + if (node.type === 'placeholder') { + return node.value; + } + assert(node.dataset.hifiType); + return $(node)[node.dataset.hifiType]('instance').value(); + }, + setNodeValue: function(node, newValue) { + assert(node, 'JQuerySettings::setNodeValue -- invalid node:' + node); + node = node.jquery ? node[0] : node; + if (node.type === 'placeholder') { + return node.value = newValue; + } + var hifiType = assert(node.dataset.hifiType); + return $(node)[hifiType]('instance').value(newValue); + }, + }; +})(this); diff --git a/unpublishedScripts/marketplace/camera-move/modules/movement-utils.js b/unpublishedScripts/marketplace/camera-move/modules/movement-utils.js new file mode 100644 index 0000000000..feedc75c83 --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/modules/movement-utils.js @@ -0,0 +1,773 @@ +// movement-utils.js -- helper classes for managing related Controller.*Event and input API bindings + +/* eslint-disable comma-dangle, no-empty */ +/* global require: true, DriveKeys, console, __filename, __dirname */ +/* eslint-env commonjs */ +"use strict"; + +module.exports = { + version: '0.0.2c', + + CameraControls: CameraControls, + MovementEventMapper: MovementEventMapper, + MovementMapping: MovementMapping, + VelocityTracker: VelocityTracker, + VirtualDriveKeys: VirtualDriveKeys, + + applyEasing: applyEasing, + calculateThrust: calculateThrust, + vec3damp: vec3damp, + vec3eclamp: vec3eclamp, + + DriveModes: { + POSITION: 'position', // ~ MyAvatar.position + MOTOR: 'motor', // ~ MyAvatar.motorVelocity + THRUST: 'thrust', // ~ MyAvatar.setThrust + }, +}; + +var MAPPING_TEMPLATE = require('./movement-utils.mapping.json'); +var WANT_DEBUG = false; + +function log() { + // eslint-disable-next-line no-console + (typeof Script === 'object' ? print : console.log)('movement-utils | ' + [].slice.call(arguments).join(' ')); +} + +var debugPrint = function() {}; + +log(module.exports.version); + +var _utils = require('./_utils.js'), + assert = _utils.assert; + +if (WANT_DEBUG) { + require = _utils.makeDebugRequire(__dirname); + _utils = require('./_utils.js'); // re-require in debug mode + debugPrint = log; +} + +Object.assign = Object.assign || _utils.assign; + +var enumMeta = require('./EnumMeta.js'); +assert(enumMeta.version >= '0.0.1', 'enumMeta >= 0.0.1 expected but got: ' + enumMeta.version); + +Object.assign(MovementEventMapper, { + CAPTURE_DRIVE_KEYS: 'drive-keys', + CAPTURE_ACTION_EVENTS: 'action-events', +}); + +function MovementEventMapper(options) { + assert('namespace' in options, '.namespace expected ' + Object.keys(options) ); + this.namespace = options.namespace; + this.enabled = false; + + this.options = Object.assign({ + namespace: this.namespace, + captureMode: MovementEventMapper.CAPTURE_ACTION_EVENTS, + excludeNames: null, + mouseSmooth: true, + keyboardMultiplier: 1.0, + mouseMultiplier: 1.0, + eventFilter: null, + controllerMapping: MAPPING_TEMPLATE, + }, options); + + this.isShifted = false; + this.isGrounded = false; + this.isRightMouseButton = false; + this.rightMouseButtonReleased = undefined; + + this.inputMapping = new MovementMapping(this.options); + this.inputMapping.virtualActionEvent.connect(this, 'onVirtualActionEvent'); +} +MovementEventMapper.prototype = { + constructor: MovementEventMapper, + defaultEventFilter: function(from, event) { + return event.actionValue; + }, + getState: function(options) { + var state = this.states ? this.states.getDriveKeys(options) : {}; + + state.enabled = this.enabled; + + state.mouseSmooth = this.options.mouseSmooth; + state.captureMode = this.options.captureMode; + state.mouseMultiplier = this.options.mouseMultiplier; + state.keyboardMultiplier = this.options.keyboardMultiplier; + + state.isGrounded = this.isGrounded; + state.isShifted = this.isShifted; + state.isRightMouseButton = this.isRightMouseButton; + state.rightMouseButtonReleased = this.rightMouseButtonReleased; + + return state; + }, + updateOptions: function(options) { + return _updateOptions(this.options, options || {}, this.constructor.name); + }, + applyOptions: function(options, applyNow) { + if (this.updateOptions(options) && applyNow) { + this.reset(); + } + }, + reset: function() { + if (this.enabled) { + this.disable(); + this.enable(); + } + }, + disable: function() { + this.inputMapping.disable(); + this.bindEvents(false); + this.enabled = false; + }, + enable: function() { + if (!this.enabled) { + this.enabled = true; + this.states = new VirtualDriveKeys({ + eventFilter: this.options.eventFilter && _utils.bind(this, this.options.eventFilter) + }); + this.bindEvents(true); + this.inputMapping.updateOptions(this.options); + this.inputMapping.enable(); + } + }, + bindEvents: function bindEvents(capture) { + var captureMode = this.options.captureMode; + assert(function assertion() { + return captureMode === MovementEventMapper.CAPTURE_ACTION_EVENTS || + captureMode === MovementEventMapper.CAPTURE_DRIVE_KEYS; + }); + log('bindEvents....', capture, this.options.captureMode); + var exclude = Array.isArray(this.options.excludeNames) && this.options.excludeNames; + + var tmp; + if (!capture || this.options.captureMode === MovementEventMapper.CAPTURE_ACTION_EVENTS) { + tmp = capture ? 'captureActionEvents' : 'releaseActionEvents'; + log('bindEvents -- ', tmp.toUpperCase()); + Controller[tmp](); + } + if (!capture || this.options.captureMode === MovementEventMapper.CAPTURE_DRIVE_KEYS) { + tmp = capture ? 'disableDriveKey' : 'enableDriveKey'; + log('bindEvents -- ', tmp.toUpperCase()); + for (var p in DriveKeys) { + if (capture && (exclude && ~exclude.indexOf(p))) { + log(tmp.toUpperCase(), 'excluding DriveKey===' + p); + } else { + MyAvatar[tmp](DriveKeys[p]); + } + } + } + try { + Controller.actionEvent[capture ? 'connect' : 'disconnect'](this, 'onActionEvent'); + } catch (e) { } + + if (!capture || !/person/i.test(Camera.mode)) { + Controller[capture ? 'captureWheelEvents' : 'releaseWheelEvents'](); + try { + Controller.wheelEvent[capture ? 'connect' : 'disconnect'](this, 'onWheelEvent'); + } catch (e) { /* eslint-disable-line empty-block */ } + } + }, + onWheelEvent: function onWheelEvent(event) { + var actionID = enumMeta.ACTION_TRANSLATE_CAMERA_Z, + actionValue = -event.delta; + return this.onActionEvent(actionID, actionValue, event); + }, + onActionEvent: function(actionID, actionValue, extra) { + var actionName = enumMeta.Controller.ActionNames[actionID], + driveKeyName = enumMeta.getDriveKeyNameFromActionName(actionName), + prefix = (actionValue > 0 ? '+' : actionValue < 0 ? '-' : ' '); + + var event = { + id: prefix + actionName, + actionName: actionName, + driveKey: DriveKeys[driveKeyName], + driveKeyName: driveKeyName, + actionValue: actionValue, + extra: extra + }; + // debugPrint('onActionEvent', actionID, actionName, driveKeyName); + this.states.handleActionEvent('Actions.' + actionName, event); + }, + onVirtualActionEvent: function(from, event) { + if (from === 'Application.Grounded') { + this.isGrounded = !!event.applicationValue; + } else if (from === 'Keyboard.Shift') { + this.isShifted = !!event.value; + } else if (from === 'Keyboard.RightMouseButton') { + this.isRightMouseButton = !!event.value; + this.rightMouseButtonReleased = !event.value ? new Date : undefined; + } + this.states.handleActionEvent(from, event); + } +}; // MovementEventMapper.prototype + +// ---------------------------------------------------------------------------- +// helper JS class to track drive keys -> translation / rotation influences +function VirtualDriveKeys(options) { + options = options || {}; + Object.defineProperties(this, { + $pendingReset: { value: {} }, + $eventFilter: { value: options.eventFilter }, + $valueUpdated: { value: _utils.signal(function valueUpdated(action, newValue, oldValue){}) } + }); +} +VirtualDriveKeys.prototype = { + constructor: VirtualDriveKeys, + update: function update(dt) { + Object.keys(this.$pendingReset).forEach(_utils.bind(this, function(i) { + var event = this.$pendingReset[i].event; + (event.driveKey in this) && this.setValue(event, 0); + })); + }, + getValue: function(driveKey, defaultValue) { + return driveKey in this ? this[driveKey] : defaultValue; + }, + _defaultFilter: function(from, event) { + return event.actionValue; + }, + handleActionEvent: function(from, event) { + var value = this.$eventFilter ? this.$eventFilter(from, event, this._defaultFilter) : event.actionValue; + return event.driveKeyName && this.setValue(event, value); + }, + setValue: function(event, value) { + var driveKeyName = event.driveKeyName, + driveKey = DriveKeys[driveKeyName], + id = event.id, + previous = this[driveKey], + autoReset = (driveKeyName === 'ZOOM'); + + this[driveKey] = value; + + if (previous !== value) { + this.$valueUpdated(event, value, previous); + } + if (value === 0.0) { + delete this.$pendingReset[id]; + } else if (autoReset) { + this.$pendingReset[id] = { event: event, value: value }; + } + }, + reset: function() { + Object.keys(this).forEach(_utils.bind(this, function(p) { + this[p] = 0.0; + })); + Object.keys(this.$pendingReset).forEach(_utils.bind(this, function(p) { + delete this.$pendingReset[p]; + })); + }, + toJSON: function() { + var obj = {}; + for (var key in this) { + if (enumMeta.DriveKeyNames[key]) { + obj[enumMeta.DriveKeyNames[key]] = this[key]; + } + } + return obj; + }, + getDriveKeys: function(options) { + options = options || {}; + try { + return { + translation: { + x: this.getValue(DriveKeys.TRANSLATE_X) || 0, + y: this.getValue(DriveKeys.TRANSLATE_Y) || 0, + z: this.getValue(DriveKeys.TRANSLATE_Z) || 0 + }, + rotation: { + x: this.getValue(DriveKeys.PITCH) || 0, + y: this.getValue(DriveKeys.YAW) || 0, + z: 'ROLL' in DriveKeys && this.getValue(DriveKeys.ROLL) || 0 + }, + zoom: Vec3.multiply(this.getValue(DriveKeys.ZOOM) || 0, Vec3.ONE) + }; + } finally { + options.update && this.update(options.update); + } + } +}; + +// ---------------------------------------------------------------------------- +// MovementMapping + +function MovementMapping(options) { + options = options || {}; + assert('namespace' in options && 'controllerMapping' in options); + this.namespace = options.namespace; + this.enabled = false; + this.options = { + keyboardMultiplier: 1.0, + mouseMultiplier: 1.0, + mouseSmooth: true, + captureMode: MovementEventMapper.CAPTURE_ACTION_EVENTS, + excludeNames: null, + controllerMapping: MAPPING_TEMPLATE, + }; + this.updateOptions(options); + this.virtualActionEvent = _utils.signal(function virtualActionEvent(from, event) {}); +} +MovementMapping.prototype = { + constructor: MovementMapping, + enable: function() { + this.enabled = true; + if (this.mapping) { + this.mapping.disable(); + } + this.mapping = this._createMapping(); + log('ENABLE CONTROLLER MAPPING', this.mapping.name); + this.mapping.enable(); + }, + disable: function() { + this.enabled = false; + if (this.mapping) { + log('DISABLE CONTROLLER MAPPING', this.mapping.name); + this.mapping.disable(); + } + }, + reset: function() { + var enabled = this.enabled; + enabled && this.disable(); + this.mapping = this._createMapping(); + enabled && this.enable(); + }, + updateOptions: function(options) { + return _updateOptions(this.options, options || {}, this.constructor.name); + }, + applyOptions: function(options, applyNow) { + if (this.updateOptions(options) && applyNow) { + this.reset(); + } + }, + onShiftKey: function onShiftKey(value, key) { + var event = { + type: value ? 'keypress' : 'keyrelease', + keyboardKey: key, + keyboardText: 'SHIFT', + keyboardValue: value, + actionName: 'Shift', + actionValue: !!value, + value: !!value, + at: +new Date + }; + this.virtualActionEvent('Keyboard.Shift', event); + }, + onRightMouseButton: function onRightMouseButton(value, key) { + var event = { + type: value ? 'mousepress' : 'mouserelease', + keyboardKey: key, + keyboardValue: value, + actionName: 'RightMouseButton', + actionValue: !!value, + value: !!value, + at: +new Date + }; + this.virtualActionEvent('Keyboard.RightMouseButton', event); + }, + onApplicationEvent: function _onApplicationEvent(key, name, value) { + var event = { + type: 'application', + actionName: 'Application.' + name, + applicationKey: key, + applicationName: name, + applicationValue: value, + actionValue: !!value, + value: !!value + }; + this.virtualActionEvent('Application.' + name, event); + }, + _createMapping: function() { + this._mapping = this._getTemplate(); + var mappingJSON = JSON.stringify(this._mapping, 0, 2); + var mapping = Controller.parseMapping(mappingJSON); + debugPrint(mappingJSON); + mapping.name = mapping.name || this._mapping.name; + + mapping.from(Controller.Hardware.Keyboard.Shift).peek().to(_utils.bind(this, 'onShiftKey')); + mapping.from(Controller.Hardware.Keyboard.RightMouseButton).peek().to(_utils.bind(this, 'onRightMouseButton')); + + var boundApplicationHandler = _utils.bind(this, 'onApplicationEvent'); + Object.keys(Controller.Hardware.Application).forEach(function(name) { + var key = Controller.Hardware.Application[name]; + debugPrint('observing Controller.Hardware.Application.'+ name, key); + mapping.from(key).to(function(value) { + boundApplicationHandler(key, name, value); + }); + }); + + return mapping; + }, + _getTemplate: function() { + assert(this.options.controllerMapping, 'MovementMapping._getTemplate -- !this.options.controllerMapping'); + var template = JSON.parse(JSON.stringify(this.options.controllerMapping)); // make a local copy + template.name = this.namespace; + template.channels = template.channels.filter(function(item) { + // ignore any "JSON comment" or other bindings without a from spec + return item.from && item.from.makeAxis; + }); + var exclude = Array.isArray(this.options.excludeNames) ? this.options.excludeNames : []; + if (!this.options.mouseSmooth) { + exclude.push('Keyboard.RightMouseButton'); + } + + log('EXCLUSIONS:' + exclude); + + template.channels = template.channels.filter(_utils.bind(this, function(item, i) { + debugPrint('channel['+i+']', item.from && item.from.makeAxis, item.to, JSON.stringify(item.filters) || ''); + // var hasFilters = Array.isArray(item.filters) && !item.filters[1]; + item.filters = Array.isArray(item.filters) ? item.filters : + typeof item.filters === 'string' ? [ { type: item.filters }] : [ item.filters ]; + + if (/Mouse/.test(item.from && item.from.makeAxis)) { + item.filters.push({ type: 'scale', scale: this.options.mouseMultiplier }); + log('applied mouse multiplier:', item.from.makeAxis, item.when, item.to, this.options.mouseMultiplier); + } else if (/Keyboard/.test(item.from && item.from.makeAxis)) { + item.filters.push({ type: 'scale', scale: this.options.keyboardMultiplier }); + log('applied keyboard multiplier:', item.from.makeAxis, item.when, item.to, this.options.keyboardMultiplier); + } + item.filters = item.filters.filter(Boolean); + if (~exclude.indexOf(item.to)) { + log('EXCLUDING item.to === ' + item.to); + return false; + } + var when = Array.isArray(item.when) ? item.when : [item.when]; + for (var j=0; j < when.length; j++) { + if (~exclude.indexOf(when[j])) { + log('EXCLUDING item.when contains ' + when[j]); + return false; + } + } + function shouldInclude(p, i) { + if (~exclude.indexOf(p)) { + log('EXCLUDING from.makeAxis[][' + i + '] === ' + p); + return false; + } + return true; + } + + if (item.from && Array.isArray(item.from.makeAxis)) { + var makeAxis = item.from.makeAxis; + item.from.makeAxis = makeAxis.map(function(axis) { + if (Array.isArray(axis)) { + return axis.filter(shouldInclude); + } else { + return shouldInclude(axis, -1) && axis; + } + }).filter(Boolean); + } + return true; + })); + debugPrint(JSON.stringify(template,0,2)); + return template; + } +}; // MovementMapping.prototype + +// update target properties from source, but iff the property already exists in target +function _updateOptions(target, source, debugName) { + debugName = debugName || '_updateOptions'; + var changed = 0; + if (!source || typeof source !== 'object') { + return changed; + } + for (var p in target) { + if (p in source && target[p] !== source[p]) { + log(debugName, 'updating source.'+p, target[p] + ' -> ' + source[p]); + target[p] = source[p]; + changed++; + } + } + for (p in source) { + (!(p in target)) && log(debugName, 'warning: ignoring unknown option:', p, (source[p] +'').substr(0, 40)+'...'); + } + return changed; +} + +// ---------------------------------------------------------------------------- +function calculateThrust(maxVelocity, targetVelocity, previousThrust) { + var THRUST_FALLOFF = 0.1; // round to ZERO if component is below this threshold + // Note: MyAvatar.setThrust might need an update to account for the recent avatar density changes... + // For now, this discovered scaling factor seems to accomodate a similar easing effect to the other movement models. + var magicScalingFactor = 12.0 * (maxVelocity + 120) / 16 - Math.sqrt( maxVelocity / 8 ); + + var targetThrust = Vec3.multiply(magicScalingFactor, targetVelocity); + targetThrust = vec3eclamp(targetThrust, THRUST_FALLOFF, maxVelocity); + if (Vec3.length(MyAvatar.velocity) > maxVelocity) { + targetThrust = Vec3.multiply(0.5, targetThrust); + } + return targetThrust; +} + +// ---------------------------------------------------------------------------- +// clamp components and magnitude to maxVelocity, rounding to Vec3.ZERO if below epsilon +function vec3eclamp(velocity, epsilon, maxVelocity) { + velocity = { + x: Math.max(-maxVelocity, Math.min(maxVelocity, velocity.x)), + y: Math.max(-maxVelocity, Math.min(maxVelocity, velocity.y)), + z: Math.max(-maxVelocity, Math.min(maxVelocity, velocity.z)) + }; + + if (Math.abs(velocity.x) < epsilon) { + velocity.x = 0; + } + if (Math.abs(velocity.y) < epsilon) { + velocity.y = 0; + } + if (Math.abs(velocity.z) < epsilon) { + velocity.z = 0; + } + + var length = Vec3.length(velocity); + if (length > maxVelocity) { + velocity = Vec3.multiply(maxVelocity, Vec3.normalize(velocity)); + } else if (length < epsilon) { + velocity = Vec3.ZERO; + } + return velocity; +} + +function vec3damp(active, positiveEffect, negativeEffect) { + // If force isn't being applied in a direction, incorporate negative effect (drag); + negativeEffect = { + x: active.x ? 0 : negativeEffect.x, + y: active.y ? 0 : negativeEffect.y, + z: active.z ? 0 : negativeEffect.z, + }; + return Vec3.subtract(Vec3.sum(active, positiveEffect), negativeEffect); +} + +// ---------------------------------------------------------------------------- +function VelocityTracker(defaultValues) { + Object.defineProperty(this, 'defaultValues', { configurable: true, value: defaultValues }); +} +VelocityTracker.prototype = { + constructor: VelocityTracker, + reset: function() { + Object.assign(this, this.defaultValues); + }, + integrate: function(targetState, currentVelocities, drag, settings) { + var args = [].slice.call(arguments); + this._applyIntegration('translation', args); + this._applyIntegration('rotation', args); + this._applyIntegration('zoom', args); + }, + _applyIntegration: function(component, args) { + return this._integrate.apply(this, [component].concat(args)); + }, + _integrate: function(component, targetState, currentVelocities, drag, settings) { + assert(targetState[component], component + ' not found in targetState (which has: ' + Object.keys(targetState) + ')'); + var result = vec3damp( + targetState[component], + currentVelocities[component], + drag[component] + ); + var maxVelocity = settings[component].maxVelocity; + return this[component] = vec3eclamp(result, settings.epsilon, maxVelocity); + }, +}; + +// ---------------------------------------------------------------------------- +// ---------------------------------------------------------------------------- +Object.assign(CameraControls, { + SCRIPT_UPDATE: 'update', + ANIMATION_FRAME: 'requestAnimationFrame', // emulated + NEXT_TICK: 'nextTick', // emulated + SET_IMMEDIATE: 'setImmediate', // emulated + //WORKER_THREAD: 'workerThread', +}); + +function CameraControls(options) { + options = options || {}; + assert('update' in options && 'threadMode' in options); + this.updateObject = typeof options.update === 'function' ? options : options.update; + assert(typeof this.updateObject.update === 'function', + 'construction options expected either { update: function(){}... } object or a function(){}'); + this.update = _utils.bind(this.updateObject, 'update'); + this.threadMode = options.threadMode; + this.fps = options.fps || 60; + this.getRuntimeSeconds = options.getRuntimeSeconds || function() { + return +new Date / 1000.0; + }; + this.backupOptions = _utils.DeferredUpdater.createGroup({ + MyAvatar: MyAvatar, + Camera: Camera, + Reticle: Reticle, + }); + + this.enabled = false; + this.enabledChanged = _utils.signal(function enabledChanged(enabled){}); + this.modeChanged = _utils.signal(function modeChanged(mode, oldMode){}); +} +CameraControls.prototype = { + constructor: CameraControls, + $animate: null, + $start: function() { + if (this.$animate) { + return; + } + + var lastTime; + switch (this.threadMode) { + case CameraControls.SCRIPT_UPDATE: { + this.$animate = this.update; + Script.update.connect(this, '$animate'); + this.$animate.disconnect = _utils.bind(this, function() { + Script.update.disconnect(this, '$animate'); + }); + } break; + + case CameraControls.ANIMATION_FRAME: { + this.requestAnimationFrame = _utils.createAnimationStepper({ + getRuntimeSeconds: this.getRuntimeSeconds, + fps: this.fps + }); + this.$animate = _utils.bind(this, function(dt) { + this.update(dt); + this.requestAnimationFrame(this.$animate); + }); + this.$animate.disconnect = _utils.bind(this.requestAnimationFrame, 'reset'); + this.requestAnimationFrame(this.$animate); + } break; + + case CameraControls.SET_IMMEDIATE: { + // emulate process.setImmediate (attempt to execute at start of next update frame, sans Script.update throttling) + lastTime = this.getRuntimeSeconds(); + this.$animate = Script.setInterval(_utils.bind(this, function() { + this.update(this.getRuntimeSeconds(lastTime)); + lastTime = this.getRuntimeSeconds(); + }), 5); + this.$animate.disconnect = function() { + Script.clearInterval(this); + }; + } break; + + case CameraControls.NEXT_TICK: { + // emulate process.nextTick (attempt to queue at the very next opportunity beyond current scope) + lastTime = this.getRuntimeSeconds(); + this.$animate = _utils.bind(this, function() { + this.$animate.timeout = 0; + if (this.$animate.quit) { + return; + } + this.update(this.getRuntimeSeconds(lastTime)); + lastTime = this.getRuntimeSeconds(); + this.$animate.timeout = Script.setTimeout(this.$animate, 0); + }); + this.$animate.quit = false; + this.$animate.disconnect = function() { + this.timeout && Script.clearTimeout(this.timeout); + this.timeout = 0; + this.quit = true; + }; + this.$animate(); + } break; + + default: throw new Error('unknown threadMode: ' + this.threadMode); + } + log( + '...$started update thread', '(threadMode: ' + this.threadMode + ')', + this.threadMode === CameraControls.ANIMATION_FRAME && this.fps + ); + }, + $stop: function() { + if (!this.$animate) { + return; + } + try { + this.$animate.disconnect(); + } catch (e) { + log('$animate.disconnect error: ' + e, '(threadMode: ' + this.threadMode +')'); + } + this.$animate = null; + log('...$stopped updated thread', '(threadMode: ' + this.threadMode +')'); + }, + onModeUpdated: function onModeUpdated(mode, oldMode) { + oldMode = oldMode || this.previousMode; + this.previousMode = mode; + log('onModeUpdated', oldMode + '->' + mode); + // user changed modes, so leave the current mode intact later when restoring backup values + delete this.backupOptions.Camera.$setModeString; + if (/person/.test(oldMode) && /person/.test(mode)) { + return; // disregard first -> third and third ->first transitions + } + this.modeChanged(mode, oldMode); + }, + + reset: function() { + if (this.enabled) { + this.disable(); + this.enable(); + } + }, + setEnabled: function setEnabled(enabled) { + if (!this.enabled && enabled) { + this.enable(); + } else if (this.enabled && !enabled) { + this.disable(); + } + }, + enable: function enable() { + if (this.enabled) { + throw new Error('CameraControls.enable -- already enabled..'); + } + log('ENABLE enableCameraMove', this.threadMode); + + this._backup(); + + this.previousMode = Camera.mode; + Camera.modeUpdated.connect(this, 'onModeUpdated'); + + this.$start(); + + this.enabledChanged(this.enabled = true); + }, + disable: function disable() { + log("DISABLE CameraControls"); + try { + Camera.modeUpdated.disconnect(this, 'onModeUpdated'); + } catch (e) { + debugPrint(e); + } + this.$stop(); + + this._restore(); + + if (this.enabled !== false) { + this.enabledChanged(this.enabled = false); + } + }, + _restore: function() { + var submitted = this.backupOptions.submit(); + log('restored previous values: ' + JSON.stringify(submitted,0,2)); + return submitted; + }, + _backup: function() { + this.backupOptions.reset(); + Object.assign(this.backupOptions.Reticle, { + scale: Reticle.scale, + }); + Object.assign(this.backupOptions.Camera, { + $setModeString: Camera.mode, + }); + Object.assign(this.backupOptions.MyAvatar, { + motorTimescale: MyAvatar.motorTimescale, + motorReferenceFrame: MyAvatar.motorReferenceFrame, + motorVelocity: Vec3.ZERO, + velocity: Vec3.ZERO, + angularVelocity: Vec3.ZERO, + }); + }, +}; // CameraControls + +// ---------------------------------------------------------------------------- +function applyEasing(deltaTime, direction, settings, state, scaling) { + var obj = {}; + for (var p in scaling) { + var group = settings[p], // translation | rotation | zoom + easeConst = group[direction], // easeIn | easeOut + scale = scaling[p], + stateVector = state[p]; + obj[p] = Vec3.multiply(easeConst * scale * deltaTime, stateVector); + } + return obj; +} diff --git a/unpublishedScripts/marketplace/camera-move/modules/movement-utils.mapping.json b/unpublishedScripts/marketplace/camera-move/modules/movement-utils.mapping.json new file mode 100644 index 0000000000..ec915a9fad --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/modules/movement-utils.mapping.json @@ -0,0 +1,98 @@ +{ + "name": "app-camera-move", + "channels": [ + + { "comment": "------------------ Actions.TranslateX -------------------" }, + { + "from": {"makeAxis": [["Keyboard.Left"],["Keyboard.Right"]]}, + "when": "Keyboard.Shift", + "to": "Actions.TranslateX" + }, + { + "from": {"makeAxis": [["Keyboard.A","Keyboard.TouchpadLeft"],["Keyboard.D","Keyboard.TouchpadRight"]]}, + "when": "Keyboard.Shift", + "to": "Actions.TranslateX" + }, + + { "comment": "------------------ Actions.TranslateY -------------------" }, + { + "from": {"makeAxis": [["Keyboard.C","Keyboard.PgDown"],["Keyboard.E","Keyboard.PgUp"]]}, + "when": "!Keyboard.Shift", + "to": "Actions.TranslateY" + }, + + { "comment": "------------------ Actions.TranslateZ -------------------" }, + { + "from": {"makeAxis": [["Keyboard.W"],["Keyboard.S"]]}, + "when": "!Keyboard.Shift", + "to": "Actions.TranslateZ" + }, + { + "from": {"makeAxis": [["Keyboard.Up"],["Keyboard.Down"]]}, + "when": "!Keyboard.Shift", + "to": "Actions.TranslateZ" + }, + + { "comment": "------------------ Actions.Yaw -------------------" }, + { + "from": {"makeAxis": [["Keyboard.A","Keyboard.TouchpadLeft"],["Keyboard.D","Keyboard.TouchpadRight"]]}, + "when": "!Keyboard.Shift", + "to": "Actions.Yaw", + "filters": ["invert"] + }, + { + "from": {"makeAxis": [["Keyboard.MouseMoveLeft"],["Keyboard.MouseMoveRight"]]}, + "when": "Keyboard.RightMouseButton", + "to": "Actions.Yaw", + "filters": ["invert"] + }, + { + "from": {"makeAxis": [["Keyboard.Left"],["Keyboard.Right"]]}, + "when": "!Keyboard.Shift", + "to": "Actions.Yaw", + "filters": ["invert"] + }, + { + "from": {"makeAxis": [["Keyboard.Left"],["Keyboard.Right"]]}, + "when": ["Application.SnapTurn", "!Keyboard.Shift"], + "to": "Actions.StepYaw", + "filters": + [ + "invert", + { "type": "pulse", "interval": 0.5, "resetOnZero": true }, + { "type": "scale", "scale": 22.5 } + ] + }, + + { "comment": "------------------ Actions.Pitch -------------------" }, + { + "from": {"makeAxis": [["Keyboard.W"],["Keyboard.S"]]}, + "when": "Keyboard.Shift", + "to": "Actions.Pitch", + "filters": ["invert"] + }, + { + "from": {"makeAxis": [["Keyboard.MouseMoveUp"],["Keyboard.MouseMoveDown"]]}, + "when": "Keyboard.RightMouseButton", + "to": "Actions.Pitch", + "filters": ["invert"] + }, + { + "from": {"makeAxis": [["Keyboard.Up"],["Keyboard.Down"]]}, + "when": "Keyboard.Shift", + "to": "Actions.Pitch", + "filters": ["invert"] + }, + + { "comment": "------------------ Actions.BoomIn -------------------" }, + { + "from": {"makeAxis": [["Keyboard.C","Keyboard.PgDown"],["Keyboard.E","Keyboard.PgUp"]]}, + "when": "Keyboard.Shift", + "to": "Actions.BoomIn", + "filters": [{"type": "scale","scale": 0.005}] + }, + + { "comment": "------------------ end -------------------" } + + ] +} diff --git a/unpublishedScripts/marketplace/spectator-camera/cameraOn.wav b/unpublishedScripts/marketplace/spectator-camera/cameraOn.wav new file mode 100644 index 0000000000..76dbb647b1 Binary files /dev/null and b/unpublishedScripts/marketplace/spectator-camera/cameraOn.wav differ diff --git a/unpublishedScripts/marketplace/spectator-camera/spectator-camera.fbx b/unpublishedScripts/marketplace/spectator-camera/spectator-camera.fbx new file mode 100644 index 0000000000..6584264c0d Binary files /dev/null and b/unpublishedScripts/marketplace/spectator-camera/spectator-camera.fbx differ diff --git a/unpublishedScripts/marketplace/spectator-camera/spectatorCamera.js b/unpublishedScripts/marketplace/spectator-camera/spectatorCamera.js new file mode 100644 index 0000000000..f0b943ad92 --- /dev/null +++ b/unpublishedScripts/marketplace/spectator-camera/spectatorCamera.js @@ -0,0 +1,514 @@ +"use strict"; +/*jslint vars:true, plusplus:true, forin:true*/ +/*global Tablet, Script, */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ +// +// spectatorCamera.js +// +// Created by Zach Fox on 2017-06-05 +// Copyright 2017 High Fidelity, Inc +// +// Distributed under the Apache License, Version 2.0 +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function () { // BEGIN LOCAL_SCOPE + + // FUNCTION VAR DECLARATIONS + var sendToQml, addOrRemoveButton, onTabletScreenChanged, fromQml, + onTabletButtonClicked, wireEventBridge, startup, shutdown, registerButtonMappings; + + // Function Name: inFrontOf() + // + // Description: + // -Returns the position in front of the given "position" argument, where the forward vector is based off + // the "orientation" argument and the amount in front is based off the "distance" argument. + function inFrontOf(distance, position, orientation) { + return Vec3.sum(position || MyAvatar.position, + Vec3.multiply(distance, Quat.getForward(orientation || MyAvatar.orientation))); + } + + // Function Name: spectatorCameraOn() + // + // Description: + // -Call this function to set up the spectator camera and + // spawn the camera entity. + // + // Relevant Variables: + // -spectatorCameraConfig: The render configuration of the spectator camera + // render job. It controls various attributes of the Secondary Camera, such as: + // -The entity ID to follow + // -Position + // -Orientation + // -Rendered texture size + // -Vertical field of view + // -Near clip plane distance + // -Far clip plane distance + // -viewFinderOverlay: The in-world overlay that displays the spectator camera's view. + // -camera: The in-world entity that corresponds to the spectator camera. + // -cameraIsDynamic: "false" for now - maybe it shouldn't be? False means that the camera won't drift when you let go... + // -cameraRotation: The rotation of the spectator camera. + // -cameraPosition: The position of the spectator camera. + // -glassPaneWidth: The width of the glass pane above the spectator camera that holds the viewFinderOverlay. + // -viewFinderOverlayDim: The x, y, and z dimensions of the viewFinderOverlay. + // -camera: The camera model which is grabbable. + // -viewFinderOverlay: The preview of what the spectator camera is viewing, placed inside the glass pane. + var spectatorCameraConfig = Render.getConfig("SecondaryCamera"); + var viewFinderOverlay = false; + var camera = false; + var cameraIsDynamic = false; + var cameraRotation; + var cameraPosition; + var glassPaneWidth = 0.16; + // The negative y dimension for viewFinderOverlay is necessary for now due to the way Image3DOverlay + // draws textures, but should be looked into at some point. Also the z dimension shouldn't affect + // the overlay since it is an Image3DOverlay so it is set to 0. + var viewFinderOverlayDim = { x: glassPaneWidth, y: -glassPaneWidth, z: 0 }; + function spectatorCameraOn() { + // Sets the special texture size based on the window it is displayed in, which doesn't include the menu bar + spectatorCameraConfig.enableSecondaryCameraRenderConfigs(true); + spectatorCameraConfig.resetSizeSpectatorCamera(Window.innerWidth, Window.innerHeight); + cameraRotation = Quat.multiply(MyAvatar.orientation, Quat.fromPitchYawRollDegrees(15, -155, 0)), cameraPosition = inFrontOf(0.85, Vec3.sum(MyAvatar.position, { x: 0, y: 0.28, z: 0 })); + camera = Entities.addEntity({ + "angularDamping": 1, + "damping": 1, + "collidesWith": "static,dynamic,kinematic,", + "collisionMask": 7, + "dynamic": cameraIsDynamic, + "modelURL": Script.resolvePath("spectator-camera.fbx"), + "registrationPoint": { + "x": 0.56, + "y": 0.545, + "z": 0.23 + }, + "rotation": cameraRotation, + "position": cameraPosition, + "shapeType": "simple-compound", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true}}" + }, true); + spectatorCameraConfig.attachedEntityId = camera; + updateOverlay(); + setDisplay(monitorShowsCameraView); + // Change button to active when window is first openend OR if the camera is on, false otherwise. + if (button) { + button.editProperties({ isActive: onSpectatorCameraScreen || camera }); + } + Audio.playSound(CAMERA_ON_SOUND, { + volume: 0.15, + position: cameraPosition, + localOnly: true + }); + } + + // Function Name: spectatorCameraOff() + // + // Description: + // -Call this function to shut down the spectator camera and + // destroy the camera entity. "isChangingDomains" is true when this function is called + // from the "Window.domainChanged()" signal. + var WAIT_AFTER_DOMAIN_SWITCH_BEFORE_CAMERA_DELETE_MS = 1 * 1000; + function spectatorCameraOff(isChangingDomains) { + function deleteCamera() { + Entities.deleteEntity(camera); + camera = false; + if (button) { + // Change button to active when window is first openend OR if the camera is on, false otherwise. + button.editProperties({ isActive: onSpectatorCameraScreen || camera }); + } + } + + spectatorCameraConfig.attachedEntityId = false; + spectatorCameraConfig.enableSecondaryCameraRenderConfigs(false); + if (camera) { + // Workaround for Avatar Entities not immediately having properties after + // the "Window.domainChanged()" signal is emitted. + // Should be removed after FB6155 is fixed. + if (isChangingDomains) { + Script.setTimeout(function () { + deleteCamera(); + spectatorCameraOn(); + }, WAIT_AFTER_DOMAIN_SWITCH_BEFORE_CAMERA_DELETE_MS); + } else { + deleteCamera(); + } + } + if (viewFinderOverlay) { + Overlays.deleteOverlay(viewFinderOverlay); + } + viewFinderOverlay = false; + setDisplay(monitorShowsCameraView); + } + + // Function Name: addOrRemoveButton() + // + // Description: + // -Used to add or remove the "SPECTATOR" app button from the HUD/tablet. Set the "isShuttingDown" argument + // to true if you're calling this function upon script shutdown. Set the "isHMDmode" to true if the user is + // in HMD; otherwise set to false. + // + // Relevant Variables: + // -button: The tablet button. + // -buttonName: The name of the button. + // -showSpectatorInDesktop: Set to "true" to show the "SPECTATOR" app in desktop mode. + var button = false; + var buttonName = "SPECTATOR"; + var showSpectatorInDesktop = false; + function addOrRemoveButton(isShuttingDown, isHMDMode) { + if (!tablet) { + print("Warning in addOrRemoveButton(): 'tablet' undefined!"); + return; + } + if (!button) { + if ((isHMDMode || showSpectatorInDesktop) && !isShuttingDown) { + button = tablet.addButton({ + text: buttonName, + icon: "icons/tablet-icons/spectator-i.svg", + activeIcon: "icons/tablet-icons/spectator-a.svg" + }); + button.clicked.connect(onTabletButtonClicked); + } + } else if (button) { + if ((!isHMDMode && !showSpectatorInDesktop) || isShuttingDown) { + button.clicked.disconnect(onTabletButtonClicked); + tablet.removeButton(button); + button = false; + } + } else { + print("ERROR adding/removing Spectator button!"); + } + } + + // Function Name: startup() + // + // Description: + // -startup() will be called when the script is loaded. + // + // Relevant Variables: + // -tablet: The tablet instance to be modified. + var tablet = null; + function startup() { + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + addOrRemoveButton(false, HMD.active); + tablet.screenChanged.connect(onTabletScreenChanged); + Window.domainChanged.connect(onDomainChanged); + Window.geometryChanged.connect(resizeViewFinderOverlay); + Controller.keyPressEvent.connect(keyPressEvent); + HMD.displayModeChanged.connect(onHMDChanged); + viewFinderOverlay = false; + camera = false; + registerButtonMappings(); + } + + // Function Name: wireEventBridge() + // + // Description: + // -Used to connect/disconnect the script's response to the tablet's "fromQml" signal. Set the "on" argument to enable or + // disable to event bridge. + // + // Relevant Variables: + // -hasEventBridge: true/false depending on whether we've already connected the event bridge. + var hasEventBridge = false; + function wireEventBridge(on) { + if (!tablet) { + print("Warning in wireEventBridge(): 'tablet' undefined!"); + return; + } + if (on) { + if (!hasEventBridge) { + tablet.fromQml.connect(fromQml); + hasEventBridge = true; + } + } else { + if (hasEventBridge) { + tablet.fromQml.disconnect(fromQml); + hasEventBridge = false; + } + } + } + + // Function Name: setDisplay() + // + // Description: + // -There are two bool variables that determine what the "url" argument to "setDisplayTexture(url)" should be: + // Camera on/off switch, and the "Monitor Shows" on/off switch. + // This results in four possible cases for the argument. Those four cases are: + // 1. Camera is off; "Monitor Shows" is "HMD Preview": "url" is "" + // 2. Camera is off; "Monitor Shows" is "Camera View": "url" is "" + // 3. Camera is on; "Monitor Shows" is "HMD Preview": "url" is "" + // 4. Camera is on; "Monitor Shows" is "Camera View": "url" is "resource://spectatorCameraFrame" + function setDisplay(showCameraView) { + + var url = (camera) ? (showCameraView ? "resource://spectatorCameraFrame" : "resource://hmdPreviewFrame") : ""; + sendToQml({ method: 'showPreviewTextureNotInstructions', setting: !!url, url: url}); + + // FIXME: temporary hack to avoid setting the display texture to hmdPreviewFrame + // until it is the correct mono. + if (url === "resource://hmdPreviewFrame") { + Window.setDisplayTexture(""); + } else { + Window.setDisplayTexture(url); + } + } + const MONITOR_SHOWS_CAMERA_VIEW_DEFAULT = false; + var monitorShowsCameraView = !!Settings.getValue('spectatorCamera/monitorShowsCameraView', MONITOR_SHOWS_CAMERA_VIEW_DEFAULT); + function setMonitorShowsCameraView(showCameraView) { + if (showCameraView === monitorShowsCameraView) { + return; + } + monitorShowsCameraView = showCameraView; + setDisplay(showCameraView); + Settings.setValue('spectatorCamera/monitorShowsCameraView', showCameraView); + } + function setMonitorShowsCameraViewAndSendToQml(showCameraView) { + setMonitorShowsCameraView(showCameraView); + sendToQml({ method: 'updateMonitorShowsSwitch', params: showCameraView }); + } + function keyPressEvent(event) { + if ((event.text === "0") && !event.isAutoRepeat && !event.isShifted && !event.isMeta && event.isControl && !event.isAlt) { + setMonitorShowsCameraViewAndSendToQml(!monitorShowsCameraView); + } + } + function updateOverlay() { + // The only way I found to update the viewFinderOverlay without turning the spectator camera on and off is to delete and recreate the + // overlay, which is inefficient but resizing the window shouldn't be performed often + if (viewFinderOverlay) { + Overlays.deleteOverlay(viewFinderOverlay); + } + viewFinderOverlay = Overlays.addOverlay("image3d", { + url: "resource://spectatorCameraFrame", + emissive: true, + parentID: camera, + alpha: 1, + localRotation: { w: 1, x: 0, y: 0, z: 0 }, + localPosition: { x: 0, y: 0.13, z: 0.126 }, + dimensions: viewFinderOverlayDim + }); + } + + // Function Name: resizeViewFinderOverlay() + // + // Description: + // -A function called when the window is moved/resized, which changes the viewFinderOverlay's texture and dimensions to be + // appropriately altered to fit inside the glass pane while not distorting the texture. The "geometryChanged" argument gives information + // on how the window changed, including x, y, width, and height. + // + // Relevant Variables: + // -glassPaneRatio: The aspect ratio of the glass pane, currently set as a 16:9 aspect ratio (change if model changes). + // -verticalScale: The amount the viewFinderOverlay should be scaled if the window size is vertical. + // -squareScale: The amount the viewFinderOverlay should be scaled if the window size is not vertical but is more square than the + // glass pane's aspect ratio. + function resizeViewFinderOverlay(geometryChanged) { + var glassPaneRatio = 16 / 9; + var verticalScale = 1 / glassPaneRatio; + var squareScale = verticalScale * (1 + (1 - (1 / (geometryChanged.width / geometryChanged.height)))); + + if (geometryChanged.height > geometryChanged.width) { //vertical window size + viewFinderOverlayDim = { x: (glassPaneWidth * verticalScale), y: (-glassPaneWidth * verticalScale), z: 0 }; + } else if ((geometryChanged.width / geometryChanged.height) < glassPaneRatio) { //square-ish window size, in-between vertical and horizontal + viewFinderOverlayDim = { x: (glassPaneWidth * squareScale), y: (-glassPaneWidth * squareScale), z: 0 }; + } else { //horizontal window size + viewFinderOverlayDim = { x: glassPaneWidth, y: -glassPaneWidth, z: 0 }; + } + updateOverlay(); + spectatorCameraConfig.resetSizeSpectatorCamera(geometryChanged.width, geometryChanged.height); + setDisplay(monitorShowsCameraView); + } + + const SWITCH_VIEW_FROM_CONTROLLER_DEFAULT = false; + var switchViewFromController = !!Settings.getValue('spectatorCamera/switchViewFromController', SWITCH_VIEW_FROM_CONTROLLER_DEFAULT); + function setControllerMappingStatus(status) { + if (!controllerMapping) { + return; + } + if (status) { + controllerMapping.enable(); + } else { + controllerMapping.disable(); + } + } + function setSwitchViewFromController(setting) { + if (setting === switchViewFromController) { + return; + } + switchViewFromController = setting; + setControllerMappingStatus(switchViewFromController); + Settings.setValue('spectatorCamera/switchViewFromController', setting); + } + + // Function Name: registerButtonMappings() + // + // Description: + // -Updates controller button mappings for Spectator Camera. + // + // Relevant Variables: + // -controllerMappingName: The name of the controller mapping. + // -controllerMapping: The controller mapping itself. + // -controllerType: "OculusTouch", "Vive", "Other". + var controllerMappingName; + var controllerMapping; + var controllerType = "Other"; + function registerButtonMappings() { + var VRDevices = Controller.getDeviceNames().toString(); + if (VRDevices) { + if (VRDevices.indexOf("Vive") !== -1) { + controllerType = "Vive"; + } else if (VRDevices.indexOf("OculusTouch") !== -1) { + controllerType = "OculusTouch"; + } else { + sendToQml({ method: 'updateControllerMappingCheckbox', setting: switchViewFromController, controller: controllerType }); + return; // Neither Vive nor Touch detected + } + } + + controllerMappingName = 'Hifi-SpectatorCamera-Mapping'; + controllerMapping = Controller.newMapping(controllerMappingName); + if (controllerType === "OculusTouch") { + controllerMapping.from(Controller.Standard.LS).to(function (value) { + if (value === 1.0) { + setMonitorShowsCameraViewAndSendToQml(!monitorShowsCameraView); + } + return; + }); + } else if (controllerType === "Vive") { + controllerMapping.from(Controller.Standard.LeftPrimaryThumb).to(function (value) { + if (value === 1.0) { + setMonitorShowsCameraViewAndSendToQml(!monitorShowsCameraView); + } + return; + }); + } + setControllerMappingStatus(switchViewFromController); + sendToQml({ method: 'updateControllerMappingCheckbox', setting: switchViewFromController, controller: controllerType }); + } + + // Function Name: onTabletButtonClicked() + // + // Description: + // -Fired when the Spectator Camera app button is pressed. + // + // Relevant Variables: + // -SPECTATOR_CAMERA_QML_SOURCE: The path to the SpectatorCamera QML + // -onSpectatorCameraScreen: true/false depending on whether we're looking at the spectator camera app. + var SPECTATOR_CAMERA_QML_SOURCE = Script.resourcesPath() + "qml/hifi/SpectatorCamera.qml"; + var onSpectatorCameraScreen = false; + function onTabletButtonClicked() { + if (!tablet) { + print("Warning in onTabletButtonClicked(): 'tablet' undefined!"); + return; + } + if (onSpectatorCameraScreen) { + // for toolbar-mode: go back to home screen, this will close the window. + tablet.gotoHomeScreen(); + } else { + tablet.loadQMLSource(SPECTATOR_CAMERA_QML_SOURCE); + sendToQml({ method: 'updateSpectatorCameraCheckbox', params: !!camera }); + sendToQml({ method: 'updateMonitorShowsSwitch', params: monitorShowsCameraView }); + if (!controllerMapping) { + registerButtonMappings(); + } else { + sendToQml({ method: 'updateControllerMappingCheckbox', setting: switchViewFromController, controller: controllerType }); + } + Menu.setIsOptionChecked("Disable Preview", false); + Menu.setIsOptionChecked("Mono Preview", true); + } + } + + // Function Name: onTabletScreenChanged() + // + // Description: + // -Called when the TabletScriptingInterface::screenChanged() signal is emitted. The "type" argument can be either the string + // value of "Home", "Web", "Menu", "QML", or "Closed". The "url" argument is only valid for Web and QML. + function onTabletScreenChanged(type, url) { + onSpectatorCameraScreen = (type === "QML" && url === SPECTATOR_CAMERA_QML_SOURCE); + wireEventBridge(onSpectatorCameraScreen); + // Change button to active when window is first openend OR if the camera is on, false otherwise. + if (button) { + button.editProperties({ isActive: onSpectatorCameraScreen || camera }); + } + } + + // Function Name: sendToQml() + // + // Description: + // -Use this function to send a message to the QML (i.e. to change appearances). The "message" argument is what is sent to + // SpectatorCamera QML in the format "{method, params}", like json-rpc. See also fromQml(). + function sendToQml(message) { + tablet.sendToQml(message); + } + + // Function Name: fromQml() + // + // Description: + // -Called when a message is received from SpectatorCamera.qml. The "message" argument is what is sent from the SpectatorCamera QML + // in the format "{method, params}", like json-rpc. See also sendToQml(). + function fromQml(message) { + switch (message.method) { + case 'spectatorCameraOn': + spectatorCameraOn(); + break; + case 'spectatorCameraOff': + spectatorCameraOff(); + break; + case 'setMonitorShowsCameraView': + setMonitorShowsCameraView(message.params); + break; + case 'changeSwitchViewFromControllerPreference': + setSwitchViewFromController(message.params); + break; + default: + print('Unrecognized message from SpectatorCamera.qml:', JSON.stringify(message)); + } + } + + // Function Name: onHMDChanged() + // + // Description: + // -Called from C++ when HMD mode is changed. The argument "isHMDMode" is true if HMD is on; false otherwise. + function onHMDChanged(isHMDMode) { + if (!controllerMapping) { + registerButtonMappings(); + } + setDisplay(monitorShowsCameraView); + addOrRemoveButton(false, isHMDMode); + if (!isHMDMode && !showSpectatorInDesktop) { + spectatorCameraOff(); + } + } + + // Function Name: shutdown() + // + // Description: + // -shutdown() will be called when the script ends (i.e. is stopped). + function shutdown() { + spectatorCameraOff(); + Window.domainChanged.disconnect(onDomainChanged); + Window.geometryChanged.disconnect(resizeViewFinderOverlay); + addOrRemoveButton(true, HMD.active); + if (tablet) { + tablet.screenChanged.disconnect(onTabletScreenChanged); + if (onSpectatorCameraScreen) { + tablet.gotoHomeScreen(); + } + } + HMD.displayModeChanged.disconnect(onHMDChanged); + Controller.keyPressEvent.disconnect(keyPressEvent); + if (controllerMapping) { + controllerMapping.disable(); + } + } + + // Function Name: onDomainChanged() + // + // Description: + // -A small utility function used when the Window.domainChanged() signal is fired. + function onDomainChanged() { + spectatorCameraOff(true); + } + + // These functions will be called when the script is loaded. + var CAMERA_ON_SOUND = SoundCache.getSound(Script.resolvePath("cameraOn.wav")); + startup(); + Script.scriptEnding.connect(shutdown); + +}()); // END LOCAL_SCOPE