diff --git a/.eslintrc.js b/.eslintrc.js index 9635142d1a..b4d88777f2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -34,6 +34,7 @@ module.exports = { "Quat": false, "Rates": false, "Recording": false, + "Resource": false, "Reticle": false, "Scene": false, "Script": false, diff --git a/BUILD.md b/BUILD.md index 547b79cb08..fc2359b057 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,7 +1,7 @@ ###Dependencies * [cmake](https://cmake.org/download/) ~> 3.3.2 -* [Qt](https://www.qt.io/download-open-source) ~> 5.6.1 +* [Qt](https://www.qt.io/download-open-source) ~> 5.6.2 * [OpenSSL](https://www.openssl.org/community/binaries.html) * IMPORTANT: Use the latest available version of OpenSSL to avoid security vulnerabilities. * [VHACD](https://github.com/virneo/v-hacd)(clone this repository)(Optional) @@ -46,8 +46,8 @@ This can either be entered directly into your shell session before you build or The path it needs to be set to will depend on where and how Qt5 was installed. e.g. - export QT_CMAKE_PREFIX_PATH=/usr/local/qt/5.6.1/clang_64/lib/cmake/ - export QT_CMAKE_PREFIX_PATH=/usr/local/Cellar/qt5/5.6.1-1/lib/cmake + export QT_CMAKE_PREFIX_PATH=/usr/local/qt/5.6.2/clang_64/lib/cmake/ + export QT_CMAKE_PREFIX_PATH=/usr/local/Cellar/qt5/5.6.2/lib/cmake export QT_CMAKE_PREFIX_PATH=/usr/local/opt/qt5/lib/cmake ####Generating build files @@ -64,7 +64,7 @@ Any variables that need to be set for CMake to find dependencies can be set as E For example, to pass the QT_CMAKE_PREFIX_PATH variable during build file generation: - cmake .. -DQT_CMAKE_PREFIX_PATH=/usr/local/qt/5.6.1/lib/cmake + cmake .. -DQT_CMAKE_PREFIX_PATH=/usr/local/qt/5.6.2/lib/cmake ####Finding Dependencies diff --git a/BUILD_ANDROID.md b/BUILD_ANDROID.md index bb8de0cc9a..1f144bf3ba 100644 --- a/BUILD_ANDROID.md +++ b/BUILD_ANDROID.md @@ -5,7 +5,7 @@ Please read the [general build guide](BUILD.md) for information on dependencies You will need the following tools to build our Android targets. * [cmake](http://www.cmake.org/download/) ~> 3.5.1 -* [Qt](http://www.qt.io/download-open-source/#) ~> 5.5.1 +* [Qt](http://www.qt.io/download-open-source/#) ~> 5.6.2 * [ant](http://ant.apache.org/bindownload.cgi) ~> 1.9.4 * [Android NDK](https://developer.android.com/tools/sdk/ndk/index.html) ~> r10d * [Android SDK](http://developer.android.com/sdk/installing/index.html) ~> 24.4.1.1 diff --git a/BUILD_OSX.md b/BUILD_OSX.md index 980263cbbc..afd3fa040c 100644 --- a/BUILD_OSX.md +++ b/BUILD_OSX.md @@ -16,16 +16,12 @@ For OpenSSL installed via homebrew, set OPENSSL_ROOT_DIR: Note that this uses the version from the homebrew formula at the time of this writing, and the version in the path will likely change. ###Qt -You can use the online installer or the offline installer. +Download and install the [Qt 5.6.2 for macOS](http://download.qt.io/official_releases/qt/5.6/5.6.2/qt-opensource-mac-x64-clang-5.6.2.dmg). -* [Download the online installer](https://www.qt.io/download-open-source/#section-2) - * When it asks you to select components, select the following: - * Qt > Qt 5.6 - -* [Download the offline installer](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-mac-x64-clang-5.6.1-1.dmg) +Keep the default components checked when going through the installer. Once Qt is installed, you need to manually configure the following: -* Set the QT_CMAKE_PREFIX_PATH environment variable to your `Qt5.6.1/5.6/clang_64/lib/cmake/` directory. +* Set the QT_CMAKE_PREFIX_PATH environment variable to your `Qt5.6.2/5.6/clang_64/lib/cmake/` directory. ###Xcode If Xcode is your editor of choice, you can ask CMake to generate Xcode project files instead of Unix Makefiles. diff --git a/BUILD_WIN.md b/BUILD_WIN.md index e37bf27503..841cfba3b3 100644 --- a/BUILD_WIN.md +++ b/BUILD_WIN.md @@ -8,23 +8,23 @@ Note: Newer versions of Visual Studio are not yet compatible. ###Step 2. Installing CMake -Download and install the CMake 3.8.0-rc2 "win64-x64 Installer" from the [CMake Website](https://cmake.org/download/). Make sure "Add CMake to system PATH for all users" is checked when going through the installer. +Download and install the [CMake 3.8.0 win64-x64 Installer](https://cmake.org/files/v3.8/cmake-3.8.0-win64-x64.msi). Make sure "Add CMake to system PATH for all users" is checked when going through the installer. ###Step 3. Installing Qt -Download and install the [Qt 5.6.1 Installer](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013_64-5.6.1-1.exe). Please note that the download file is large (850MB) and may take some time. +Download and install the [Qt 5.6.2 for Windows 64-bit (VS 2013)](http://download.qt.io/official_releases/qt/5.6/5.6.2/qt-opensource-windows-x86-msvc2013_64-5.6.2.exe). -Make sure to select all components when going through the installer. +Keep the default components checked when going through the installer. ###Step 4. Setting Qt Environment Variable Go to "Control Panel > System > Advanced System Settings > Environment Variables > New..." (or search “Environment Variables” in Start Search). * Set "Variable name": QT_CMAKE_PREFIX_PATH -* Set "Variable value": `C:\Qt\Qt5.6.1\5.6\msvc2013_64\lib\cmake` +* Set "Variable value": `%QT_DIR%\5.6\msvc2013_64\lib\cmake` ###Step 5. Installing OpenSSL -Download and install the "Win64 OpenSSL v1.0.2k" Installer from [this website](https://slproweb.com/products/Win32OpenSSL.html). +Download and install the [Win64 OpenSSL v1.0.2k Installer](https://slproweb.com/download/Win64OpenSSL-1_0_2k.exe). ###Step 6. Running CMake to Generate Build Files @@ -77,5 +77,5 @@ If not, add the directory where nmake is located to the PATH environment variabl ####Qt is throwing an error -Make sure you have the correct version (5.6.1-1) installed and 'QT_CMAKE_PREFIX_PATH' environment variable is set correctly. +Make sure you have the correct version (5.6.2) installed and 'QT_CMAKE_PREFIX_PATH' environment variable is set correctly. diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 46e826c596..c8ab489311 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include @@ -75,6 +76,8 @@ Agent::Agent(ReceivedMessage& message) : DependencyManager::set(ScriptEngine::AGENT_SCRIPT); DependencyManager::set(); + DependencyManager::set(); + auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); @@ -351,6 +354,10 @@ void Agent::executeScript() { // give this AvatarData object to the script engine _scriptEngine->registerGlobalObject("Avatar", scriptedAvatar.data()); + // give scripts access to the Users object + _scriptEngine->registerGlobalObject("Users", DependencyManager::get().data()); + + auto player = DependencyManager::get(); connect(player.data(), &recording::Deck::playbackStateChanged, [=] { if (player->isPlaying()) { @@ -537,7 +544,7 @@ void Agent::setIsAvatar(bool isAvatar) { connect(_avatarIdentityTimer, &QTimer::timeout, this, &Agent::sendAvatarIdentityPacket); // start the timers - _avatarIdentityTimer->start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS); + _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(); diff --git a/assignment-client/src/AssignmentAction.cpp b/assignment-client/src/AssignmentAction.cpp deleted file mode 100644 index 8d296cd6ab..0000000000 --- a/assignment-client/src/AssignmentAction.cpp +++ /dev/null @@ -1,91 +0,0 @@ -// -// AssignmentAction.cpp -// assignment-client/src/ -// -// Created by Seth Alves 2015-6-19 -// 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 -// - -#include "EntitySimulation.h" - -#include "AssignmentAction.h" - -AssignmentAction::AssignmentAction(EntityActionType type, const QUuid& id, EntityItemPointer ownerEntity) : - EntityActionInterface(type, id), - _data(QByteArray()), - _active(false), - _ownerEntity(ownerEntity) { -} - -AssignmentAction::~AssignmentAction() { -} - -void AssignmentAction::removeFromSimulation(EntitySimulationPointer simulation) const { - withReadLock([&]{ - simulation->removeAction(_id); - simulation->applyActionChanges(); - }); -} - -QByteArray AssignmentAction::serialize() const { - QByteArray result; - withReadLock([&]{ - result = _data; - }); - return result; -} - -void AssignmentAction::deserialize(QByteArray serializedArguments) { - withWriteLock([&]{ - _data = serializedArguments; - }); -} - -bool AssignmentAction::updateArguments(QVariantMap arguments) { - qDebug() << "UNEXPECTED -- AssignmentAction::updateArguments called in assignment-client."; - return false; -} - -QVariantMap AssignmentAction::getArguments() { - qDebug() << "UNEXPECTED -- AssignmentAction::getArguments called in assignment-client."; - return QVariantMap(); -} - -glm::vec3 AssignmentAction::getPosition() { - qDebug() << "UNEXPECTED -- AssignmentAction::getPosition called in assignment-client."; - return glm::vec3(0.0f); -} - -void AssignmentAction::setPosition(glm::vec3 position) { - qDebug() << "UNEXPECTED -- AssignmentAction::setPosition called in assignment-client."; -} - -glm::quat AssignmentAction::getRotation() { - qDebug() << "UNEXPECTED -- AssignmentAction::getRotation called in assignment-client."; - return glm::quat(); -} - -void AssignmentAction::setRotation(glm::quat rotation) { - qDebug() << "UNEXPECTED -- AssignmentAction::setRotation called in assignment-client."; -} - -glm::vec3 AssignmentAction::getLinearVelocity() { - qDebug() << "UNEXPECTED -- AssignmentAction::getLinearVelocity called in assignment-client."; - return glm::vec3(0.0f); -} - -void AssignmentAction::setLinearVelocity(glm::vec3 linearVelocity) { - qDebug() << "UNEXPECTED -- AssignmentAction::setLinearVelocity called in assignment-client."; -} - -glm::vec3 AssignmentAction::getAngularVelocity() { - qDebug() << "UNEXPECTED -- AssignmentAction::getAngularVelocity called in assignment-client."; - return glm::vec3(0.0f); -} - -void AssignmentAction::setAngularVelocity(glm::vec3 angularVelocity) { - qDebug() << "UNEXPECTED -- AssignmentAction::setAngularVelocity called in assignment-client."; -} diff --git a/assignment-client/src/AssignmentActionFactory.cpp b/assignment-client/src/AssignmentActionFactory.cpp deleted file mode 100644 index f99e712b72..0000000000 --- a/assignment-client/src/AssignmentActionFactory.cpp +++ /dev/null @@ -1,48 +0,0 @@ -// -// AssignmentActionFactory.cpp -// assignment-client/src/ -// -// Created by Seth Alves on 2015-6-19 -// 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 -// - -#include "AssignmentActionFactory.h" - - -EntityActionPointer assignmentActionFactory(EntityActionType type, const QUuid& id, EntityItemPointer ownerEntity) { - return EntityActionPointer(new AssignmentAction(type, id, ownerEntity)); -} - -EntityActionPointer AssignmentActionFactory::factory(EntityActionType type, - const QUuid& id, - EntityItemPointer ownerEntity, - QVariantMap arguments) { - EntityActionPointer action = assignmentActionFactory(type, id, ownerEntity); - if (action) { - bool ok = action->updateArguments(arguments); - if (ok) { - return action; - } - } - return nullptr; -} - - -EntityActionPointer AssignmentActionFactory::factoryBA(EntityItemPointer ownerEntity, QByteArray data) { - QDataStream serializedActionDataStream(data); - EntityActionType type; - QUuid id; - - serializedActionDataStream >> type; - serializedActionDataStream >> id; - - EntityActionPointer action = assignmentActionFactory(type, id, ownerEntity); - - if (action) { - action->deserialize(data); - } - return action; -} diff --git a/assignment-client/src/AssignmentActionFactory.h b/assignment-client/src/AssignmentActionFactory.h deleted file mode 100644 index 87970c9431..0000000000 --- a/assignment-client/src/AssignmentActionFactory.h +++ /dev/null @@ -1,29 +0,0 @@ -// -// AssignmentActionFactory.cpp -// assignment-client/src/ -// -// Created by Seth Alves on 2015-6-19 -// 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 -// - -#ifndef hifi_AssignmentActionFactory_h -#define hifi_AssignmentActionFactory_h - -#include "EntityActionFactoryInterface.h" -#include "AssignmentAction.h" - -class AssignmentActionFactory : public EntityActionFactoryInterface { -public: - AssignmentActionFactory() : EntityActionFactoryInterface() { } - virtual ~AssignmentActionFactory() { } - virtual EntityActionPointer factory(EntityActionType type, - const QUuid& id, - EntityItemPointer ownerEntity, - QVariantMap arguments) override; - virtual EntityActionPointer factoryBA(EntityItemPointer ownerEntity, QByteArray data) override; -}; - -#endif // hifi_AssignmentActionFactory_h diff --git a/assignment-client/src/AssignmentClient.cpp b/assignment-client/src/AssignmentClient.cpp index fe565b62f4..0869329d68 100644 --- a/assignment-client/src/AssignmentClient.cpp +++ b/assignment-client/src/AssignmentClient.cpp @@ -30,9 +30,10 @@ #include #include #include +#include #include "AssignmentFactory.h" -#include "AssignmentActionFactory.h" +#include "AssignmentDynamicFactory.h" #include "AssignmentClient.h" #include "AssignmentClientLogging.h" @@ -63,9 +64,10 @@ AssignmentClient::AssignmentClient(Assignment::Type requestAssignmentType, QStri auto animationCache = DependencyManager::set(); auto entityScriptingInterface = DependencyManager::set(false); - DependencyManager::registerInheritance(); - auto actionFactory = DependencyManager::set(); + DependencyManager::registerInheritance(); + auto dynamicFactory = DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); // setup a thread for the NodeList and its PacketReceiver QThread* nodeThread = new QThread(this); diff --git a/assignment-client/src/AssignmentDynamic.cpp b/assignment-client/src/AssignmentDynamic.cpp new file mode 100644 index 0000000000..026bc120bb --- /dev/null +++ b/assignment-client/src/AssignmentDynamic.cpp @@ -0,0 +1,83 @@ +// +// AssignmentDynamic.cpp +// assignment-client/src/ +// +// Created by Seth Alves 2015-6-19 +// 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 +// + +#include "EntitySimulation.h" + +#include "AssignmentDynamic.h" + +AssignmentDynamic::AssignmentDynamic(EntityDynamicType type, const QUuid& id, EntityItemPointer ownerEntity) : + EntityDynamicInterface(type, id), + _data(QByteArray()), + _active(false), + _ownerEntity(ownerEntity) { +} + +AssignmentDynamic::~AssignmentDynamic() { +} + +void AssignmentDynamic::removeFromSimulation(EntitySimulationPointer simulation) const { + withReadLock([&]{ + simulation->removeDynamic(_id); + simulation->applyDynamicChanges(); + }); +} + +QByteArray AssignmentDynamic::serialize() const { + QByteArray result; + withReadLock([&]{ + result = _data; + }); + return result; +} + +void AssignmentDynamic::deserialize(QByteArray serializedArguments) { + withWriteLock([&]{ + _data = serializedArguments; + }); +} + +bool AssignmentDynamic::updateArguments(QVariantMap arguments) { + qDebug() << "UNEXPECTED -- AssignmentDynamic::updateArguments called in assignment-client."; + return false; +} + +QVariantMap AssignmentDynamic::getArguments() { + qDebug() << "UNEXPECTED -- AssignmentDynamic::getArguments called in assignment-client."; + return QVariantMap(); +} + +glm::vec3 AssignmentDynamic::getPosition() { + qDebug() << "UNEXPECTED -- AssignmentDynamic::getPosition called in assignment-client."; + return glm::vec3(0.0f); +} + +glm::quat AssignmentDynamic::getRotation() { + qDebug() << "UNEXPECTED -- AssignmentDynamic::getRotation called in assignment-client."; + return glm::quat(); +} + +glm::vec3 AssignmentDynamic::getLinearVelocity() { + qDebug() << "UNEXPECTED -- AssignmentDynamic::getLinearVelocity called in assignment-client."; + return glm::vec3(0.0f); +} + +void AssignmentDynamic::setLinearVelocity(glm::vec3 linearVelocity) { + qDebug() << "UNEXPECTED -- AssignmentDynamic::setLinearVelocity called in assignment-client."; +} + +glm::vec3 AssignmentDynamic::getAngularVelocity() { + qDebug() << "UNEXPECTED -- AssignmentDynamic::getAngularVelocity called in assignment-client."; + return glm::vec3(0.0f); +} + +void AssignmentDynamic::setAngularVelocity(glm::vec3 angularVelocity) { + qDebug() << "UNEXPECTED -- AssignmentDynamic::setAngularVelocity called in assignment-client."; +} diff --git a/assignment-client/src/AssignmentAction.h b/assignment-client/src/AssignmentDynamic.h similarity index 69% rename from assignment-client/src/AssignmentAction.h rename to assignment-client/src/AssignmentDynamic.h index 98504b3545..35db8b1524 100644 --- a/assignment-client/src/AssignmentAction.h +++ b/assignment-client/src/AssignmentDynamic.h @@ -1,5 +1,5 @@ // -// AssignmentAction.h +// AssignmentDynamic.h // assignment-client/src/ // // Created by Seth Alves 2015-6-19 @@ -8,21 +8,21 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -// http://bulletphysics.org/Bullet/BulletFull/classbtActionInterface.html +// http://bulletphysics.org/Bullet/BulletFull/classbtDynamicInterface.html -#ifndef hifi_AssignmentAction_h -#define hifi_AssignmentAction_h +#ifndef hifi_AssignmentDynamic_h +#define hifi_AssignmentDynamic_h #include #include -#include "EntityActionInterface.h" +#include "EntityDynamicInterface.h" -class AssignmentAction : public EntityActionInterface, public ReadWriteLockable { +class AssignmentDynamic : public EntityDynamicInterface, public ReadWriteLockable { public: - AssignmentAction(EntityActionType type, const QUuid& id, EntityItemPointer ownerEntity); - virtual ~AssignmentAction(); + AssignmentDynamic(EntityDynamicType type, const QUuid& id, EntityItemPointer ownerEntity); + virtual ~AssignmentDynamic(); virtual void removeFromSimulation(EntitySimulationPointer simulation) const override; virtual EntityItemWeakPointer getOwnerEntity() const override { return _ownerEntity; } @@ -38,9 +38,7 @@ private: protected: virtual glm::vec3 getPosition() override; - virtual void setPosition(glm::vec3 position) override; virtual glm::quat getRotation() override; - virtual void setRotation(glm::quat rotation) override; virtual glm::vec3 getLinearVelocity() override; virtual void setLinearVelocity(glm::vec3 linearVelocity) override; virtual glm::vec3 getAngularVelocity() override; @@ -50,4 +48,4 @@ protected: EntityItemWeakPointer _ownerEntity; }; -#endif // hifi_AssignmentAction_h +#endif // hifi_AssignmentDynamic_h diff --git a/assignment-client/src/AssignmentDynamicFactory.cpp b/assignment-client/src/AssignmentDynamicFactory.cpp new file mode 100644 index 0000000000..88c7f6e06c --- /dev/null +++ b/assignment-client/src/AssignmentDynamicFactory.cpp @@ -0,0 +1,48 @@ +// +// AssignmentDynamcFactory.cpp +// assignment-client/src/ +// +// Created by Seth Alves on 2015-6-19 +// 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 +// + +#include "AssignmentDynamicFactory.h" + + +EntityDynamicPointer assignmentDynamicFactory(EntityDynamicType type, const QUuid& id, EntityItemPointer ownerEntity) { + return EntityDynamicPointer(new AssignmentDynamic(type, id, ownerEntity)); +} + +EntityDynamicPointer AssignmentDynamicFactory::factory(EntityDynamicType type, + const QUuid& id, + EntityItemPointer ownerEntity, + QVariantMap arguments) { + EntityDynamicPointer dynamic = assignmentDynamicFactory(type, id, ownerEntity); + if (dynamic) { + bool ok = dynamic->updateArguments(arguments); + if (ok) { + return dynamic; + } + } + return nullptr; +} + + +EntityDynamicPointer AssignmentDynamicFactory::factoryBA(EntityItemPointer ownerEntity, QByteArray data) { + QDataStream serializedDynamicDataStream(data); + EntityDynamicType type; + QUuid id; + + serializedDynamicDataStream >> type; + serializedDynamicDataStream >> id; + + EntityDynamicPointer dynamic = assignmentDynamicFactory(type, id, ownerEntity); + + if (dynamic) { + dynamic->deserialize(data); + } + return dynamic; +} diff --git a/assignment-client/src/AssignmentDynamicFactory.h b/assignment-client/src/AssignmentDynamicFactory.h new file mode 100644 index 0000000000..cdb9b6ae71 --- /dev/null +++ b/assignment-client/src/AssignmentDynamicFactory.h @@ -0,0 +1,29 @@ +// +// AssignmentDynamicFactory.cpp +// assignment-client/src/ +// +// Created by Seth Alves on 2015-6-19 +// 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 +// + +#ifndef hifi_AssignmentDynamicFactory_h +#define hifi_AssignmentDynamicFactory_h + +#include "EntityDynamicFactoryInterface.h" +#include "AssignmentDynamic.h" + +class AssignmentDynamicFactory : public EntityDynamicFactoryInterface { +public: + AssignmentDynamicFactory() : EntityDynamicFactoryInterface() { } + virtual ~AssignmentDynamicFactory() { } + virtual EntityDynamicPointer factory(EntityDynamicType type, + const QUuid& id, + EntityItemPointer ownerEntity, + QVariantMap arguments) override; + virtual EntityDynamicPointer factoryBA(EntityItemPointer ownerEntity, QByteArray data) override; +}; + +#endif // hifi_AssignmentDynamicFactory_h diff --git a/assignment-client/src/assets/SendAssetTask.cpp b/assignment-client/src/assets/SendAssetTask.cpp index ca8733d660..eab88e0d46 100644 --- a/assignment-client/src/assets/SendAssetTask.cpp +++ b/assignment-client/src/assets/SendAssetTask.cpp @@ -11,6 +11,8 @@ #include "SendAssetTask.h" +#include + #include #include @@ -21,6 +23,7 @@ #include #include "AssetUtils.h" +#include "ByteRange.h" #include "ClientServerUtils.h" SendAssetTask::SendAssetTask(QSharedPointer message, const SharedNodePointer& sendToNode, const QDir& resourcesDir) : @@ -34,20 +37,21 @@ SendAssetTask::SendAssetTask(QSharedPointer message, const Shar void SendAssetTask::run() { MessageID messageID; - DataOffset start, end; - + ByteRange byteRange; + _message->readPrimitive(&messageID); QByteArray assetHash = _message->read(SHA256_HASH_LENGTH); // `start` and `end` indicate the range of data to retrieve for the asset identified by `assetHash`. // `start` is inclusive, `end` is exclusive. Requesting `start` = 1, `end` = 10 will retrieve 9 bytes of data, // starting at index 1. - _message->readPrimitive(&start); - _message->readPrimitive(&end); + _message->readPrimitive(&byteRange.fromInclusive); + _message->readPrimitive(&byteRange.toExclusive); QString hexHash = assetHash.toHex(); - qDebug() << "Received a request for the file (" << messageID << "): " << hexHash << " from " << start << " to " << end; + qDebug() << "Received a request for the file (" << messageID << "): " << hexHash << " from " + << byteRange.fromInclusive << " to " << byteRange.toExclusive; qDebug() << "Starting task to send asset: " << hexHash << " for messageID " << messageID; auto replyPacketList = NLPacketList::create(PacketType::AssetGetReply, QByteArray(), true, true); @@ -56,7 +60,7 @@ void SendAssetTask::run() { replyPacketList->writePrimitive(messageID); - if (end <= start) { + if (!byteRange.isValid()) { replyPacketList->writePrimitive(AssetServerError::InvalidByteRange); } else { QString filePath = _resourcesDir.filePath(QString(hexHash)); @@ -64,15 +68,40 @@ void SendAssetTask::run() { QFile file { filePath }; if (file.open(QIODevice::ReadOnly)) { - if (file.size() < end) { + + // first fixup the range based on the now known file size + byteRange.fixupRange(file.size()); + + // check if we're being asked to read data that we just don't have + // because of the file size + if (file.size() < byteRange.fromInclusive || file.size() < byteRange.toExclusive) { replyPacketList->writePrimitive(AssetServerError::InvalidByteRange); - qCDebug(networking) << "Bad byte range: " << hexHash << " " << start << ":" << end; + qCDebug(networking) << "Bad byte range: " << hexHash << " " + << byteRange.fromInclusive << ":" << byteRange.toExclusive; } else { - auto size = end - start; - file.seek(start); - replyPacketList->writePrimitive(AssetServerError::NoError); - replyPacketList->writePrimitive(size); - replyPacketList->write(file.read(size)); + // we have a valid byte range, handle it and send the asset + auto size = byteRange.size(); + + if (byteRange.fromInclusive >= 0) { + + // this range is positive, meaning we just need to seek into the file and then read from there + file.seek(byteRange.fromInclusive); + replyPacketList->writePrimitive(AssetServerError::NoError); + replyPacketList->writePrimitive(size); + replyPacketList->write(file.read(size)); + } else { + // this range is negative, at least the first part of the read will be back into the end of the file + + // seek to the part of the file where the negative range begins + file.seek(file.size() + byteRange.fromInclusive); + + replyPacketList->writePrimitive(AssetServerError::NoError); + replyPacketList->writePrimitive(size); + + // first write everything from the negative range to the end of the file + replyPacketList->write(file.read(size)); + } + qCDebug(networking) << "Sending asset: " << hexHash; } file.close(); diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index f3da74ce5e..05dbfee912 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -71,15 +71,10 @@ AvatarMixer::~AvatarMixer() { void AvatarMixer::sendIdentityPacket(AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode) { QByteArray individualData = nodeData->getAvatar().identityByteArray(); - - auto identityPacket = NLPacket::create(PacketType::AvatarIdentity, individualData.size()); - individualData.replace(0, NUM_BYTES_RFC4122_UUID, nodeData->getNodeID().toRfc4122()); - - identityPacket->write(individualData); - - DependencyManager::get()->sendPacket(std::move(identityPacket), *destinationNode); - + auto identityPackets = NLPacketList::create(PacketType::AvatarIdentity, QByteArray(), true, true); + identityPackets->write(individualData); + DependencyManager::get()->sendPacketList(std::move(identityPackets), *destinationNode); ++_sumIdentityPackets; } diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index 6e3dd150a4..2ad8bb58ed 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -263,16 +263,8 @@ void AvatarMixerSlave::broadcastAvatarData(const SharedNodePointer& node) { // make sure we haven't already sent this data from this sender to this receiver // or that somehow we haven't sent if (lastSeqToReceiver == lastSeqFromSender && lastSeqToReceiver != 0) { - // don't ignore this avatar if we haven't sent any update for a long while - // in an effort to prevent other interfaces from deleting a stale avatar instance - uint64_t lastBroadcastTime = nodeData->getLastBroadcastTime(avatarNode->getUUID()); - const AvatarMixerClientData* otherNodeData = reinterpret_cast(avatarNode->getLinkedData()); - const uint64_t AVATAR_UPDATE_STALE = AVATAR_UPDATE_TIMEOUT - USECS_PER_SECOND; - if (lastBroadcastTime > otherNodeData->getIdentityChangeTimestamp() && - lastBroadcastTime + AVATAR_UPDATE_STALE > startIgnoreCalculation) { - ++numAvatarsHeldBack; - shouldIgnore = true; - } + ++numAvatarsHeldBack; + shouldIgnore = true; } else if (lastSeqFromSender - lastSeqToReceiver > 1) { // this is a skip - we still send the packet but capture the presence of the skip so we see it happening ++numAvatarsWithSkippedFrames; @@ -285,7 +277,7 @@ void AvatarMixerSlave::broadcastAvatarData(const SharedNodePointer& node) { int avatarRank = 0; // this is overly conservative, because it includes some avatars we might not consider - int remainingAvatars = (int)sortedAvatars.size(); + int remainingAvatars = (int)sortedAvatars.size(); while (!sortedAvatars.empty()) { AvatarPriority sortData = sortedAvatars.top(); diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index f2dbe5d1d2..af5f2c904e 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -11,12 +11,14 @@ #include "OctreeServer.h" +#include #include #include #include #include +#include #include #include #include @@ -924,6 +926,57 @@ void OctreeServer::handleJurisdictionRequestPacket(QSharedPointerqueueReceivedPacket(message, senderNode); } +void OctreeServer::handleOctreeFileReplacement(QSharedPointer message) { + if (!_isFinished && !_isShuttingDown) { + // these messages are only allowed to come from the domain server, so make sure that is the case + auto nodeList = DependencyManager::get(); + if (message->getSenderSockAddr() == nodeList->getDomainHandler().getSockAddr()) { + // it's far cleaner to load up the new content upon server startup + // so here we just store a special file at our persist path + // and then force a stop of the server so that it can pick it up when it relaunches + if (!_persistAbsoluteFilePath.isEmpty()) { + + // before we restart the server and make it try and load this data, let's make sure it is valid + auto compressedOctree = message->getMessage(); + QByteArray jsonOctree; + + // assume we have GZipped content + bool wasCompressed = gunzip(compressedOctree, jsonOctree); + if (!wasCompressed) { + // the source was not compressed, assume we were sent regular JSON data + jsonOctree = compressedOctree; + } + + // check the JSON data to verify it is an object + if (QJsonDocument::fromJson(jsonOctree).isObject()) { + if (!wasCompressed) { + // source was not compressed, we compress it before we write it locally + gzip(jsonOctree, compressedOctree); + } + + // write the compressed octree data to a special file + auto replacementFilePath = _persistAbsoluteFilePath.append(OctreePersistThread::REPLACEMENT_FILE_EXTENSION); + QFile replacementFile(replacementFilePath); + if (replacementFile.open(QIODevice::WriteOnly) && replacementFile.write(compressedOctree) != -1) { + // we've now written our replacement file, time to take the server down so it can + // process it when it comes back up + qInfo() << "Wrote octree replacement file to" << replacementFilePath << "- stopping server"; + setFinished(true); + } else { + qWarning() << "Could not write replacement octree data to file - refusing to process"; + } + } else { + qDebug() << "Received replacement octree file that is invalid - refusing to process"; + } + } else { + qDebug() << "Cannot perform octree file replacement since current persist file path is not yet known"; + } + } else { + qDebug() << "Received an octree file replacement that was not from our domain server - refusing to process"; + } + } +} + bool OctreeServer::readOptionBool(const QString& optionName, const QJsonObject& settingsSectionObject, bool& result) { result = false; // assume it doesn't exist bool optionAvailable = false; @@ -1148,6 +1201,7 @@ void OctreeServer::domainSettingsRequestComplete() { packetReceiver.registerListener(getMyQueryMessageType(), this, "handleOctreeQueryPacket"); packetReceiver.registerListener(PacketType::OctreeDataNack, this, "handleOctreeDataNackPacket"); packetReceiver.registerListener(PacketType::JurisdictionRequest, this, "handleJurisdictionRequestPacket"); + packetReceiver.registerListener(PacketType::OctreeFileReplacement, this, "handleOctreeFileReplacement"); readConfiguration(); @@ -1173,25 +1227,25 @@ void OctreeServer::domainSettingsRequestComplete() { // If persist filename does not exist, let's see if there is one beside the application binary // If there is, let's copy it over to our target persist directory QDir persistPath { _persistFilePath }; - QString persistAbsoluteFilePath = persistPath.absolutePath(); + _persistAbsoluteFilePath = persistPath.absolutePath(); if (persistPath.isRelative()) { // if the domain settings passed us a relative path, make an absolute path that is relative to the // default data directory - persistAbsoluteFilePath = QDir(PathUtils::getAppDataFilePath("entities/")).absoluteFilePath(_persistFilePath); + _persistAbsoluteFilePath = QDir(PathUtils::getAppDataFilePath("entities/")).absoluteFilePath(_persistFilePath); } static const QString ENTITY_PERSIST_EXTENSION = ".json.gz"; // force the persist file to end with .json.gz - if (!persistAbsoluteFilePath.endsWith(ENTITY_PERSIST_EXTENSION, Qt::CaseInsensitive)) { - persistAbsoluteFilePath += ENTITY_PERSIST_EXTENSION; + if (!_persistAbsoluteFilePath.endsWith(ENTITY_PERSIST_EXTENSION, Qt::CaseInsensitive)) { + _persistAbsoluteFilePath += ENTITY_PERSIST_EXTENSION; } else { // make sure the casing of .json.gz is correct - persistAbsoluteFilePath.replace(ENTITY_PERSIST_EXTENSION, ENTITY_PERSIST_EXTENSION, Qt::CaseInsensitive); + _persistAbsoluteFilePath.replace(ENTITY_PERSIST_EXTENSION, ENTITY_PERSIST_EXTENSION, Qt::CaseInsensitive); } - if (!QFile::exists(persistAbsoluteFilePath)) { + if (!QFile::exists(_persistAbsoluteFilePath)) { qDebug() << "Persist file does not exist, checking for existence of persist file next to application"; static const QString OLD_DEFAULT_PERSIST_FILENAME = "resources/models.json.gz"; @@ -1217,7 +1271,7 @@ void OctreeServer::domainSettingsRequestComplete() { pathToCopyFrom = oldDefaultPersistPath; } - QDir persistFileDirectory { QDir::cleanPath(persistAbsoluteFilePath + "/..") }; + QDir persistFileDirectory { QDir::cleanPath(_persistAbsoluteFilePath + "/..") }; if (!persistFileDirectory.exists()) { qDebug() << "Creating data directory " << persistFileDirectory.absolutePath(); @@ -1225,15 +1279,15 @@ void OctreeServer::domainSettingsRequestComplete() { } if (shouldCopy) { - qDebug() << "Old persist file found, copying from " << pathToCopyFrom << " to " << persistAbsoluteFilePath; + qDebug() << "Old persist file found, copying from " << pathToCopyFrom << " to " << _persistAbsoluteFilePath; - QFile::copy(pathToCopyFrom, persistAbsoluteFilePath); + QFile::copy(pathToCopyFrom, _persistAbsoluteFilePath); } else { qDebug() << "No existing persist file found"; } } - auto persistFileDirectory = QFileInfo(persistAbsoluteFilePath).absolutePath(); + auto persistFileDirectory = QFileInfo(_persistAbsoluteFilePath).absolutePath(); if (_backupDirectoryPath.isEmpty()) { // Use the persist file's directory to store backups _backupDirectoryPath = persistFileDirectory; @@ -1264,7 +1318,7 @@ void OctreeServer::domainSettingsRequestComplete() { qDebug() << "Backups will be stored in: " << _backupDirectoryPath; // now set up PersistThread - _persistThread = new OctreePersistThread(_tree, persistAbsoluteFilePath, _backupDirectoryPath, _persistInterval, + _persistThread = new OctreePersistThread(_tree, _persistAbsoluteFilePath, _backupDirectoryPath, _persistInterval, _wantBackup, _settings, _debugTimestampNow, _persistAsFileType); _persistThread->initialize(true); } diff --git a/assignment-client/src/octree/OctreeServer.h b/assignment-client/src/octree/OctreeServer.h index 3bb327eb06..8db8d845de 100644 --- a/assignment-client/src/octree/OctreeServer.h +++ b/assignment-client/src/octree/OctreeServer.h @@ -136,6 +136,7 @@ private slots: void handleOctreeQueryPacket(QSharedPointer message, SharedNodePointer senderNode); void handleOctreeDataNackPacket(QSharedPointer message, SharedNodePointer senderNode); void handleJurisdictionRequestPacket(QSharedPointer message, SharedNodePointer senderNode); + void handleOctreeFileReplacement(QSharedPointer message); void removeSendThread(); protected: @@ -172,6 +173,7 @@ protected: QString _statusHost; QString _persistFilePath; + QString _persistAbsoluteFilePath; QString _persistAsFileType; QString _backupDirectoryPath; int _packetsPerClientPerInterval; diff --git a/assignment-client/src/scripts/EntityScriptServer.cpp b/assignment-client/src/scripts/EntityScriptServer.cpp index 954c25a342..270a22e17b 100644 --- a/assignment-client/src/scripts/EntityScriptServer.cpp +++ b/assignment-client/src/scripts/EntityScriptServer.cpp @@ -481,14 +481,14 @@ void EntityScriptServer::deletingEntity(const EntityItemID& entityID) { } } -void EntityScriptServer::entityServerScriptChanging(const EntityItemID& entityID, const bool reload) { +void EntityScriptServer::entityServerScriptChanging(const EntityItemID& entityID, bool reload) { if (_entityViewer.getTree() && !_shuttingDown) { _entitiesScriptEngine->unloadEntityScript(entityID, true); checkAndCallPreload(entityID, reload); } } -void EntityScriptServer::checkAndCallPreload(const EntityItemID& entityID, const bool reload) { +void EntityScriptServer::checkAndCallPreload(const EntityItemID& entityID, bool reload) { if (_entityViewer.getTree() && !_shuttingDown && _entitiesScriptEngine) { EntityItemPointer entity = _entityViewer.getTree()->findEntityByEntityItemID(entityID); diff --git a/assignment-client/src/scripts/EntityScriptServer.h b/assignment-client/src/scripts/EntityScriptServer.h index a468e62958..687641d6be 100644 --- a/assignment-client/src/scripts/EntityScriptServer.h +++ b/assignment-client/src/scripts/EntityScriptServer.h @@ -67,8 +67,8 @@ private: void addingEntity(const EntityItemID& entityID); void deletingEntity(const EntityItemID& entityID); - void entityServerScriptChanging(const EntityItemID& entityID, const bool reload); - void checkAndCallPreload(const EntityItemID& entityID, const bool reload = false); + void entityServerScriptChanging(const EntityItemID& entityID, bool reload); + void checkAndCallPreload(const EntityItemID& entityID, bool reload = false); void cleanupOldKilledListeners(); diff --git a/cmake/externals/faceshift/CMakeLists.txt b/cmake/externals/faceshift/CMakeLists.txt deleted file mode 100644 index c4f2055435..0000000000 --- a/cmake/externals/faceshift/CMakeLists.txt +++ /dev/null @@ -1,47 +0,0 @@ -set(EXTERNAL_NAME faceshift) - -include(ExternalProject) -ExternalProject_Add( - ${EXTERNAL_NAME} - URL https://hifi-public.s3.amazonaws.com/dependencies/faceshift.zip - CMAKE_ARGS ${ANDROID_CMAKE_ARGS} -DCMAKE_INSTALL_PREFIX:PATH= - BINARY_DIR ${EXTERNAL_PROJECT_PREFIX}/build - LOG_DOWNLOAD 1 - LOG_CONFIGURE 1 - LOG_BUILD 1 -) - -# URL_MD5 1bdcb8a0b8d5b1ede434cc41efade41d - -# Hide this external target (for ide users) -set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals") - -ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR) - -string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) -set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${INSTALL_DIR}/include CACHE FILEPATH "Path to Faceshift include directory") - -set(LIBRARY_DEBUG_PATH "lib/Debug") -set(LIBRARY_RELEASE_PATH "lib/Release") - -if (WIN32) - set(LIBRARY_PREFIX "") - set(LIBRARY_EXT "lib") - # use selected configuration in release path when building on Windows - set(LIBRARY_RELEASE_PATH "$<$:build/RelWithDebInfo>") - set(LIBRARY_RELEASE_PATH "${LIBRARY_RELEASE_PATH}$<$:build/MinSizeRel>") - set(LIBRARY_RELEASE_PATH "${LIBRARY_RELEASE_PATH}$<$,$>:lib/Release>") -elseif (APPLE) - set(LIBRARY_EXT "a") - set(LIBRARY_PREFIX "lib") - - if (CMAKE_GENERATOR STREQUAL "Unix Makefiles") - set(LIBRARY_DEBUG_PATH "build") - set(LIBRARY_RELEASE_PATH "build") - endif () -endif() - -set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG - ${INSTALL_DIR}/${LIBRARY_DEBUG_PATH}/${LIBRARY_PREFIX}faceshift.${LIBRARY_EXT} CACHE FILEPATH "Faceshift libraries") -set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE -${INSTALL_DIR}/${LIBRARY_RELEASE_PATH}/${LIBRARY_PREFIX}faceshift.${LIBRARY_EXT} CACHE FILEPATH "Faceshift libraries") diff --git a/cmake/externals/nvtt/CMakeLists.txt b/cmake/externals/nvtt/CMakeLists.txt new file mode 100644 index 0000000000..0e1c240c77 --- /dev/null +++ b/cmake/externals/nvtt/CMakeLists.txt @@ -0,0 +1,87 @@ +include(ExternalProject) +include(SelectLibraryConfigurations) + +set(EXTERNAL_NAME nvtt) + +string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) + +if (WIN32) + ExternalProject_Add( + ${EXTERNAL_NAME} + URL http://s3.amazonaws.com/hifi-public/dependencies/nvtt-win-2.1.0.zip + URL_MD5 3ea6eeadbcc69071acf9c49ba565760e + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND "" + LOG_DOWNLOAD 1 + ) + + ExternalProject_Get_Property(${EXTERNAL_NAME} SOURCE_DIR) + + set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${SOURCE_DIR}/include CACHE PATH "Location of NVTT include directory") + set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${SOURCE_DIR}/Release/x64/nvtt.lib CACHE FILEPATH "Path to NVTT release library") + set(${EXTERNAL_NAME_UPPER}_DLL_PATH "${SOURCE_DIR}/Release>/x64" CACHE PATH "Location of NVTT release DLL") +else () + + if (ANDROID) + set(ANDROID_CMAKE_ARGS "-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}" "-DANDROID_NATIVE_API_LEVEL=19") + endif () + + ExternalProject_Add( + ${EXTERNAL_NAME} + URL http://hifi-public.s3.amazonaws.com/dependencies/nvidia-texture-tools-2.1.0.zip + URL_MD5 81b8fa6a9ee3f986088eb6e2215d6a57 + CONFIGURE_COMMAND CMAKE_ARGS ${ANDROID_CMAKE_ARGS} -DNVTT_SHARED=1 -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -DCMAKE_INSTALL_PREFIX:PATH= + LOG_DOWNLOAD 1 + LOG_CONFIGURE 1 + LOG_BUILD 1 + ) + + ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR) + + set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${INSTALL_DIR}/include CACHE PATH "Location of NVTT include directory") + + if (APPLE) + set(_LIB_EXT "dylib") + else () + set(_LIB_EXT "so") + endif () + + set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/lib/libnvtt.${_LIB_EXT} CACHE FILEPATH "Path to NVTT library") + + if (APPLE) + # on OS X we have to use install_name_tool to fix the paths found in the NVTT shared libraries + # so that they can be found and linked during the linking phase + set(_NVTT_LIB_DIR "${INSTALL_DIR}/lib") + + # first fix the install names of all present libraries + ExternalProject_Add_Step( + ${EXTERNAL_NAME} + change-install-name + COMMENT "Calling install_name_tool on NVTT libraries to fix install name for dylib linking" + COMMAND ${CMAKE_COMMAND} -DINSTALL_NAME_LIBRARY_DIR=${_NVTT_LIB_DIR} -P ${EXTERNAL_PROJECT_DIR}/OSXInstallNameChange.cmake + DEPENDEES install + WORKING_DIRECTORY + LOG 1 + ) + + # then, for the main library (libnvtt) fix the paths to the dependency libraries (core, image, math) + ExternalProject_Add_Step( + ${EXTERNAL_NAME} + change-dependency-paths + COMMENT "Calling install_name_tool on NVTT libraries to fix paths for dependency libraries" + COMMAND install_name_tool -change libnvimage.dylib ${INSTALL_DIR}/lib/libnvimage.dylib libnvtt.dylib + COMMAND install_name_tool -change libnvcore.dylib ${INSTALL_DIR}/lib/libnvcore.dylib libnvtt.dylib + COMMAND install_name_tool -change libnvmath.dylib ${INSTALL_DIR}/lib/libnvmath.dylib libnvtt.dylib + COMMAND install_name_tool -change libnvcore.dylib ${INSTALL_DIR}/lib/libnvcore.dylib libnvimage.dylib + COMMAND install_name_tool -change libnvmath.dylib ${INSTALL_DIR}/lib/libnvmath.dylib libnvimage.dylib + COMMAND install_name_tool -change libnvcore.dylib ${INSTALL_DIR}/lib/libnvcore.dylib libnvmath.dylib + DEPENDEES install + WORKING_DIRECTORY /lib + LOG 1 + ) + endif () +endif () + +# Hide this external target (for IDE users) +set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals") diff --git a/cmake/macros/TargetFaceshift.cmake b/cmake/macros/TargetFaceshift.cmake deleted file mode 100644 index 99f65d942a..0000000000 --- a/cmake/macros/TargetFaceshift.cmake +++ /dev/null @@ -1,14 +0,0 @@ -# -# Copyright 2015 High Fidelity, Inc. -# Created by Bradley Austin Davis on 2015/10/10 -# -# Distributed under the Apache License, Version 2.0. -# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -# -macro(TARGET_FACESHIFT) - add_dependency_external_projects(faceshift) - find_package(Faceshift REQUIRED) - target_include_directories(${TARGET_NAME} PRIVATE ${FACESHIFT_INCLUDE_DIRS}) - target_link_libraries(${TARGET_NAME} ${FACESHIFT_LIBRARIES}) - add_definitions(-DHAVE_FACESHIFT) -endmacro() \ No newline at end of file diff --git a/cmake/modules/FindFaceshift.cmake b/cmake/modules/FindFaceshift.cmake deleted file mode 100644 index bd77951273..0000000000 --- a/cmake/modules/FindFaceshift.cmake +++ /dev/null @@ -1,26 +0,0 @@ -# -# FindFaceshift.cmake -# -# Try to find the Faceshift networking library -# -# You must provide a FACESHIFT_ROOT_DIR which contains lib and include directories -# -# Once done this will define -# -# FACESHIFT_FOUND - system found Faceshift -# FACESHIFT_INCLUDE_DIRS - the Faceshift include directory -# FACESHIFT_LIBRARIES - Link this to use Faceshift -# -# Created on 8/30/2013 by Andrzej Kapolka -# Copyright 2013 High Fidelity, Inc. -# -# Distributed under the Apache License, Version 2.0. -# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -# - -include(SelectLibraryConfigurations) -select_library_configurations(FACESHIFT) - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(Faceshift DEFAULT_MSG FACESHIFT_INCLUDE_DIRS FACESHIFT_LIBRARIES) -mark_as_advanced(FACESHIFT_INCLUDE_DIRS FACESHIFT_LIBRARIES FACESHIFT_SEARCH_DIRS) \ No newline at end of file diff --git a/cmake/modules/FindNVTT.cmake b/cmake/modules/FindNVTT.cmake new file mode 100644 index 0000000000..8fae621d81 --- /dev/null +++ b/cmake/modules/FindNVTT.cmake @@ -0,0 +1,37 @@ +# +# FindNVTT.cmake +# +# Try to find NVIDIA texture tools library and include path. +# Once done this will define +# +# NVTT_FOUND +# NVTT_INCLUDE_DIRS +# NVTT_LIBRARIES +# NVTT_DLL_PATH +# +# Created on 4/14/2017 by Stephen Birarda +# 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("${MACRO_DIR}/HifiLibrarySearchHints.cmake") +hifi_library_search_hints("nvtt") + +find_path(NVTT_INCLUDE_DIRS nvtt/nvtt.h PATH_SUFFIXES include HINTS ${NVTT_SEARCH_DIRS}) + +include(FindPackageHandleStandardArgs) + +find_library(NVTT_LIBRARY_RELEASE nvtt PATH_SUFFIXES "lib" "Release.x64/lib" HINTS ${NVTT_SEARCH_DIRS}) +find_library(NVTT_LIBRARY_DEBUG nvtt PATH_SUFFIXES "lib" "Debug.x64/lib" HINTS ${NVTT_SEARCH_DIRS}) + +include(SelectLibraryConfigurations) +select_library_configurations(NVTT) + +if (WIN32) + find_path(NVTT_DLL_PATH nvtt.dll PATH_SUFFIXES "Release.x64/bin" HINTS ${NVTT_SEARCH_DIRS}) + find_package_handle_standard_args(NVTT DEFAULT_MSG NVTT_INCLUDE_DIRS NVTT_LIBRARIES NVTT_DLL_PATH) +else () + find_package_handle_standard_args(NVTT DEFAULT_MSG NVTT_INCLUDE_DIRS NVTT_LIBRARIES) +endif () diff --git a/domain-server/resources/web/content/index.shtml b/domain-server/resources/web/content/index.shtml new file mode 100644 index 0000000000..c7eb765878 --- /dev/null +++ b/domain-server/resources/web/content/index.shtml @@ -0,0 +1,44 @@ + + +
+
+
+ +
+
+ +
+
+
+
+

Upload Entities File

+
+
+
+

+ Upload an entities file (e.g.: models.json.gz) to replace the content of this domain.
+ Note: Your domain's content will be replaced by the content you upload, but the backup files of your domain's content will not immediately be changed. +

+

+ If your domain has any content that you would like to re-use at a later date, save a manual backup of your models.json.gz file, which is usually stored at the following paths:
+

C:\Users\[username]\AppData\Roaming\High Fidelity\assignment-client/entities/models.json.gz
+
/Users/[username]/Library/Application Support/High Fidelity/assignment-client/entities/models.json.gz
+
/home/[username]/.local/share/High Fidelity/assignment-client/entities/models.json.gz
+

+
+ +
+
+ +
+
+
+
+
+ + + + + diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js new file mode 100644 index 0000000000..2e6e084164 --- /dev/null +++ b/domain-server/resources/web/content/js/content.js @@ -0,0 +1,45 @@ +$(document).ready(function(){ + + function showSpinnerAlert(title) { + swal({ + title: title, + text: '
', + html: true, + showConfirmButton: false, + allowEscapeKey: false + }); + } + + var frm = $('#upload-form'); + frm.submit(function (ev) { + $.ajax({ + type: frm.attr('method'), + url: frm.attr('action'), + data: new FormData($(this)[0]), + cache: false, + contentType: false, + processData: false, + success: function (data) { + swal({ + title: 'Uploaded', + type: 'success', + text: 'Your Entity Server is restarting to replace its local content with the uploaded file.', + confirmButtonText: 'OK' + }) + }, + error: function (data) { + swal({ + title: '', + type: 'error', + text: 'Your entities file could not be transferred to the Entity Server.
Verify that the file is a .json or .json.gz entities file and try again.', + html: true, + confirmButtonText: 'OK', + }); + } + }); + + ev.preventDefault(); + + showSpinnerAlert("Uploading Entities File"); + }); +}); diff --git a/domain-server/resources/web/header.html b/domain-server/resources/web/header.html index b4eee406f2..965f86b0a1 100644 --- a/domain-server/resources/web/header.html +++ b/domain-server/resources/web/header.html @@ -36,6 +36,7 @@
  • New Assignment
  • +
  • Content
  • Settings
  • diff --git a/domain-server/resources/web/settings/js/sweetalert.min.js b/domain-server/resources/web/js/sweetalert.min.js similarity index 100% rename from domain-server/resources/web/settings/js/sweetalert.min.js rename to domain-server/resources/web/js/sweetalert.min.js diff --git a/domain-server/resources/web/settings/index.shtml b/domain-server/resources/web/settings/index.shtml index 3eb7a53726..1812c52dad 100644 --- a/domain-server/resources/web/settings/index.shtml +++ b/domain-server/resources/web/settings/index.shtml @@ -99,7 +99,7 @@ - + diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index c187239351..a3692c974e 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -435,23 +435,8 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect return SharedNodePointer(); } - QUuid hintNodeID; - - // in case this is a node that's failing to connect - // double check we don't have a node whose sockets match exactly already in the list - limitedNodeList->eachNodeBreakable([&nodeConnection, &hintNodeID](const SharedNodePointer& node){ - if (node->getPublicSocket() == nodeConnection.publicSockAddr - && node->getLocalSocket() == nodeConnection.localSockAddr) { - // we have a node that already has these exact sockets - this occurs if a node - // is unable to connect to the domain - hintNodeID = node->getUUID(); - return false; - } - return true; - }); - // add the connecting node (or re-use the matched one from eachNodeBreakable above) - SharedNodePointer newNode = addVerifiedNodeFromConnectRequest(nodeConnection, hintNodeID); + SharedNodePointer newNode = addVerifiedNodeFromConnectRequest(nodeConnection); // set the edit rights for this user newNode->setPermissions(userPerms); @@ -479,11 +464,12 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect return newNode; } -SharedNodePointer DomainGatekeeper::addVerifiedNodeFromConnectRequest(const NodeConnectionData& nodeConnection, - QUuid nodeID) { +SharedNodePointer DomainGatekeeper::addVerifiedNodeFromConnectRequest(const NodeConnectionData& nodeConnection) { HifiSockAddr discoveredSocket = nodeConnection.senderSockAddr; SharedNetworkPeer connectedPeer = _icePeers.value(nodeConnection.connectUUID); + QUuid nodeID; + if (connectedPeer) { // this user negotiated a connection with us via ICE, so re-use their ICE client ID nodeID = nodeConnection.connectUUID; @@ -493,10 +479,8 @@ SharedNodePointer DomainGatekeeper::addVerifiedNodeFromConnectRequest(const Node discoveredSocket = *connectedPeer->getActiveSocket(); } } else { - // we got a connectUUID we didn't recognize, either use the hinted node ID or randomly generate a new one - if (nodeID.isNull()) { - nodeID = QUuid::createUuid(); - } + // we got a connectUUID we didn't recognize, randomly generate a new one + nodeID = QUuid::createUuid(); } auto limitedNodeList = DependencyManager::get(); diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index 163f255411..e2d36c4cea 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -76,8 +76,7 @@ private: SharedNodePointer processAgentConnectRequest(const NodeConnectionData& nodeConnection, const QString& username, const QByteArray& usernameSignature); - SharedNodePointer addVerifiedNodeFromConnectRequest(const NodeConnectionData& nodeConnection, - QUuid nodeID = QUuid()); + SharedNodePointer addVerifiedNodeFromConnectRequest(const NodeConnectionData& nodeConnection); bool verifyUserSignature(const QString& username, const QByteArray& usernameSignature, const HifiSockAddr& senderSockAddr); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 620b11d8ad..782c54419d 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1633,6 +1633,15 @@ QString pathForAssignmentScript(const QUuid& assignmentUUID) { return directory.absoluteFilePath(uuidStringWithoutCurlyBraces(assignmentUUID)); } +QString DomainServer::pathForRedirect(QString path) const { + // make sure the passed path has a leading slash + if (!path.startsWith('/')) { + path.insert(0, '/'); + } + + return "http://" + _hostname + ":" + QString::number(_httpManager.serverPort()) + path; +} + const QString URI_OAUTH = "/oauth"; bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler) { const QString JSON_MIME_TYPE = "application/json"; @@ -1640,6 +1649,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url const QString URI_ASSIGNMENT = "/assignment"; const QString URI_NODES = "/nodes"; const QString URI_SETTINGS = "/settings"; + const QString URI_ENTITY_FILE_UPLOAD = "/content/upload"; const QString UUID_REGEX_STRING = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"; @@ -1869,6 +1879,25 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url // respond with a 200 code for successful upload connection->respond(HTTPConnection::StatusCode200); + return true; + } else if (url.path() == URI_ENTITY_FILE_UPLOAD) { + // this is an entity file upload, ask the HTTPConnection to parse the data + QList formData = connection->parseFormData(); + + Headers redirectHeaders; + + if (formData.size() > 0 && formData[0].second.size() > 0) { + // invoke our method to hand the new octree file off to the octree server + QMetaObject::invokeMethod(this, "handleOctreeFileReplacement", + Qt::QueuedConnection, Q_ARG(QByteArray, formData[0].second)); + + // respond with a 200 for success + connection->respond(HTTPConnection::StatusCode200); + } else { + // respond with a 400 for failure + connection->respond(HTTPConnection::StatusCode400); + } + return true; } } else if (connection->requestOperation() == QNetworkAccessManager::DeleteOperation) { @@ -2159,8 +2188,7 @@ Headers DomainServer::setupCookieHeadersFromProfileReply(QNetworkReply* profileR cookieHeaders.insert("Set-Cookie", cookieString.toUtf8()); // redirect the user back to the homepage so they can present their cookie and be authenticated - QString redirectString = "http://" + _hostname + ":" + QString::number(_httpManager.serverPort()); - cookieHeaders.insert("Location", redirectString.toUtf8()); + cookieHeaders.insert("Location", pathForRedirect().toUtf8()); return cookieHeaders; } @@ -2560,3 +2588,20 @@ void DomainServer::setupGroupCacheRefresh() { _metaverseGroupCacheTimer->start(REFRESH_GROUPS_INTERVAL_MSECS); } } + +void DomainServer::handleOctreeFileReplacement(QByteArray octreeFile) { + // enumerate the nodes and find any octree type servers with active sockets + + auto limitedNodeList = DependencyManager::get(); + limitedNodeList->eachMatchingNode([](const SharedNodePointer& node) { + return node->getType() == NodeType::EntityServer && node->getActiveSocket(); + }, [&octreeFile, limitedNodeList](const SharedNodePointer& octreeNode) { + // setup a packet to send to this octree server with the new octree file data + auto octreeFilePacketList = NLPacketList::create(PacketType::OctreeFileReplacement, QByteArray(), true, true); + octreeFilePacketList->write(octreeFile); + + qDebug() << "Sending an octree file replacement of" << octreeFile.size() << "bytes to" << octreeNode; + + limitedNodeList->sendPacketList(std::move(octreeFilePacketList), *octreeNode); + }); +} diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 4c5c42acee..63b82cb37d 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -100,6 +100,8 @@ private slots: void handleSuccessfulICEServerAddressUpdate(QNetworkReply& requestReply); void handleFailedICEServerAddressUpdate(QNetworkReply& requestReply); + void handleOctreeFileReplacement(QByteArray octreeFile); + signals: void iceServerChanged(); void userConnected(); @@ -161,6 +163,8 @@ private: void setupGroupCacheRefresh(); + QString pathForRedirect(QString path = QString()) const; + SubnetList _acSubnetWhitelist; DomainGatekeeper _gatekeeper; diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 0006bdd778..d7e4b1ae7c 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -194,7 +194,7 @@ link_hifi_libraries( recording fbx networking model-networking entities avatars audio audio-client animation script-engine physics render-utils entities-renderer avatars-renderer ui auto-updater - controllers plugins + controllers plugins image trackers ui-plugins display-plugins input-plugins ${NON_ANDROID_LIBRARIES} ) @@ -202,7 +202,6 @@ link_hifi_libraries( # include the binary directory of render-utils for shader includes target_include_directories(${TARGET_NAME} PRIVATE "${CMAKE_BINARY_DIR}/libraries/render-utils") -#fixme find a way to express faceshift as a plugin target_bullet() target_opengl() @@ -210,10 +209,6 @@ if (NOT ANDROID) target_glew() endif () -if (WIN32 OR APPLE) - target_faceshift() -endif() - # perform standard include and linking for found externals foreach(EXTERNAL ${OPTIONAL_EXTERNALS}) diff --git a/interface/resources/avatar/avatar-animation.json b/interface/resources/avatar/avatar-animation.json index 975f01855d..9efe3dd29b 100644 --- a/interface/resources/avatar/avatar-animation.json +++ b/interface/resources/avatar/avatar-animation.json @@ -50,6 +50,12 @@ "type": "inverseKinematics", "data": { "targets": [ + { + "jointName": "Hips", + "positionVar": "hipsPosition", + "rotationVar": "hipsRotation", + "typeVar": "hipsType" + }, { "jointName": "RightHand", "positionVar": "rightHandPosition", @@ -75,10 +81,10 @@ "typeVar": "leftFootType" }, { - "jointName": "Neck", - "positionVar": "neckPosition", - "rotationVar": "neckRotation", - "typeVar": "neckType" + "jointName": "Spine2", + "positionVar": "spine2Position", + "rotationVar": "spine2Rotation", + "typeVar": "spine2Type" }, { "jointName": "Head", @@ -91,20 +97,27 @@ "children": [] }, { - "id": "manipulatorOverlay", + "id": "hipsManipulatorOverlay", "type": "overlay", "data": { - "alpha": 1.0, - "boneSet": "spineOnly" + "alpha": 0.0, + "boneSet": "hipsOnly" }, "children": [ { - "id": "spineLean", + "id": "hipsManipulator", "type": "manipulator", "data": { "alpha": 0.0, + "alphaVar": "hipsManipulatorAlpha", "joints": [ - { "type": "absoluteRotation", "jointName": "Spine", "var": "lean" } + { + "jointName": "Hips", + "rotationType": "absolute", + "translationType": "absolute", + "rotationVar": "hipsManipulatorRotation", + "translationVar": "hipsManipulatorPosition" + } ] }, "children": [] diff --git a/interface/resources/controllers/standard.json b/interface/resources/controllers/standard.json index 53285ea974..62eec9bc3c 100644 --- a/interface/resources/controllers/standard.json +++ b/interface/resources/controllers/standard.json @@ -61,6 +61,11 @@ { "from": "Standard.RightHand", "to": "Actions.RightHand" }, { "from": "Standard.LeftFoot", "to": "Actions.LeftFoot" }, - { "from": "Standard.RightFoot", "to": "Actions.RightFoot" } + { "from": "Standard.RightFoot", "to": "Actions.RightFoot" }, + + { "from": "Standard.Hips", "to": "Actions.Hips" }, + { "from": "Standard.Spine2", "to": "Actions.Spine2" }, + + { "from": "Standard.Head", "to": "Actions.Head" } ] } diff --git a/interface/resources/fonts/hifi-glyphs.ttf b/interface/resources/fonts/hifi-glyphs.ttf index 138d7f3dda..93f6fe6d13 100644 Binary files a/interface/resources/fonts/hifi-glyphs.ttf and b/interface/resources/fonts/hifi-glyphs.ttf differ diff --git a/interface/resources/html/raiseAndLowerKeyboard.js b/interface/resources/html/raiseAndLowerKeyboard.js index 63e016c5d4..27ead23124 100644 --- a/interface/resources/html/raiseAndLowerKeyboard.js +++ b/interface/resources/html/raiseAndLowerKeyboard.js @@ -19,7 +19,7 @@ function shouldRaiseKeyboard() { var nodeName = document.activeElement.nodeName; var nodeType = document.activeElement.type; - if (nodeName === "INPUT" && (nodeType === "text" || nodeType === "number" || nodeType === "password") + if (nodeName === "INPUT" && ["email", "number", "password", "tel", "text", "url"].indexOf(nodeType) !== -1 || document.activeElement.nodeName === "TEXTAREA") { return true; } else { diff --git a/interface/resources/icons/loader-red-countdown-ring.gif b/interface/resources/icons/loader-red-countdown-ring.gif new file mode 100644 index 0000000000..eb15b9aedd Binary files /dev/null and b/interface/resources/icons/loader-red-countdown-ring.gif differ diff --git a/interface/resources/icons/tablet-icons/avatar-record-a.svg b/interface/resources/icons/tablet-icons/avatar-record-a.svg new file mode 100644 index 0000000000..7358bdb0db --- /dev/null +++ b/interface/resources/icons/tablet-icons/avatar-record-a.svg @@ -0,0 +1,109 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/avatar-record-i.svg b/interface/resources/icons/tablet-icons/avatar-record-i.svg new file mode 100644 index 0000000000..5e139a6497 --- /dev/null +++ b/interface/resources/icons/tablet-icons/avatar-record-i.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/doppleganger-a.svg b/interface/resources/icons/tablet-icons/doppleganger-a.svg new file mode 100644 index 0000000000..100986647e --- /dev/null +++ b/interface/resources/icons/tablet-icons/doppleganger-a.svg @@ -0,0 +1,94 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/doppleganger-i.svg b/interface/resources/icons/tablet-icons/doppleganger-i.svg new file mode 100644 index 0000000000..0c55e0e0c7 --- /dev/null +++ b/interface/resources/icons/tablet-icons/doppleganger-i.svg @@ -0,0 +1,94 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/goto-msg.svg b/interface/resources/icons/tablet-icons/goto-msg.svg new file mode 100644 index 0000000000..9b576ab1bf --- /dev/null +++ b/interface/resources/icons/tablet-icons/goto-msg.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/interface/resources/qml/Stats.qml b/interface/resources/qml/Stats.qml index 98e7f61ff7..dde6c445c8 100644 --- a/interface/resources/qml/Stats.qml +++ b/interface/resources/qml/Stats.qml @@ -211,6 +211,11 @@ Item { text: "Downloads: " + root.downloads + "/" + root.downloadLimit + ", Pending: " + root.downloadsPending; } + StatText { + visible: root.expanded; + text: "Processing: " + root.processing + + ", Pending: " + root.processingPending; + } StatText { visible: root.expanded && root.downloadUrls.length > 0; text: "Download URLs:" diff --git a/interface/resources/qml/controls/TabletWebScreen.qml b/interface/resources/qml/controls/TabletWebScreen.qml new file mode 100644 index 0000000000..fec91046d8 --- /dev/null +++ b/interface/resources/qml/controls/TabletWebScreen.qml @@ -0,0 +1,132 @@ +import QtQuick 2.5 +import QtWebEngine 1.1 +import QtWebChannel 1.0 +import "../controls-uit" as HiFiControls +import HFTabletWebEngineProfile 1.0 + +Item { + property alias url: root.url + property alias scriptURL: root.userScriptUrl + property alias eventBridge: eventBridgeWrapper.eventBridge + property alias canGoBack: root.canGoBack; + property var goBack: root.goBack; + property alias urlTag: root.urlTag + property bool keyboardEnabled: true // FIXME - Keyboard HMD only: Default to false + property bool keyboardRaised: false + property bool punctuationMode: false + + // FIXME - Keyboard HMD only: Make Interface either set keyboardRaised property directly in OffscreenQmlSurface + // or provide HMDinfo object to QML in RenderableWebEntityItem and do the following. + /* + onKeyboardRaisedChanged: { + keyboardEnabled = HMDinfo.active; + } + */ + + QtObject { + id: eventBridgeWrapper + WebChannel.id: "eventBridgeWrapper" + property var eventBridge; + } + + property alias viewProfile: root.profile + + WebEngineView { + id: root + objectName: "webEngineView" + x: 0 + y: 0 + width: parent.width + height: keyboardEnabled && keyboardRaised ? parent.height - keyboard.height : parent.height + + profile: HFTabletWebEngineProfile { + id: webviewProfile + storageName: "qmlTabletWebEngine" + } + + property string userScriptUrl: "" + + // creates a global EventBridge object. + WebEngineScript { + id: createGlobalEventBridge + sourceCode: eventBridgeJavaScriptToInject + injectionPoint: WebEngineScript.DocumentCreation + worldId: WebEngineScript.MainWorld + } + + // detects when to raise and lower virtual keyboard + WebEngineScript { + id: raiseAndLowerKeyboard + injectionPoint: WebEngineScript.Deferred + sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js" + worldId: WebEngineScript.MainWorld + } + + // User script. + WebEngineScript { + id: userScript + sourceUrl: root.userScriptUrl + injectionPoint: WebEngineScript.DocumentReady // DOM ready but page load may not be finished. + worldId: WebEngineScript.MainWorld + } + + property string urlTag: "noDownload=false"; + + userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ] + + property string newUrl: "" + + webChannel.registeredObjects: [eventBridgeWrapper] + + Component.onCompleted: { + // Ensure the JS from the web-engine makes it to our logging + root.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { + console.log("Web Entity JS message: " + sourceID + " " + lineNumber + " " + message); + }); + + root.profile.httpUserAgent = "Mozilla/5.0 Chrome (HighFidelityInterface)"; + } + + onFeaturePermissionRequested: { + grantFeaturePermission(securityOrigin, feature, true); + } + + onLoadingChanged: { + keyboardRaised = false; + punctuationMode = false; + keyboard.resetShiftMode(false); + + // Required to support clicking on "hifi://" links + if (WebEngineView.LoadStartedStatus == loadRequest.status) { + var url = loadRequest.url.toString(); + url = (url.indexOf("?") >= 0) ? url + urlTag : url + "?" + urlTag; + if (urlHandler.canHandleUrl(url)) { + if (urlHandler.handleUrl(url)) { + root.stop(); + } + } + } + } + + onNewViewRequested:{ + // desktop is not defined for web-entities or tablet + if (typeof desktop !== "undefined") { + desktop.openBrowserWindow(request, profile); + } else { + tabletRoot.openBrowserWindow(request, profile); + } + } + } + + HiFiControls.Keyboard { + id: keyboard + raised: parent.keyboardEnabled && parent.keyboardRaised + numeric: parent.punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + } + +} diff --git a/interface/resources/qml/controls/TabletWebView.qml b/interface/resources/qml/controls/TabletWebView.qml index 9a08e8b866..dd0c3c8135 100644 --- a/interface/resources/qml/controls/TabletWebView.qml +++ b/interface/resources/qml/controls/TabletWebView.qml @@ -23,6 +23,11 @@ Item { property bool keyboardRaised: false property bool punctuationMode: false property bool isDesktop: false + property bool removingPage: false + property bool loadingPage: false + property alias webView: webview + property alias profile: webview.profile + property bool remove: false property int currentPage: -1 // used as a model for repeater @@ -79,6 +84,13 @@ Item { horizontalCenter: parent.horizontalCenter; } } + + + MouseArea { + anchors.fill: parent + preventStealing: true + propagateComposedEvents: true + } } ListModel { @@ -94,16 +106,24 @@ Item { } function goBack() { - if (webview.canGoBack) { - pagesModel.remove(currentPage); + if (webview.canGoBack && !isUrlLoaded(webview.url)) { + if (currentPage > 0) { + removingPage = true; + pagesModel.remove(currentPage); + } webview.goBack(); } else if (currentPage > 0) { + removingPage = true; pagesModel.remove(currentPage); } } function closeWebEngine() { + if (remove) { + web.destroy(); + return; + } if (parentStackItem) { parentStackItem.pop(); } else { @@ -121,6 +141,10 @@ Item { urlAppend(url) } + function isUrlLoaded(url) { + return (pagesModel.get(currentPage).webUrl === url); + } + function reloadPage() { view.reloadAndBypassCache() view.setActiveFocusOnPress(true); @@ -128,6 +152,10 @@ Item { } function urlAppend(url) { + if (removingPage) { + removingPage = false; + return; + } var lurl = decodeURIComponent(url) if (lurl[lurl.length - 1] !== "/") { lurl = lurl + "/" @@ -140,6 +168,7 @@ Item { onCurrentPageChanged: { if (currentPage >= 0 && currentPage < pagesModel.count) { + timer.start(); webview.url = pagesModel.get(currentPage).webUrl; web.url = webview.url; web.address = webview.url; @@ -158,7 +187,7 @@ Item { Timer { id: timer - interval: 100 + interval: 200 running: false repeat: false onTriggered: timer.stop(); @@ -230,10 +259,10 @@ Item { keyboardRaised = false; punctuationMode = false; keyboard.resetShiftMode(false); - // Required to support clicking on "hifi://" links if (WebEngineView.LoadStartedStatus == loadRequest.status) { - urlAppend(loadRequest.url.toString()) + urlAppend(loadRequest.url.toString()); + loadingPage = true; var url = loadRequest.url.toString(); if (urlHandler.canHandleUrl(url)) { if (urlHandler.handleUrl(url)) { @@ -241,12 +270,27 @@ Item { } } } + + if (WebEngineView.LoadFailedStatus == loadRequest.status) { + console.log(" Tablet WebEngineView failed to laod url: " + loadRequest.url.toString()); + } } onNewViewRequested: { request.openIn(webview); } } + + HiFiControls.Keyboard { + id: keyboard + raised: parent.keyboardEnabled && parent.keyboardRaised + + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + } Component.onCompleted: { web.isDesktop = (typeof desktop !== "undefined"); diff --git a/interface/resources/qml/controls/WebView.qml b/interface/resources/qml/controls/WebView.qml index 52f277520f..04ff731a25 100644 --- a/interface/resources/qml/controls/WebView.qml +++ b/interface/resources/qml/controls/WebView.qml @@ -113,7 +113,7 @@ Item { if (typeof desktop !== "undefined") { desktop.openBrowserWindow(request, profile); } else { - console.log("onNewViewRequested: desktop not defined"); + tabletRoot.openBrowserWindow(request, profile); } } } diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index d8aedf6666..42db16aa72 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -466,6 +466,11 @@ FocusScope { return fileDialogBuilder.createObject(desktop, properties); } + Component { id: assetDialogBuilder; AssetDialog { } } + function assetDialog(properties) { + return assetDialogBuilder.createObject(desktop, properties); + } + function unfocusWindows() { // First find the active focus item, and unfocus it, all the way // up the parent chain to the window diff --git a/interface/resources/qml/dialogs/AssetDialog.qml b/interface/resources/qml/dialogs/AssetDialog.qml new file mode 100644 index 0000000000..8d19d38efb --- /dev/null +++ b/interface/resources/qml/dialogs/AssetDialog.qml @@ -0,0 +1,58 @@ +// +// AssetDialog.qml +// +// Created by David Rowe on 18 Apr 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 +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import Qt.labs.settings 1.0 + +import "../styles-uit" +import "../windows" + +import "assetDialog" + +ModalWindow { + id: root + resizable: true + implicitWidth: 480 + implicitHeight: 360 + + minSize: Qt.vector2d(360, 240) + draggable: true + + Settings { + category: "AssetDialog" + property alias width: root.width + property alias height: root.height + property alias x: root.x + property alias y: root.y + } + + // Set from OffscreenUi::assetDialog(). + property alias caption: root.title + property alias dir: assetDialogContent.dir + property alias filter: assetDialogContent.filter + property alias options: assetDialogContent.options + + // Dialog results. + signal selectedAsset(var asset); + signal canceled(); + + property int titleWidth: 0 // For ModalFrame. + + HifiConstants { id: hifi } + + AssetDialogContent { + id: assetDialogContent + + width: pane.width + height: pane.height + anchors.margins: 0 + } +} diff --git a/interface/resources/qml/dialogs/TabletAssetDialog.qml b/interface/resources/qml/dialogs/TabletAssetDialog.qml new file mode 100644 index 0000000000..016deec094 --- /dev/null +++ b/interface/resources/qml/dialogs/TabletAssetDialog.qml @@ -0,0 +1,53 @@ +// +// TabletAssetDialog.qml +// +// Created by David Rowe on 18 Apr 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 +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +import "../styles-uit" +import "../windows" + +import "assetDialog" + +TabletModalWindow { + id: root + anchors.fill: parent + width: parent.width + height: parent.height + + // Set from OffscreenUi::assetDialog(). + property alias caption: root.title + property alias dir: assetDialogContent.dir + property alias filter: assetDialogContent.filter + property alias options: assetDialogContent.options + + // Dialog results. + signal selectedAsset(var asset); + signal canceled(); + + property int titleWidth: 0 // For TabletModalFrame. + + TabletModalFrame { + id: frame + anchors.fill: parent + + AssetDialogContent { + id: assetDialogContent + singleClickNavigate: true + width: parent.width - 12 + height: parent.height - frame.frameMarginTop - 12 + anchors { + horizontalCenter: parent.horizontalCenter + top: parent.top + topMargin: parent.height - height - 6 + } + } + } +} diff --git a/interface/resources/qml/dialogs/assetDialog/AssetDialogContent.qml b/interface/resources/qml/dialogs/assetDialog/AssetDialogContent.qml new file mode 100644 index 0000000000..8c0501e3b4 --- /dev/null +++ b/interface/resources/qml/dialogs/assetDialog/AssetDialogContent.qml @@ -0,0 +1,536 @@ +// +// AssetDialogContent.qml +// +// Created by David Rowe on 19 Apr 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 +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +import "../../controls-uit" +import "../../styles-uit" + +import "../fileDialog" + +Item { + // Set from OffscreenUi::assetDialog() + property alias dir: assetTableModel.folder + property alias filter: selectionType.filtersString // FIXME: Currently only supports simple filters, "*.xxx". + property int options // Not used. + + property bool selectDirectory: false + + // Not implemented. + //property bool saveDialog: false; + //property bool multiSelect: false; + + property bool singleClickNavigate: false + + HifiConstants { id: hifi } + + Component.onCompleted: { + homeButton.destination = dir; + + if (selectDirectory) { + d.currentSelectionIsFolder = true; + d.currentSelectionPath = assetTableModel.folder; + } + + assetTableView.forceActiveFocus(); + } + + Item { + id: assetDialogItem + anchors.fill: parent + clip: true + + MouseArea { + // Clear selection when click on internal unused area. + anchors.fill: parent + drag.target: root + onClicked: { + d.clearSelection(); + frame.forceActiveFocus(); + assetTableView.forceActiveFocus(); + } + } + + Row { + id: navControls + anchors { + top: parent.top + topMargin: hifi.dimensions.contentMargin.y + left: parent.left + } + spacing: hifi.dimensions.contentSpacing.x + + GlyphButton { + id: upButton + glyph: hifi.glyphs.levelUp + width: height + size: 30 + enabled: assetTableModel.parentFolder !== "" + onClicked: d.navigateUp(); + } + + GlyphButton { + id: homeButton + property string destination: "" + glyph: hifi.glyphs.home + size: 28 + width: height + enabled: destination !== "" + //onClicked: d.navigateHome(); + onClicked: assetTableModel.folder = destination; + } + } + + ComboBox { + id: pathSelector + anchors { + top: parent.top + topMargin: hifi.dimensions.contentMargin.y + left: navControls.right + leftMargin: hifi.dimensions.contentSpacing.x + right: parent.right + } + z: 10 + + property string lastValidFolder: assetTableModel.folder + + function calculatePathChoices(folder) { + var folders = folder.split("/"), + choices = [], + i, length; + + if (folders[folders.length - 1] === "") { + folders.pop(); + } + + choices.push(folders[0]); + + for (i = 1, length = folders.length; i < length; i++) { + choices.push(choices[i - 1] + "/" + folders[i]); + } + + if (folders[0] === "") { + choices[0] = "/"; + } + + choices.reverse(); + + if (choices.length > 0) { + pathSelector.model = choices; + } + } + + onLastValidFolderChanged: { + var folder = lastValidFolder; + calculatePathChoices(folder); + } + + onCurrentTextChanged: { + var folder = currentText; + + if (folder !== "/") { + folder += "/"; + } + + if (folder !== assetTableModel.folder) { + if (root.selectDirectory) { + currentSelection.text = currentText; + d.currentSelectionPath = currentText; + } + assetTableModel.folder = folder; + assetTableView.forceActiveFocus(); + } + } + } + + QtObject { + id: d + + property string currentSelectionPath + property bool currentSelectionIsFolder + property var tableViewConnection: Connections { target: assetTableView; onCurrentRowChanged: d.update(); } + + function update() { + var row = assetTableView.currentRow; + + if (row === -1) { + if (!root.selectDirectory) { + currentSelection.text = ""; + currentSelectionIsFolder = false; + } + return; + } + + var rowInfo = assetTableModel.get(row); + currentSelectionPath = rowInfo.filePath; + currentSelectionIsFolder = rowInfo.fileIsDir; + if (root.selectDirectory || !currentSelectionIsFolder) { + currentSelection.text = currentSelectionPath; + } else { + currentSelection.text = ""; + } + } + + function navigateUp() { + if (assetTableModel.parentFolder !== "") { + assetTableModel.folder = assetTableModel.parentFolder; + return true; + } + return false; + } + + function navigateHome() { + assetTableModel.folder = homeButton.destination; + return true; + } + + function clearSelection() { + assetTableView.selection.clear(); + assetTableView.currentRow = -1; + update(); + } + } + + ListModel { + id: assetTableModel + + property string folder + property string parentFolder: "" + readonly property string rootFolder: "/" + + onFolderChanged: { + parentFolder = calculateParentFolder(); + update(); + } + + function calculateParentFolder() { + if (folder !== "/") { + return folder.slice(0, folder.slice(0, -1).lastIndexOf("/") + 1); + } + return ""; + } + + function isFolder(row) { + if (row === -1) { + return false; + } + return get(row).fileIsDir; + } + + function onGetAllMappings(error, map) { + var mappings, + fileTypeFilter, + index, + path, + fileName, + fileType, + fileIsDir, + isValid, + subDirectory, + subDirectories = [], + fileNameSort, + rows = 0, + lower, + middle, + upper, + i, + length; + + clear(); + + if (error === "") { + mappings = Object.keys(map); + fileTypeFilter = filter.replace("*", "").toLowerCase(); + + for (i = 0, length = mappings.length; i < length; i++) { + index = mappings[i].lastIndexOf("/"); + + path = mappings[i].slice(0, mappings[i].lastIndexOf("/") + 1); + fileName = mappings[i].slice(path.length); + fileType = fileName.slice(fileName.lastIndexOf(".")); + fileIsDir = false; + isValid = false; + + if (fileType.toLowerCase() === fileTypeFilter) { + if (path === folder) { + isValid = !selectDirectory; + } else if (path.length > folder.length) { + subDirectory = path.slice(folder.length); + index = subDirectory.indexOf("/"); + if (index === subDirectory.lastIndexOf("/")) { + fileName = subDirectory.slice(0, index); + if (subDirectories.indexOf(fileName) === -1) { + fileIsDir = true; + isValid = true; + subDirectories.push(fileName); + } + } + } + } + + if (isValid) { + fileNameSort = (fileIsDir ? "*" : "") + fileName.toLowerCase(); + + lower = 0; + upper = rows; + while (lower < upper) { + middle = Math.floor((lower + upper) / 2); + var lessThan; + if (fileNameSort < get(middle)["fileNameSort"]) { + lessThan = true; + upper = middle; + } else { + lessThan = false; + lower = middle + 1; + } + } + + insert(lower, { + fileName: fileName, + filePath: path + (fileIsDir ? "" : fileName), + fileIsDir: fileIsDir, + fileNameSort: fileNameSort + }); + + rows++; + } + } + + } else { + console.log("Error getting mappings from Asset Server"); + } + } + + function update() { + d.clearSelection(); + clear(); + Assets.getAllMappings(onGetAllMappings); + } + } + + Table { + id: assetTableView + colorScheme: hifi.colorSchemes.light + anchors { + top: navControls.bottom + topMargin: hifi.dimensions.contentSpacing.y + left: parent.left + right: parent.right + bottom: currentSelection.top + bottomMargin: hifi.dimensions.contentSpacing.y + currentSelection.controlHeight - currentSelection.height + } + + model: assetTableModel + + focus: true + + onClicked: { + if (singleClickNavigate) { + navigateToRow(row); + } + } + + onDoubleClicked: navigateToRow(row); + Keys.onReturnPressed: navigateToCurrentRow(); + Keys.onEnterPressed: navigateToCurrentRow(); + + itemDelegate: Item { + clip: true + + FontLoader { id: firaSansSemiBold; source: "../../../fonts/FiraSans-SemiBold.ttf"; } + FontLoader { id: firaSansRegular; source: "../../../fonts/FiraSans-Regular.ttf"; } + + FiraSansSemiBold { + text: styleData.value + elide: styleData.elideMode + anchors { + left: parent.left + leftMargin: hifi.dimensions.tablePadding + right: parent.right + rightMargin: hifi.dimensions.tablePadding + verticalCenter: parent.verticalCenter + } + size: hifi.fontSizes.tableText + color: hifi.colors.baseGrayHighlight + font.family: (styleData.row !== -1 && assetTableView.model.get(styleData.row).fileIsDir) + ? firaSansSemiBold.name : firaSansRegular.name + } + } + + TableViewColumn { + id: fileNameColumn + role: "fileName" + title: "Name" + width: assetTableView.width + movable: false + resizable: false + } + + function navigateToRow(row) { + currentRow = row; + navigateToCurrentRow(); + } + + function navigateToCurrentRow() { + if (model.isFolder(currentRow)) { + model.folder = model.get(currentRow).filePath; + } else { + okAction.trigger(); + } + } + + Timer { + id: prefixClearTimer + interval: 1000 + repeat: false + running: false + onTriggered: assetTableView.prefix = ""; + } + + property string prefix: "" + + function addToPrefix(event) { + if (!event.text || event.text === "") { + return false; + } + var newPrefix = prefix + event.text.toLowerCase(); + var matchedIndex = -1; + for (var i = 0; i < model.count; ++i) { + var name = model.get(i).fileName.toLowerCase(); + if (0 === name.indexOf(newPrefix)) { + matchedIndex = i; + break; + } + } + + if (matchedIndex !== -1) { + assetTableView.selection.clear(); + assetTableView.selection.select(matchedIndex); + assetTableView.currentRow = matchedIndex; + assetTableView.prefix = newPrefix; + } + prefixClearTimer.restart(); + return true; + } + + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Backspace: + case Qt.Key_Tab: + case Qt.Key_Backtab: + event.accepted = false; + break; + + default: + if (addToPrefix(event)) { + event.accepted = true + } else { + event.accepted = false; + } + break; + } + } + } + + TextField { + id: currentSelection + label: selectDirectory ? "Directory:" : "File name:" + anchors { + left: parent.left + right: selectionType.visible ? selectionType.left: parent.right + rightMargin: selectionType.visible ? hifi.dimensions.contentSpacing.x : 0 + bottom: buttonRow.top + bottomMargin: hifi.dimensions.contentSpacing.y + } + readOnly: true + activeFocusOnTab: !readOnly + onActiveFocusChanged: if (activeFocus) { selectAll(); } + onAccepted: okAction.trigger(); + } + + FileTypeSelection { + id: selectionType + anchors { + top: currentSelection.top + left: buttonRow.left + right: parent.right + } + visible: !selectDirectory && filtersCount > 1 + KeyNavigation.left: assetTableView + KeyNavigation.right: openButton + } + + Action { + id: okAction + text: currentSelection.text && root.selectDirectory && assetTableView.currentRow === -1 ? "Choose" : "Open" + enabled: currentSelection.text || !root.selectDirectory && d.currentSelectionIsFolder ? true : false + onTriggered: { + if (!root.selectDirectory && !d.currentSelectionIsFolder + || root.selectDirectory && assetTableView.currentRow === -1) { + selectedAsset(d.currentSelectionPath); + root.destroy(); + } else { + assetTableView.navigateToCurrentRow(); + } + } + } + + Action { + id: cancelAction + text: "Cancel" + onTriggered: { + canceled(); + root.destroy(); + } + } + + Row { + id: buttonRow + anchors { + right: parent.right + bottom: parent.bottom + } + spacing: hifi.dimensions.contentSpacing.y + + Button { + id: openButton + color: hifi.buttons.blue + action: okAction + Keys.onReturnPressed: okAction.trigger() + KeyNavigation.up: selectionType + KeyNavigation.left: selectionType + KeyNavigation.right: cancelButton + } + + Button { + id: cancelButton + action: cancelAction + KeyNavigation.up: selectionType + KeyNavigation.left: openButton + KeyNavigation.right: assetTableView.contentItem + Keys.onReturnPressed: { canceled(); root.enabled = false } + } + } + } + + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Backspace: + event.accepted = d.navigateUp(); + break; + + case Qt.Key_Home: + event.accepted = d.navigateHome(); + break; + + } + } +} diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index b72901fbdf..9617b41150 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -17,7 +17,7 @@ import QtGraphicalEffects 1.0 import "toolbars" import "../styles-uit" -Rectangle { +Item { id: root; property string userName: ""; property string placeName: ""; @@ -31,10 +31,11 @@ Rectangle { property bool drillDownToPlace: false; property bool showPlace: isConcurrency; - property string messageColor: hifi.colors.blueAccent; + property string messageColor: isAnnouncement ? "white" : hifi.colors.blueAccent; property string timePhrase: pastTime(timestamp); property int onlineUsers: 0; property bool isConcurrency: action === 'concurrency'; + property bool isAnnouncement: action === 'announcement'; property bool isStacked: !isConcurrency && drillDownToPlace; property int textPadding: 10; @@ -44,7 +45,7 @@ Rectangle { property int textSizeSmall: 18; property int stackShadowNarrowing: 5; property string defaultThumbnail: Qt.resolvedUrl("../../images/default-domain.gif"); - property int shadowHeight: 20; + property int shadowHeight: 10; HifiConstants { id: hifi } function pastTime(timestamp) { // Answer a descriptive string @@ -69,6 +70,44 @@ Rectangle { } property bool hasGif: imageUrl.indexOf('.gif') === (imageUrl.length - 4); + + function pluralize(count, singular, optionalPlural) { + return (count === 1) ? singular : (optionalPlural || (singular + "s")); + } + + DropShadow { + visible: isStacked; + anchors.fill: shadow1; + source: shadow1; + verticalOffset: 2; + radius: 4; + samples: 9; + color: hifi.colors.baseGrayShadow; + } + Rectangle { + id: shadow1; + visible: isStacked; + width: parent.width - stackShadowNarrowing; + height: shadowHeight; + anchors { + top: parent.bottom; + horizontalCenter: parent.horizontalCenter; + } + } + DropShadow { + anchors.fill: base; + source: base; + verticalOffset: 2; + radius: 4; + samples: 9; + color: hifi.colors.baseGrayShadow; + } + Rectangle { + id: base; + color: "white"; + anchors.fill: parent; + } + AnimatedImage { id: animation; // Always visible, to drive loading, but initially covered up by lobby during load. @@ -80,7 +119,7 @@ Rectangle { id: lobby; visible: !hasGif || (animation.status !== Image.Ready); width: parent.width - (isConcurrency ? 0 : (2 * smallMargin)); - height: parent.height - (isConcurrency ? 0 : smallMargin); + height: parent.height -(isAnnouncement ? smallMargin : messageHeight) - (isConcurrency ? 0 : smallMargin); source: thumbnail || defaultThumbnail; fillMode: Image.PreserveAspectCrop; anchors { @@ -95,41 +134,13 @@ Rectangle { } } } - Rectangle { - id: shadow1; - visible: isStacked; - width: parent.width - stackShadowNarrowing; - height: shadowHeight / 2; - anchors { - top: parent.bottom; - horizontalCenter: parent.horizontalCenter; - } - gradient: Gradient { - GradientStop { position: 0.0; color: "gray" } - GradientStop { position: 1.0; color: "white" } - } - } - Rectangle { - id: shadow2; - visible: isStacked; - width: shadow1.width - stackShadowNarrowing; - height: shadowHeight / 2; - anchors { - top: shadow1.bottom; - horizontalCenter: parent.horizontalCenter; - } - gradient: Gradient { - GradientStop { position: 0.0; color: "gray" } - GradientStop { position: 1.0; color: "white" } - } - } property int dropHorizontalOffset: 0; property int dropVerticalOffset: 1; property int dropRadius: 2; property int dropSamples: 9; property int dropSpread: 0; DropShadow { - visible: true; + visible: showPlace; // Do we have to check for whatever the modern equivalent is for desktop.gradientsSupported? source: place; anchors.fill: place; horizontalOffset: dropHorizontalOffset; @@ -139,12 +150,12 @@ Rectangle { color: hifi.colors.black; spread: dropSpread; } - RalewayLight { + RalewaySemiBold { id: place; visible: showPlace; text: placeName; color: hifi.colors.white; - size: 38; + size: textSize; elide: Text.ElideRight; // requires constrained width anchors { top: parent.top; @@ -154,56 +165,68 @@ Rectangle { } } Rectangle { - id: rectRow - z: 1 - width: message.width + (users.visible ? users.width + bottomRow.spacing : 0) - + (icon.visible ? icon.width + bottomRow.spacing: 0) + bottomRow.spacing; - height: messageHeight + 1; - radius: 25 - - anchors { - bottom: parent.bottom - left: parent.left - leftMargin: textPadding - bottomMargin: textPadding + id: lozenge; + visible: isAnnouncement; + color: hifi.colors.redHighlight; + anchors.fill: infoRow; + radius: lozenge.height / 2.0; + border.width: lozengeHot.containsMouse ? 4 : 0; + border.color: "white"; + } + Row { + id: infoRow; + Image { + id: icon; + source: isAnnouncement ? "../../images/Announce-Blast.svg" : "../../images/snap-icon.svg"; + width: 40; + height: 40; + visible: ((action === 'snapshot') || isAnnouncement) && (messageHeight >= 40); } - - Row { - id: bottomRow - FiraSansRegular { - id: users; - visible: isConcurrency; - text: onlineUsers; - size: textSize; - color: messageColor; - anchors.verticalCenter: message.verticalCenter; - } - Image { - id: icon; - source: "../../images/snap-icon.svg" - width: 40; - height: 40; - visible: action === 'snapshot'; - } - RalewayRegular { - id: message; - text: isConcurrency ? ((onlineUsers === 1) ? "person" : "people") : (drillDownToPlace ? "snapshots" : ("by " + userName)); - size: textSizeSmall; - color: messageColor; - elide: Text.ElideRight; // requires a width to be specified` - anchors { - bottom: parent.bottom; - bottomMargin: parent.spacing; - } - } - spacing: textPadding; - height: messageHeight; + FiraSansRegular { + id: users; + visible: isConcurrency || isAnnouncement; + text: onlineUsers; + size: textSize; + color: messageColor; + anchors.verticalCenter: message.verticalCenter; + } + RalewayRegular { + id: message; + visible: !isAnnouncement; + text: isConcurrency ? pluralize(onlineUsers, "person", "people") : (drillDownToPlace ? "snapshots" : ("by " + userName)); + size: textSizeSmall; + color: messageColor; + elide: Text.ElideRight; // requires a width to be specified` + width: root.width - textPadding + - (icon.visible ? icon.width + parent.spacing : 0) + - (users.visible ? users.width + parent.spacing : 0) + - (actionIcon.width + (2 * smallMargin)); anchors { bottom: parent.bottom; - left: parent.left; - leftMargin: 4 + bottomMargin: parent.spacing; } } + Column { + visible: isAnnouncement; + RalewayRegular { + text: pluralize(onlineUsers, "connection") + " "; // hack padding + size: textSizeSmall; + color: messageColor; + } + RalewayRegular { + text: pluralize(onlineUsers, "is here now", "are here now"); + size: textSizeSmall * 0.7; + color: messageColor; + } + } + spacing: textPadding; + height: messageHeight; + anchors { + bottom: parent.bottom; + left: parent.left; + leftMargin: textPadding; + bottomMargin: isAnnouncement ? textPadding : 0; + } } // These two can be supplied to provide hover behavior. // For example, AddressBarDialog provides functions that set the current list view item @@ -218,39 +241,37 @@ Rectangle { onEntered: hoverThunk(); onExited: unhoverThunk(); } - Rectangle { - id: rectIcon - z: 1 - width: 32 - height: 32 - radius: 15 + StateImage { + id: actionIcon; + visible: !isAnnouncement; + imageURL: "../../images/info-icon-2-state.svg"; + size: 30; + buttonState: messageArea.containsMouse ? 1 : 0; anchors { bottom: parent.bottom; right: parent.right; - bottomMargin: textPadding; - rightMargin: textPadding; - } - - StateImage { - id: actionIcon; - imageURL: "../../images/info-icon-2-state.svg"; - size: 32; - buttonState: messageArea.containsMouse ? 1 : 0; - anchors { - bottom: parent.bottom; - right: parent.right; - //margins: smallMargin; - } + margins: smallMargin; } } - + function go() { + goFunction(drillDownToPlace ? ("/places/" + placeName) : ("/user_stories/" + storyId)); + } MouseArea { id: messageArea; - width: rectIcon.width; - height: rectIcon.height; - anchors.fill: rectIcon + visible: !isAnnouncement; + width: parent.width; + height: messageHeight; + anchors.top: lobby.bottom; acceptedButtons: Qt.LeftButton; - onClicked: goFunction(drillDownToPlace ? ("/places/" + placeName) : ("/user_stories/" + storyId)); + onClicked: go(); + hoverEnabled: true; + } + MouseArea { + id: lozengeHot; + visible: lozenge.visible; + anchors.fill: lozenge; + acceptedButtons: Qt.LeftButton; + onClicked: go(); hoverEnabled: true; } } diff --git a/interface/resources/qml/hifi/Feed.qml b/interface/resources/qml/hifi/Feed.qml new file mode 100644 index 0000000000..fd3472b7be --- /dev/null +++ b/interface/resources/qml/hifi/Feed.qml @@ -0,0 +1,247 @@ +// +// Feed.qml +// qml/hifi +// +// Displays a particular type of feed +// +// Created by Howard Stearns on 4/18/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 Hifi 1.0 +import QtQuick 2.5 +import QtGraphicalEffects 1.0 +import "toolbars" +import "../styles-uit" + +Column { + id: root; + visible: false; + + property int cardWidth: 212; + property int cardHeight: 152; + property int textPadding: 10; + property int smallMargin: 4; + property int messageHeight: 40; + property int textSize: 24; + property int textSizeSmall: 18; + property int stackShadowNarrowing: 5; + property int stackedCardShadowHeight: 4; + property int labelSize: 20; + + property string metaverseServerUrl: ''; + property string actions: 'snapshot'; + // sendToScript doesn't get wired until after everything gets created. So we have to queue fillDestinations on nextTick. + Component.onCompleted: delay.start(); + property string labelText: actions; + property string filter: ''; + onFilterChanged: filterChoicesByText(); + property var goFunction: null; + property var rpc: null; + + HifiConstants { id: hifi } + ListModel { id: suggestions; } + + function resolveUrl(url) { + return (url.indexOf('/') === 0) ? (metaverseServerUrl + url) : url; + } + function makeModelData(data) { // create a new obj from data + // ListModel elements will only ever have those properties that are defined by the first obj that is added. + // So here we make sure that we have all the properties we need, regardless of whether it is a place data or user story. + var name = data.place_name, + tags = data.tags || [data.action, data.username], + description = data.description || "", + thumbnail_url = data.thumbnail_url || ""; + if (actions === 'concurrency,snapshot') { + // A temporary hack for simulating announcements. We won't use this in production, but if requested, we'll use this data like announcements. + data.details.connections = 4; + data.action = 'announcement'; + } + return { + place_name: name, + username: data.username || "", + path: data.path || "", + created_at: data.created_at || "", + action: data.action || "", + thumbnail_url: resolveUrl(thumbnail_url), + image_url: resolveUrl(data.details && data.details.image_url), + + metaverseId: (data.id || "").toString(), // Some are strings from server while others are numbers. Model objects require uniformity. + + tags: tags, + description: description, + online_users: data.details.connections || data.details.concurrency || 0, + drillDownToPlace: false, + + searchText: [name].concat(tags, description || []).join(' ').toUpperCase() + } + } + property var allStories: []; + property var placeMap: ({}); // Used for making stacks. + property int requestId: 0; + function handleError(url, error, data, cb) { // cb(error) and answer truthy if needed, else falsey + if (!error && (data.status === 'success')) { + return; + } + if (!error) { // Create a message from the data + error = data.status + ': ' + data.error; + } + if (typeof(error) === 'string') { // Make a proper Error object + error = new Error(error); + } + error.message += ' in ' + url; // Include the url. + cb(error); + return true; + } + function getUserStoryPage(pageNumber, cb, cb1) { // cb(error) after all pages of domain data have been added to model + // If supplied, cb1 will be run after the first page IFF it is not the last, for responsiveness. + var options = [ + 'now=' + new Date().toISOString(), + 'include_actions=' + actions, + 'restriction=' + (Account.isLoggedIn() ? 'open,hifi' : 'open'), + 'require_online=true', + 'protocol=' + encodeURIComponent(AddressManager.protocolVersion()), + 'page=' + pageNumber + ]; + var url = metaverseBase + 'user_stories?' + options.join('&'); + var thisRequestId = ++requestId; + rpc('request', url, function (error, data) { + if (thisRequestId !== requestId) { + error = 'stale'; + } + if (handleError(url, error, data, cb)) { + return; // abandon stale requests + } + allStories = allStories.concat(data.user_stories.map(makeModelData)); + if ((data.current_page < data.total_pages) && (data.current_page <= 10)) { // just 10 pages = 100 stories for now + if ((pageNumber === 1) && cb1) { + cb1(); + } + return getUserStoryPage(pageNumber + 1, cb); + } + cb(); + }); + } + property var delay: Timer { // No setTimeout or nextTick in QML. + interval: 0; + onTriggered: fillDestinations(); + } + function fillDestinations() { // Public + function report(label, error) { + console.log(label, actions, error || 'ok', allStories.length, 'filtered to', suggestions.count); + } + var filter = makeFilteredStoryProcessor(), counter = 0; + allStories = []; + suggestions.clear(); + placeMap = {}; + getUserStoryPage(1, function (error) { + allStories.slice(counter).forEach(filter); + report('user stories update', error); + root.visible = !!suggestions.count; + }, function () { // If there's more than a page, put what we have in the model right away, keeping track of how many are processed. + allStories.forEach(function (story) { + counter++; + filter(story); + root.visible = !!suggestions.count; + }); + report('user stories'); + }); + } + function identity(x) { + return x; + } + function makeFilteredStoryProcessor() { // answer a function(storyData) that adds it to suggestions if it matches + var words = filter.toUpperCase().split(/\s+/).filter(identity); + function suggestable(story) { + if (story.action === 'snapshot') { + return true; + } + return story.place_name !== AddressManager.placename; // Not our entry, but do show other entry points to current domain. + } + function matches(story) { + if (!words.length) { + return suggestable(story); + } + return words.every(function (word) { + return story.searchText.indexOf(word) >= 0; + }); + } + function addToSuggestions(place) { + var collapse = ((actions === 'concurrency,snapshot') && (place.action !== 'concurrency')) || (place.action === 'announcement'); + if (collapse) { + var existing = placeMap[place.place_name]; + if (existing) { + existing.drillDownToPlace = true; + return; + } + } + suggestions.append(place); + if (collapse) { + placeMap[place.place_name] = suggestions.get(suggestions.count - 1); + } else if (place.action === 'concurrency') { + suggestions.get(suggestions.count - 1).drillDownToPlace = true; // Don't change raw place object (in allStories). + } + } + return function (story) { + if (matches(story)) { + addToSuggestions(story); + } + }; + } + function filterChoicesByText() { + suggestions.clear(); + placeMap = {}; + allStories.forEach(makeFilteredStoryProcessor()); + root.visible = !!suggestions.count; + } + + RalewayBold { + id: label; + text: labelText; + color: hifi.colors.blueAccent; + size: labelSize; + } + ListView { + id: scroll; + model: suggestions; + orientation: ListView.Horizontal; + highlightMoveDuration: -1; + highlightMoveVelocity: -1; + highlight: Rectangle { color: "transparent"; border.width: 4; border.color: hifiStyleConstants.colors.primaryHighlight; z: 1; } + currentIndex: -1; + + spacing: 12; + width: parent.width; + height: cardHeight + stackedCardShadowHeight; + delegate: Card { + id: card; + width: cardWidth; + height: cardHeight; + goFunction: root.goFunction; + userName: model.username; + placeName: model.place_name; + hifiUrl: model.place_name + model.path; + thumbnail: model.thumbnail_url; + imageUrl: model.image_url; + action: model.action; + timestamp: model.created_at; + onlineUsers: model.online_users; + storyId: model.metaverseId; + drillDownToPlace: model.drillDownToPlace; + + textPadding: root.textPadding; + smallMargin: root.smallMargin; + messageHeight: root.messageHeight; + textSize: root.textSize; + textSizeSmall: root.textSizeSmall; + stackShadowNarrowing: root.stackShadowNarrowing; + shadowHeight: root.stackedCardShadowHeight; + + hoverThunk: function () { scroll.currentIndex = index; } + unhoverThunk: function () { scroll.currentIndex = -1; } + } + } +} diff --git a/interface/resources/qml/hifi/tablet/Edit.qml b/interface/resources/qml/hifi/tablet/Edit.qml index 4abe698fbc..ea31eb26d8 100644 --- a/interface/resources/qml/hifi/tablet/Edit.qml +++ b/interface/resources/qml/hifi/tablet/Edit.qml @@ -1,19 +1,11 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 -import QtWebEngine 1.1 -import QtWebChannel 1.0 -import QtQuick.Controls.Styles 1.4 -import "../../controls" -import "../toolbars" -import HFWebEngineProfile 1.0 -import QtGraphicalEffects 1.0 -import "../../controls-uit" as HifiControls import "../../styles-uit" StackView { id: editRoot objectName: "stack" - initialItem: editBasePage + initialItem: Qt.resolvedUrl('EditTabView.qml') property var eventBridge; signal sendToScript(var message); @@ -30,270 +22,10 @@ StackView { editRoot.pop(); } - - Component { - id: editBasePage - TabView { - id: editTabView - // anchors.fill: parent - height: 60 - - Tab { - title: "CREATE" - active: true - enabled: true - property string originalUrl: "" - - Rectangle { - color: "#404040" - - Text { - color: "#ffffff" - text: "Choose an Entity Type to Create:" - font.pixelSize: 14 - font.bold: true - anchors.top: parent.top - anchors.topMargin: 28 - anchors.left: parent.left - anchors.leftMargin: 28 - } - - Flow { - id: createEntitiesFlow - spacing: 35 - anchors.right: parent.right - anchors.rightMargin: 55 - anchors.left: parent.left - anchors.leftMargin: 55 - anchors.top: parent.top - anchors.topMargin: 70 - - - NewEntityButton { - icon: "icons/create-icons/94-model-01.svg" - text: "MODEL" - onClicked: { - editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newModelButton" } - }); - editTabView.currentIndex = 2 - } - } - - NewEntityButton { - icon: "icons/create-icons/21-cube-01.svg" - text: "CUBE" - onClicked: { - editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newCubeButton" } - }); - editTabView.currentIndex = 2 - } - } - - NewEntityButton { - icon: "icons/create-icons/22-sphere-01.svg" - text: "SPHERE" - onClicked: { - editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newSphereButton" } - }); - editTabView.currentIndex = 2 - } - } - - NewEntityButton { - icon: "icons/create-icons/24-light-01.svg" - text: "LIGHT" - onClicked: { - editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newLightButton" } - }); - editTabView.currentIndex = 2 - } - } - - NewEntityButton { - icon: "icons/create-icons/20-text-01.svg" - text: "TEXT" - onClicked: { - editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newTextButton" } - }); - editTabView.currentIndex = 2 - } - } - - NewEntityButton { - icon: "icons/create-icons/25-web-1-01.svg" - text: "WEB" - onClicked: { - editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newWebButton" } - }); - editTabView.currentIndex = 2 - } - } - - NewEntityButton { - icon: "icons/create-icons/23-zone-01.svg" - text: "ZONE" - onClicked: { - editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newZoneButton" } - }); - editTabView.currentIndex = 2 - } - } - - NewEntityButton { - icon: "icons/create-icons/90-particles-01.svg" - text: "PARTICLE" - onClicked: { - editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newParticleButton" } - }); - editTabView.currentIndex = 2 - } - } - } - - HifiControls.Button { - id: assetServerButton - text: "Open This Domain's Asset Server" - color: hifi.buttons.black - colorScheme: hifi.colorSchemes.dark - anchors.right: parent.right - anchors.rightMargin: 55 - anchors.left: parent.left - anchors.leftMargin: 55 - anchors.top: createEntitiesFlow.bottom - anchors.topMargin: 35 - onClicked: { - editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "openAssetBrowserButton" } - }); - } - } - - HifiControls.Button { - text: "Import Entities (.json)" - color: hifi.buttons.black - colorScheme: hifi.colorSchemes.dark - anchors.right: parent.right - anchors.rightMargin: 55 - anchors.left: parent.left - anchors.leftMargin: 55 - anchors.top: assetServerButton.bottom - anchors.topMargin: 20 - onClicked: { - editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "importEntitiesButton" } - }); - } - } - } - } - - Tab { - title: "LIST" - active: true - enabled: true - property string originalUrl: "" - - WebView { - id: entityListToolWebView - url: "../../../../../scripts/system/html/entityList.html" - eventBridge: editRoot.eventBridge - anchors.fill: parent - enabled: true - } - } - - Tab { - title: "PROPERTIES" - active: true - enabled: true - property string originalUrl: "" - - WebView { - id: entityPropertiesWebView - url: "../../../../../scripts/system/html/entityProperties.html" - eventBridge: editRoot.eventBridge - anchors.fill: parent - enabled: true - } - } - - Tab { - title: "GRID" - active: true - enabled: true - property string originalUrl: "" - - WebView { - id: gridControlsWebView - url: "../../../../../scripts/system/html/gridControls.html" - eventBridge: editRoot.eventBridge - anchors.fill: parent - enabled: true - } - } - - Tab { - title: "P" - active: true - enabled: true - property string originalUrl: "" - - WebView { - id: particleExplorerWebView - url: "../../../../../scripts/system/particle_explorer/particleExplorer.html" - eventBridge: editRoot.eventBridge - anchors.fill: parent - enabled: true - } - } - - - style: TabViewStyle { - frameOverlap: 1 - tab: Rectangle { - color: styleData.selected ? "#404040" :"black" - implicitWidth: text.width + 42 - implicitHeight: 40 - Text { - id: text - anchors.centerIn: parent - text: styleData.title - font.pixelSize: 16 - font.bold: true - color: styleData.selected ? "white" : "white" - property string glyphtext: "" - HiFiGlyphs { - anchors.centerIn: parent - size: 30 - color: "#ffffff" - text: text.glyphtext - } - Component.onCompleted: if (styleData.title == "P") { - text.text = " "; - text.glyphtext = "\ue004"; - } - } - } - tabBar: Rectangle { - color: "black" - anchors.right: parent.right - anchors.rightMargin: 0 - anchors.left: parent.left - anchors.leftMargin: 0 - anchors.bottom: parent.bottom - anchors.bottomMargin: 0 - anchors.top: parent.top - anchors.topMargin: 0 - } - } - } + // Passes script messages to the item on the top of the stack + function fromScript(message) { + var currentItem = editRoot.currentItem; + if (currentItem && currentItem.fromScript) + currentItem.fromScript(message); } } diff --git a/interface/resources/qml/hifi/tablet/EditTabView.qml b/interface/resources/qml/hifi/tablet/EditTabView.qml new file mode 100644 index 0000000000..35f2b82f0f --- /dev/null +++ b/interface/resources/qml/hifi/tablet/EditTabView.qml @@ -0,0 +1,318 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtWebEngine 1.1 +import QtWebChannel 1.0 +import QtQuick.Controls.Styles 1.4 +import "../../controls" +import "../toolbars" +import HFWebEngineProfile 1.0 +import QtGraphicalEffects 1.0 +import "../../controls-uit" as HifiControls +import "../../styles-uit" + + +TabView { + id: editTabView + // anchors.fill: parent + height: 60 + + Tab { + title: "CREATE" + active: true + enabled: true + property string originalUrl: "" + + Rectangle { + color: "#404040" + + Text { + color: "#ffffff" + text: "Choose an Entity Type to Create:" + font.pixelSize: 14 + font.bold: true + anchors.top: parent.top + anchors.topMargin: 28 + anchors.left: parent.left + anchors.leftMargin: 28 + } + + Flow { + id: createEntitiesFlow + spacing: 35 + anchors.right: parent.right + anchors.rightMargin: 55 + anchors.left: parent.left + anchors.leftMargin: 55 + anchors.top: parent.top + anchors.topMargin: 70 + + + NewEntityButton { + icon: "icons/create-icons/94-model-01.svg" + text: "MODEL" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "newModelButton" } + }); + editTabView.currentIndex = 2 + } + } + + NewEntityButton { + icon: "icons/create-icons/21-cube-01.svg" + text: "CUBE" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "newCubeButton" } + }); + editTabView.currentIndex = 2 + } + } + + NewEntityButton { + icon: "icons/create-icons/22-sphere-01.svg" + text: "SPHERE" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "newSphereButton" } + }); + editTabView.currentIndex = 2 + } + } + + NewEntityButton { + icon: "icons/create-icons/24-light-01.svg" + text: "LIGHT" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "newLightButton" } + }); + editTabView.currentIndex = 2 + } + } + + NewEntityButton { + icon: "icons/create-icons/20-text-01.svg" + text: "TEXT" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "newTextButton" } + }); + editTabView.currentIndex = 2 + } + } + + NewEntityButton { + icon: "icons/create-icons/25-web-1-01.svg" + text: "WEB" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "newWebButton" } + }); + editTabView.currentIndex = 2 + } + } + + NewEntityButton { + icon: "icons/create-icons/23-zone-01.svg" + text: "ZONE" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "newZoneButton" } + }); + editTabView.currentIndex = 2 + } + } + + NewEntityButton { + icon: "icons/create-icons/90-particles-01.svg" + text: "PARTICLE" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "newParticleButton" } + }); + editTabView.currentIndex = 4 + } + } + } + + HifiControls.Button { + id: assetServerButton + text: "Open This Domain's Asset Server" + color: hifi.buttons.black + colorScheme: hifi.colorSchemes.dark + anchors.right: parent.right + anchors.rightMargin: 55 + anchors.left: parent.left + anchors.leftMargin: 55 + anchors.top: createEntitiesFlow.bottom + anchors.topMargin: 35 + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "openAssetBrowserButton" } + }); + } + } + + HifiControls.Button { + text: "Import Entities (.json)" + color: hifi.buttons.black + colorScheme: hifi.colorSchemes.dark + anchors.right: parent.right + anchors.rightMargin: 55 + anchors.left: parent.left + anchors.leftMargin: 55 + anchors.top: assetServerButton.bottom + anchors.topMargin: 20 + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "importEntitiesButton" } + }); + } + } + } + } + + Tab { + title: "LIST" + active: true + enabled: true + property string originalUrl: "" + + WebView { + id: entityListToolWebView + url: "../../../../../scripts/system/html/entityList.html" + eventBridge: editRoot.eventBridge + anchors.fill: parent + enabled: true + } + } + + Tab { + title: "PROPERTIES" + active: true + enabled: true + property string originalUrl: "" + + WebView { + id: entityPropertiesWebView + url: "../../../../../scripts/system/html/entityProperties.html" + eventBridge: editRoot.eventBridge + anchors.fill: parent + enabled: true + } + } + + Tab { + title: "GRID" + active: true + enabled: true + property string originalUrl: "" + + WebView { + id: gridControlsWebView + url: "../../../../../scripts/system/html/gridControls.html" + eventBridge: editRoot.eventBridge + anchors.fill: parent + enabled: true + } + } + + Tab { + title: "P" + active: true + enabled: true + property string originalUrl: "" + + WebView { + id: particleExplorerWebView + url: "../../../../../scripts/system/particle_explorer/particleExplorer.html" + eventBridge: editRoot.eventBridge + anchors.fill: parent + enabled: true + } + } + + + style: TabViewStyle { + frameOverlap: 1 + tab: Rectangle { + color: styleData.selected ? "#404040" :"black" + implicitWidth: text.width + 42 + implicitHeight: 40 + Text { + id: text + anchors.centerIn: parent + text: styleData.title + font.pixelSize: 16 + font.bold: true + color: styleData.selected ? "white" : "white" + property string glyphtext: "" + HiFiGlyphs { + anchors.centerIn: parent + size: 30 + color: "#ffffff" + text: text.glyphtext + } + Component.onCompleted: if (styleData.title == "P") { + text.text = " "; + text.glyphtext = "\ue004"; + } + } + } + tabBar: Rectangle { + color: "black" + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + anchors.top: parent.top + anchors.topMargin: 0 + } + } + + function fromScript(message) { + switch (message.method) { + case 'selectTab': + selectTab(message.params.id); + break; + default: + console.warn('Unrecognized message:', JSON.stringify(message)); + } + } + + // Changes the current tab based on tab index or title as input + function selectTab(id) { + if (typeof id === 'number') { + if (id >= 0 && id <= 4) { + editTabView.currentIndex = id; + } else { + console.warn('Attempt to switch to invalid tab:', id); + } + } else if (typeof id === 'string'){ + switch (id.toLowerCase()) { + case 'create': + editTabView.currentIndex = 0; + break; + case 'list': + editTabView.currentIndex = 1; + break; + case 'properties': + editTabView.currentIndex = 2; + break; + case 'grid': + editTabView.currentIndex = 3; + break; + case 'particle': + editTabView.currentIndex = 4; + break; + default: + console.warn('Attempt to switch to invalid tab:', id); + } + } else { + console.warn('Attempt to switch tabs with invalid input:', JSON.stringify(id)); + } + } +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/tablet/InputRecorder.qml b/interface/resources/qml/hifi/tablet/InputRecorder.qml new file mode 100644 index 0000000000..76b122d07d --- /dev/null +++ b/interface/resources/qml/hifi/tablet/InputRecorder.qml @@ -0,0 +1,170 @@ +// +// Created by Dante Ruiz 2017/04/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 +// + +import QtQuick 2.5 +import Hifi 1.0 +import QtQuick.Controls 1.4 +import QtQuick.Dialogs 1.2 as OriginalDialogs + +import "../../styles-uit" +import "../../controls-uit" as HifiControls +import "../../windows" +import "../../dialogs" + +Rectangle { + id: inputRecorder + property var eventBridge; + HifiConstants { id: hifi } + signal sendToScript(var message); + color: hifi.colors.baseGray; + property string path: "" + property string dir: "" + property var dialog: null; + property bool recording: false; + + Component { id: fileDialog; TabletFileDialog { } } + Row { + id: topButtons + width: parent.width + height: 40 + spacing: 40 + anchors { + left: parent.left + right: parent.right + top: parent.top + topMargin: 10 + } + + HifiControls.Button { + id: start + text: "Start Recoring" + color: hifi.buttons.black + enabled: true + onClicked: { + if (inputRecorder.recording) { + sendToScript({method: "Stop"}); + inputRecorder.recording = false; + start.text = "Start Recording"; + selectedFile.text = "Current recording is not saved"; + } else { + sendToScript({method: "Start"}); + inputRecorder.recording = true; + start.text = "Stop Recording"; + } + } + } + + HifiControls.Button { + id: save + text: "Save Recording" + color: hifi.buttons.black + enabled: true + onClicked: { + sendToScript({method: "Save"}); + selectedFile.text = ""; + } + } + + HifiControls.Button { + id: playBack + anchors.right: browse.left + anchors.top: selectedFile.bottom + anchors.topMargin: 10 + + text: "Play Recording" + color: hifi.buttons.black + enabled: true + onClicked: { + sendToScript({method: "playback"}); + HMD.closeTablet(); + } + } + + } + + HifiControls.VerticalSpacer {} + + HifiControls.TextField { + id: selectedFile + anchors.left: parent.left + anchors.right: parent.right + anchors.top: topButtons.top + anchors.topMargin: 40 + + colorScheme: hifi.colorSchemes.dark + readOnly: true + + } + + + + HifiControls.Button { + id: browse + anchors.right: parent.right + anchors.top: selectedFile.bottom + anchors.topMargin: 10 + + text: "Load..." + color: hifi.buttons.black + enabled: true + onClicked: { + dialog = fileDialog.createObject(inputRecorder); + dialog.caption = "InputRecorder"; + console.log(dialog.dir); + dialog.dir = "file:///" + inputRecorder.dir; + dialog.selectedFile.connect(getFileSelected); + } + } + + Column { + id: notes + anchors.centerIn: parent; + spacing: 20 + + Text { + text: "All files are saved under the folder 'hifi-input-recording' in AppData directory"; + color: "white" + font.pointSize: 10 + } + + Text { + text: "To cancel a recording playback press Alt-B" + color: "white" + font.pointSize: 10 + } + } + + function getFileSelected(file) { + selectedFile.text = file; + inputRecorder.path = file; + sendToScript({method: "Load", params: {file: path }}); + } + + function fromScript(message) { + switch (message.method) { + case "update": + updateButtonStatus(message.params); + break; + case "path": + console.log(message.params); + inputRecorder.dir = message.params; + break; + } + } + + function updateButtonStatus(status) { + inputRecorder.recording = status; + + if (inputRecorder.recording) { + start.text = "Stop Recording"; + } else { + start.text = "Start Recording"; + } + } +} + diff --git a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml index bed1f82ac2..7159b078ee 100644 --- a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml +++ b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml @@ -30,18 +30,33 @@ StackView { width: parent !== null ? parent.width : undefined height: parent !== null ? parent.height : undefined property var eventBridge; - property var allStories: []; - property int cardWidth: 460; - property int cardHeight: 320; + property int cardWidth: 212; + property int cardHeight: 152; property string metaverseBase: addressBarDialog.metaverseServerUrl + "/api/v1/"; - property var tablet: null; + // This version only implements rpc(method, parameters, callback(error, result)) calls initiated from here, not initiated from .js, nor "notifications". + property var rpcCalls: ({}); + property var rpcCounter: 0; + signal sendToScript(var message); + function rpc(method, parameters, callback) { + rpcCalls[rpcCounter] = callback; + var message = {method: method, params: parameters, id: rpcCounter++, jsonrpc: "2.0"}; + sendToScript(message); + } + function fromScript(message) { + var callback = rpcCalls[message.id]; + if (!callback) { + console.log('No callback for message fromScript', JSON.stringify(message)); + return; + } + delete rpcCalls[message.id]; + callback(message.error, message.result); + } + Component { id: tabletWebView; TabletWebView {} } Component.onCompleted: { - fillDestinations(); updateLocationText(false); - fillDestinations(); addressLine.focus = !HMD.active; root.parentChanged.connect(center); center(); @@ -57,7 +72,7 @@ StackView { } - function resetAfterTeleport() { + function resetAfterTeleport() { //storyCardFrame.shown = root.shown = false; } function goCard(targetString) { @@ -65,6 +80,7 @@ StackView { var card = tabletWebView.createObject(); card.url = addressBarDialog.metaverseServerUrl + targetString; card.parentStackItem = root; + card.eventBridge = root.eventBridge; root.push(card); return; } @@ -156,7 +172,7 @@ StackView { left: parent.left; } - HifiStyles.RalewayLight { + HifiStyles.RalewayRegular { id: notice; font.pixelSize: hifi.fonts.pixelSize * 0.7; anchors { @@ -189,7 +205,6 @@ StackView { } font.pixelSize: hifi.fonts.pixelSize * 0.75 onTextChanged: { - filterChoicesByText(); updateLocationText(text.length > 0); } onAccepted: { @@ -224,109 +239,80 @@ StackView { } } } + Rectangle { - id: topBar - height: 37 - color: hifiStyleConstants.colors.white - - anchors.right: parent.right - anchors.rightMargin: 0 - anchors.left: parent.left - anchors.leftMargin: 0 - anchors.topMargin: 0 - anchors.top: addressBar.bottom - - Row { - id: thing - spacing: 5 * hifi.layout.spacing - - anchors { - top: parent.top; - left: parent.left - leftMargin: 25 - } - - TabletTextButton { - id: allTab; - text: "ALL"; - property string includeActions: 'snapshot,concurrency'; - selected: allTab === selectedTab; - action: tabSelect; - } - - TabletTextButton { - id: placeTab; - text: "PLACES"; - property string includeActions: 'concurrency'; - selected: placeTab === selectedTab; - action: tabSelect; - - } - - TabletTextButton { - id: snapTab; - text: "SNAP"; - property string includeActions: 'snapshot'; - selected: snapTab === selectedTab; - action: tabSelect; + id: bgMain; + anchors { + top: addressBar.bottom; + bottom: parent.keyboardEnabled ? keyboard.top : parent.bottom; + left: parent.left; + right: parent.right; + } + Rectangle { + id: addressShadow; + width: parent.width; + height: 42 - 33; + gradient: Gradient { + GradientStop { position: 0.0; color: "gray" } + GradientStop { position: 1.0; color: "white" } } } - - } - - Rectangle { - id: bgMain - color: hifiStyleConstants.colors.white - anchors.bottom: parent.keyboardEnabled ? keyboard.top : parent.bottom - anchors.bottomMargin: 0 - anchors.right: parent.right - anchors.rightMargin: 0 - anchors.left: parent.left - anchors.leftMargin: 0 - anchors.top: topBar.bottom - anchors.topMargin: 0 - - ListModel { id: suggestions } - - ListView { - id: scroll - - property int stackedCardShadowHeight: 0; - clip: true - spacing: 14 + Rectangle { // Column margins require QtQuick 2.7, which we don't use yet. + id: column; + property real pad: 10; + width: bgMain.width - column.pad; + height: stack.height; + color: "transparent"; anchors { - bottom: parent.bottom - top: parent.top - left: parent.left - right: parent.right - leftMargin: 10 + left: parent.left; + leftMargin: column.pad; + top: addressShadow.bottom; + topMargin: column.pad; } - - model: suggestions - orientation: ListView.Vertical - - delegate: Card { - width: cardWidth; - height: cardHeight; - goFunction: goCard; - userName: model.username; - placeName: model.place_name; - hifiUrl: model.place_name + model.path; - thumbnail: model.thumbnail_url; - imageUrl: model.image_url; - action: model.action; - timestamp: model.created_at; - onlineUsers: model.online_users; - storyId: model.metaverseId; - drillDownToPlace: model.drillDownToPlace; - shadowHeight: scroll.stackedCardShadowHeight; - hoverThunk: function () { scroll.currentIndex = index; } - unhoverThunk: function () { scroll.currentIndex = -1; } + Column { + id: stack; + width: column.width; + spacing: 33 - places.labelSize; + Feed { + id: happeningNow; + width: parent.width; + cardWidth: 312 + (2 * 4); + cardHeight: 163 + (2 * 4); + metaverseServerUrl: addressBarDialog.metaverseServerUrl; + labelText: 'HAPPENING NOW'; + actions: 'announcement'; + filter: addressLine.text; + goFunction: goCard; + rpc: root.rpc; + } + Feed { + id: places; + width: parent.width; + cardWidth: 210; + cardHeight: 110 + messageHeight; + messageHeight: 44; + metaverseServerUrl: addressBarDialog.metaverseServerUrl; + labelText: 'PLACES'; + actions: 'concurrency'; + filter: addressLine.text; + goFunction: goCard; + rpc: root.rpc; + } + Feed { + id: snapshots; + width: parent.width; + cardWidth: 143 + (2 * 4); + cardHeight: 75 + messageHeight + 4; + messageHeight: 32; + textPadding: 6; + metaverseServerUrl: addressBarDialog.metaverseServerUrl; + labelText: 'RECENT SNAPS'; + actions: 'snapshot'; + filter: addressLine.text; + goFunction: goCard; + rpc: root.rpc; + } } - - highlightMoveDuration: -1; - highlightMoveVelocity: -1; - highlight: Rectangle { color: "transparent"; border.width: 4; border.color: hifiStyleConstants.colors.blueHighlight; z: 1; } } } @@ -364,175 +350,13 @@ StackView { } - function getRequest(url, cb) { // cb(error, responseOfCorrectContentType) of url. General for 'get' text/html/json, but without redirects. - // TODO: make available to other .qml. - var request = new XMLHttpRequest(); - // QT bug: apparently doesn't handle onload. Workaround using readyState. - request.onreadystatechange = function () { - var READY_STATE_DONE = 4; - var HTTP_OK = 200; - if (request.readyState >= READY_STATE_DONE) { - var error = (request.status !== HTTP_OK) && request.status.toString() + ':' + request.statusText, - response = !error && request.responseText, - contentType = !error && request.getResponseHeader('content-type'); - if (!error && contentType.indexOf('application/json') === 0) { - try { - response = JSON.parse(response); - } catch (e) { - error = e; - } - } - cb(error, response); - } - }; - request.open("GET", url, true); - request.send(); - } - - function identity(x) { - return x; - } - - function handleError(url, error, data, cb) { // cb(error) and answer truthy if needed, else falsey - if (!error && (data.status === 'success')) { - return; - } - if (!error) { // Create a message from the data - error = data.status + ': ' + data.error; - } - if (typeof(error) === 'string') { // Make a proper Error object - error = new Error(error); - } - error.message += ' in ' + url; // Include the url. - cb(error); - return true; - } - - - function resolveUrl(url) { - return (url.indexOf('/') === 0) ? (addressBarDialog.metaverseServerUrl + url) : url; - } - - function makeModelData(data) { // create a new obj from data - // ListModel elements will only ever have those properties that are defined by the first obj that is added. - // So here we make sure that we have all the properties we need, regardless of whether it is a place data or user story. - var name = data.place_name, - tags = data.tags || [data.action, data.username], - description = data.description || "", - thumbnail_url = data.thumbnail_url || ""; - return { - place_name: name, - username: data.username || "", - path: data.path || "", - created_at: data.created_at || "", - action: data.action || "", - thumbnail_url: resolveUrl(thumbnail_url), - image_url: resolveUrl(data.details.image_url), - - metaverseId: (data.id || "").toString(), // Some are strings from server while others are numbers. Model objects require uniformity. - - tags: tags, - description: description, - online_users: data.details.concurrency || 0, - drillDownToPlace: false, - - searchText: [name].concat(tags, description || []).join(' ').toUpperCase() - } - } - function suggestable(place) { - if (place.action === 'snapshot') { - return true; - } - return (place.place_name !== AddressManager.placename); // Not our entry, but do show other entry points to current domain. - } - property var selectedTab: allTab; - function tabSelect(textButton) { - selectedTab = textButton; - fillDestinations(); - } - property var placeMap: ({}); - function addToSuggestions(place) { - var collapse = allTab.selected && (place.action !== 'concurrency'); - if (collapse) { - var existing = placeMap[place.place_name]; - if (existing) { - existing.drillDownToPlace = true; - return; - } - } - suggestions.append(place); - if (collapse) { - placeMap[place.place_name] = suggestions.get(suggestions.count - 1); - } else if (place.action === 'concurrency') { - suggestions.get(suggestions.count - 1).drillDownToPlace = true; // Don't change raw place object (in allStories). - } - } - property int requestId: 0; - function getUserStoryPage(pageNumber, cb) { // cb(error) after all pages of domain data have been added to model - var options = [ - 'now=' + new Date().toISOString(), - 'include_actions=' + selectedTab.includeActions, - 'restriction=' + (Account.isLoggedIn() ? 'open,hifi' : 'open'), - 'require_online=true', - 'protocol=' + encodeURIComponent(AddressManager.protocolVersion()), - 'page=' + pageNumber - ]; - var url = metaverseBase + 'user_stories?' + options.join('&'); - var thisRequestId = ++requestId; - getRequest(url, function (error, data) { - if ((thisRequestId !== requestId) || handleError(url, error, data, cb)) { - return; - } - var stories = data.user_stories.map(function (story) { // explicit single-argument function - return makeModelData(story, url); - }); - allStories = allStories.concat(stories); - stories.forEach(makeFilteredPlaceProcessor()); - if ((data.current_page < data.total_pages) && (data.current_page <= 10)) { // just 10 pages = 100 stories for now - return getUserStoryPage(pageNumber + 1, cb); - } - cb(); - }); - } - function makeFilteredPlaceProcessor() { // answer a function(placeData) that adds it to suggestions if it matches - var words = addressLine.text.toUpperCase().split(/\s+/).filter(identity), - data = allStories; - function matches(place) { - if (!words.length) { - return suggestable(place); - } - return words.every(function (word) { - return place.searchText.indexOf(word) >= 0; - }); - } - return function (place) { - if (matches(place)) { - addToSuggestions(place); - } - }; - } - function filterChoicesByText() { - suggestions.clear(); - placeMap = {}; - allStories.forEach(makeFilteredPlaceProcessor()); - } - - function fillDestinations() { - allStories = []; - suggestions.clear(); - placeMap = {}; - getUserStoryPage(1, function (error) { - console.log('user stories query', error || 'ok', allStories.length); - }); - } - function updateLocationText(enteringAddress) { if (enteringAddress) { notice.text = "Go To a place, @user, path, or network address:"; notice.color = hifiStyleConstants.colors.baseGrayHighlight; } else { - notice.text = AddressManager.isConnected ? "Your location:" : "Not Connected"; - notice.color = AddressManager.isConnected ? hifiStyleConstants.colors.baseGrayHighlight : hifiStyleConstants.colors.redHighlight; + notice.text = AddressManager.isConnected ? "YOUR LOCATION" : "NOT CONNECTED"; + notice.color = AddressManager.isConnected ? hifiStyleConstants.colors.blueHighlight : hifiStyleConstants.colors.redHighlight; // Display hostname, which includes ip address, localhost, and other non-placenames. location.text = (AddressManager.placename || AddressManager.hostname || '') + (AddressManager.pathname ? AddressManager.pathname.match(/\/[^\/]+/)[0] : ''); } diff --git a/interface/resources/qml/hifi/tablet/TabletAudioPreferences.qml b/interface/resources/qml/hifi/tablet/TabletAudioPreferences.qml index b21bc238ac..2046071e4c 100644 --- a/interface/resources/qml/hifi/tablet/TabletAudioPreferences.qml +++ b/interface/resources/qml/hifi/tablet/TabletAudioPreferences.qml @@ -23,7 +23,7 @@ StackView { signal sendToScript(var message); function pushSource(path) { - profileRoot.push(Qt.reslovedUrl(path)); + profileRoot.push(Qt.resolvedUrl(path)); } function popSource() { diff --git a/interface/resources/qml/hifi/tablet/TabletGeneralPreferences.qml b/interface/resources/qml/hifi/tablet/TabletGeneralPreferences.qml index d23daddd8d..85377aaeda 100644 --- a/interface/resources/qml/hifi/tablet/TabletGeneralPreferences.qml +++ b/interface/resources/qml/hifi/tablet/TabletGeneralPreferences.qml @@ -23,7 +23,7 @@ StackView { signal sendToScript(var message); function pushSource(path) { - profileRoot.push(Qt.reslovedUrl(path)); + profileRoot.push(Qt.resolvedUrl(path)); } function popSource() { diff --git a/interface/resources/qml/hifi/tablet/TabletGraphicsPreferences.qml b/interface/resources/qml/hifi/tablet/TabletGraphicsPreferences.qml index 67c466f991..95ee2c3a72 100644 --- a/interface/resources/qml/hifi/tablet/TabletGraphicsPreferences.qml +++ b/interface/resources/qml/hifi/tablet/TabletGraphicsPreferences.qml @@ -23,7 +23,7 @@ StackView { signal sendToScript(var message); function pushSource(path) { - profileRoot.push(Qt.reslovedUrl(path)); + profileRoot.push(Qt.resolvedUrl(path)); } function popSource() { diff --git a/interface/resources/qml/hifi/tablet/TabletLodPreferences.qml b/interface/resources/qml/hifi/tablet/TabletLodPreferences.qml index f61f6f8c4e..6f38fee8b9 100644 --- a/interface/resources/qml/hifi/tablet/TabletLodPreferences.qml +++ b/interface/resources/qml/hifi/tablet/TabletLodPreferences.qml @@ -23,7 +23,7 @@ StackView { signal sendToScript(var message); function pushSource(path) { - profileRoot.push(Qt.reslovedUrl(path)); + profileRoot.push(Qt.resolvedUrl(path)); } function popSource() { diff --git a/interface/resources/qml/hifi/tablet/TabletNetworkingPreferences.qml b/interface/resources/qml/hifi/tablet/TabletNetworkingPreferences.qml index db47c78c48..7184d91044 100644 --- a/interface/resources/qml/hifi/tablet/TabletNetworkingPreferences.qml +++ b/interface/resources/qml/hifi/tablet/TabletNetworkingPreferences.qml @@ -23,7 +23,7 @@ StackView { signal sendToScript(var message); function pushSource(path) { - profileRoot.push(Qt.reslovedUrl(path)); + profileRoot.push(Qt.resolvedUrl(path)); } function popSource() { diff --git a/interface/resources/qml/hifi/tablet/TabletRoot.qml b/interface/resources/qml/hifi/tablet/TabletRoot.qml index 446d4c91ff..33af7da1ae 100644 --- a/interface/resources/qml/hifi/tablet/TabletRoot.qml +++ b/interface/resources/qml/hifi/tablet/TabletRoot.qml @@ -1,7 +1,9 @@ import QtQuick 2.0 import Hifi 1.0 import QtQuick.Controls 1.4 +import HFTabletWebEngineProfile 1.0 import "../../dialogs" +import "../../controls" Item { id: tabletRoot @@ -11,6 +13,7 @@ Item { property var rootMenu; property var openModal: null; property var openMessage: null; + property var openBrowser: null; property string subMenu: "" signal showDesktop(); property bool shown: true @@ -44,6 +47,12 @@ Item { return openModal; } + Component { id: assetDialogBuilder; TabletAssetDialog { } } + function assetDialog(properties) { + openModal = assetDialogBuilder.createObject(tabletRoot, properties); + return openModal; + } + function setMenuProperties(rootMenu, subMenu) { tabletRoot.rootMenu = rootMenu; tabletRoot.subMenu = subMenu; @@ -81,13 +90,18 @@ Item { loader.item.gotoPreviousApp = true; } } + + function loadWebBase() { + loader.source = ""; + loader.source = "TabletWebView.qml"; + } function returnToPreviousApp() { tabletApps.remove(currentApp); var isWebPage = tabletApps.get(currentApp).isWebUrl; if (isWebPage) { - var webUrl = tabletApps.get(currentApp).appWebUrl; - var scriptUrl = tabletApps.get(currentApp).scriptUrl; + var webUrl = tabletApps.get(currentApp).appWebUrl; + var scriptUrl = tabletApps.get(currentApp).scriptUrl; loadSource("TabletWebView.qml"); loadWebUrl(webUrl, scriptUrl); } else { @@ -95,6 +109,16 @@ Item { } } + function openBrowserWindow(request, profile) { + var component = Qt.createComponent("../../controls/TabletWebView.qml"); + var newWindow = component.createObject(tabletRoot); + newWindow.eventBridge = tabletRoot.eventBridge; + newWindow.remove = true; + newWindow.profile = profile; + request.openIn(newWindow.webView); + tabletRoot.openBrowser = newWindow; + } + function loadWebUrl(url, injectedJavaScriptUrl) { tabletApps.clear(); loader.item.url = url; @@ -174,6 +198,11 @@ Item { openModal.destroy(); openModal = null; } + + if (openBrowser) { + openBrowser.destroy(); + openBrowser = null; + } } } diff --git a/interface/resources/qml/hifi/tablet/TabletWebView.qml b/interface/resources/qml/hifi/tablet/TabletWebView.qml index 0f697d634e..ff6be0480f 100644 --- a/interface/resources/qml/hifi/tablet/TabletWebView.qml +++ b/interface/resources/qml/hifi/tablet/TabletWebView.qml @@ -3,7 +3,7 @@ import QtWebEngine 1.2 import "../../controls" as Controls -Controls.WebView { +Controls.TabletWebScreen { } diff --git a/interface/resources/qml/hifi/tablet/WindowRoot.qml b/interface/resources/qml/hifi/tablet/WindowRoot.qml index 5f842df7b7..470fd4a830 100644 --- a/interface/resources/qml/hifi/tablet/WindowRoot.qml +++ b/interface/resources/qml/hifi/tablet/WindowRoot.qml @@ -38,6 +38,11 @@ Windows.ScrollingWindow { loader.source = url; } + function loadWebBase() { + loader.source = ""; + loader.source = "WindowWebView.qml"; + } + function loadWebUrl(url, injectedJavaScriptUrl) { loader.item.url = url; loader.item.scriptURL = injectedJavaScriptUrl; diff --git a/interface/resources/qml/hifi/tablet/WindowWebView.qml b/interface/resources/qml/hifi/tablet/WindowWebView.qml new file mode 100644 index 0000000000..0f697d634e --- /dev/null +++ b/interface/resources/qml/hifi/tablet/WindowWebView.qml @@ -0,0 +1,10 @@ +import QtQuick 2.0 +import QtWebEngine 1.2 + +import "../../controls" as Controls + +Controls.WebView { + +} + + diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index fbf28a8d99..a8dbb2f4fc 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -78,6 +78,7 @@ #include #include #include +#include #include #include #include @@ -127,6 +128,7 @@ #include #include #include +#include #include "AudioClient.h" @@ -135,12 +137,10 @@ #include "avatar/ScriptAvatar.h" #include "CrashHandler.h" #include "devices/DdeFaceTracker.h" -#include "devices/EyeTracker.h" -#include "devices/Faceshift.h" #include "devices/Leapmotion.h" #include "DiscoverabilityManager.h" #include "GLCanvas.h" -#include "InterfaceActionFactory.h" +#include "InterfaceDynamicFactory.h" #include "InterfaceLogging.h" #include "LODManager.h" #include "ModelPackager.h" @@ -462,7 +462,7 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::registerInheritance(); DependencyManager::registerInheritance(); - DependencyManager::registerInheritance(); + DependencyManager::registerInheritance(); DependencyManager::registerInheritance(); // Set dependencies @@ -479,7 +479,6 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); - DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -516,7 +515,7 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); - DependencyManager::set(); + DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); controller::StateController::setStateVariables({ { STATE_IN_HMD, STATE_CAMERA_FULL_SCREEN_MIRROR, @@ -534,6 +533,7 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(nullptr, qApp->getOcteeSceneStats()); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); return previousSessionCrashed; } @@ -796,7 +796,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(&domainHandler, SIGNAL(resetting()), SLOT(resettingDomain())); connect(&domainHandler, SIGNAL(connectedToDomain(const QString&)), SLOT(updateWindowTitle())); connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle())); - connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(clearDomainOctreeDetails())); + connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, &Application::clearDomainAvatars); connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, [this]() { getOverlays().deleteOverlay(getTabletScreenID()); getOverlays().deleteOverlay(getTabletHomeButtonID()); @@ -1208,10 +1208,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo this->installEventFilter(this); - // initialize our face trackers after loading the menu settings - auto faceshiftTracker = DependencyManager::get(); - faceshiftTracker->init(); - connect(faceshiftTracker.data(), &FaceTracker::muteToggled, this, &Application::faceTrackerMuteToggled); #ifdef HAVE_DDE auto ddeTracker = DependencyManager::get(); ddeTracker->init(); @@ -1445,8 +1441,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo QString skyboxUrl { PathUtils::resourcesPath() + "images/Default-Sky-9-cubemap.jpg" }; QString skyboxAmbientUrl { PathUtils::resourcesPath() + "images/Default-Sky-9-ambient.jpg" }; - _defaultSkyboxTexture = textureCache->getImageTexture(skyboxUrl, NetworkTexture::CUBE_TEXTURE, { { "generateIrradiance", false } }); - _defaultSkyboxAmbientTexture = textureCache->getImageTexture(skyboxAmbientUrl, NetworkTexture::CUBE_TEXTURE, { { "generateIrradiance", true } }); + _defaultSkyboxTexture = textureCache->getImageTexture(skyboxUrl, image::TextureUsage::CUBE_TEXTURE, { { "generateIrradiance", false } }); + _defaultSkyboxAmbientTexture = textureCache->getImageTexture(skyboxAmbientUrl, image::TextureUsage::CUBE_TEXTURE, { { "generateIrradiance", true } }); _defaultSkybox->setCubemap(_defaultSkyboxTexture); @@ -1465,46 +1461,53 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo const auto testScript = property(hifi::properties::TEST).toUrl(); scriptEngines->loadScript(testScript, false); } else { - // Get sandbox content set version, if available + enum HandControllerType { + Vive, + Oculus + }; + static const std::map MIN_CONTENT_VERSION = { + { Vive, 1 }, + { Oculus, 27 } + }; + + // Get sandbox content set version auto acDirPath = PathUtils::getAppDataPath() + "../../" + BuildInfo::MODIFIED_ORGANIZATION + "/assignment-client/"; auto contentVersionPath = acDirPath + "content-version.txt"; qCDebug(interfaceapp) << "Checking " << contentVersionPath << " for content version"; - auto contentVersion = 0; + int contentVersion = 0; QFile contentVersionFile(contentVersionPath); if (contentVersionFile.open(QIODevice::ReadOnly | QIODevice::Text)) { QString line = contentVersionFile.readAll(); - // toInt() returns 0 if the conversion fails, so we don't need to specifically check for failure - contentVersion = line.toInt(); + contentVersion = line.toInt(); // returns 0 if conversion fails } - qCDebug(interfaceapp) << "Server content version: " << contentVersion; - static const int MIN_VIVE_CONTENT_VERSION = 1; - static const int MIN_OCULUS_TOUCH_CONTENT_VERSION = 27; - - bool hasSufficientTutorialContent = false; + // Get controller availability bool hasHandControllers = false; - - // Only specific hand controllers are currently supported, so only send users to the tutorial - // if they have one of those hand controllers. + HandControllerType handControllerType = Vive; if (PluginUtils::isViveControllerAvailable()) { hasHandControllers = true; - hasSufficientTutorialContent = contentVersion >= MIN_VIVE_CONTENT_VERSION; + handControllerType = Vive; } else if (PluginUtils::isOculusTouchControllerAvailable()) { hasHandControllers = true; - hasSufficientTutorialContent = contentVersion >= MIN_OCULUS_TOUCH_CONTENT_VERSION; + handControllerType = Oculus; } + // Check tutorial content versioning + bool hasTutorialContent = contentVersion >= MIN_CONTENT_VERSION.at(handControllerType); + + // Check HMD use (may be technically available without being in use) + bool hasHMD = PluginUtils::isHMDAvailable(); + bool isUsingHMD = hasHMD && hasHandControllers && _displayPlugin->isHmd(); + + Setting::Handle tutorialComplete { "tutorialComplete", false }; Setting::Handle firstRun { Settings::firstRun, true }; - bool hasHMDAndHandControllers = PluginUtils::isHMDAvailable() && hasHandControllers; - Setting::Handle tutorialComplete { "tutorialComplete", false }; + bool isTutorialComplete = tutorialComplete.get(); + bool shouldGoToTutorial = isUsingHMD && hasTutorialContent && !isTutorialComplete; - bool shouldGoToTutorial = hasHMDAndHandControllers && hasSufficientTutorialContent && !tutorialComplete.get(); - - qCDebug(interfaceapp) << "Has HMD + Hand Controllers: " << hasHMDAndHandControllers << ", current plugin: " << _displayPlugin->getName(); - qCDebug(interfaceapp) << "Has sufficient tutorial content (" << contentVersion << ") : " << hasSufficientTutorialContent; - qCDebug(interfaceapp) << "Tutorial complete: " << tutorialComplete.get(); - qCDebug(interfaceapp) << "Should go to tutorial: " << shouldGoToTutorial; + qCDebug(interfaceapp) << "HMD:" << hasHMD << ", Hand Controllers: " << hasHandControllers << ", Using HMD: " << isUsingHMD; + qCDebug(interfaceapp) << "Tutorial version:" << contentVersion << ", sufficient:" << hasTutorialContent << + ", complete:" << isTutorialComplete << ", should go:" << shouldGoToTutorial; // when --url in command line, teleport to location const QString HIFI_URL_COMMAND_LINE_KEY = "--url"; @@ -1541,7 +1544,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // If this is a first run we short-circuit the address passed in if (isFirstRun) { - if (hasHMDAndHandControllers) { + if (isUsingHMD) { if (sandboxIsRunning) { qCDebug(interfaceapp) << "Home sandbox appears to be running, going to Home."; DependencyManager::get()->goToLocalSandbox(); @@ -2044,6 +2047,7 @@ void Application::initializeUi() { rootContext->setContextProperty("Scene", DependencyManager::get().data()); rootContext->setContextProperty("Render", _renderEngine->getConfiguration().get()); rootContext->setContextProperty("Reticle", getApplicationCompositor().getReticleInterface()); + rootContext->setContextProperty("Snapshot", DependencyManager::get().data()); rootContext->setContextProperty("ApplicationCompositor", &getApplicationCompositor()); @@ -2746,6 +2750,9 @@ void Application::keyPressEvent(QKeyEvent* event) { if (isMeta) { auto offscreenUi = DependencyManager::get(); offscreenUi->load("Browser.qml"); + } else if (isOption) { + controller::InputRecorder* inputRecorder = controller::InputRecorder::getInstance(); + inputRecorder->stopPlayback(); } break; @@ -3613,20 +3620,13 @@ ivec2 Application::getMouse() const { } FaceTracker* Application::getActiveFaceTracker() { - auto faceshift = DependencyManager::get(); auto dde = DependencyManager::get(); - return (dde->isActive() ? static_cast(dde.data()) : - (faceshift->isActive() ? static_cast(faceshift.data()) : nullptr)); + return dde->isActive() ? static_cast(dde.data()) : nullptr; } FaceTracker* Application::getSelectedFaceTracker() { FaceTracker* faceTracker = nullptr; -#ifdef HAVE_FACESHIFT - if (Menu::getInstance()->isOptionChecked(MenuOption::Faceshift)) { - faceTracker = DependencyManager::get().data(); - } -#endif #ifdef HAVE_DDE if (Menu::getInstance()->isOptionChecked(MenuOption::UseCamera)) { faceTracker = DependencyManager::get().data(); @@ -3636,15 +3636,8 @@ FaceTracker* Application::getSelectedFaceTracker() { } void Application::setActiveFaceTracker() const { -#if defined(HAVE_FACESHIFT) || defined(HAVE_DDE) - bool isMuted = Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking); -#endif -#ifdef HAVE_FACESHIFT - auto faceshiftTracker = DependencyManager::get(); - faceshiftTracker->setIsMuted(isMuted); - faceshiftTracker->setEnabled(Menu::getInstance()->isOptionChecked(MenuOption::Faceshift) && !isMuted); -#endif #ifdef HAVE_DDE + bool isMuted = Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking); bool isUsingDDE = Menu::getInstance()->isOptionChecked(MenuOption::UseCamera); Menu::getInstance()->getActionForOption(MenuOption::BinaryEyelidControl)->setVisible(isUsingDDE); Menu::getInstance()->getActionForOption(MenuOption::CoupleEyelids)->setVisible(isUsingDDE); @@ -4362,7 +4355,13 @@ void Application::update(float deltaTime) { controller::InputCalibrationData calibrationData = { myAvatar->getSensorToWorldMatrix(), createMatFromQuatAndPos(myAvatar->getOrientation(), myAvatar->getPosition()), - myAvatar->getHMDSensorMatrix() + myAvatar->getHMDSensorMatrix(), + myAvatar->getCenterEyeCalibrationMat(), + myAvatar->getHeadCalibrationMat(), + myAvatar->getSpine2CalibrationMat(), + myAvatar->getHipsCalibrationMat(), + myAvatar->getLeftFootCalibrationMat(), + myAvatar->getRightFootCalibrationMat() }; InputPluginPointer keyboardMousePlugin; @@ -4410,6 +4409,13 @@ void Application::update(float deltaTime) { controller::Pose rightFootPose = userInputMapper->getPoseState(controller::Action::RIGHT_FOOT); myAvatar->setFootControllerPosesInSensorFrame(leftFootPose.transform(avatarToSensorMatrix), rightFootPose.transform(avatarToSensorMatrix)); + controller::Pose hipsPose = userInputMapper->getPoseState(controller::Action::HIPS); + controller::Pose spine2Pose = userInputMapper->getPoseState(controller::Action::SPINE2); + myAvatar->setSpineControllerPosesInSensorFrame(hipsPose.transform(avatarToSensorMatrix), spine2Pose.transform(avatarToSensorMatrix)); + + controller::Pose headPose = userInputMapper->getPoseState(controller::Action::HEAD); + myAvatar->setHeadControllerPoseInSensorFrame(headPose.transform(avatarToSensorMatrix)); + updateThreads(deltaTime); // If running non-threaded, then give the threads some time to process... updateDialogs(deltaTime); // update various stats dialogs if present @@ -4440,7 +4446,7 @@ void Application::update(float deltaTime) { _entitySimulation->setObjectsToChange(stillNeedChange); }); - _entitySimulation->applyActionChanges(); + _entitySimulation->applyDynamicChanges(); avatarManager->getObjectsToRemoveFromPhysics(motionStates); _physicsEngine->removeObjects(motionStates); @@ -4450,8 +4456,8 @@ void Application::update(float deltaTime) { _physicsEngine->changeObjects(motionStates); myAvatar->prepareForPhysicsSimulation(); - _physicsEngine->forEachAction([&](EntityActionPointer action) { - action->prepareForPhysicsSimulation(); + _physicsEngine->forEachDynamic([&](EntityDynamicPointer dynamic) { + dynamic->prepareForPhysicsSimulation(); }); } { @@ -5120,7 +5126,6 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se } void Application::resetSensors(bool andReload) { - DependencyManager::get()->reset(); DependencyManager::get()->reset(); DependencyManager::get()->reset(); getActiveDisplayPlugin()->resetSensors(); @@ -5166,7 +5171,6 @@ void Application::clearDomainOctreeDetails() { qCDebug(interfaceapp) << "Clearing domain octree details..."; resetPhysicsReadyInformation(); - getMyAvatar()->setAvatarEntityDataChanged(true); // to recreate worn entities // reset our node to stats and node to jurisdiction maps... since these must be changing... _entityServerJurisdictions.withWriteLock([&] { @@ -5185,14 +5189,18 @@ void Application::clearDomainOctreeDetails() { skyStage->setBackgroundMode(model::SunSkyStage::SKY_DEFAULT); _recentlyClearedDomain = true; - - DependencyManager::get()->clearOtherAvatars(); + DependencyManager::get()->clearUnusedResources(); DependencyManager::get()->clearUnusedResources(); DependencyManager::get()->clearUnusedResources(); DependencyManager::get()->clearUnusedResources(); } +void Application::clearDomainAvatars() { + getMyAvatar()->setAvatarEntityDataChanged(true); // to recreate worn entities + DependencyManager::get()->clearOtherAvatars(); +} + void Application::domainChanged(const QString& domainHostname) { updateWindowTitle(); // disable physics until we have enough information about our new location to not cause craziness. @@ -5205,11 +5213,7 @@ void Application::resettingDomain() { } void Application::nodeAdded(SharedNodePointer node) const { - if (node->getType() == NodeType::AvatarMixer) { - // new avatar mixer, send off our identity packet right away - getMyAvatar()->sendIdentityPacket(); - getMyAvatar()->resetLastSent(); - } + // nothing to do here } void Application::nodeActivated(SharedNodePointer node) { @@ -5245,6 +5249,13 @@ void Application::nodeActivated(SharedNodePointer node) { if (node->getType() == NodeType::AudioMixer) { DependencyManager::get()->negotiateAudioFormat(); } + + if (node->getType() == NodeType::AvatarMixer) { + // new avatar mixer, send off our identity packet right away + getMyAvatar()->markIdentityDataChanged(); + getMyAvatar()->sendIdentityPacket(); + getMyAvatar()->resetLastSent(); + } } void Application::nodeKilled(SharedNodePointer node) { @@ -5259,33 +5270,8 @@ void Application::nodeKilled(SharedNodePointer node) { if (node->getType() == NodeType::AudioMixer) { QMetaObject::invokeMethod(DependencyManager::get().data(), "audioMixerKilled"); } else if (node->getType() == NodeType::EntityServer) { - QUuid nodeUUID = node->getUUID(); - // see if this is the first we've heard of this node... - _entityServerJurisdictions.withReadLock([&] { - if (_entityServerJurisdictions.find(nodeUUID) == _entityServerJurisdictions.end()) { - return; - } - - auto rootCode = _entityServerJurisdictions[nodeUUID].getRootOctalCode(); - VoxelPositionSize rootDetails; - voxelDetailsForCode(rootCode.get(), rootDetails); - - qCDebug(interfaceapp, "model server going away...... v[%f, %f, %f, %f]", - (double)rootDetails.x, (double)rootDetails.y, (double)rootDetails.z, (double)rootDetails.s); - - }); - - // If the model server is going away, remove it from our jurisdiction map so we don't send voxels to a dead server - _entityServerJurisdictions.withWriteLock([&] { - _entityServerJurisdictions.erase(_entityServerJurisdictions.find(nodeUUID)); - }); - - // also clean up scene stats for that server - _octreeServerSceneStats.withWriteLock([&] { - if (_octreeServerSceneStats.find(nodeUUID) != _octreeServerSceneStats.end()) { - _octreeServerSceneStats.erase(nodeUUID); - } - }); + // we lost an entity server, clear all of the domain octree details + clearDomainOctreeDetails(); } else if (node->getType() == NodeType::AvatarMixer) { // our avatar mixer has gone away - clear the hash of avatars DependencyManager::get()->clearOtherAvatars(); @@ -5490,6 +5476,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("Menu", MenuScriptingInterface::getInstance()); scriptEngine->registerGlobalObject("Stats", Stats::getInstance()); scriptEngine->registerGlobalObject("Settings", SettingsScriptingInterface::getInstance()); + scriptEngine->registerGlobalObject("Snapshot", DependencyManager::get().data()); scriptEngine->registerGlobalObject("AudioDevice", AudioDeviceScriptingInterface::getInstance()); scriptEngine->registerGlobalObject("AudioStats", DependencyManager::get()->getStats().data()); scriptEngine->registerGlobalObject("AudioScope", DependencyManager::get().data()); @@ -6434,7 +6421,7 @@ void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRa // Get a screenshot and save it QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); // If we're not doing an animated snapshot as well... - if (!includeAnimated || !(SnapshotAnimated::alsoTakeAnimatedSnapshot.get())) { + if (!includeAnimated) { // Tell the dependency manager that the capture of the still snapshot has taken place. emit DependencyManager::get()->stillSnapshotTaken(path, notify); } else { @@ -6759,11 +6746,6 @@ void Application::updateDisplayMode() { return; } - UserActivityLogger::getInstance().logAction("changed_display_mode", { - { "previous_display_mode", _displayPlugin ? _displayPlugin->getName() : "" }, - { "display_mode", newDisplayPlugin ? newDisplayPlugin->getName() : "" } - }); - auto offscreenUi = DependencyManager::get(); // Make the switch atomic from the perspective of other threads @@ -6818,13 +6800,16 @@ void Application::updateDisplayMode() { offscreenUi->getDesktop()->setProperty("repositionLocked", wasRepositionLocked); } + bool isHmd = _displayPlugin->isHmd(); + qCDebug(interfaceapp) << "Entering into" << (isHmd ? "HMD" : "Desktop") << "Mode"; + + // Only log/emit after a successful change + UserActivityLogger::getInstance().logAction("changed_display_mode", { + { "previous_display_mode", _displayPlugin ? _displayPlugin->getName() : "" }, + { "display_mode", newDisplayPlugin ? newDisplayPlugin->getName() : "" }, + { "hmd", isHmd } + }); emit activeDisplayPluginChanged(); - - if (_displayPlugin->isHmd()) { - qCDebug(interfaceapp) << "Entering into HMD Mode"; - } else { - qCDebug(interfaceapp) << "Entering into Desktop Mode"; - } // reset the avatar, to set head and hand palms back to a reasonable default pose. getMyAvatar()->reset(false); diff --git a/interface/src/Application.h b/interface/src/Application.h index dff1de2860..041f1f8930 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -409,6 +409,7 @@ public slots: private slots: void showDesktop(); void clearDomainOctreeDetails(); + void clearDomainAvatars(); void aboutToQuit(); void resettingDomain(); diff --git a/interface/src/DiscoverabilityManager.cpp b/interface/src/DiscoverabilityManager.cpp index 98bfa9c0c7..36f6d8633e 100644 --- a/interface/src/DiscoverabilityManager.cpp +++ b/interface/src/DiscoverabilityManager.cpp @@ -23,6 +23,8 @@ #include "DiscoverabilityManager.h" #include "Menu.h" +#include + const Discoverability::Mode DEFAULT_DISCOVERABILITY_MODE = Discoverability::Friends; DiscoverabilityManager::DiscoverabilityManager() : @@ -37,6 +39,13 @@ const QString API_USER_HEARTBEAT_PATH = "/api/v1/user/heartbeat"; const QString SESSION_ID_KEY = "session_id"; void DiscoverabilityManager::updateLocation() { + // since we store the last location and compare it to + // the current one in this function, we need to do this in + // the object's main thread (or use a mutex) + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "updateLocation"); + return; + } auto accountManager = DependencyManager::get(); auto addressManager = DependencyManager::get(); auto& domainHandler = DependencyManager::get()->getDomainHandler(); @@ -143,7 +152,7 @@ void DiscoverabilityManager::removeLocation() { void DiscoverabilityManager::setDiscoverabilityMode(Discoverability::Mode discoverabilityMode) { if (static_cast(_mode.get()) != discoverabilityMode) { - + // update the setting to the new value _mode.set(static_cast(discoverabilityMode)); updateLocation(); // update right away diff --git a/interface/src/InterfaceActionFactory.cpp b/interface/src/InterfaceActionFactory.cpp deleted file mode 100644 index 2bc4608e86..0000000000 --- a/interface/src/InterfaceActionFactory.cpp +++ /dev/null @@ -1,82 +0,0 @@ -// -// InterfaceActionFactory.cpp -// libraries/entities/src -// -// Created by Seth Alves on 2015-6-2 -// 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 -// - - - -#include -#include -#include -#include -#include - -#include "InterfaceActionFactory.h" - - -EntityActionPointer interfaceActionFactory(EntityActionType type, const QUuid& id, EntityItemPointer ownerEntity) { - switch (type) { - case ACTION_TYPE_NONE: - return EntityActionPointer(); - case ACTION_TYPE_OFFSET: - return std::make_shared(id, ownerEntity); - case ACTION_TYPE_SPRING: - return std::make_shared(id, ownerEntity); - case ACTION_TYPE_HOLD: - return std::make_shared(id, ownerEntity); - case ACTION_TYPE_TRAVEL_ORIENTED: - return std::make_shared(id, ownerEntity); - } - - Q_ASSERT_X(false, Q_FUNC_INFO, "Unknown entity action type"); - return EntityActionPointer(); -} - - -EntityActionPointer InterfaceActionFactory::factory(EntityActionType type, - const QUuid& id, - EntityItemPointer ownerEntity, - QVariantMap arguments) { - EntityActionPointer action = interfaceActionFactory(type, id, ownerEntity); - if (action) { - bool ok = action->updateArguments(arguments); - if (ok) { - if (action->lifetimeIsOver()) { - return nullptr; - } - return action; - } - } - return nullptr; -} - - -EntityActionPointer InterfaceActionFactory::factoryBA(EntityItemPointer ownerEntity, QByteArray data) { - QDataStream serializedArgumentStream(data); - EntityActionType type; - QUuid id; - - serializedArgumentStream >> type; - serializedArgumentStream >> id; - - EntityActionPointer action = interfaceActionFactory(type, id, ownerEntity); - - if (action) { - action->deserialize(data); - if (action->lifetimeIsOver()) { - static QString repeatedMessage = - LogHandler::getInstance().addRepeatedMessageRegex(".*factoryBA lifetimeIsOver during action creation.*"); - qDebug() << "InterfaceActionFactory::factoryBA lifetimeIsOver during action creation --" - << action->getExpires() << "<" << usecTimestampNow(); - return nullptr; - } - } - - return action; -} diff --git a/interface/src/InterfaceDynamicFactory.cpp b/interface/src/InterfaceDynamicFactory.cpp new file mode 100644 index 0000000000..5951ccef9e --- /dev/null +++ b/interface/src/InterfaceDynamicFactory.cpp @@ -0,0 +1,88 @@ +// +// InterfaceDynamicFactory.cpp +// libraries/entities/src +// +// Created by Seth Alves on 2015-6-2 +// 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 +// + + + +#include +#include +#include +#include +#include +#include +#include + +#include "InterfaceDynamicFactory.h" + + +EntityDynamicPointer interfaceDynamicFactory(EntityDynamicType type, const QUuid& id, EntityItemPointer ownerEntity) { + switch (type) { + case DYNAMIC_TYPE_NONE: + return EntityDynamicPointer(); + case DYNAMIC_TYPE_OFFSET: + return std::make_shared(id, ownerEntity); + case DYNAMIC_TYPE_SPRING: + return std::make_shared(id, ownerEntity); + case DYNAMIC_TYPE_HOLD: + return std::make_shared(id, ownerEntity); + case DYNAMIC_TYPE_TRAVEL_ORIENTED: + return std::make_shared(id, ownerEntity); + case DYNAMIC_TYPE_HINGE: + return std::make_shared(id, ownerEntity); + case DYNAMIC_TYPE_FAR_GRAB: + return std::make_shared(id, ownerEntity); + } + + Q_ASSERT_X(false, Q_FUNC_INFO, "Unknown entity dynamic type"); + return EntityDynamicPointer(); +} + + +EntityDynamicPointer InterfaceDynamicFactory::factory(EntityDynamicType type, + const QUuid& id, + EntityItemPointer ownerEntity, + QVariantMap arguments) { + EntityDynamicPointer dynamic = interfaceDynamicFactory(type, id, ownerEntity); + if (dynamic) { + bool ok = dynamic->updateArguments(arguments); + if (ok) { + if (dynamic->lifetimeIsOver()) { + return nullptr; + } + return dynamic; + } + } + return nullptr; +} + + +EntityDynamicPointer InterfaceDynamicFactory::factoryBA(EntityItemPointer ownerEntity, QByteArray data) { + QDataStream serializedArgumentStream(data); + EntityDynamicType type; + QUuid id; + + serializedArgumentStream >> type; + serializedArgumentStream >> id; + + EntityDynamicPointer dynamic = interfaceDynamicFactory(type, id, ownerEntity); + + if (dynamic) { + dynamic->deserialize(data); + if (dynamic->lifetimeIsOver()) { + static QString repeatedMessage = + LogHandler::getInstance().addRepeatedMessageRegex(".*factoryBA lifetimeIsOver during dynamic creation.*"); + qDebug() << "InterfaceDynamicFactory::factoryBA lifetimeIsOver during dynamic creation --" + << dynamic->getExpires() << "<" << usecTimestampNow(); + return nullptr; + } + } + + return dynamic; +} diff --git a/interface/src/InterfaceActionFactory.h b/interface/src/InterfaceDynamicFactory.h similarity index 51% rename from interface/src/InterfaceActionFactory.h rename to interface/src/InterfaceDynamicFactory.h index 3e8a17d871..b0696442cb 100644 --- a/interface/src/InterfaceActionFactory.h +++ b/interface/src/InterfaceDynamicFactory.h @@ -1,5 +1,5 @@ // -// InterfaceActionFactory.cpp +// InterfaceDynamicFactory.cpp // interface/src/ // // Created by Seth Alves on 2015-6-10 @@ -9,21 +9,21 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#ifndef hifi_InterfaceActionFactory_h -#define hifi_InterfaceActionFactory_h +#ifndef hifi_InterfaceDynamicFactory_h +#define hifi_InterfaceDynamicFactory_h -#include "EntityActionFactoryInterface.h" +#include "EntityDynamicFactoryInterface.h" -class InterfaceActionFactory : public EntityActionFactoryInterface { +class InterfaceDynamicFactory : public EntityDynamicFactoryInterface { public: - InterfaceActionFactory() : EntityActionFactoryInterface() { } - virtual ~InterfaceActionFactory() { } - virtual EntityActionPointer factory(EntityActionType type, + InterfaceDynamicFactory() : EntityDynamicFactoryInterface() { } + virtual ~InterfaceDynamicFactory() { } + virtual EntityDynamicPointer factory(EntityDynamicType type, const QUuid& id, EntityItemPointer ownerEntity, QVariantMap arguments) override; - virtual EntityActionPointer factoryBA(EntityItemPointer ownerEntity, + virtual EntityDynamicPointer factoryBA(EntityItemPointer ownerEntity, QByteArray data) override; }; -#endif // hifi_InterfaceActionFactory_h +#endif // hifi_InterfaceDynamicFactory_h diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index c99178d8cc..fcd539ca7d 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -34,7 +34,6 @@ #include "avatar/AvatarManager.h" #include "AvatarBookmarks.h" #include "devices/DdeFaceTracker.h" -#include "devices/Faceshift.h" #include "MainWindow.h" #include "render/DrawStatus.h" #include "scripting/MenuScriptingInterface.h" @@ -156,6 +155,8 @@ Menu::Menu() { // Audio > Show Level Meter addCheckableActionToQMenuAndActionHash(audioMenu, MenuOption::AudioTools, 0, false); + addCheckableActionToQMenuAndActionHash(audioMenu, MenuOption::AudioNoiseReduction, 0, true, + audioIO.data(), SLOT(toggleAudioNoiseReduction())); // Avatar menu ---------------------------------- MenuWrapper* avatarMenu = addMenu("Avatar"); @@ -196,6 +197,9 @@ Menu::Menu() { 0, // QML Qt::Key_Apostrophe, qApp, SLOT(resetSensors())); + addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::EnableCharacterController, 0, true, + avatar.get(), SLOT(updateMotionBehaviorFromMenu())); + // Avatar > AvatarBookmarks related menus -- Note: the AvatarBookmarks class adds its own submenus here. auto avatarBookmarks = DependencyManager::get(); avatarBookmarks->setupMenus(this, avatarMenu); @@ -446,12 +450,6 @@ Menu::Menu() { qApp, SLOT(setActiveFaceTracker())); faceTrackerGroup->addAction(noFaceTracker); -#ifdef HAVE_FACESHIFT - QAction* faceshiftFaceTracker = addCheckableActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::Faceshift, - 0, false, - qApp, SLOT(setActiveFaceTracker())); - faceTrackerGroup->addAction(faceshiftFaceTracker); -#endif #ifdef HAVE_DDE QAction* ddeFaceTracker = addCheckableActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::UseCamera, 0, true, @@ -472,11 +470,10 @@ Menu::Menu() { QAction* ddeCalibrate = addActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::CalibrateCamera, 0, DependencyManager::get().data(), SLOT(calibrate())); ddeCalibrate->setVisible(true); // DDE face tracking is on by default -#endif -#if defined(HAVE_FACESHIFT) || defined(HAVE_DDE) faceTrackingMenu->addSeparator(); addCheckableActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::MuteFaceTracking, - Qt::CTRL | Qt::SHIFT | Qt::Key_F, true); // DDE face tracking is on by default + [](bool mute) { FaceTracker::setIsMuted(mute); }, + Qt::CTRL | Qt::SHIFT | Qt::Key_F, FaceTracker::isMuted()); addCheckableActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::AutoMuteAudio, 0, false); #endif @@ -532,10 +529,6 @@ Menu::Menu() { avatar.get(), SLOT(updateMotionBehaviorFromMenu()), UNSPECIFIED_POSITION, "Developer"); - addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::EnableCharacterController, 0, true, - avatar.get(), SLOT(updateMotionBehaviorFromMenu()), - UNSPECIFIED_POSITION, "Developer"); - // Developer > Hands >>> MenuWrapper* handOptionsMenu = developerMenu->addMenu("Hands"); addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::DisplayHandTargets, 0, false, @@ -622,8 +615,6 @@ Menu::Menu() { QString("../../hifi/tablet/TabletAudioPreferences.qml"), "AudioPreferencesDialog"); }); - addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::AudioNoiseReduction, 0, true, - audioIO.data(), SLOT(toggleAudioNoiseReduction())); addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::EchoServerAudio, 0, false, audioIO.data(), SLOT(toggleServerEcho())); addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::EchoLocalAudio, 0, false, diff --git a/interface/src/Menu.h b/interface/src/Menu.h index b6f70f5339..479a78e7c2 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -36,7 +36,7 @@ namespace MenuOption { const QString AssetMigration = "ATP Asset Migration"; const QString AssetServer = "Asset Browser"; const QString Attachments = "Attachments..."; - const QString AudioNoiseReduction = "Audio Noise Reduction"; + const QString AudioNoiseReduction = "Noise Reduction"; const QString AudioScope = "Show Scope"; const QString AudioScopeFiftyFrames = "Fifty"; const QString AudioScopeFiveFrames = "Five"; @@ -96,7 +96,7 @@ namespace MenuOption { const QString DontRenderEntitiesAsScene = "Don't Render Entities as Scene"; const QString EchoLocalAudio = "Echo Local Audio"; const QString EchoServerAudio = "Echo Server Audio"; - const QString EnableCharacterController = "Enable avatar collisions"; + const QString EnableCharacterController = "Collide with world"; const QString EnableInverseKinematics = "Enable Inverse Kinematics"; const QString EntityScriptServerLog = "Entity Script Server Log"; const QString ExpandMyAvatarSimulateTiming = "Expand /myAvatar/simulation"; @@ -105,7 +105,6 @@ namespace MenuOption { const QString ExpandPaintGLTiming = "Expand /paintGL"; const QString ExpandPhysicsSimulationTiming = "Expand /physics"; const QString ExpandUpdateTiming = "Expand /update"; - const QString Faceshift = "Faceshift"; const QString FirstPerson = "First Person"; const QString FivePointCalibration = "5 Point Calibration"; const QString FixGaze = "Fix Gaze (no saccade)"; diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 5b996a3cdf..ce8ec44f6c 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -115,8 +115,6 @@ Avatar::Avatar(QThread* thread, RigPointer rig) : } Avatar::~Avatar() { - assert(isDead()); // mark dead before calling the dtor - auto treeRenderer = DependencyManager::get(); EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; if (entityTree) { @@ -510,12 +508,13 @@ static TextRenderer3D* textRenderer(TextRendererType type) { void Avatar::addToScene(AvatarSharedPointer self, const render::ScenePointer& scene, render::Transaction& transaction) { auto avatarPayload = new render::Payload(self); auto avatarPayloadPointer = Avatar::PayloadPointer(avatarPayload); - _renderItemID = scene->allocateID(); - transaction.resetItem(_renderItemID, avatarPayloadPointer); - _skeletonModel->addToScene(scene, transaction); + if (_skeletonModel->addToScene(scene, transaction)) { + _renderItemID = scene->allocateID(); + transaction.resetItem(_renderItemID, avatarPayloadPointer); - for (auto& attachmentModel : _attachmentModels) { - attachmentModel->addToScene(scene, transaction); + for (auto& attachmentModel : _attachmentModels) { + attachmentModel->addToScene(scene, transaction); + } } } @@ -929,6 +928,17 @@ QVector Avatar::getJointRotations() const { return jointRotations; } +QVector Avatar::getJointTranslations() const { + if (QThread::currentThread() != thread()) { + return AvatarData::getJointTranslations(); + } + QVector jointTranslations(_skeletonModel->getJointStateCount()); + for (int i = 0; i < _skeletonModel->getJointStateCount(); ++i) { + _skeletonModel->getJointTranslation(i, jointTranslations[i]); + } + return jointTranslations; +} + glm::quat Avatar::getJointRotation(int index) const { glm::quat rotation; _skeletonModel->getJointRotation(index, rotation); @@ -1112,11 +1122,20 @@ void Avatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { void Avatar::setModelURLFinished(bool success) { if (!success && _skeletonModelURL != AvatarData::defaultFullAvatarModelUrl()) { - qCWarning(interfaceapp) << "Using default after failing to load Avatar model: " << _skeletonModelURL; - // call _skeletonModel.setURL, but leave our copy of _skeletonModelURL alone. This is so that - // we don't redo this every time we receive an identity packet from the avatar with the bad url. - QMetaObject::invokeMethod(_skeletonModel.get(), "setURL", - Qt::QueuedConnection, Q_ARG(QUrl, AvatarData::defaultFullAvatarModelUrl())); + const int MAX_SKELETON_DOWNLOAD_ATTEMPTS = 4; // NOTE: we don't want to be as generous as ResourceCache is, we only want 4 attempts + if (_skeletonModel->getResourceDownloadAttemptsRemaining() <= 0 || + _skeletonModel->getResourceDownloadAttempts() > MAX_SKELETON_DOWNLOAD_ATTEMPTS) { + qCWarning(interfaceapp) << "Using default after failing to load Avatar model: " << _skeletonModelURL + << "after" << _skeletonModel->getResourceDownloadAttempts() << "attempts."; + // call _skeletonModel.setURL, but leave our copy of _skeletonModelURL alone. This is so that + // we don't redo this every time we receive an identity packet from the avatar with the bad url. + QMetaObject::invokeMethod(_skeletonModel.get(), "setURL", + Qt::QueuedConnection, Q_ARG(QUrl, AvatarData::defaultFullAvatarModelUrl())); + } else { + qCWarning(interfaceapp) << "Avatar model: " << _skeletonModelURL + << "failed to load... attempts:" << _skeletonModel->getResourceDownloadAttempts() + << "out of:" << MAX_SKELETON_DOWNLOAD_ATTEMPTS; + } } } diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index 8c055885fd..14d1da530a 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -112,6 +112,7 @@ public: virtual QVector getJointRotations() const override; virtual glm::quat getJointRotation(int index) const override; + virtual QVector getJointTranslations() const override; virtual glm::vec3 getJointTranslation(int index) const override; virtual int getJointIndex(const QString& name) const override; virtual QStringList getJointNames() const override; diff --git a/interface/src/avatar/AvatarActionFarGrab.cpp b/interface/src/avatar/AvatarActionFarGrab.cpp new file mode 100644 index 0000000000..afa21e58d7 --- /dev/null +++ b/interface/src/avatar/AvatarActionFarGrab.cpp @@ -0,0 +1,64 @@ +// +// AvatarActionFarGrab.cpp +// interface/src/avatar/ +// +// Created by Seth Alves 2017-4-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 "AvatarActionFarGrab.h" + +AvatarActionFarGrab::AvatarActionFarGrab(const QUuid& id, EntityItemPointer ownerEntity) : + ObjectActionSpring(id, ownerEntity) { + _type = DYNAMIC_TYPE_FAR_GRAB; +#if WANT_DEBUG + qDebug() << "AvatarActionFarGrab::AvatarActionFarGrab"; +#endif +} + +AvatarActionFarGrab::~AvatarActionFarGrab() { +#if WANT_DEBUG + qDebug() << "AvatarActionFarGrab::~AvatarActionFarGrab"; +#endif +} + + +QByteArray AvatarActionFarGrab::serialize() const { + QByteArray serializedActionArguments; + QDataStream dataStream(&serializedActionArguments, QIODevice::WriteOnly); + + dataStream << DYNAMIC_TYPE_FAR_GRAB; + dataStream << getID(); + dataStream << ObjectActionSpring::springVersion; + + serializeParameters(dataStream); + + return serializedActionArguments; +} + +void AvatarActionFarGrab::deserialize(QByteArray serializedArguments) { + QDataStream dataStream(serializedArguments); + + EntityDynamicType type; + dataStream >> type; + + QUuid id; + dataStream >> id; + + if (type != getType() || id != getID()) { + qDebug() << "AvatarActionFarGrab::deserialize type or ID don't match." << type << id << getID(); + return; + } + + uint16_t serializationVersion; + dataStream >> serializationVersion; + if (serializationVersion != ObjectActionSpring::springVersion) { + assert(false); + return; + } + + deserializeParameters(serializedArguments, dataStream); +} diff --git a/interface/src/avatar/AvatarActionFarGrab.h b/interface/src/avatar/AvatarActionFarGrab.h new file mode 100644 index 0000000000..46c9f65dcf --- /dev/null +++ b/interface/src/avatar/AvatarActionFarGrab.h @@ -0,0 +1,27 @@ +// +// AvatarActionFarGrab.h +// interface/src/avatar/ +// +// Created by Seth Alves 2017-4-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 +// + +#ifndef hifi_AvatarActionFarGrab_h +#define hifi_AvatarActionFarGrab_h + +#include +#include + +class AvatarActionFarGrab : public ObjectActionSpring { +public: + AvatarActionFarGrab(const QUuid& id, EntityItemPointer ownerEntity); + virtual ~AvatarActionFarGrab(); + + QByteArray serialize() const override; + virtual void deserialize(QByteArray serializedArguments) override; +}; + +#endif // hifi_AvatarActionFarGrab_h diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index 7f58c86aec..98ff687eb3 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -23,7 +23,7 @@ const int AvatarActionHold::velocitySmoothFrames = 6; AvatarActionHold::AvatarActionHold(const QUuid& id, EntityItemPointer ownerEntity) : ObjectActionSpring(id, ownerEntity) { - _type = ACTION_TYPE_HOLD; + _type = DYNAMIC_TYPE_HOLD; _measuredLinearVelocities.resize(AvatarActionHold::velocitySmoothFrames); auto myAvatar = DependencyManager::get()->getMyAvatar(); @@ -323,28 +323,28 @@ bool AvatarActionHold::updateArguments(QVariantMap arguments) { bool ignoreIK; bool needUpdate = false; - bool somethingChanged = ObjectAction::updateArguments(arguments); + bool somethingChanged = ObjectDynamic::updateArguments(arguments); withReadLock([&]{ bool ok = true; - relativePosition = EntityActionInterface::extractVec3Argument("hold", arguments, "relativePosition", ok, false); + relativePosition = EntityDynamicInterface::extractVec3Argument("hold", arguments, "relativePosition", ok, false); if (!ok) { relativePosition = _relativePosition; } ok = true; - relativeRotation = EntityActionInterface::extractQuatArgument("hold", arguments, "relativeRotation", ok, false); + relativeRotation = EntityDynamicInterface::extractQuatArgument("hold", arguments, "relativeRotation", ok, false); if (!ok) { relativeRotation = _relativeRotation; } ok = true; - timeScale = EntityActionInterface::extractFloatArgument("hold", arguments, "timeScale", ok, false); + timeScale = EntityDynamicInterface::extractFloatArgument("hold", arguments, "timeScale", ok, false); if (!ok) { timeScale = _linearTimeScale; } ok = true; - hand = EntityActionInterface::extractStringArgument("hold", arguments, "hand", ok, false); + hand = EntityDynamicInterface::extractStringArgument("hold", arguments, "hand", ok, false); if (!ok || !(hand == "left" || hand == "right")) { hand = _hand; } @@ -353,20 +353,20 @@ bool AvatarActionHold::updateArguments(QVariantMap arguments) { holderID = myAvatar->getSessionUUID(); ok = true; - kinematic = EntityActionInterface::extractBooleanArgument("hold", arguments, "kinematic", ok, false); + kinematic = EntityDynamicInterface::extractBooleanArgument("hold", arguments, "kinematic", ok, false); if (!ok) { kinematic = _kinematic; } ok = true; - kinematicSetVelocity = EntityActionInterface::extractBooleanArgument("hold", arguments, + kinematicSetVelocity = EntityDynamicInterface::extractBooleanArgument("hold", arguments, "kinematicSetVelocity", ok, false); if (!ok) { kinematicSetVelocity = _kinematicSetVelocity; } ok = true; - ignoreIK = EntityActionInterface::extractBooleanArgument("hold", arguments, "ignoreIK", ok, false); + ignoreIK = EntityDynamicInterface::extractBooleanArgument("hold", arguments, "ignoreIK", ok, false); if (!ok) { ignoreIK = _ignoreIK; } @@ -400,8 +400,8 @@ bool AvatarActionHold::updateArguments(QVariantMap arguments) { auto ownerEntity = _ownerEntity.lock(); if (ownerEntity) { - ownerEntity->setActionDataDirty(true); - ownerEntity->setActionDataNeedsTransmit(true); + ownerEntity->setDynamicDataDirty(true); + ownerEntity->setDynamicDataNeedsTransmit(true); } }); } @@ -410,7 +410,7 @@ bool AvatarActionHold::updateArguments(QVariantMap arguments) { } QVariantMap AvatarActionHold::getArguments() { - QVariantMap arguments = ObjectAction::getArguments(); + QVariantMap arguments = ObjectDynamic::getArguments(); withReadLock([&]{ arguments["holderID"] = _holderID; arguments["relativePosition"] = glmToQMap(_relativePosition); @@ -429,7 +429,7 @@ QByteArray AvatarActionHold::serialize() const { QDataStream dataStream(&serializedActionArguments, QIODevice::WriteOnly); withReadLock([&]{ - dataStream << ACTION_TYPE_HOLD; + dataStream << DYNAMIC_TYPE_HOLD; dataStream << getID(); dataStream << AvatarActionHold::holdVersion; @@ -451,7 +451,7 @@ QByteArray AvatarActionHold::serialize() const { void AvatarActionHold::deserialize(QByteArray serializedArguments) { QDataStream dataStream(serializedArguments); - EntityActionType type; + EntityDynamicType type; dataStream >> type; assert(type == getType()); diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 585776b395..c4bcb67a16 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -213,10 +213,6 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { } } avatar->animateScaleChanges(deltaTime); - if (avatar->shouldDie()) { - avatar->die(); - removeAvatar(avatar->getID()); - } const float OUT_OF_VIEW_THRESHOLD = 0.5f * AvatarData::OUT_OF_VIEW_PENALTY; uint64_t now = usecTimestampNow(); @@ -330,44 +326,12 @@ AvatarSharedPointer AvatarManager::newSharedAvatar() { return std::make_shared(qApp->thread(), std::make_shared()); } -void AvatarManager::processAvatarDataPacket(QSharedPointer message, SharedNodePointer sendingNode) { - PerformanceTimer perfTimer("receiveAvatar"); - // enumerate over all of the avatars in this packet - // only add them if mixerWeakPointer points to something (meaning that mixer is still around) - while (message->getBytesLeftToRead()) { - AvatarSharedPointer avatarData = parseAvatarData(message, sendingNode); - if (avatarData) { - auto avatar = std::static_pointer_cast(avatarData); - if (avatar->isInScene()) { - if (!_shouldRender) { - // rare transition so we process the transaction immediately - const render::ScenePointer& scene = qApp->getMain3DScene(); - render::Transaction transaction; - avatar->removeFromScene(avatar, scene, transaction); - if (scene) { - scene->enqueueTransaction(transaction); - } - } - } else if (_shouldRender) { - // very rare transition so we process the transaction immediately - const render::ScenePointer& scene = qApp->getMain3DScene(); - render::Transaction transaction; - avatar->addToScene(avatar, scene, transaction); - if (scene) { - scene->enqueueTransaction(transaction); - } - } - } - } -} - void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason) { AvatarHashMap::handleRemovedAvatar(removedAvatar, removalReason); // removedAvatar is a shared pointer to an AvatarData but we need to get to the derived Avatar // class in this context so we can call methods that don't exist at the base class. - Avatar* avatar = static_cast(removedAvatar.get()); - avatar->die(); + auto avatar = std::static_pointer_cast(removedAvatar); AvatarMotionState* motionState = avatar->getMotionState(); if (motionState) { @@ -403,14 +367,11 @@ void AvatarManager::clearOtherAvatars() { if (avatar->isInScene()) { avatar->removeFromScene(avatar, scene, transaction); } - AvatarMotionState* motionState = avatar->getMotionState(); - if (motionState) { - _motionStatesThatMightUpdate.remove(motionState); - _motionStatesToAddToPhysics.remove(motionState); - _motionStatesToRemoveFromPhysics.push_back(motionState); - } + handleRemovedAvatar(avatar); + avatarIterator = _avatarHash.erase(avatarIterator); + } else { + ++avatarIterator; } - ++avatarIterator; } scene->enqueueTransaction(transaction); _myAvatar->clearLookAtTargetAvatar(); diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 6eabbd081f..45f1a597eb 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -98,9 +98,6 @@ public slots: void setShouldShowReceiveStats(bool shouldShowReceiveStats) { _shouldShowReceiveStats = shouldShowReceiveStats; } void updateAvatarRenderStatus(bool shouldRenderAvatars); -protected slots: - void processAvatarDataPacket(QSharedPointer message, SharedNodePointer sendingNode) override; - private: explicit AvatarManager(QObject* parent = 0); explicit AvatarManager(const AvatarManager& other); diff --git a/interface/src/avatar/Head.cpp b/interface/src/avatar/Head.cpp index 282acf6bf5..16e5776d87 100644 --- a/interface/src/avatar/Head.cpp +++ b/interface/src/avatar/Head.cpp @@ -13,6 +13,7 @@ #include #include +#include #include "Application.h" #include "Avatar.h" @@ -22,8 +23,6 @@ #include "Menu.h" #include "Util.h" #include "devices/DdeFaceTracker.h" -#include "devices/EyeTracker.h" -#include "devices/Faceshift.h" #include using namespace std; @@ -209,14 +208,14 @@ void Head::simulate(float deltaTime, bool isMine) { // use data to update fake Faceshift blendshape coefficients calculateMouthShapes(deltaTime); - DependencyManager::get()->updateFakeCoefficients(_leftEyeBlink, - _rightEyeBlink, - _browAudioLift, - _audioJawOpen, - _mouth2, - _mouth3, - _mouth4, - _blendshapeCoefficients); + FaceTracker::updateFakeCoefficients(_leftEyeBlink, + _rightEyeBlink, + _browAudioLift, + _audioJawOpen, + _mouth2, + _mouth3, + _mouth4, + _blendshapeCoefficients); applyEyelidOffset(getOrientation()); diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index f4f078c9e5..5e285f21ba 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -41,9 +41,9 @@ #include #include #include +#include #include "Application.h" -#include "devices/Faceshift.h" #include "AvatarManager.h" #include "AvatarActionHold.h" #include "Menu.h" @@ -82,6 +82,18 @@ const float MyAvatar::ZOOM_MIN = 0.5f; const float MyAvatar::ZOOM_MAX = 25.0f; const float MyAvatar::ZOOM_DEFAULT = 1.5f; +// default values, used when avatar is missing joints... (avatar space) +// static const glm::quat DEFAULT_AVATAR_MIDDLE_EYE_ROT { Quaternions::Y_180 }; +static const glm::vec3 DEFAULT_AVATAR_MIDDLE_EYE_POS { 0.0f, 0.6f, 0.0f }; +static const glm::vec3 DEFAULT_AVATAR_HEAD_POS { 0.0f, 0.53f, 0.0f }; +static const glm::vec3 DEFAULT_AVATAR_NECK_POS { 0.0f, 0.445f, 0.025f }; +static const glm::vec3 DEFAULT_AVATAR_SPINE2_POS { 0.0f, 0.32f, 0.02f }; +static const glm::vec3 DEFAULT_AVATAR_HIPS_POS { 0.0f, 0.0f, 0.0f }; +static const glm::vec3 DEFAULT_AVATAR_LEFTFOOT_POS { -0.08f, -0.96f, 0.029f}; +static const glm::quat DEFAULT_AVATAR_LEFTFOOT_ROT { -0.40167322754859924f, 0.9154590368270874f, -0.005437685176730156f, -0.023744143545627594f }; +static const glm::vec3 DEFAULT_AVATAR_RIGHTFOOT_POS { 0.08f, -0.96f, 0.029f }; +static const glm::quat DEFAULT_AVATAR_RIGHTFOOT_ROT { -0.4016716778278351f, 0.9154615998268127f, 0.0053307069465518f, 0.023696165531873703f }; + MyAvatar::MyAvatar(QThread* thread, RigPointer rig) : Avatar(thread, rig), _wasPushing(false), @@ -412,9 +424,7 @@ void MyAvatar::update(float deltaTime) { Q_ARG(glm::vec3, (getPosition() - halfBoundingBoxDimensions)), Q_ARG(glm::vec3, (halfBoundingBoxDimensions*2.0f))); - uint64_t now = usecTimestampNow(); - if (now > _identityPacketExpiry || _avatarEntityDataLocallyEdited) { - _identityPacketExpiry = now + AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS; + if (getIdentityDataChanged()) { sendIdentityPacket(); } @@ -652,18 +662,13 @@ void MyAvatar::updateFromTrackers(float deltaTime) { } FaceTracker* tracker = qApp->getActiveFaceTracker(); - bool inFacetracker = tracker && !tracker->isMuted(); + bool inFacetracker = tracker && !FaceTracker::isMuted(); if (inHmd) { estimatedPosition = extractTranslation(getHMDSensorMatrix()); estimatedPosition.x *= -1.0f; - _trackedHeadPosition = estimatedPosition; - - const float OCULUS_LEAN_SCALE = 0.05f; - estimatedPosition /= OCULUS_LEAN_SCALE; } else if (inFacetracker) { estimatedPosition = tracker->getHeadTranslation(); - _trackedHeadPosition = estimatedPosition; estimatedRotation = glm::degrees(safeEulerAngles(tracker->getHeadRotation())); } @@ -1258,7 +1263,7 @@ void MyAvatar::useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelN setSkeletonModelURL(fullAvatarURL); UserActivityLogger::getInstance().changedModel("skeleton", urlString); } - _identityPacketExpiry = 0; // triggers an identity packet next update() + markIdentityDataChanged(); } void MyAvatar::setAttachmentData(const QVector& attachmentData) { @@ -1380,6 +1385,65 @@ controller::Pose MyAvatar::getRightFootControllerPoseInAvatarFrame() const { return getRightFootControllerPoseInWorldFrame().transform(invAvatarMatrix); } +void MyAvatar::setSpineControllerPosesInSensorFrame(const controller::Pose& hips, const controller::Pose& spine2) { + if (controller::InputDevice::getLowVelocityFilter()) { + auto oldHipsPose = getHipsControllerPoseInSensorFrame(); + auto oldSpine2Pose = getSpine2ControllerPoseInSensorFrame(); + _hipsControllerPoseInSensorFrameCache.set(applyLowVelocityFilter(oldHipsPose, hips)); + _spine2ControllerPoseInSensorFrameCache.set(applyLowVelocityFilter(oldSpine2Pose, spine2)); + } else { + _hipsControllerPoseInSensorFrameCache.set(hips); + _spine2ControllerPoseInSensorFrameCache.set(spine2); + } +} + +controller::Pose MyAvatar::getHipsControllerPoseInSensorFrame() const { + return _hipsControllerPoseInSensorFrameCache.get(); +} + +controller::Pose MyAvatar::getSpine2ControllerPoseInSensorFrame() const { + return _spine2ControllerPoseInSensorFrameCache.get(); +} + +controller::Pose MyAvatar::getHipsControllerPoseInWorldFrame() const { + return _hipsControllerPoseInSensorFrameCache.get().transform(getSensorToWorldMatrix()); +} + +controller::Pose MyAvatar::getSpine2ControllerPoseInWorldFrame() const { + return _spine2ControllerPoseInSensorFrameCache.get().transform(getSensorToWorldMatrix()); +} + +controller::Pose MyAvatar::getHipsControllerPoseInAvatarFrame() const { + glm::mat4 invAvatarMatrix = glm::inverse(createMatFromQuatAndPos(getOrientation(), getPosition())); + return getHipsControllerPoseInWorldFrame().transform(invAvatarMatrix); +} + +controller::Pose MyAvatar::getSpine2ControllerPoseInAvatarFrame() const { + glm::mat4 invAvatarMatrix = glm::inverse(createMatFromQuatAndPos(getOrientation(), getPosition())); + return getSpine2ControllerPoseInWorldFrame().transform(invAvatarMatrix); +} + +void MyAvatar::setHeadControllerPoseInSensorFrame(const controller::Pose& head) { + if (controller::InputDevice::getLowVelocityFilter()) { + auto oldHeadPose = getHeadControllerPoseInSensorFrame(); + _headControllerPoseInSensorFrameCache.set(applyLowVelocityFilter(oldHeadPose, head)); + } else { + _headControllerPoseInSensorFrameCache.set(head); + } +} + +controller::Pose MyAvatar::getHeadControllerPoseInSensorFrame() const { + return _headControllerPoseInSensorFrameCache.get(); +} + +controller::Pose MyAvatar::getHeadControllerPoseInWorldFrame() const { + return _headControllerPoseInSensorFrameCache.get().transform(getSensorToWorldMatrix()); +} + +controller::Pose MyAvatar::getHeadControllerPoseInAvatarFrame() const { + glm::mat4 invAvatarMatrix = glm::inverse(createMatFromQuatAndPos(getOrientation(), getPosition())); + return getHeadControllerPoseInWorldFrame().transform(invAvatarMatrix); +} void MyAvatar::updateMotors() { _characterController.clearMotors(); @@ -2222,22 +2286,17 @@ glm::mat4 MyAvatar::deriveBodyFromHMDSensor() const { const glm::quat hmdOrientation = getHMDSensorOrientation(); const glm::quat hmdOrientationYawOnly = cancelOutRollAndPitch(hmdOrientation); - // 2 meter tall dude (in rig coordinates) - const glm::vec3 DEFAULT_RIG_MIDDLE_EYE_POS(0.0f, 0.9f, 0.0f); - const glm::vec3 DEFAULT_RIG_NECK_POS(0.0f, 0.70f, 0.0f); - const glm::vec3 DEFAULT_RIG_HIPS_POS(0.0f, 0.05f, 0.0f); - int rightEyeIndex = _rig->indexOfJoint("RightEye"); int leftEyeIndex = _rig->indexOfJoint("LeftEye"); int neckIndex = _rig->indexOfJoint("Neck"); int hipsIndex = _rig->indexOfJoint("Hips"); - glm::vec3 rigMiddleEyePos = DEFAULT_RIG_MIDDLE_EYE_POS; + glm::vec3 rigMiddleEyePos = DEFAULT_AVATAR_MIDDLE_EYE_POS; if (leftEyeIndex >= 0 && rightEyeIndex >= 0) { rigMiddleEyePos = (_rig->getAbsoluteDefaultPose(leftEyeIndex).trans() + _rig->getAbsoluteDefaultPose(rightEyeIndex).trans()) / 2.0f; } - glm::vec3 rigNeckPos = neckIndex != -1 ? _rig->getAbsoluteDefaultPose(neckIndex).trans() : DEFAULT_RIG_NECK_POS; - glm::vec3 rigHipsPos = hipsIndex != -1 ? _rig->getAbsoluteDefaultPose(hipsIndex).trans() : DEFAULT_RIG_HIPS_POS; + glm::vec3 rigNeckPos = neckIndex != -1 ? _rig->getAbsoluteDefaultPose(neckIndex).trans() : DEFAULT_AVATAR_NECK_POS; + glm::vec3 rigHipsPos = hipsIndex != -1 ? _rig->getAbsoluteDefaultPose(hipsIndex).trans() : DEFAULT_AVATAR_HIPS_POS; glm::vec3 localEyes = (rigMiddleEyePos - rigHipsPos); glm::vec3 localNeck = (rigNeckPos - rigHipsPos); @@ -2601,6 +2660,79 @@ glm::vec3 MyAvatar::getAbsoluteJointTranslationInObjectFrame(int index) const { } } +glm::mat4 MyAvatar::getCenterEyeCalibrationMat() const { + // TODO: as an optimization cache this computation, then invalidate the cache when the avatar model is changed. + int rightEyeIndex = _rig->indexOfJoint("RightEye"); + int leftEyeIndex = _rig->indexOfJoint("LeftEye"); + if (rightEyeIndex >= 0 && leftEyeIndex >= 0) { + auto centerEyePos = (getAbsoluteDefaultJointTranslationInObjectFrame(rightEyeIndex) + getAbsoluteDefaultJointTranslationInObjectFrame(leftEyeIndex)) * 0.5f; + auto centerEyeRot = Quaternions::Y_180; + return createMatFromQuatAndPos(centerEyeRot, centerEyePos); + } else { + return createMatFromQuatAndPos(DEFAULT_AVATAR_MIDDLE_EYE_POS, DEFAULT_AVATAR_MIDDLE_EYE_POS); + } +} + +glm::mat4 MyAvatar::getHeadCalibrationMat() const { + // TODO: as an optimization cache this computation, then invalidate the cache when the avatar model is changed. + int headIndex = _rig->indexOfJoint("Head"); + if (headIndex >= 0) { + auto headPos = getAbsoluteDefaultJointTranslationInObjectFrame(headIndex); + auto headRot = getAbsoluteDefaultJointRotationInObjectFrame(headIndex); + return createMatFromQuatAndPos(headRot, headPos); + } else { + return createMatFromQuatAndPos(DEFAULT_AVATAR_HEAD_POS, DEFAULT_AVATAR_HEAD_POS); + } +} + +glm::mat4 MyAvatar::getSpine2CalibrationMat() const { + // TODO: as an optimization cache this computation, then invalidate the cache when the avatar model is changed. + int spine2Index = _rig->indexOfJoint("Spine2"); + if (spine2Index >= 0) { + auto spine2Pos = getAbsoluteDefaultJointTranslationInObjectFrame(spine2Index); + auto spine2Rot = getAbsoluteDefaultJointRotationInObjectFrame(spine2Index); + return createMatFromQuatAndPos(spine2Rot, spine2Pos); + } else { + return createMatFromQuatAndPos(DEFAULT_AVATAR_SPINE2_POS, DEFAULT_AVATAR_SPINE2_POS); + } +} + +glm::mat4 MyAvatar::getHipsCalibrationMat() const { + // TODO: as an optimization cache this computation, then invalidate the cache when the avatar model is changed. + int hipsIndex = _rig->indexOfJoint("Hips"); + if (hipsIndex >= 0) { + auto hipsPos = getAbsoluteDefaultJointTranslationInObjectFrame(hipsIndex); + auto hipsRot = getAbsoluteDefaultJointRotationInObjectFrame(hipsIndex); + return createMatFromQuatAndPos(hipsRot, hipsPos); + } else { + return createMatFromQuatAndPos(DEFAULT_AVATAR_HIPS_POS, DEFAULT_AVATAR_HIPS_POS); + } +} + +glm::mat4 MyAvatar::getLeftFootCalibrationMat() const { + // TODO: as an optimization cache this computation, then invalidate the cache when the avatar model is changed. + int leftFootIndex = _rig->indexOfJoint("LeftFoot"); + if (leftFootIndex >= 0) { + auto leftFootPos = getAbsoluteDefaultJointTranslationInObjectFrame(leftFootIndex); + auto leftFootRot = getAbsoluteDefaultJointRotationInObjectFrame(leftFootIndex); + return createMatFromQuatAndPos(leftFootRot, leftFootPos); + } else { + return createMatFromQuatAndPos(DEFAULT_AVATAR_LEFTFOOT_POS, DEFAULT_AVATAR_LEFTFOOT_POS); + } +} + +glm::mat4 MyAvatar::getRightFootCalibrationMat() const { + // TODO: as an optimization cache this computation, then invalidate the cache when the avatar model is changed. + int rightFootIndex = _rig->indexOfJoint("RightFoot"); + if (rightFootIndex >= 0) { + auto rightFootPos = getAbsoluteDefaultJointTranslationInObjectFrame(rightFootIndex); + auto rightFootRot = getAbsoluteDefaultJointRotationInObjectFrame(rightFootIndex); + return createMatFromQuatAndPos(rightFootRot, rightFootPos); + } else { + return createMatFromQuatAndPos(DEFAULT_AVATAR_RIGHTFOOT_POS, DEFAULT_AVATAR_RIGHTFOOT_POS); + } +} + bool MyAvatar::pinJoint(int index, const glm::vec3& position, const glm::quat& orientation) { auto hipsIndex = getJointIndex("Hips"); if (index != hipsIndex) { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 74af44c99a..6a1e457a97 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -353,7 +353,6 @@ public: eyeContactTarget getEyeContactTarget(); - Q_INVOKABLE glm::vec3 getTrackedHeadPosition() const { return _trackedHeadPosition; } Q_INVOKABLE glm::vec3 getHeadPosition() const { return getHead()->getPosition(); } Q_INVOKABLE float getHeadFinalYaw() const { return getHead()->getFinalYaw(); } Q_INVOKABLE float getHeadFinalRoll() const { return getHead()->getFinalRoll(); } @@ -453,6 +452,19 @@ public: controller::Pose getLeftFootControllerPoseInAvatarFrame() const; controller::Pose getRightFootControllerPoseInAvatarFrame() const; + void setSpineControllerPosesInSensorFrame(const controller::Pose& hips, const controller::Pose& spine2); + controller::Pose getHipsControllerPoseInSensorFrame() const; + controller::Pose getSpine2ControllerPoseInSensorFrame() const; + controller::Pose getHipsControllerPoseInWorldFrame() const; + controller::Pose getSpine2ControllerPoseInWorldFrame() const; + controller::Pose getHipsControllerPoseInAvatarFrame() const; + controller::Pose getSpine2ControllerPoseInAvatarFrame() const; + + void setHeadControllerPoseInSensorFrame(const controller::Pose& head); + controller::Pose getHeadControllerPoseInSensorFrame() const; + controller::Pose getHeadControllerPoseInWorldFrame() const; + controller::Pose getHeadControllerPoseInAvatarFrame() const; + bool hasDriveInput() const; Q_INVOKABLE void setCharacterControllerEnabled(bool enabled); @@ -461,10 +473,22 @@ public: virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override; virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override; + // all calibration matrices are in absolute avatar space. + glm::mat4 getCenterEyeCalibrationMat() const; + glm::mat4 getHeadCalibrationMat() const; + glm::mat4 getSpine2CalibrationMat() const; + glm::mat4 getHipsCalibrationMat() const; + glm::mat4 getLeftFootCalibrationMat() const; + glm::mat4 getRightFootCalibrationMat() const; + void addHoldAction(AvatarActionHold* holdAction); // thread-safe void removeHoldAction(AvatarActionHold* holdAction); // thread-safe void updateHoldActions(const AnimPose& prePhysicsPose, const AnimPose& postUpdatePose); + // derive avatar body position and orientation from the current HMD Sensor location. + // results are in HMD frame + glm::mat4 deriveBodyFromHMDSensor() const; + public slots: void increaseSize(); void decreaseSize(); @@ -553,9 +577,7 @@ private: void setVisibleInSceneIfReady(Model* model, const render::ScenePointer& scene, bool visiblity); - // derive avatar body position and orientation from the current HMD Sensor location. - // results are in HMD frame - glm::mat4 deriveBodyFromHMDSensor() const; +private: virtual void updatePalms() override {} void lateUpdatePalms(); @@ -691,9 +713,11 @@ private: // These are stored in SENSOR frame ThreadSafeValueCache _leftHandControllerPoseInSensorFrameCache { controller::Pose() }; ThreadSafeValueCache _rightHandControllerPoseInSensorFrameCache { controller::Pose() }; - ThreadSafeValueCache _leftFootControllerPoseInSensorFrameCache{ controller::Pose() }; ThreadSafeValueCache _rightFootControllerPoseInSensorFrameCache{ controller::Pose() }; + ThreadSafeValueCache _hipsControllerPoseInSensorFrameCache{ controller::Pose() }; + ThreadSafeValueCache _spine2ControllerPoseInSensorFrameCache{ controller::Pose() }; + ThreadSafeValueCache _headControllerPoseInSensorFrameCache{ controller::Pose() }; bool _hmdLeanRecenterEnabled = true; @@ -701,8 +725,6 @@ private: std::mutex _holdActionsMutex; std::vector _holdActions; - uint64_t _identityPacketExpiry { 0 }; - float AVATAR_MOVEMENT_ENERGY_CONSTANT { 0.001f }; float AUDIO_ENERGY_CONSTANT { 0.000001f }; float MAX_AVATAR_MOVEMENT_PER_FRAME { 30.0f }; diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 81a5bf38dc..f81a83523b 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -107,27 +107,49 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { Rig::HeadParameters headParams; - if (qApp->isHMDMode()) { - headParams.isInHMD = true; - - // get HMD position from sensor space into world space, and back into rig space - glm::mat4 worldHMDMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); - glm::mat4 rigToWorld = createMatFromQuatAndPos(getRotation(), getTranslation()); - glm::mat4 worldToRig = glm::inverse(rigToWorld); - glm::mat4 rigHMDMat = worldToRig * worldHMDMat; - - headParams.rigHeadPosition = extractTranslation(rigHMDMat); - headParams.rigHeadOrientation = extractRotation(rigHMDMat); - headParams.worldHeadOrientation = extractRotation(worldHMDMat); + // input action is the highest priority source for head orientation. + auto avatarHeadPose = myAvatar->getHeadControllerPoseInAvatarFrame(); + if (avatarHeadPose.isValid()) { + glm::mat4 rigHeadMat = Matrices::Y_180 * createMatFromQuatAndPos(avatarHeadPose.getRotation(), avatarHeadPose.getTranslation()); + headParams.rigHeadPosition = extractTranslation(rigHeadMat); + headParams.rigHeadOrientation = glmExtractRotation(rigHeadMat); + headParams.headEnabled = true; } else { - headParams.isInHMD = false; - - // We don't have a valid localHeadPosition. - headParams.rigHeadOrientation = Quaternions::Y_180 * head->getFinalOrientationInLocalFrame(); - headParams.worldHeadOrientation = head->getFinalOrientationInWorldFrame(); + if (qApp->isHMDMode()) { + // get HMD position from sensor space into world space, and back into rig space + glm::mat4 worldHMDMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); + glm::mat4 rigToWorld = createMatFromQuatAndPos(getRotation(), getTranslation()); + glm::mat4 worldToRig = glm::inverse(rigToWorld); + glm::mat4 rigHMDMat = worldToRig * worldHMDMat; + _rig->computeHeadFromHMD(AnimPose(rigHMDMat), headParams.rigHeadPosition, headParams.rigHeadOrientation); + headParams.headEnabled = true; + } else { + // even though full head IK is disabled, the rig still needs the head orientation to rotate the head up and down in desktop mode. + // preMult 180 is necessary to convert from avatar to rig coordinates. + // postMult 180 is necessary to convert head from -z forward to z forward. + headParams.rigHeadOrientation = Quaternions::Y_180 * head->getFinalOrientationInLocalFrame() * Quaternions::Y_180; + headParams.headEnabled = false; + } + } + + auto avatarHipsPose = myAvatar->getHipsControllerPoseInAvatarFrame(); + if (avatarHipsPose.isValid()) { + glm::mat4 rigHipsMat = Matrices::Y_180 * createMatFromQuatAndPos(avatarHipsPose.getRotation(), avatarHipsPose.getTranslation()); + headParams.hipsMatrix = rigHipsMat; + headParams.hipsEnabled = true; + } else { + headParams.hipsEnabled = false; + } + + auto avatarSpine2Pose = myAvatar->getSpine2ControllerPoseInAvatarFrame(); + if (avatarSpine2Pose.isValid()) { + glm::mat4 rigSpine2Mat = Matrices::Y_180 * createMatFromQuatAndPos(avatarSpine2Pose.getRotation(), avatarSpine2Pose.getTranslation()); + headParams.spine2Matrix = rigSpine2Mat; + headParams.spine2Enabled = true; + } else { + headParams.spine2Enabled = false; } - headParams.neckJointIndex = geometry.neckJointIndex; headParams.isTalking = head->getTimeWithoutTalking() <= 1.5f; _rig->updateFromHeadParameters(headParams, deltaTime); @@ -187,7 +209,6 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { Model::updateRig(deltaTime, parentTransform); Rig::EyeParameters eyeParams; - eyeParams.worldHeadOrientation = headParams.worldHeadOrientation; eyeParams.eyeLookAt = lookAt; eyeParams.eyeSaccade = head->getSaccade(); eyeParams.modelRotation = getRotation(); @@ -219,7 +240,6 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { head->setBaseRoll(glm::degrees(-eulers.z)); Rig::EyeParameters eyeParams; - eyeParams.worldHeadOrientation = head->getFinalOrientationInWorldFrame(); eyeParams.eyeLookAt = lookAt; eyeParams.eyeSaccade = glm::vec3(0.0f); eyeParams.modelRotation = getRotation(); diff --git a/interface/src/devices/DdeFaceTracker.h b/interface/src/devices/DdeFaceTracker.h index 973c3b224e..f125dfc3cf 100644 --- a/interface/src/devices/DdeFaceTracker.h +++ b/interface/src/devices/DdeFaceTracker.h @@ -22,7 +22,7 @@ #include #include -#include "FaceTracker.h" +#include class DdeFaceTracker : public FaceTracker, public Dependency { Q_OBJECT diff --git a/interface/src/devices/Faceshift.cpp b/interface/src/devices/Faceshift.cpp deleted file mode 100644 index 81c099c740..0000000000 --- a/interface/src/devices/Faceshift.cpp +++ /dev/null @@ -1,310 +0,0 @@ -// -// Faceshift.cpp -// interface/src/devices -// -// Created by Andrzej Kapolka on 9/3/13. -// Copyright 2013 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include - -#include -#include -#include - -#include "Faceshift.h" -#include "Menu.h" -#include "Util.h" -#include "InterfaceLogging.h" - -#ifdef HAVE_FACESHIFT -using namespace fs; -#endif - -using namespace std; - -const QString DEFAULT_FACESHIFT_HOSTNAME = "localhost"; -const quint16 FACESHIFT_PORT = 33433; - -Faceshift::Faceshift() : - _hostname("faceshiftHostname", DEFAULT_FACESHIFT_HOSTNAME) -{ -#ifdef HAVE_FACESHIFT - connect(&_tcpSocket, SIGNAL(connected()), SLOT(noteConnected())); - connect(&_tcpSocket, SIGNAL(error(QAbstractSocket::SocketError)), SLOT(noteError(QAbstractSocket::SocketError))); - connect(&_tcpSocket, SIGNAL(readyRead()), SLOT(readFromSocket())); - connect(&_tcpSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), SIGNAL(connectionStateChanged())); - connect(&_tcpSocket, SIGNAL(disconnected()), SLOT(noteDisconnected())); - - connect(&_udpSocket, SIGNAL(readyRead()), SLOT(readPendingDatagrams())); - - _udpSocket.bind(FACESHIFT_PORT); -#endif -} - -#ifdef HAVE_FACESHIFT -void Faceshift::init() { - FaceTracker::init(); - setEnabled(Menu::getInstance()->isOptionChecked(MenuOption::Faceshift) && !_isMuted); -} - -void Faceshift::update(float deltaTime) { - if (!isActive()) { - return; - } - FaceTracker::update(deltaTime); - - // get the euler angles relative to the window - glm::vec3 eulers = glm::degrees(safeEulerAngles(_headRotation * glm::quat(glm::radians(glm::vec3( - (_eyeGazeLeftPitch + _eyeGazeRightPitch) / 2.0f, (_eyeGazeLeftYaw + _eyeGazeRightYaw) / 2.0f, 0.0f))))); - - // compute and subtract the long term average - const float LONG_TERM_AVERAGE_SMOOTHING = 0.999f; - if (!_longTermAverageInitialized) { - _longTermAverageEyePitch = eulers.x; - _longTermAverageEyeYaw = eulers.y; - _longTermAverageInitialized = true; - - } else { - _longTermAverageEyePitch = glm::mix(eulers.x, _longTermAverageEyePitch, LONG_TERM_AVERAGE_SMOOTHING); - _longTermAverageEyeYaw = glm::mix(eulers.y, _longTermAverageEyeYaw, LONG_TERM_AVERAGE_SMOOTHING); - } - _estimatedEyePitch = eulers.x - _longTermAverageEyePitch; - _estimatedEyeYaw = eulers.y - _longTermAverageEyeYaw; -} - -void Faceshift::reset() { - if (_tcpSocket.state() == QAbstractSocket::ConnectedState) { - qCDebug(interfaceapp, "Faceshift: Reset"); - - FaceTracker::reset(); - - string message; - fsBinaryStream::encode_message(message, fsMsgCalibrateNeutral()); - send(message); - } - _longTermAverageInitialized = false; -} - -bool Faceshift::isActive() const { - const quint64 ACTIVE_TIMEOUT_USECS = 1000000; - return (usecTimestampNow() - _lastReceiveTimestamp) < ACTIVE_TIMEOUT_USECS; -} - -bool Faceshift::isTracking() const { - return isActive() && _tracking; -} -#endif - - -bool Faceshift::isConnectedOrConnecting() const { - return _tcpSocket.state() == QAbstractSocket::ConnectedState || - (_tcpRetryCount == 0 && _tcpSocket.state() != QAbstractSocket::UnconnectedState); -} - -void Faceshift::updateFakeCoefficients(float leftBlink, float rightBlink, float browUp, - float jawOpen, float mouth2, float mouth3, float mouth4, QVector& coefficients) const { - const int MMMM_BLENDSHAPE = 34; - const int FUNNEL_BLENDSHAPE = 40; - const int SMILE_LEFT_BLENDSHAPE = 28; - const int SMILE_RIGHT_BLENDSHAPE = 29; - const int MAX_FAKE_BLENDSHAPE = 40; // Largest modified blendshape from above and below - - coefficients.resize(max((int)coefficients.size(), MAX_FAKE_BLENDSHAPE + 1)); - qFill(coefficients.begin(), coefficients.end(), 0.0f); - coefficients[_leftBlinkIndex] = leftBlink; - coefficients[_rightBlinkIndex] = rightBlink; - coefficients[_browUpCenterIndex] = browUp; - coefficients[_browUpLeftIndex] = browUp; - coefficients[_browUpRightIndex] = browUp; - coefficients[_jawOpenIndex] = jawOpen; - coefficients[SMILE_LEFT_BLENDSHAPE] = coefficients[SMILE_RIGHT_BLENDSHAPE] = mouth4; - coefficients[MMMM_BLENDSHAPE] = mouth2; - coefficients[FUNNEL_BLENDSHAPE] = mouth3; -} - -void Faceshift::setEnabled(bool enabled) { - // Don't enable until have explicitly initialized - if (!_isInitialized) { - return; - } -#ifdef HAVE_FACESHIFT - if ((_tcpEnabled = enabled)) { - connectSocket(); - } else { - qCDebug(interfaceapp, "Faceshift: Disconnecting..."); - _tcpSocket.disconnectFromHost(); - } -#endif -} - -void Faceshift::connectSocket() { - if (_tcpEnabled) { - if (!_tcpRetryCount) { - qCDebug(interfaceapp, "Faceshift: Connecting..."); - } - - _tcpSocket.connectToHost(_hostname.get(), FACESHIFT_PORT); - _tracking = false; - } -} - -void Faceshift::noteConnected() { -#ifdef HAVE_FACESHIFT - qCDebug(interfaceapp, "Faceshift: Connected"); - // request the list of blendshape names - string message; - fsBinaryStream::encode_message(message, fsMsgSendBlendshapeNames()); - send(message); -#endif -} - -void Faceshift::noteDisconnected() { -#ifdef HAVE_FACESHIFT - qCDebug(interfaceapp, "Faceshift: Disconnected"); -#endif -} - -void Faceshift::noteError(QAbstractSocket::SocketError error) { - if (!_tcpRetryCount) { - // Only spam log with fail to connect the first time, so that we can keep waiting for server - qCWarning(interfaceapp) << "Faceshift: " << _tcpSocket.errorString(); - } - // retry connection after a 2 second delay - if (_tcpEnabled) { - _tcpRetryCount++; - QTimer::singleShot(2000, this, SLOT(connectSocket())); - } -} - -void Faceshift::readPendingDatagrams() { - QByteArray buffer; - while (_udpSocket.hasPendingDatagrams()) { - buffer.resize(_udpSocket.pendingDatagramSize()); - _udpSocket.readDatagram(buffer.data(), buffer.size()); - receive(buffer); - } -} - -void Faceshift::readFromSocket() { - receive(_tcpSocket.readAll()); -} - -void Faceshift::send(const std::string& message) { - _tcpSocket.write(message.data(), message.size()); -} - -void Faceshift::receive(const QByteArray& buffer) { -#ifdef HAVE_FACESHIFT - _lastReceiveTimestamp = usecTimestampNow(); - - _stream.received(buffer.size(), buffer.constData()); - fsMsgPtr msg; - for (fsMsgPtr msg; (msg = _stream.get_message()); ) { - switch (msg->id()) { - case fsMsg::MSG_OUT_TRACKING_STATE: { - const fsTrackingData& data = static_pointer_cast(msg)->tracking_data(); - if ((_tracking = data.m_trackingSuccessful)) { - glm::quat newRotation = glm::quat(data.m_headRotation.w, -data.m_headRotation.x, - data.m_headRotation.y, -data.m_headRotation.z); - // Compute angular velocity of the head - glm::quat r = glm::normalize(newRotation * glm::inverse(_headRotation)); - float theta = 2 * acos(r.w); - if (theta > EPSILON) { - float rMag = glm::length(glm::vec3(r.x, r.y, r.z)); - _headAngularVelocity = theta / _averageFrameTime * glm::vec3(r.x, r.y, r.z) / rMag; - } else { - _headAngularVelocity = glm::vec3(0,0,0); - } - const float ANGULAR_VELOCITY_FILTER_STRENGTH = 0.3f; - _headRotation = safeMix(_headRotation, newRotation, glm::clamp(glm::length(_headAngularVelocity) * - ANGULAR_VELOCITY_FILTER_STRENGTH, 0.0f, 1.0f)); - - const float TRANSLATION_SCALE = 0.02f; - glm::vec3 newHeadTranslation = glm::vec3(data.m_headTranslation.x, data.m_headTranslation.y, - -data.m_headTranslation.z) * TRANSLATION_SCALE; - - _headLinearVelocity = (newHeadTranslation - _lastHeadTranslation) / _averageFrameTime; - - const float LINEAR_VELOCITY_FILTER_STRENGTH = 0.3f; - float velocityFilter = glm::clamp(1.0f - glm::length(_headLinearVelocity) * - LINEAR_VELOCITY_FILTER_STRENGTH, 0.0f, 1.0f); - _filteredHeadTranslation = velocityFilter * _filteredHeadTranslation + (1.0f - velocityFilter) * newHeadTranslation; - - _lastHeadTranslation = newHeadTranslation; - _headTranslation = _filteredHeadTranslation; - - _eyeGazeLeftPitch = -data.m_eyeGazeLeftPitch; - _eyeGazeLeftYaw = data.m_eyeGazeLeftYaw; - _eyeGazeRightPitch = -data.m_eyeGazeRightPitch; - _eyeGazeRightYaw = data.m_eyeGazeRightYaw; - _blendshapeCoefficients = QVector::fromStdVector(data.m_coeffs); - - const float FRAME_AVERAGING_FACTOR = 0.99f; - quint64 usecsNow = usecTimestampNow(); - if (_lastMessageReceived != 0) { - _averageFrameTime = FRAME_AVERAGING_FACTOR * _averageFrameTime + - (1.0f - FRAME_AVERAGING_FACTOR) * (float)(usecsNow - _lastMessageReceived) / 1000000.0f; - } - _lastMessageReceived = usecsNow; - } - break; - } - case fsMsg::MSG_OUT_BLENDSHAPE_NAMES: { - const vector& names = static_pointer_cast(msg)->blendshape_names(); - for (int i = 0; i < (int)names.size(); i++) { - if (names[i] == "EyeBlink_L") { - _leftBlinkIndex = i; - - } else if (names[i] == "EyeBlink_R") { - _rightBlinkIndex = i; - - } else if (names[i] == "EyeOpen_L") { - _leftEyeOpenIndex = i; - - } else if (names[i] == "EyeOpen_R") { - _rightEyeOpenIndex = i; - - } else if (names[i] == "BrowsD_L") { - _browDownLeftIndex = i; - - } else if (names[i] == "BrowsD_R") { - _browDownRightIndex = i; - - } else if (names[i] == "BrowsU_C") { - _browUpCenterIndex = i; - - } else if (names[i] == "BrowsU_L") { - _browUpLeftIndex = i; - - } else if (names[i] == "BrowsU_R") { - _browUpRightIndex = i; - - } else if (names[i] == "JawOpen") { - _jawOpenIndex = i; - - } else if (names[i] == "MouthSmile_L") { - _mouthSmileLeftIndex = i; - - } else if (names[i] == "MouthSmile_R") { - _mouthSmileRightIndex = i; - } - } - break; - } - default: - break; - } - } -#endif - - FaceTracker::countFrame(); -} - -void Faceshift::setHostname(const QString& hostname) { - _hostname.set(hostname); -} - diff --git a/interface/src/devices/Faceshift.h b/interface/src/devices/Faceshift.h deleted file mode 100644 index 2c5889857c..0000000000 --- a/interface/src/devices/Faceshift.h +++ /dev/null @@ -1,155 +0,0 @@ -// -// Faceshift.h -// interface/src/devices -// -// Created by Andrzej Kapolka on 9/3/13. -// Copyright 2013 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_Faceshift_h -#define hifi_Faceshift_h - -#include -#include - -#ifdef HAVE_FACESHIFT -#include -#endif - -#include -#include - -#include "FaceTracker.h" - -const float STARTING_FACESHIFT_FRAME_TIME = 0.033f; - -/// Handles interaction with the Faceshift software, which provides head position/orientation and facial features. -class Faceshift : public FaceTracker, public Dependency { - Q_OBJECT - SINGLETON_DEPENDENCY - -public: -#ifdef HAVE_FACESHIFT - // If we don't have faceshift, use the base class' methods - virtual void init() override; - virtual void update(float deltaTime) override; - virtual void reset() override; - - virtual bool isActive() const override; - virtual bool isTracking() const override; -#endif - - bool isConnectedOrConnecting() const; - - const glm::vec3& getHeadAngularVelocity() const { return _headAngularVelocity; } - - // these pitch/yaw angles are in degrees - float getEyeGazeLeftPitch() const { return _eyeGazeLeftPitch; } - float getEyeGazeLeftYaw() const { return _eyeGazeLeftYaw; } - - float getEyeGazeRightPitch() const { return _eyeGazeRightPitch; } - float getEyeGazeRightYaw() const { return _eyeGazeRightYaw; } - - float getLeftBlink() const { return getBlendshapeCoefficient(_leftBlinkIndex); } - float getRightBlink() const { return getBlendshapeCoefficient(_rightBlinkIndex); } - float getLeftEyeOpen() const { return getBlendshapeCoefficient(_leftEyeOpenIndex); } - float getRightEyeOpen() const { return getBlendshapeCoefficient(_rightEyeOpenIndex); } - - float getBrowDownLeft() const { return getBlendshapeCoefficient(_browDownLeftIndex); } - float getBrowDownRight() const { return getBlendshapeCoefficient(_browDownRightIndex); } - float getBrowUpCenter() const { return getBlendshapeCoefficient(_browUpCenterIndex); } - float getBrowUpLeft() const { return getBlendshapeCoefficient(_browUpLeftIndex); } - float getBrowUpRight() const { return getBlendshapeCoefficient(_browUpRightIndex); } - - float getMouthSize() const { return getBlendshapeCoefficient(_jawOpenIndex); } - float getMouthSmileLeft() const { return getBlendshapeCoefficient(_mouthSmileLeftIndex); } - float getMouthSmileRight() const { return getBlendshapeCoefficient(_mouthSmileRightIndex); } - - QString getHostname() { return _hostname.get(); } - void setHostname(const QString& hostname); - - void updateFakeCoefficients(float leftBlink, - float rightBlink, - float browUp, - float jawOpen, - float mouth2, - float mouth3, - float mouth4, - QVector& coefficients) const; - -signals: - void connectionStateChanged(); - -public slots: - void setEnabled(bool enabled) override; - -private slots: - void connectSocket(); - void noteConnected(); - void noteError(QAbstractSocket::SocketError error); - void readPendingDatagrams(); - void readFromSocket(); - void noteDisconnected(); - -private: - Faceshift(); - virtual ~Faceshift() {} - - void send(const std::string& message); - void receive(const QByteArray& buffer); - - QTcpSocket _tcpSocket; - QUdpSocket _udpSocket; - -#ifdef HAVE_FACESHIFT - fs::fsBinaryStream _stream; -#endif - - bool _tcpEnabled = true; - int _tcpRetryCount = 0; - bool _tracking = false; - quint64 _lastReceiveTimestamp = 0; - quint64 _lastMessageReceived = 0; - float _averageFrameTime = STARTING_FACESHIFT_FRAME_TIME; - - glm::vec3 _headAngularVelocity = glm::vec3(0.0f); - glm::vec3 _headLinearVelocity = glm::vec3(0.0f); - glm::vec3 _lastHeadTranslation = glm::vec3(0.0f); - glm::vec3 _filteredHeadTranslation = glm::vec3(0.0f); - - // degrees - float _eyeGazeLeftPitch = 0.0f; - float _eyeGazeLeftYaw = 0.0f; - float _eyeGazeRightPitch = 0.0f; - float _eyeGazeRightYaw = 0.0f; - - // degrees - float _longTermAverageEyePitch = 0.0f; - float _longTermAverageEyeYaw = 0.0f; - bool _longTermAverageInitialized = false; - - Setting::Handle _hostname; - - // see http://support.faceshift.com/support/articles/35129-export-of-blendshapes - int _leftBlinkIndex = 0; - int _rightBlinkIndex = 1; - int _leftEyeOpenIndex = 8; - int _rightEyeOpenIndex = 9; - - // Brows - int _browDownLeftIndex = 14; - int _browDownRightIndex = 15; - int _browUpCenterIndex = 16; - int _browUpLeftIndex = 17; - int _browUpRightIndex = 18; - - int _mouthSmileLeftIndex = 28; - int _mouthSmileRightIndex = 29; - - int _jawOpenIndex = 21; -}; - -#endif // hifi_Faceshift_h diff --git a/interface/src/devices/Leapmotion.h b/interface/src/devices/Leapmotion.h index d7981a65e8..6ecec8ccf9 100644 --- a/interface/src/devices/Leapmotion.h +++ b/interface/src/devices/Leapmotion.h @@ -14,7 +14,7 @@ #include -#include "MotionTracker.h" +#include #ifdef HAVE_LEAPMOTION #include diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 39b37e3d19..99dbb1a28e 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -79,6 +79,25 @@ int main(int argc, const char* argv[]) { instanceMightBeRunning = false; } + QCommandLineParser parser; + QCommandLineOption checkMinSpecOption("checkMinSpec", "Check if machine meets minimum specifications"); + QCommandLineOption runServerOption("runServer", "Whether to run the server"); + QCommandLineOption serverContentPathOption("serverContentPath", "Where to find server content", "serverContentPath"); + QCommandLineOption allowMultipleInstancesOption("allowMultipleInstances", "Allow multiple instances to run"); + parser.addOption(checkMinSpecOption); + parser.addOption(runServerOption); + parser.addOption(serverContentPathOption); + parser.addOption(allowMultipleInstancesOption); + parser.parse(arguments); + bool runServer = parser.isSet(runServerOption); + bool serverContentPathOptionIsSet = parser.isSet(serverContentPathOption); + QString serverContentPathOptionValue = serverContentPathOptionIsSet ? parser.value(serverContentPathOption) : QString(); + bool allowMultipleInstances = parser.isSet(allowMultipleInstancesOption); + + if (allowMultipleInstances) { + instanceMightBeRunning = false; + } + if (instanceMightBeRunning) { // Try to connect and send message to existing interface instance QLocalSocket socket; @@ -137,18 +156,6 @@ int main(int argc, const char* argv[]) { } } - QCommandLineParser parser; - QCommandLineOption checkMinSpecOption("checkMinSpec", "Check if machine meets minimum specifications"); - QCommandLineOption runServerOption("runServer", "Whether to run the server"); - QCommandLineOption serverContentPathOption("serverContentPath", "Where to find server content", "serverContentPath"); - parser.addOption(checkMinSpecOption); - parser.addOption(runServerOption); - parser.addOption(serverContentPathOption); - parser.parse(arguments); - bool runServer = parser.isSet(runServerOption); - bool serverContentPathOptionIsSet = parser.isSet(serverContentPathOption); - QString serverContentPathOptionValue = serverContentPathOptionIsSet ? parser.value(serverContentPathOption) : QString(); - QElapsedTimer startupTime; startupTime.start(); diff --git a/interface/src/scripting/ControllerScriptingInterface.cpp b/interface/src/scripting/ControllerScriptingInterface.cpp index 0d0c2ef668..f3ec3cd79d 100644 --- a/interface/src/scripting/ControllerScriptingInterface.cpp +++ b/interface/src/scripting/ControllerScriptingInterface.cpp @@ -17,7 +17,7 @@ #include #include "Application.h" -#include "devices/MotionTracker.h" +#include void ControllerScriptingInterface::handleMetaEvent(HFMetaEvent* event) { if (event->type() == HFActionEvent::startType()) { diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 39c2f2e402..1e14c24da3 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -28,6 +28,7 @@ static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); static const QString LAST_BROWSE_LOCATION_SETTING = "LastBrowseLocation"; +static const QString LAST_BROWSE_ASSETS_LOCATION_SETTING = "LastBrowseAssetsLocation"; QScriptValue CustomPromptResultToScriptValue(QScriptEngine* engine, const CustomPromptResult& result) { @@ -149,6 +150,15 @@ void WindowScriptingInterface::setPreviousBrowseLocation(const QString& location Setting::Handle(LAST_BROWSE_LOCATION_SETTING).set(location); } +QString WindowScriptingInterface::getPreviousBrowseAssetLocation() const { + QString ASSETS_ROOT_PATH = "/"; + return Setting::Handle(LAST_BROWSE_ASSETS_LOCATION_SETTING, ASSETS_ROOT_PATH).get(); +} + +void WindowScriptingInterface::setPreviousBrowseAssetLocation(const QString& location) { + Setting::Handle(LAST_BROWSE_ASSETS_LOCATION_SETTING).set(location); +} + /// Makes sure that the reticle is visible, use this in blocking forms that require a reticle and /// might be in same thread as a script that sets the reticle to invisible void WindowScriptingInterface::ensureReticleVisible() const { @@ -158,6 +168,28 @@ void WindowScriptingInterface::ensureReticleVisible() const { } } +/// Display a "browse to directory" dialog. If `directory` is an invalid file or directory the browser will start at the current +/// working directory. +/// \param const QString& title title of the window +/// \param const QString& directory directory to start the file browser at +/// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog` +/// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue` +QScriptValue WindowScriptingInterface::browseDir(const QString& title, const QString& directory) { + ensureReticleVisible(); + QString path = directory; + if (path.isEmpty()) { + path = getPreviousBrowseLocation(); + } +#ifndef Q_OS_WIN + path = fixupPathForMac(directory); +#endif + QString result = OffscreenUi::getExistingDirectory(nullptr, title, path); + if (!result.isEmpty()) { + setPreviousBrowseLocation(QFileInfo(result).absolutePath()); + } + return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result); +} + /// Display an open file dialog. If `directory` is an invalid file or directory the browser will start at the current /// working directory. /// \param const QString& title title of the window @@ -202,6 +234,31 @@ QScriptValue WindowScriptingInterface::save(const QString& title, const QString& return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result); } +/// Display a select asset dialog that lets the user select an asset from the Asset Server. If `directory` is an invalid +/// directory the browser will start at the root directory. +/// \param const QString& title title of the window +/// \param const QString& directory directory to start the asset browser at +/// \param const QString& nameFilter filter to filter asset names by - see `QFileDialog` +/// \return QScriptValue asset path as a string if one was selected, otherwise `QScriptValue::NullValue` +QScriptValue WindowScriptingInterface::browseAssets(const QString& title, const QString& directory, const QString& nameFilter) { + ensureReticleVisible(); + QString path = directory; + if (path.isEmpty()) { + path = getPreviousBrowseAssetLocation(); + } + if (path.left(1) != "/") { + path = "/" + path; + } + if (path.right(1) != "/") { + path = path + "/"; + } + QString result = OffscreenUi::getOpenAssetName(nullptr, title, path, nameFilter); + if (!result.isEmpty()) { + setPreviousBrowseAssetLocation(QFileInfo(result).absolutePath()); + } + return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result); +} + void WindowScriptingInterface::showAssetServer(const QString& upload) { QMetaObject::invokeMethod(qApp, "showAssetServerWidget", Qt::QueuedConnection, Q_ARG(QString, upload)); } @@ -243,6 +300,10 @@ void WindowScriptingInterface::makeConnection(bool success, const QString& userN } } +void WindowScriptingInterface::displayAnnouncement(const QString& message) { + emit announcement(message); +} + bool WindowScriptingInterface::isPhysicsEnabled() { return qApp->isPhysicsEnabled(); } diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index d4ff278fea..2b1e48d918 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -51,12 +51,15 @@ public slots: QScriptValue confirm(const QString& message = ""); QScriptValue prompt(const QString& message = "", const QString& defaultText = ""); CustomPromptResult customPrompt(const QVariant& config); + QScriptValue browseDir(const QString& title = "", const QString& directory = ""); QScriptValue browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); + QScriptValue browseAssets(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); void showAssetServer(const QString& upload = ""); void copyToClipboard(const QString& text); void takeSnapshot(bool notify = true, bool includeAnimated = false, float aspectRatio = 0.0f); void makeConnection(bool success, const QString& userNameOrError); + void displayAnnouncement(const QString& message); void shareSnapshot(const QString& path, const QUrl& href = QUrl("")); bool isPhysicsEnabled(); @@ -72,12 +75,13 @@ signals: void svoImportRequested(const QString& url); void domainConnectionRefused(const QString& reasonMessage, int reasonCode, const QString& extraInfo); void stillSnapshotTaken(const QString& pathStillSnapshot, bool notify); - void snapshotShared(const QString& error); + void snapshotShared(bool isError, const QString& reply); void processingGifStarted(const QString& pathStillSnapshot); void processingGifCompleted(const QString& pathAnimatedSnapshot); void connectionAdded(const QString& connectionName); void connectionError(const QString& errorString); + void announcement(const QString& message); void messageBoxClosed(int id, int button); @@ -88,6 +92,9 @@ private: QString getPreviousBrowseLocation() const; void setPreviousBrowseLocation(const QString& location); + QString getPreviousBrowseAssetLocation() const; + void setPreviousBrowseAssetLocation(const QString& location); + void ensureReticleVisible() const; int createMessageBox(QString title, QString text, int buttons, int defaultButton); diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index 7239e49d89..a99fe002ee 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -94,7 +94,7 @@ void ApplicationOverlay::renderQmlUi(RenderArgs* renderArgs) { PROFILE_RANGE(app, __FUNCTION__); if (!_uiTexture) { - _uiTexture = gpu::TexturePointer(gpu::Texture::createExternal(OffscreenQmlSurface::getDiscardLambda())); + _uiTexture = gpu::Texture::createExternal(OffscreenQmlSurface::getDiscardLambda()); _uiTexture->setSource(__FUNCTION__); } // Once we move UI rendering and screen rendering to different @@ -207,13 +207,13 @@ void ApplicationOverlay::buildFramebufferObject() { auto width = uiSize.x; auto height = uiSize.y; if (!_overlayFramebuffer->getDepthStencilBuffer()) { - auto overlayDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(DEPTH_FORMAT, width, height, gpu::Texture::SINGLE_MIP, DEFAULT_SAMPLER)); + auto overlayDepthTexture = gpu::Texture::createRenderBuffer(DEPTH_FORMAT, width, height, gpu::Texture::SINGLE_MIP, DEFAULT_SAMPLER); _overlayFramebuffer->setDepthStencilBuffer(overlayDepthTexture, DEPTH_FORMAT); } if (!_overlayFramebuffer->getRenderBuffer(0)) { const gpu::Sampler OVERLAY_SAMPLER(gpu::Sampler::FILTER_MIN_MAG_LINEAR, gpu::Sampler::WRAP_CLAMP); - auto colorBuffer = gpu::TexturePointer(gpu::Texture::createRenderBuffer(COLOR_FORMAT, width, height, gpu::Texture::SINGLE_MIP, OVERLAY_SAMPLER)); + auto colorBuffer = gpu::Texture::createRenderBuffer(COLOR_FORMAT, width, height, gpu::Texture::SINGLE_MIP, OVERLAY_SAMPLER); _overlayFramebuffer->setRenderBuffer(0, colorBuffer); } } diff --git a/interface/src/ui/AvatarInputs.cpp b/interface/src/ui/AvatarInputs.cpp index 341915e57f..2b715eac9d 100644 --- a/interface/src/ui/AvatarInputs.cpp +++ b/interface/src/ui/AvatarInputs.cpp @@ -11,9 +11,9 @@ #include #include +#include #include "Application.h" -#include "devices/FaceTracker.h" #include "Menu.h" HIFI_QML_DEF(AvatarInputs) diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index a12d9020ae..bf4be7fd17 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include #include @@ -116,11 +115,6 @@ void setupPreferences() { auto preference = new BrowsePreference(SNAPSHOTS, "Put my snapshots here", getter, setter); preferences->addPreference(preference); } - { - auto getter = []()->bool { return SnapshotAnimated::alsoTakeAnimatedSnapshot.get(); }; - auto setter = [](bool value) { SnapshotAnimated::alsoTakeAnimatedSnapshot.set(value); }; - preferences->addPreference(new CheckPreference(SNAPSHOTS, "Take Animated GIF Snapshot", getter, setter)); - } { auto getter = []()->float { return SnapshotAnimated::snapshotAnimatedDuration.get(); }; auto setter = [](float value) { SnapshotAnimated::snapshotAnimatedDuration.set(value); }; @@ -207,13 +201,6 @@ void setupPreferences() { auto setter = [](float value) { FaceTracker::setEyeDeflection(value); }; preferences->addPreference(new SliderPreference(AVATAR_TUNING, "Face tracker eye deflection", getter, setter)); } - { - auto getter = []()->QString { return DependencyManager::get()->getHostname(); }; - auto setter = [](const QString& value) { DependencyManager::get()->setHostname(value); }; - auto preference = new EditPreference(AVATAR_TUNING, "Faceshift hostname", getter, setter); - preference->setPlaceholderText("localhost"); - preferences->addPreference(preference); - } { auto getter = [=]()->QString { return myAvatar->getAnimGraphOverrideUrl().toString(); }; auto setter = [=](const QString& value) { myAvatar->setAnimGraphOverrideUrl(QUrl(value)); }; diff --git a/interface/src/ui/Snapshot.cpp b/interface/src/ui/Snapshot.cpp index f75190530c..59ecce5bc7 100644 --- a/interface/src/ui/Snapshot.cpp +++ b/interface/src/ui/Snapshot.cpp @@ -194,3 +194,10 @@ void Snapshot::uploadSnapshot(const QString& filename, const QUrl& href) { multiPart); } +QString Snapshot::getSnapshotsLocation() { + return snapshotsLocation.get(""); +} + +void Snapshot::setSnapshotsLocation(const QString& location) { + snapshotsLocation.set(location); +} diff --git a/interface/src/ui/Snapshot.h b/interface/src/ui/Snapshot.h index 14e1bc2e9f..93ffbbc7bb 100644 --- a/interface/src/ui/Snapshot.h +++ b/interface/src/ui/Snapshot.h @@ -18,6 +18,7 @@ #include #include +#include class QFile; class QTemporaryFile; @@ -32,7 +33,9 @@ private: QUrl _URL; }; -class Snapshot { +class Snapshot : public QObject, public Dependency { + Q_OBJECT + SINGLETON_DEPENDENCY public: static QString saveSnapshot(QImage image); static QTemporaryFile* saveTempSnapshot(QImage image); @@ -40,6 +43,10 @@ public: static Setting::Handle snapshotsLocation; static void uploadSnapshot(const QString& filename, const QUrl& href = QUrl("")); + +public slots: + Q_INVOKABLE QString getSnapshotsLocation(); + Q_INVOKABLE void setSnapshotsLocation(const QString& location); private: static QFile* savedFileForSnapshot(QImage & image, bool isTemporary); }; diff --git a/interface/src/ui/SnapshotUploader.cpp b/interface/src/ui/SnapshotUploader.cpp index 411e892de5..aa37608476 100644 --- a/interface/src/ui/SnapshotUploader.cpp +++ b/interface/src/ui/SnapshotUploader.cpp @@ -49,6 +49,7 @@ void SnapshotUploader::uploadSuccess(QNetworkReply& reply) { userStoryObject.insert("place_name", placeName); userStoryObject.insert("path", currentPath); userStoryObject.insert("action", "snapshot"); + userStoryObject.insert("audience", "for_url"); rootObject.insert("user_story", userStoryObject); auto accountManager = DependencyManager::get(); @@ -61,7 +62,7 @@ void SnapshotUploader::uploadSuccess(QNetworkReply& reply) { QJsonDocument(rootObject).toJson()); } else { - emit DependencyManager::get()->snapshotShared(contents); + emit DependencyManager::get()->snapshotShared(true, contents); delete this; } } @@ -72,12 +73,13 @@ void SnapshotUploader::uploadFailure(QNetworkReply& reply) { if (replyString.size() == 0) { replyString = reply.errorString(); } - emit DependencyManager::get()->snapshotShared(replyString); // maybe someday include _inWorldLocation, _filename? + emit DependencyManager::get()->snapshotShared(true, replyString); // maybe someday include _inWorldLocation, _filename? delete this; } void SnapshotUploader::createStorySuccess(QNetworkReply& reply) { - emit DependencyManager::get()->snapshotShared(QString()); + QString replyString = reply.readAll(); + emit DependencyManager::get()->snapshotShared(false, replyString); delete this; } @@ -87,7 +89,7 @@ void SnapshotUploader::createStoryFailure(QNetworkReply& reply) { if (replyString.size() == 0) { replyString = reply.errorString(); } - emit DependencyManager::get()->snapshotShared(replyString); + emit DependencyManager::get()->snapshotShared(true, replyString); delete this; } diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index 01740b88ca..803104dd6d 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -31,6 +31,7 @@ #include "Menu.h" #include "Util.h" #include "SequenceNumberStats.h" +#include "StatTracker.h" HIFI_QML_DEF(Stats) @@ -250,6 +251,9 @@ void Stats::updateStats(bool force) { STAT_UPDATE(downloads, loadingRequests.size()); STAT_UPDATE(downloadLimit, ResourceCache::getRequestLimit()) STAT_UPDATE(downloadsPending, ResourceCache::getPendingRequestCount()); + STAT_UPDATE(processing, DependencyManager::get()->getStat("Processing").toInt()); + STAT_UPDATE(processingPending, DependencyManager::get()->getStat("PendingProcessing").toInt()); + // See if the active download urls have changed bool shouldUpdateUrls = _downloads != _downloadUrls.size(); diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index a2ed125008..85c64bae90 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -89,6 +89,8 @@ class Stats : public QQuickItem { STATS_PROPERTY(int, downloadLimit, 0) STATS_PROPERTY(int, downloadsPending, 0) Q_PROPERTY(QStringList downloadUrls READ downloadUrls NOTIFY downloadUrlsChanged) + STATS_PROPERTY(int, processing, 0) + STATS_PROPERTY(int, processingPending, 0) STATS_PROPERTY(int, triangles, 0) STATS_PROPERTY(int, quads, 0) STATS_PROPERTY(int, materialSwitches, 0) @@ -214,6 +216,8 @@ signals: void downloadLimitChanged(); void downloadsPendingChanged(); void downloadUrlsChanged(); + void processingChanged(); + void processingPendingChanged(); void trianglesChanged(); void quadsChanged(); void materialSwitchesChanged(); diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index ccaa1d4fbc..e993166558 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -126,6 +126,55 @@ void ModelOverlay::setProperties(const QVariantMap& properties) { QMetaObject::invokeMethod(_model.get(), "setTextures", Qt::AutoConnection, Q_ARG(const QVariantMap&, textureMap)); } + + // relative + auto jointTranslationsValue = properties["jointTranslations"]; + if (jointTranslationsValue.canConvert(QVariant::List)) { + const QVariantList& jointTranslations = jointTranslationsValue.toList(); + int translationCount = jointTranslations.size(); + int jointCount = _model->getJointStateCount(); + if (translationCount < jointCount) { + jointCount = translationCount; + } + for (int i=0; i < jointCount; i++) { + const auto& translationValue = jointTranslations[i]; + if (translationValue.isValid()) { + _model->setJointTranslation(i, true, vec3FromVariant(translationValue), 1.0f); + } + } + _updateModel = true; + } + + // relative + auto jointRotationsValue = properties["jointRotations"]; + if (jointRotationsValue.canConvert(QVariant::List)) { + const QVariantList& jointRotations = jointRotationsValue.toList(); + int rotationCount = jointRotations.size(); + int jointCount = _model->getJointStateCount(); + if (rotationCount < jointCount) { + jointCount = rotationCount; + } + for (int i=0; i < jointCount; i++) { + const auto& rotationValue = jointRotations[i]; + if (rotationValue.isValid()) { + _model->setJointRotation(i, true, quatFromVariant(rotationValue), 1.0f); + } + } + _updateModel = true; + } +} + +template +vectorType ModelOverlay::mapJoints(mapFunction function) const { + vectorType result; + if (_model && _model->isActive()) { + const int jointCount = _model->getJointStateCount(); + result.reserve(jointCount); + for (int i = 0; i < jointCount; i++) { + result << function(i); + } + } + return result; } QVariant ModelOverlay::getProperty(const QString& property) { @@ -150,6 +199,58 @@ QVariant ModelOverlay::getProperty(const QString& property) { } } + if (property == "jointNames") { + if (_model && _model->isActive()) { + // note: going through Rig because Model::getJointNames() (which proxies to FBXGeometry) was always empty + const RigPointer rig = _model->getRig(); + if (rig) { + return mapJoints([rig](int jointIndex) -> QString { + return rig->nameOfJoint(jointIndex); + }); + } + } + } + + // relative + if (property == "jointRotations") { + return mapJoints( + [this](int jointIndex) -> QVariant { + glm::quat rotation; + _model->getJointRotation(jointIndex, rotation); + return quatToVariant(rotation); + }); + } + + // relative + if (property == "jointTranslations") { + return mapJoints( + [this](int jointIndex) -> QVariant { + glm::vec3 translation; + _model->getJointTranslation(jointIndex, translation); + return vec3toVariant(translation); + }); + } + + // absolute + if (property == "jointOrientations") { + return mapJoints( + [this](int jointIndex) -> QVariant { + glm::quat orientation; + _model->getJointRotationInWorldFrame(jointIndex, orientation); + return quatToVariant(orientation); + }); + } + + // absolute + if (property == "jointPositions") { + return mapJoints( + [this](int jointIndex) -> QVariant { + glm::vec3 position; + _model->getJointPositionInWorldFrame(jointIndex, position); + return vec3toVariant(position); + }); + } + return Volume3DOverlay::getProperty(property); } diff --git a/interface/src/ui/overlays/ModelOverlay.h b/interface/src/ui/overlays/ModelOverlay.h index a3ddeed480..8afe9a20b6 100644 --- a/interface/src/ui/overlays/ModelOverlay.h +++ b/interface/src/ui/overlays/ModelOverlay.h @@ -41,6 +41,12 @@ public: void locationChanged(bool tellPhysics) override; +protected: + // helper to extract metadata from our Model's rigged joints + template using mapFunction = std::function; + template + vectorType mapJoints(mapFunction function) const; + private: ModelPointer _model; diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index f80589e5a1..fedead5aa5 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -51,6 +51,7 @@ #include "ui/AvatarInputs.h" #include "avatar/AvatarManager.h" #include "scripting/GlobalServicesScriptingInterface.h" +#include "ui/Snapshot.h" static const float DPI = 30.47f; static const float INCHES_TO_METERS = 1.0f / 39.3701f; @@ -177,6 +178,7 @@ void Web3DOverlay::loadSourceURL() { _webSurface->getRootContext()->setContextProperty("Quat", new Quat()); _webSurface->getRootContext()->setContextProperty("MyAvatar", DependencyManager::get()->getMyAvatar().get()); _webSurface->getRootContext()->setContextProperty("Entities", DependencyManager::get().data()); + _webSurface->getRootContext()->setContextProperty("Snapshot", DependencyManager::get().data()); if (_webSurface->getRootItem() && _webSurface->getRootItem()->objectName() == "tabletRoot") { auto tabletScriptingInterface = DependencyManager::get(); @@ -298,7 +300,7 @@ void Web3DOverlay::render(RenderArgs* args) { if (!_texture) { auto webSurface = _webSurface; - _texture = gpu::TexturePointer(gpu::Texture::createExternal(OffscreenQmlSurface::getDiscardLambda())); + _texture = gpu::Texture::createExternal(OffscreenQmlSurface::getDiscardLambda()); _texture->setSource(__FUNCTION__); } OffscreenQmlSurface::TextureAndFence newTextureAndFence; diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index 2c9376d591..6edd969568 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -86,7 +86,9 @@ void AnimInverseKinematics::setTargetVars( 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; + for (auto& targetVar : _targetVarVec) { if (targetVar.jointIndex == -1) { // this targetVar hasn't been validated yet... @@ -105,15 +107,18 @@ void AnimInverseKinematics::computeTargets(const AnimVariantMap& animVars, std:: AnimPose defaultPose = _skeleton->getAbsolutePose(targetVar.jointIndex, underPoses); glm::quat rotation = animVars.lookupRigToGeometry(targetVar.rotationVar, defaultPose.rot()); glm::vec3 translation = animVars.lookupRigToGeometry(targetVar.positionVar, defaultPose.trans()); - if (target.getType() == IKTarget::Type::HipsRelativeRotationAndPosition) { - translation += _hipsOffset; - } + target.setPose(rotation, translation); target.setIndex(targetVar.jointIndex); 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; + } } } } @@ -242,18 +247,21 @@ int AnimInverseKinematics::solveTargetWithCCD(const IKTarget& target, AnimPoseVe // the tip's parent-relative as we proceed up the chain glm::quat tipParentOrientation = absolutePoses[pivotIndex].rot(); + // 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) { + // rotate tip directly to target orientation tipOrientation = target.getRotation(); - glm::quat tipRelativeRotation = glm::normalize(tipOrientation * glm::inverse(tipParentOrientation)); + glm::quat tipRelativeRotation = glm::inverse(tipParentOrientation) * tipOrientation; - // enforce tip's constraint + // then enforce tip's constraint RotationConstraint* constraint = getConstraint(tipIndex); if (constraint) { bool constrained = constraint->apply(tipRelativeRotation); if (constrained) { - tipOrientation = glm::normalize(tipRelativeRotation * tipParentOrientation); - tipRelativeRotation = glm::normalize(tipOrientation * glm::inverse(tipParentOrientation)); + tipOrientation = tipParentOrientation * tipRelativeRotation; + tipRelativeRotation = tipRelativeRotation; } } // store the relative rotation change in the accumulator @@ -277,7 +285,9 @@ int AnimInverseKinematics::solveTargetWithCCD(const IKTarget& target, AnimPoseVe const float MIN_AXIS_LENGTH = 1.0e-4f; RotationConstraint* constraint = getConstraint(pivotIndex); - if (constraint && constraint->isLowerSpine() && tipIndex != _headIndex) { + + // only allow swing on lowerSpine if there is a hips IK target. + if (_hipsTargetIndex < 0 && constraint && constraint->isLowerSpine() && tipIndex != _headIndex) { // for these types of targets we only allow twist at the lower-spine // (this prevents the hand targets from bending the spine too much and thereby driving the hips too far) glm::vec3 twistAxis = absolutePoses[pivotIndex].trans() - absolutePoses[pivotsParentIndex].trans(); @@ -420,13 +430,13 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars _relativePoses[i].trans() = underPoses[i].trans(); } - if (!_relativePoses.empty()) { + if (!underPoses.empty()) { // Sometimes the underpose itself can violate the constraints. Rather than // clamp the animation we dynamically expand each constraint to accomodate it. std::map::iterator constraintItr = _constraints.begin(); while (constraintItr != _constraints.end()) { int index = constraintItr->first; - constraintItr->second->dynamicallyAdjustLimits(_relativePoses[index].rot()); + constraintItr->second->dynamicallyAdjustLimits(underPoses[index].rot()); ++constraintItr; } } @@ -441,64 +451,76 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars computeTargets(animVars, targets, underPoses); } - // debug render ik targets - if (context.getEnableDebugDrawIKTargets()) { - const vec4 WHITE(1.0f); - glm::mat4 rigToAvatarMat = createMatFromQuatAndPos(Quaternions::Y_180, glm::vec3()); - - for (auto& target : targets) { - glm::mat4 geomTargetMat = createMatFromQuatAndPos(target.getRotation(), target.getTranslation()); - glm::mat4 avatarTargetMat = rigToAvatarMat * context.getGeometryToRigMatrix() * geomTargetMat; - - QString name = QString("ikTarget%1").arg(target.getIndex()); - DebugDraw::getInstance().addMyAvatarMarker(name, glmExtractRotation(avatarTargetMat), extractTranslation(avatarTargetMat), WHITE); - } - } else if (context.getEnableDebugDrawIKTargets() != _previousEnableDebugIKTargets) { - // remove markers if they were added last frame. - for (auto& target : targets) { - QString name = QString("ikTarget%1").arg(target.getIndex()); - DebugDraw::getInstance().removeMyAvatarMarker(name); - } - } - - _previousEnableDebugIKTargets = context.getEnableDebugDrawIKTargets(); - if (targets.empty()) { - // no IK targets but still need to enforce constraints - std::map::iterator constraintItr = _constraints.begin(); - while (constraintItr != _constraints.end()) { - int index = constraintItr->first; - glm::quat rotation = _relativePoses[index].rot(); - constraintItr->second->apply(rotation); - _relativePoses[index].rot() = rotation; - ++constraintItr; - } + _relativePoses = underPoses; } else { { PROFILE_RANGE_EX(simulation_animation, "ik/shiftHips", 0xffff00ff, 0); - // 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) { - // but only if offset is long enough - float scaleFactor = ((offsetLength - MIN_HIPS_OFFSET_LENGTH) / offsetLength); - if (_hipsParentIndex == -1) { - // the hips are the root so _hipsOffset is in the correct frame - _relativePoses[_hipsIndex].trans() = underPoses[_hipsIndex].trans() + scaleFactor * _hipsOffset; + if (_hipsTargetIndex >= 0 && _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 { - // the hips are NOT the root so we need to transform _hipsOffset into hips local-frame - glm::quat hipsFrameRotation = _relativePoses[_hipsParentIndex].rot(); - int index = _skeleton->getParentIndex(_hipsParentIndex); - while (index != -1) { - hipsFrameRotation *= _relativePoses[index].rot(); - index = _skeleton->getParentIndex(index); + _relativePoses[_hipsIndex] = absPose; + } + } else { + // 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() = underPoses[_hipsIndex].trans() + hipsOffset; + } else { + auto absHipsPose = _skeleton->getAbsolutePose(_hipsIndex, underPoses); + absHipsPose.trans() += hipsOffset; + _relativePoses[_hipsIndex] = _skeleton->getAbsolutePose(_hipsParentIndex, _relativePoses).inverse() * absHipsPose; } - _relativePoses[_hipsIndex].trans() = underPoses[_hipsIndex].trans() - + glm::inverse(glm::normalize(hipsFrameRotation)) * (scaleFactor * _hipsOffset); } } + + // update all HipsRelative targets to account for the hips shift/ik target. + auto shiftedHipsAbsPose = _skeleton->getAbsolutePose(_hipsIndex, _relativePoses); + auto underHipsAbsPose = _skeleton->getAbsolutePose(_hipsIndex, underPoses); + auto absHipsOffset = shiftedHipsAbsPose.trans() - underHipsAbsPose.trans(); + for (auto& target: targets) { + if (target.getType() == IKTarget::Type::HipsRelativeRotationAndPosition) { + auto pose = target.getPose(); + pose.trans() = pose.trans() + absHipsOffset; + target.setPose(pose.rot(), pose.trans()); + } + } + } + + { + PROFILE_RANGE_EX(simulation_animation, "ik/debugDraw", 0xffff00ff, 0); + + // debug render ik targets + if (context.getEnableDebugDrawIKTargets()) { + const vec4 WHITE(1.0f); + glm::mat4 rigToAvatarMat = createMatFromQuatAndPos(Quaternions::Y_180, glm::vec3()); + + for (auto& target : targets) { + glm::mat4 geomTargetMat = createMatFromQuatAndPos(target.getRotation(), target.getTranslation()); + glm::mat4 avatarTargetMat = rigToAvatarMat * context.getGeometryToRigMatrix() * geomTargetMat; + + QString name = QString("ikTarget%1").arg(target.getIndex()); + DebugDraw::getInstance().addMyAvatarMarker(name, glmExtractRotation(avatarTargetMat), extractTranslation(avatarTargetMat), WHITE); + } + } else if (context.getEnableDebugDrawIKTargets() != _previousEnableDebugIKTargets) { + // remove markers if they were added last frame. + for (auto& target : targets) { + QString name = QString("ikTarget%1").arg(target.getIndex()); + DebugDraw::getInstance().removeMyAvatarMarker(name); + } + } + + _previousEnableDebugIKTargets = context.getEnableDebugDrawIKTargets(); } { @@ -506,64 +528,70 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars solveWithCyclicCoordinateDescent(targets); } - { + if (_hipsTargetIndex < 0) { PROFILE_RANGE_EX(simulation_animation, "ik/measureHipsOffset", 0xffff00ff, 0); - - // 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 newHipsOffset = Vectors::ZERO; - for (auto& target: targets) { - int targetIndex = target.getIndex(); - if (targetIndex == _headIndex && _headIndex != -1) { - // special handling for headTarget - if (target.getType() == IKTarget::Type::RotationOnly) { - // we want to shift the hips to bring the underPose closer - // to where the head happens to be (overpose) - glm::vec3 under = _skeleton->getAbsolutePose(_headIndex, underPoses).trans(); - glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans(); - const float HEAD_OFFSET_SLAVE_FACTOR = 0.65f; - newHipsOffset += HEAD_OFFSET_SLAVE_FACTOR * (actual - under); - } 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; - // and ignore all other targets - newHipsOffset = _hipsOffset; - break; - } else if (target.getType() == IKTarget::Type::RotationAndPosition) { - glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans(); - glm::vec3 targetPosition = target.getTranslation(); - newHipsOffset += targetPosition - actualPosition; - - // Add downward pressure on the hips - newHipsOffset *= 0.95f; - newHipsOffset -= 1.0f; - } - } else if (target.getType() == IKTarget::Type::RotationAndPosition) { - glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans(); - glm::vec3 targetPosition = target.getTranslation(); - newHipsOffset += targetPosition - actualPosition; - } - } - - // 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; - - // clamp the hips offset - float hipsOffsetLength = glm::length(_hipsOffset); - if (hipsOffsetLength > _maxHipsOffsetLength) { - _hipsOffset *= _maxHipsOffsetLength / hipsOffsetLength; - } - + computeHipsOffset(targets, underPoses, dt); + } else { + _hipsOffset = Vectors::ZERO; } } } return _relativePoses; } +void AnimInverseKinematics::computeHipsOffset(const std::vector& targets, const AnimPoseVec& underPoses, float dt) { + // 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 newHipsOffset = Vectors::ZERO; + for (auto& target: targets) { + int targetIndex = target.getIndex(); + if (targetIndex == _headIndex && _headIndex != -1) { + // special handling for headTarget + if (target.getType() == IKTarget::Type::RotationOnly) { + // we want to shift the hips to bring the underPose closer + // to where the head happens to be (overpose) + glm::vec3 under = _skeleton->getAbsolutePose(_headIndex, underPoses).trans(); + glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans(); + const float HEAD_OFFSET_SLAVE_FACTOR = 0.65f; + newHipsOffset += HEAD_OFFSET_SLAVE_FACTOR * (actual - under); + } 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; + // and ignore all other targets + newHipsOffset = _hipsOffset; + break; + } else if (target.getType() == IKTarget::Type::RotationAndPosition) { + glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans(); + glm::vec3 targetPosition = target.getTranslation(); + newHipsOffset += targetPosition - actualPosition; + + // Add downward pressure on the hips + const float PRESSURE_SCALE_FACTOR = 0.95f; + const float PRESSURE_TRANSLATION_OFFSET = 1.0f; + newHipsOffset *= PRESSURE_SCALE_FACTOR; + newHipsOffset -= PRESSURE_TRANSLATION_OFFSET; + } + } else if (target.getType() == IKTarget::Type::RotationAndPosition) { + glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans(); + glm::vec3 targetPosition = target.getTranslation(); + newHipsOffset += targetPosition - actualPosition; + } + } + + // 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; + + // clamp the hips offset + float hipsOffsetLength = glm::length(_hipsOffset); + if (hipsOffsetLength > _maxHipsOffsetLength) { + _hipsOffset *= _maxHipsOffsetLength / hipsOffsetLength; + } +} + void AnimInverseKinematics::setMaxHipsOffsetLength(float maxLength) { // manually adjust scale here const float METERS_TO_CENTIMETERS = 100.0f; @@ -594,6 +622,22 @@ void AnimInverseKinematics::clearConstraints() { _constraints.clear(); } +// set up swing limits around a swingTwistConstraint in an ellipse, where lateralSwingTheta is the swing limit for lateral swings (side to side) +// anteriorSwingTheta is swing limit for forward and backward swings. (where x-axis of reference rotation is sideways and -z-axis is forward) +static void setEllipticalSwingLimits(SwingTwistConstraint* stConstraint, float lateralSwingTheta, float anteriorSwingTheta) { + assert(stConstraint); + const int NUM_SUBDIVISIONS = 8; + std::vector minDots; + minDots.reserve(NUM_SUBDIVISIONS); + float dTheta = TWO_PI / NUM_SUBDIVISIONS; + float theta = 0.0f; + for (int i = 0; i < NUM_SUBDIVISIONS; i++) { + minDots.push_back(cosf(glm::length(glm::vec2(anteriorSwingTheta * cosf(theta), lateralSwingTheta * sinf(theta))))); + theta += dTheta; + } + stConstraint->setSwingLimits(minDots); +} + void AnimInverseKinematics::initConstraints() { if (!_skeleton) { return; @@ -783,41 +827,31 @@ void AnimInverseKinematics::initConstraints() { } else if (baseName.startsWith("Spine", Qt::CaseSensitive)) { SwingTwistConstraint* stConstraint = new SwingTwistConstraint(); stConstraint->setReferenceRotation(_defaultRelativePoses[i].rot()); - const float MAX_SPINE_TWIST = PI / 12.0f; + const float MAX_SPINE_TWIST = PI / 20.0f; stConstraint->setTwistLimits(-MAX_SPINE_TWIST, MAX_SPINE_TWIST); - std::vector minDots; - const float MAX_SPINE_SWING = PI / 10.0f; - minDots.push_back(cosf(MAX_SPINE_SWING)); - stConstraint->setSwingLimits(minDots); + // limit lateral swings more then forward-backward swings + const float MAX_SPINE_LATERAL_SWING = PI / 30.0f; + const float MAX_SPINE_ANTERIOR_SWING = PI / 20.0f; + setEllipticalSwingLimits(stConstraint, MAX_SPINE_LATERAL_SWING, MAX_SPINE_ANTERIOR_SWING); + if (0 == baseName.compare("Spine1", Qt::CaseSensitive) || 0 == baseName.compare("Spine", Qt::CaseSensitive)) { stConstraint->setLowerSpine(true); } constraint = static_cast(stConstraint); - } else if (baseName.startsWith("Hips2", Qt::CaseSensitive)) { - SwingTwistConstraint* stConstraint = new SwingTwistConstraint(); - stConstraint->setReferenceRotation(_defaultRelativePoses[i].rot()); - const float MAX_SPINE_TWIST = PI / 8.0f; - stConstraint->setTwistLimits(-MAX_SPINE_TWIST, MAX_SPINE_TWIST); - std::vector minDots; - const float MAX_SPINE_SWING = PI / 14.0f; - minDots.push_back(cosf(MAX_SPINE_SWING)); - stConstraint->setSwingLimits(minDots); - - constraint = static_cast(stConstraint); } else if (0 == baseName.compare("Neck", Qt::CaseSensitive)) { SwingTwistConstraint* stConstraint = new SwingTwistConstraint(); stConstraint->setReferenceRotation(_defaultRelativePoses[i].rot()); - const float MAX_NECK_TWIST = PI / 9.0f; + const float MAX_NECK_TWIST = PI / 10.0f; stConstraint->setTwistLimits(-MAX_NECK_TWIST, MAX_NECK_TWIST); - std::vector minDots; - const float MAX_NECK_SWING = PI / 8.0f; - minDots.push_back(cosf(MAX_NECK_SWING)); - stConstraint->setSwingLimits(minDots); + // limit lateral swings more then forward-backward swings + const float MAX_NECK_LATERAL_SWING = PI / 10.0f; + const float MAX_NECK_ANTERIOR_SWING = PI / 8.0f; + setEllipticalSwingLimits(stConstraint, MAX_NECK_LATERAL_SWING, MAX_NECK_ANTERIOR_SWING); constraint = static_cast(stConstraint); } else if (0 == baseName.compare("Head", Qt::CaseSensitive)) { @@ -872,7 +906,7 @@ void AnimInverseKinematics::initConstraints() { // we determine the max/min angles by rotating the swing limit lines from parent- to child-frame // then measure the angles to swing the yAxis into alignment - const float MIN_KNEE_ANGLE = 0.0f; + const float MIN_KNEE_ANGLE = 0.097f; // ~5 deg const float MAX_KNEE_ANGLE = 7.0f * PI / 8.0f; glm::quat invReferenceRotation = glm::inverse(referenceRotation); glm::vec3 minSwingAxis = invReferenceRotation * glm::angleAxis(MIN_KNEE_ANGLE, hingeAxis) * Vectors::UNIT_Y; diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h index 366e5f765e..c91b7aa9c4 100644 --- a/libraries/animation/src/AnimInverseKinematics.h +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -55,6 +55,7 @@ protected: RotationConstraint* getConstraint(int index); void clearConstraints(); void initConstraints(); + void computeHipsOffset(const std::vector& targets, const AnimPoseVec& underPoses, float dt); // no copies AnimInverseKinematics(const AnimInverseKinematics&) = delete; @@ -91,6 +92,7 @@ protected: int _headIndex { -1 }; int _hipsIndex { -1 }; int _hipsParentIndex { -1 }; + int _hipsTargetIndex { -1 }; // _maxTargetIndex is tracked to help optimize the recalculation of absolute poses // during the the cyclic coordinate descent algorithm diff --git a/libraries/animation/src/AnimManipulator.cpp b/libraries/animation/src/AnimManipulator.cpp index 111501898a..070949ab3b 100644 --- a/libraries/animation/src/AnimManipulator.cpp +++ b/libraries/animation/src/AnimManipulator.cpp @@ -12,6 +12,16 @@ #include "AnimUtil.h" #include "AnimationLogging.h" +AnimManipulator::JointVar::JointVar(const QString& jointNameIn, Type rotationTypeIn, Type translationTypeIn, + const QString& rotationVarIn, const QString& translationVarIn) : + jointName(jointNameIn), + rotationType(rotationTypeIn), + translationType(translationTypeIn), + rotationVar(rotationVarIn), + translationVar(translationVarIn), + jointIndex(-1), + hasPerformedJointLookup(false) {} + AnimManipulator::AnimManipulator(const QString& id, float alpha) : AnimNode(AnimNode::Type::Manipulator, id), _alpha(alpha) { @@ -36,7 +46,10 @@ const AnimPoseVec& AnimManipulator::overlay(const AnimVariantMap& animVars, cons } for (auto& jointVar : _jointVars) { + if (!jointVar.hasPerformedJointLookup) { + + // map from joint name to joint index and cache the result. jointVar.jointIndex = _skeleton->nameToJointIndex(jointVar.jointName); if (jointVar.jointIndex < 0) { qCWarning(animation) << "AnimManipulator could not find jointName" << jointVar.jointName << "in skeleton"; @@ -100,34 +113,62 @@ AnimPose AnimManipulator::computeRelativePoseFromJointVar(const AnimVariantMap& AnimPose defaultAbsPose = _skeleton->getAbsolutePose(jointVar.jointIndex, underPoses); - if (jointVar.type == JointVar::Type::AbsoluteRotation || jointVar.type == JointVar::Type::AbsolutePosition) { + // compute relative translation + glm::vec3 relTrans; + switch (jointVar.translationType) { + case JointVar::Type::Absolute: { + glm::vec3 absTrans = animVars.lookupRigToGeometry(jointVar.translationVar, defaultAbsPose.trans()); - if (jointVar.type == JointVar::Type::AbsoluteRotation) { - defaultAbsPose.rot() = animVars.lookupRigToGeometry(jointVar.var, defaultAbsPose.rot()); - } else if (jointVar.type == JointVar::Type::AbsolutePosition) { - defaultAbsPose.trans() = animVars.lookupRigToGeometry(jointVar.var, defaultAbsPose.trans()); + // convert to from absolute to relative. + AnimPose parentAbsPose; + int parentIndex = _skeleton->getParentIndex(jointVar.jointIndex); + if (parentIndex >= 0) { + parentAbsPose = _skeleton->getAbsolutePose(parentIndex, underPoses); + } + + // convert from absolute to relative + relTrans = transformPoint(parentAbsPose.inverse(), absTrans); + break; } - - // because jointVar is absolute, we must use an absolute parent frame to convert into a relative pose. - AnimPose parentAbsPose = AnimPose::identity; - int parentIndex = _skeleton->getParentIndex(jointVar.jointIndex); - if (parentIndex >= 0) { - parentAbsPose = _skeleton->getAbsolutePose(parentIndex, underPoses); - } - - // convert from absolute to relative - return parentAbsPose.inverse() * defaultAbsPose; - - } else { - - // override the default rel pose - AnimPose relPose = defaultRelPose; - if (jointVar.type == JointVar::Type::RelativeRotation) { - relPose.rot() = animVars.lookupRigToGeometry(jointVar.var, defaultRelPose.rot()); - } else if (jointVar.type == JointVar::Type::RelativePosition) { - relPose.trans() = animVars.lookupRigToGeometry(jointVar.var, defaultRelPose.trans()); - } - - return relPose; + case JointVar::Type::Relative: + relTrans = animVars.lookupRigToGeometryVector(jointVar.translationVar, defaultRelPose.trans()); + break; + case JointVar::Type::UnderPose: + relTrans = underPoses[jointVar.jointIndex].trans(); + break; + case JointVar::Type::Default: + default: + relTrans = defaultRelPose.trans(); + break; } + + glm::quat relRot; + switch (jointVar.rotationType) { + case JointVar::Type::Absolute: { + glm::quat absRot = animVars.lookupRigToGeometry(jointVar.translationVar, defaultAbsPose.rot()); + + // convert to from absolute to relative. + AnimPose parentAbsPose; + int parentIndex = _skeleton->getParentIndex(jointVar.jointIndex); + if (parentIndex >= 0) { + parentAbsPose = _skeleton->getAbsolutePose(parentIndex, underPoses); + } + + // convert from absolute to relative + relRot = glm::inverse(parentAbsPose.rot()) * absRot; + break; + } + case JointVar::Type::Relative: + relRot = animVars.lookupRigToGeometry(jointVar.translationVar, defaultRelPose.rot()); + break; + case JointVar::Type::UnderPose: + relRot = underPoses[jointVar.jointIndex].rot(); + break; + case JointVar::Type::Default: + default: + relRot = defaultRelPose.rot(); + break; + } + + return AnimPose(glm::vec3(1), relRot, relTrans); } diff --git a/libraries/animation/src/AnimManipulator.h b/libraries/animation/src/AnimManipulator.h index 26f50a7dd9..1134f75da9 100644 --- a/libraries/animation/src/AnimManipulator.h +++ b/libraries/animation/src/AnimManipulator.h @@ -31,17 +31,20 @@ public: struct JointVar { enum class Type { - AbsoluteRotation = 0, - AbsolutePosition, - RelativeRotation, - RelativePosition, + Absolute, + Relative, + UnderPose, + Default, NumTypes }; - JointVar(const QString& varIn, const QString& jointNameIn, Type typeIn) : var(varIn), jointName(jointNameIn), type(typeIn), jointIndex(-1), hasPerformedJointLookup(false) {} - QString var = ""; + JointVar(const QString& jointNameIn, Type rotationType, Type translationType, const QString& rotationVarIn, const QString& translationVarIn); QString jointName = ""; - Type type = Type::AbsoluteRotation; + Type rotationType = Type::Absolute; + Type translationType = Type::Absolute; + QString rotationVar = ""; + QString translationVar = ""; + int jointIndex = -1; bool hasPerformedJointLookup = false; bool isRelative = false; diff --git a/libraries/animation/src/AnimNodeLoader.cpp b/libraries/animation/src/AnimNodeLoader.cpp index 876913fc58..bda4541f36 100644 --- a/libraries/animation/src/AnimNodeLoader.cpp +++ b/libraries/animation/src/AnimNodeLoader.cpp @@ -79,10 +79,10 @@ static AnimStateMachine::InterpType stringToInterpType(const QString& str) { static const char* animManipulatorJointVarTypeToString(AnimManipulator::JointVar::Type type) { switch (type) { - case AnimManipulator::JointVar::Type::AbsoluteRotation: return "absoluteRotation"; - case AnimManipulator::JointVar::Type::AbsolutePosition: return "absolutePosition"; - case AnimManipulator::JointVar::Type::RelativeRotation: return "relativeRotation"; - case AnimManipulator::JointVar::Type::RelativePosition: return "relativePosition"; + case AnimManipulator::JointVar::Type::Absolute: return "absolute"; + case AnimManipulator::JointVar::Type::Relative: return "relative"; + case AnimManipulator::JointVar::Type::UnderPose: return "underPose"; + case AnimManipulator::JointVar::Type::Default: return "default"; case AnimManipulator::JointVar::Type::NumTypes: return nullptr; }; return nullptr; @@ -339,7 +339,8 @@ static const char* boneSetStrings[AnimOverlay::NumBoneSets] = { "spineOnly", "empty", "leftHand", - "rightHand" + "rightHand", + "hipsOnly" }; static AnimOverlay::BoneSet stringToBoneSetEnum(const QString& str) { @@ -406,17 +407,25 @@ static AnimNode::Pointer loadManipulatorNode(const QJsonObject& jsonObj, const Q } auto jointObj = jointValue.toObject(); - READ_STRING(type, jointObj, id, jsonUrl, nullptr); READ_STRING(jointName, jointObj, id, jsonUrl, nullptr); - READ_STRING(var, jointObj, id, jsonUrl, nullptr); + READ_STRING(rotationType, jointObj, id, jsonUrl, nullptr); + READ_STRING(translationType, jointObj, id, jsonUrl, nullptr); + READ_STRING(rotationVar, jointObj, id, jsonUrl, nullptr); + READ_STRING(translationVar, jointObj, id, jsonUrl, nullptr); - AnimManipulator::JointVar::Type jointVarType = stringToAnimManipulatorJointVarType(type); - if (jointVarType == AnimManipulator::JointVar::Type::NumTypes) { - qCCritical(animation) << "AnimNodeLoader, bad type in \"joints\", id =" << id << ", url =" << jsonUrl.toDisplayString(); - return nullptr; + AnimManipulator::JointVar::Type jointVarRotationType = stringToAnimManipulatorJointVarType(rotationType); + if (jointVarRotationType == AnimManipulator::JointVar::Type::NumTypes) { + qCWarning(animation) << "AnimNodeLoader, bad rotationType in \"joints\", id =" << id << ", url =" << jsonUrl.toDisplayString(); + jointVarRotationType = AnimManipulator::JointVar::Type::Default; } - AnimManipulator::JointVar jointVar(var, jointName, jointVarType); + AnimManipulator::JointVar::Type jointVarTranslationType = stringToAnimManipulatorJointVarType(translationType); + if (jointVarTranslationType == AnimManipulator::JointVar::Type::NumTypes) { + qCWarning(animation) << "AnimNodeLoader, bad translationType in \"joints\", id =" << id << ", url =" << jsonUrl.toDisplayString(); + jointVarTranslationType = AnimManipulator::JointVar::Type::Default; + } + + AnimManipulator::JointVar jointVar(jointName, jointVarRotationType, jointVarTranslationType, rotationVar, translationVar); node->addJointVar(jointVar); }; diff --git a/libraries/animation/src/AnimOverlay.cpp b/libraries/animation/src/AnimOverlay.cpp index dbc635af66..e086413dde 100644 --- a/libraries/animation/src/AnimOverlay.cpp +++ b/libraries/animation/src/AnimOverlay.cpp @@ -34,6 +34,7 @@ void AnimOverlay::buildBoneSet(BoneSet boneSet) { case SpineOnlyBoneSet: buildSpineOnlyBoneSet(); break; case LeftHandBoneSet: buildLeftHandBoneSet(); break; case RightHandBoneSet: buildRightHandBoneSet(); break; + case HipsOnlyBoneSet: buildHipsOnlyBoneSet(); break; default: case EmptyBoneSet: buildEmptyBoneSet(); break; } @@ -188,6 +189,13 @@ void AnimOverlay::buildRightHandBoneSet() { }); } +void AnimOverlay::buildHipsOnlyBoneSet() { + assert(_skeleton); + buildEmptyBoneSet(); + int hipsJoint = _skeleton->nameToJointIndex("Hips"); + _boneSetVec[hipsJoint] = 1.0f; +} + // for AnimDebugDraw rendering const AnimPoseVec& AnimOverlay::getPosesInternal() const { return _poses; diff --git a/libraries/animation/src/AnimOverlay.h b/libraries/animation/src/AnimOverlay.h index 2f34c07309..ed9439feb7 100644 --- a/libraries/animation/src/AnimOverlay.h +++ b/libraries/animation/src/AnimOverlay.h @@ -37,6 +37,7 @@ public: EmptyBoneSet, LeftHandBoneSet, RightHandBoneSet, + HipsOnlyBoneSet, NumBoneSets }; @@ -75,6 +76,7 @@ public: void buildEmptyBoneSet(); void buildLeftHandBoneSet(); void buildRightHandBoneSet(); + void buildHipsOnlyBoneSet(); // no copies AnimOverlay(const AnimOverlay&) = delete; diff --git a/libraries/animation/src/AnimVariant.h b/libraries/animation/src/AnimVariant.h index 3466013ff6..d383b5abb8 100644 --- a/libraries/animation/src/AnimVariant.h +++ b/libraries/animation/src/AnimVariant.h @@ -165,6 +165,15 @@ public: } } + glm::vec3 lookupRigToGeometryVector(const QString& key, const glm::vec3& defaultValue) const { + if (key.isEmpty()) { + return defaultValue; + } else { + auto iter = _map.find(key); + return iter != _map.end() ? transformVectorFast(_rigToGeometryMat, iter->second.getVec3()) : defaultValue; + } + } + const glm::quat& lookupRaw(const QString& key, const glm::quat& defaultValue) const { if (key.isEmpty()) { return defaultValue; diff --git a/libraries/animation/src/IKTarget.h b/libraries/animation/src/IKTarget.h index 9ea34a6165..acb01d9861 100644 --- a/libraries/animation/src/IKTarget.h +++ b/libraries/animation/src/IKTarget.h @@ -21,13 +21,14 @@ public: RotationOnly, HmdHead, HipsRelativeRotationAndPosition, - Unknown, + Unknown }; IKTarget() {} const glm::vec3& getTranslation() const { return _pose.trans(); } const glm::quat& getRotation() const { return _pose.rot(); } + const AnimPose& getPose() const { return _pose; } int getIndex() const { return _index; } Type getType() const { return _type; } diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index fb0867e2de..700761b248 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -46,7 +46,6 @@ static bool isEqual(const glm::quat& p, const glm::quat& q) { const glm::vec3 DEFAULT_RIGHT_EYE_POS(-0.3f, 0.9f, 0.0f); const glm::vec3 DEFAULT_LEFT_EYE_POS(0.3f, 0.9f, 0.0f); const glm::vec3 DEFAULT_HEAD_POS(0.0f, 0.75f, 0.0f); -const glm::vec3 DEFAULT_NECK_POS(0.0f, 0.70f, 0.0f); void Rig::overrideAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame) { @@ -1020,98 +1019,81 @@ glm::quat Rig::getJointDefaultRotationInParentFrame(int jointIndex) { } void Rig::updateFromHeadParameters(const HeadParameters& params, float dt) { - updateNeckJoint(params.neckJointIndex, params); + updateHeadAnimVars(params); _animVars.set("isTalking", params.isTalking); _animVars.set("notIsTalking", !params.isTalking); + + if (params.hipsEnabled) { + _animVars.set("hipsType", (int)IKTarget::Type::RotationAndPosition); + _animVars.set("hipsPosition", extractTranslation(params.hipsMatrix)); + _animVars.set("hipsRotation", glmExtractRotation(params.hipsMatrix)); + } else { + _animVars.set("hipsType", (int)IKTarget::Type::Unknown); + } + + if (params.spine2Enabled) { + _animVars.set("spine2Type", (int)IKTarget::Type::RotationAndPosition); + _animVars.set("spine2Position", extractTranslation(params.spine2Matrix)); + _animVars.set("spine2Rotation", glmExtractRotation(params.spine2Matrix)); + } else { + _animVars.set("spine2Type", (int)IKTarget::Type::Unknown); + } } void Rig::updateFromEyeParameters(const EyeParameters& params) { - updateEyeJoint(params.leftEyeJointIndex, params.modelTranslation, params.modelRotation, - params.worldHeadOrientation, params.eyeLookAt, params.eyeSaccade); - updateEyeJoint(params.rightEyeJointIndex, params.modelTranslation, params.modelRotation, - params.worldHeadOrientation, params.eyeLookAt, params.eyeSaccade); + updateEyeJoint(params.leftEyeJointIndex, params.modelTranslation, params.modelRotation, params.eyeLookAt, params.eyeSaccade); + updateEyeJoint(params.rightEyeJointIndex, params.modelTranslation, params.modelRotation, params.eyeLookAt, params.eyeSaccade); } -void Rig::computeHeadNeckAnimVars(const AnimPose& hmdPose, glm::vec3& headPositionOut, glm::quat& headOrientationOut, - glm::vec3& neckPositionOut, glm::quat& neckOrientationOut) const { +void Rig::computeHeadFromHMD(const AnimPose& hmdPose, glm::vec3& headPositionOut, glm::quat& headOrientationOut) const { // the input hmd values are in avatar/rig space const glm::vec3& hmdPosition = hmdPose.trans(); - const glm::quat& hmdOrientation = hmdPose.rot(); + + // the HMD looks down the negative z axis, but the head bone looks down the z axis, so apply a 180 degree rotation. + const glm::quat& hmdOrientation = hmdPose.rot() * Quaternions::Y_180; // TODO: cache jointIndices int rightEyeIndex = indexOfJoint("RightEye"); int leftEyeIndex = indexOfJoint("LeftEye"); int headIndex = indexOfJoint("Head"); - int neckIndex = indexOfJoint("Neck"); glm::vec3 absRightEyePos = rightEyeIndex != -1 ? getAbsoluteDefaultPose(rightEyeIndex).trans() : DEFAULT_RIGHT_EYE_POS; glm::vec3 absLeftEyePos = leftEyeIndex != -1 ? getAbsoluteDefaultPose(leftEyeIndex).trans() : DEFAULT_LEFT_EYE_POS; glm::vec3 absHeadPos = headIndex != -1 ? getAbsoluteDefaultPose(headIndex).trans() : DEFAULT_HEAD_POS; - glm::vec3 absNeckPos = neckIndex != -1 ? getAbsoluteDefaultPose(neckIndex).trans() : DEFAULT_NECK_POS; glm::vec3 absCenterEyePos = (absRightEyePos + absLeftEyePos) / 2.0f; glm::vec3 eyeOffset = absCenterEyePos - absHeadPos; - glm::vec3 headOffset = absHeadPos - absNeckPos; - // apply simplistic head/neck model - - // head headPositionOut = hmdPosition - hmdOrientation * eyeOffset; + headOrientationOut = hmdOrientation; - - // neck - neckPositionOut = hmdPosition - hmdOrientation * (headOffset + eyeOffset); - - // slerp between default orientation and hmdOrientation - neckOrientationOut = safeMix(hmdOrientation, _animSkeleton->getRelativeDefaultPose(neckIndex).rot(), 0.5f); } -void Rig::updateNeckJoint(int index, const HeadParameters& params) { - if (_animSkeleton && index >= 0 && index < _animSkeleton->getNumJoints()) { - glm::quat yFlip180 = glm::angleAxis(PI, glm::vec3(0.0f, 1.0f, 0.0f)); - if (params.isInHMD) { - glm::vec3 headPos, neckPos; - glm::quat headRot, neckRot; - - AnimPose hmdPose(glm::vec3(1.0f), params.rigHeadOrientation * yFlip180, params.rigHeadPosition); - computeHeadNeckAnimVars(hmdPose, headPos, headRot, neckPos, neckRot); - - // debug rendering -#ifdef DEBUG_RENDERING - const glm::vec4 red(1.0f, 0.0f, 0.0f, 1.0f); - const glm::vec4 green(0.0f, 1.0f, 0.0f, 1.0f); - - // transform from bone into avatar space - AnimPose headPose(glm::vec3(1), headRot, headPos); - DebugDraw::getInstance().addMyAvatarMarker("headTarget", headPose.rot, headPose.trans, red); - - // transform from bone into avatar space - AnimPose neckPose(glm::vec3(1), neckRot, neckPos); - DebugDraw::getInstance().addMyAvatarMarker("neckTarget", neckPose.rot, neckPose.trans, green); -#endif - - _animVars.set("headPosition", headPos); - _animVars.set("headRotation", headRot); - _animVars.set("headType", (int)IKTarget::Type::HmdHead); - _animVars.set("neckPosition", neckPos); - _animVars.set("neckRotation", neckRot); - _animVars.set("neckType", (int)IKTarget::Type::Unknown); // 'Unknown' disables the target - +void Rig::updateHeadAnimVars(const HeadParameters& params) { + if (_animSkeleton) { + if (params.headEnabled) { + _animVars.set("headPosition", params.rigHeadPosition); + _animVars.set("headRotation", params.rigHeadOrientation); + if (params.hipsEnabled) { + // Since there is an explicit hips ik target, switch the head to use the more generic RotationAndPosition IK chain type. + // this will allow the spine to bend more, ensuring that it can reach the head target position. + _animVars.set("headType", (int)IKTarget::Type::RotationAndPosition); + } else { + // When there is no hips IK target, use the HmdHead IK chain type. This will make the spine very stiff, + // but because the IK _hipsOffset is enabled, the hips will naturally follow underneath the head. + _animVars.set("headType", (int)IKTarget::Type::HmdHead); + } } else { _animVars.unset("headPosition"); - _animVars.set("headRotation", params.rigHeadOrientation * yFlip180); - _animVars.set("headAndNeckType", (int)IKTarget::Type::RotationOnly); + _animVars.set("headRotation", params.rigHeadOrientation); _animVars.set("headType", (int)IKTarget::Type::RotationOnly); - _animVars.unset("neckPosition"); - _animVars.unset("neckRotation"); - _animVars.set("neckType", (int)IKTarget::Type::RotationOnly); } } } -void Rig::updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm::quat& modelRotation, const glm::quat& worldHeadOrientation, const glm::vec3& lookAtSpot, const glm::vec3& saccade) { +void Rig::updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm::quat& modelRotation, const glm::vec3& lookAtSpot, const glm::vec3& saccade) { // TODO: does not properly handle avatar scale. @@ -1161,13 +1143,19 @@ void Rig::updateFromHandAndFeetParameters(const HandAndFeetParameters& params, f const glm::vec3 bodyCapsuleStart = bodyCapsuleCenter - glm::vec3(0, params.bodyCapsuleHalfHeight, 0); const glm::vec3 bodyCapsuleEnd = bodyCapsuleCenter + glm::vec3(0, params.bodyCapsuleHalfHeight, 0); + // TODO: add isHipsEnabled + bool bodySensorTrackingEnabled = params.isLeftFootEnabled || params.isRightFootEnabled; + if (params.isLeftEnabled) { - // prevent the hand IK targets from intersecting the body capsule glm::vec3 handPosition = params.leftPosition; - glm::vec3 displacement; - if (findSphereCapsulePenetration(handPosition, HAND_RADIUS, bodyCapsuleStart, bodyCapsuleEnd, bodyCapsuleRadius, displacement)) { - handPosition -= displacement; + + if (!bodySensorTrackingEnabled) { + // prevent the hand IK targets from intersecting the body capsule + glm::vec3 displacement; + if (findSphereCapsulePenetration(handPosition, HAND_RADIUS, bodyCapsuleStart, bodyCapsuleEnd, bodyCapsuleRadius, displacement)) { + handPosition -= displacement; + } } _animVars.set("leftHandPosition", handPosition); @@ -1181,11 +1169,14 @@ void Rig::updateFromHandAndFeetParameters(const HandAndFeetParameters& params, f if (params.isRightEnabled) { - // prevent the hand IK targets from intersecting the body capsule glm::vec3 handPosition = params.rightPosition; - glm::vec3 displacement; - if (findSphereCapsulePenetration(handPosition, HAND_RADIUS, bodyCapsuleStart, bodyCapsuleEnd, bodyCapsuleRadius, displacement)) { - handPosition -= displacement; + + if (!bodySensorTrackingEnabled) { + // prevent the hand IK targets from intersecting the body capsule + glm::vec3 displacement; + if (findSphereCapsulePenetration(handPosition, HAND_RADIUS, bodyCapsuleStart, bodyCapsuleEnd, bodyCapsuleRadius, displacement)) { + handPosition -= displacement; + } } _animVars.set("rightHandPosition", handPosition); diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 2cd20c2704..2d024628f5 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -42,16 +42,17 @@ public: }; struct HeadParameters { - glm::quat worldHeadOrientation = glm::quat(); // world space (-z forward) - glm::quat rigHeadOrientation = glm::quat(); // rig space (-z forward) - glm::vec3 rigHeadPosition = glm::vec3(); // rig space - bool isInHMD = false; - int neckJointIndex = -1; + glm::mat4 hipsMatrix = glm::mat4(); // rig space + glm::mat4 spine2Matrix = glm::mat4(); // rig space + glm::quat rigHeadOrientation = glm::quat(); // rig space (-z forward) + glm::vec3 rigHeadPosition = glm::vec3(); // rig space + bool hipsEnabled = false; + bool headEnabled = false; + bool spine2Enabled = false; bool isTalking = false; }; struct EyeParameters { - glm::quat worldHeadOrientation = glm::quat(); glm::vec3 eyeLookAt = glm::vec3(); // world space glm::vec3 eyeSaccade = glm::vec3(); // world space glm::vec3 modelTranslation = glm::vec3(); @@ -228,6 +229,9 @@ public: void setEnableDebugDrawIKTargets(bool enableDebugDrawIKTargets) { _enableDebugDrawIKTargets = enableDebugDrawIKTargets; } + // input assumed to be in rig space + void computeHeadFromHMD(const AnimPose& hmdPose, glm::vec3& headPositionOut, glm::quat& headOrientationOut) const; + signals: void onLoadComplete(); @@ -237,10 +241,9 @@ protected: void applyOverridePoses(); void buildAbsoluteRigPoses(const AnimPoseVec& relativePoses, AnimPoseVec& absolutePosesOut); - void updateNeckJoint(int index, const HeadParameters& params); - void computeHeadNeckAnimVars(const AnimPose& hmdPose, glm::vec3& headPositionOut, glm::quat& headOrientationOut, - glm::vec3& neckPositionOut, glm::quat& neckOrientationOut) const; - void updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm::quat& modelRotation, const glm::quat& worldHeadOrientation, const glm::vec3& lookAt, const glm::vec3& saccade); + void updateHeadAnimVars(const HeadParameters& params); + + void updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm::quat& modelRotation, const glm::vec3& lookAt, const glm::vec3& saccade); void calcAnimAlpha(float speed, const std::vector& referenceSpeeds, float* alphaOut) const; AnimPose _modelOffset; // model to rig space diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 4a2de0a64b..b684aac89c 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1006,7 +1006,7 @@ void AudioClient::handleAudioInput(QByteArray& audioBuffer) { _timeSinceLastClip += (float)numSamples / (float)AudioConstants::SAMPLE_RATE; } - emit inputReceived({ audioBuffer.data(), numSamples }); + emit inputReceived(audioBuffer); if (_noiseGate.openedInLastBlock()) { emit noiseGateOpened(); diff --git a/libraries/audio/src/SoundCache.cpp b/libraries/audio/src/SoundCache.cpp index 6b34c68959..1646540da6 100644 --- a/libraries/audio/src/SoundCache.cpp +++ b/libraries/audio/src/SoundCache.cpp @@ -14,6 +14,8 @@ #include "AudioLogging.h" #include "SoundCache.h" +static const int SOUNDS_LOADING_PRIORITY { -7 }; // Make sure sounds load after the low rez texture mips + int soundPointerMetaTypeId = qRegisterMetaType(); SoundCache::SoundCache(QObject* parent) : @@ -37,5 +39,7 @@ SharedSoundPointer SoundCache::getSound(const QUrl& url) { QSharedPointer SoundCache::createResource(const QUrl& url, const QSharedPointer& fallback, const void* extra) { qCDebug(audio) << "Requesting sound at" << url.toString(); - return QSharedPointer(new Sound(url), &Resource::deleter); + auto resource = QSharedPointer(new Sound(url), &Resource::deleter); + resource->setLoadPriority(this, SOUNDS_LOADING_PRIORITY); + return resource; } diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 20894104ff..6c265ef1b6 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -393,9 +393,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent if (isFingerPointing) { setAtBit(flags, HAND_STATE_FINGER_POINTING_BIT); } - // faceshift state + // face tracker state if (_headData->_isFaceTrackerConnected) { - setAtBit(flags, IS_FACESHIFT_CONNECTED); + setAtBit(flags, IS_FACE_TRACKER_CONNECTED); } // eye tracker state if (_headData->_isEyeTrackerConnected) { @@ -883,7 +883,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { auto newHandState = getSemiNibbleAt(bitItems, HAND_STATE_START_BIT) + (oneAtBit(bitItems, HAND_STATE_FINGER_POINTING_BIT) ? IS_FINGER_POINTING_FLAG : 0); - auto newFaceTrackerConnected = oneAtBit(bitItems, IS_FACESHIFT_CONNECTED); + auto newFaceTrackerConnected = oneAtBit(bitItems, IS_FACE_TRACKER_CONNECTED); auto newEyeTrackerConnected = oneAtBit(bitItems, IS_EYE_TRACKER_CONNECTED); bool keyStateChanged = (_keyState != newKeyState); @@ -1392,6 +1392,22 @@ void AvatarData::setJointRotations(QVector jointRotations) { } } +QVector AvatarData::getJointTranslations() const { + if (QThread::currentThread() != thread()) { + QVector result; + QMetaObject::invokeMethod(const_cast(this), + "getJointTranslations", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QVector, result)); + return result; + } + QReadLocker readLock(&_jointDataLock); + QVector jointTranslations(_jointData.size()); + for (int i = 0; i < _jointData.size(); ++i) { + jointTranslations[i] = _jointData[i].translation; + } + return jointTranslations; +} + void AvatarData::setJointTranslations(QVector jointTranslations) { if (QThread::currentThread() != thread()) { QVector result; @@ -1457,7 +1473,22 @@ QStringList AvatarData::getJointNames() const { void AvatarData::parseAvatarIdentityPacket(const QByteArray& data, Identity& identityOut) { QDataStream packetStream(data); - packetStream >> identityOut.uuid >> identityOut.skeletonModelURL >> identityOut.attachmentData >> identityOut.displayName >> identityOut.sessionDisplayName >> identityOut.avatarEntityData; + packetStream >> identityOut.uuid + >> identityOut.skeletonModelURL + >> identityOut.attachmentData + >> identityOut.displayName + >> identityOut.sessionDisplayName + >> identityOut.avatarEntityData + >> identityOut.updatedAt; + +#ifdef WANT_DEBUG + qCDebug(avatars) << __FUNCTION__ + << "identityOut.uuid:" << identityOut.uuid + << "identityOut.skeletonModelURL:" << identityOut.skeletonModelURL + << "identityOut.displayName:" << identityOut.displayName + << "identityOut.sessionDisplayName:" << identityOut.sessionDisplayName; +#endif + } static const QUrl emptyURL(""); @@ -1468,6 +1499,12 @@ QUrl AvatarData::cannonicalSkeletonModelURL(const QUrl& emptyURL) const { void AvatarData::processAvatarIdentity(const Identity& identity, bool& identityChanged, bool& displayNameChanged) { + if (identity.updatedAt < _identityUpdatedAt) { + qCDebug(avatars) << "Ignoring late identity packet for avatar " << getSessionUUID() + << "identity.updatedAt:" << identity.updatedAt << "_identityUpdatedAt:" << _identityUpdatedAt; + return; + } + if (_firstSkeletonCheck || (identity.skeletonModelURL != cannonicalSkeletonModelURL(emptyURL))) { setSkeletonModelURL(identity.skeletonModelURL); identityChanged = true; @@ -1497,24 +1534,35 @@ void AvatarData::processAvatarIdentity(const Identity& identity, bool& identityC setAvatarEntityData(identity.avatarEntityData); identityChanged = true; } - // flag this avatar as non-stale by updating _averageBytesReceived - const int BOGUS_NUM_BYTES = 1; - _averageBytesReceived.updateAverage(BOGUS_NUM_BYTES); + + // use the timestamp from this identity, since we want to honor the updated times in "server clock" + // this will overwrite any changes we made locally to this AvatarData's _identityUpdatedAt + _identityUpdatedAt = identity.updatedAt; } QByteArray AvatarData::identityByteArray() const { QByteArray identityData; QDataStream identityStream(&identityData, QIODevice::Append); - const QUrl& urlToSend = cannonicalSkeletonModelURL(emptyURL); + const QUrl& urlToSend = cannonicalSkeletonModelURL(emptyURL); // depends on _skeletonModelURL _avatarEntitiesLock.withReadLock([&] { - identityStream << getSessionUUID() << urlToSend << _attachmentData << _displayName << getSessionDisplayNameForTransport() << _avatarEntityData; + identityStream << getSessionUUID() + << urlToSend + << _attachmentData + << _displayName + << getSessionDisplayNameForTransport() // depends on _sessionDisplayName + << _avatarEntityData + << _identityUpdatedAt; }); return identityData; } void AvatarData::setSkeletonModelURL(const QUrl& skeletonModelURL) { + if (skeletonModelURL.isEmpty()) { + qCDebug(avatars) << __FUNCTION__ << "caller called with empty URL."; + } + const QUrl& expanded = skeletonModelURL.isEmpty() ? AvatarData::defaultFullAvatarModelUrl() : skeletonModelURL; if (expanded == _skeletonModelURL) { return; @@ -1523,6 +1571,7 @@ void AvatarData::setSkeletonModelURL(const QUrl& skeletonModelURL) { qCDebug(avatars) << "Changing skeleton model for avatar" << getSessionUUID() << "to" << _skeletonModelURL.toString(); updateJointMappings(); + markIdentityDataChanged(); } void AvatarData::setDisplayName(const QString& displayName) { @@ -1532,6 +1581,7 @@ void AvatarData::setDisplayName(const QString& displayName) { sendIdentityPacket(); qCDebug(avatars) << "Changing display name for avatar to" << displayName; + markIdentityDataChanged(); } QVector AvatarData::getAttachmentData() const { @@ -1550,6 +1600,7 @@ void AvatarData::setAttachmentData(const QVector& attachmentData return; } _attachmentData = attachmentData; + markIdentityDataChanged(); } void AvatarData::attach(const QString& modelURL, const QString& jointName, @@ -1679,7 +1730,6 @@ void AvatarData::sendAvatarDataPacket() { void AvatarData::sendIdentityPacket() { auto nodeList = DependencyManager::get(); - QByteArray identityData = identityByteArray(); auto packetList = NLPacketList::create(PacketType::AvatarIdentity, QByteArray(), true, true); @@ -1693,6 +1743,7 @@ void AvatarData::sendIdentityPacket() { }); _avatarEntityDataLocallyEdited = false; + _identityDataChanged = false; } void AvatarData::updateJointMappings() { @@ -2229,10 +2280,12 @@ void AvatarData::updateAvatarEntity(const QUuid& entityID, const QByteArray& ent if (_avatarEntityData.size() < MAX_NUM_AVATAR_ENTITIES) { _avatarEntityData.insert(entityID, entityData); _avatarEntityDataLocallyEdited = true; + markIdentityDataChanged(); } } else { itr.value() = entityData; _avatarEntityDataLocallyEdited = true; + markIdentityDataChanged(); } }); } @@ -2246,6 +2299,7 @@ void AvatarData::clearAvatarEntity(const QUuid& entityID) { _avatarEntitiesLock.withWriteLock([&] { _avatarEntityData.remove(entityID); _avatarEntityDataLocallyEdited = true; + markIdentityDataChanged(); }); } diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 8319eb5249..b2cc912007 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -99,7 +99,7 @@ const quint32 AVATAR_MOTION_SCRIPTABLE_BITS = // Referential Data - R is found in the 7th bit const int KEY_STATE_START_BIT = 0; // 1st and 2nd bits const int HAND_STATE_START_BIT = 2; // 3rd and 4th bits -const int IS_FACESHIFT_CONNECTED = 4; // 5th bit +const int IS_FACE_TRACKER_CONNECTED = 4; // 5th bit const int IS_EYE_TRACKER_CONNECTED = 5; // 6th bit (was CHAT_CIRCLING) const int HAS_REFERENTIAL = 6; // 7th bit const int HAND_STATE_FINGER_POINTING_BIT = 7; // 8th bit @@ -110,9 +110,7 @@ const char LEFT_HAND_POINTING_FLAG = 1; const char RIGHT_HAND_POINTING_FLAG = 2; const char IS_FINGER_POINTING_FLAG = 4; -const qint64 AVATAR_UPDATE_TIMEOUT = 5 * USECS_PER_SECOND; - -// AvatarData state flags - we store the details about the packet encoding in the first byte, +// AvatarData state flags - we store the details about the packet encoding in the first byte, // before the "header" structure const char AVATARDATA_FLAGS_MINIMUM = 0; @@ -220,7 +218,7 @@ namespace AvatarDataPacket { } PACKED_END; const size_t AVATAR_LOCAL_POSITION_SIZE = 12; - // only present if IS_FACESHIFT_CONNECTED flag is set in AvatarInfo.flags + // only present if IS_FACE_TRACKER_CONNECTED flag is set in AvatarInfo.flags PACKED_BEGIN struct FaceTrackerInfo { float leftEyeBlink; float rightEyeBlink; @@ -497,6 +495,7 @@ public: Q_INVOKABLE glm::vec3 getJointTranslation(const QString& name) const; Q_INVOKABLE virtual QVector getJointRotations() const; + Q_INVOKABLE virtual QVector getJointTranslations() const; Q_INVOKABLE virtual void setJointRotations(QVector jointRotations); Q_INVOKABLE virtual void setJointTranslations(QVector jointTranslations); @@ -530,6 +529,7 @@ public: QString displayName; QString sessionDisplayName; AvatarEntityMap avatarEntityData; + quint64 updatedAt; }; static void parseAvatarIdentityPacket(const QByteArray& data, Identity& identityOut); @@ -546,7 +546,10 @@ public: virtual void setSkeletonModelURL(const QUrl& skeletonModelURL); virtual void setDisplayName(const QString& displayName); - virtual void setSessionDisplayName(const QString& sessionDisplayName) { _sessionDisplayName = sessionDisplayName; }; + virtual void setSessionDisplayName(const QString& sessionDisplayName) { + _sessionDisplayName = sessionDisplayName; + markIdentityDataChanged(); + } Q_INVOKABLE QVector getAttachmentData() const; Q_INVOKABLE virtual void setAttachmentData(const QVector& attachmentData); @@ -564,7 +567,6 @@ public: void setOwningAvatarMixer(const QWeakPointer& owningAvatarMixer) { _owningAvatarMixer = owningAvatarMixer; } - int getUsecsSinceLastUpdate() const { return _averageBytesReceived.getUsecsSinceLastEvent(); } int getAverageBytesReceivedPerSecond() const; int getReceiveRate() const; @@ -600,9 +602,6 @@ public: return _lastSentJointData; } - - bool shouldDie() const { return _owningAvatarMixer.isNull() || getUsecsSinceLastUpdate() > AVATAR_UPDATE_TIMEOUT; } - static const float OUT_OF_VIEW_PENALTY; static void sortAvatars( @@ -619,11 +618,15 @@ public: static float _avatarSortCoefficientCenter; static float _avatarSortCoefficientAge; - + bool getIdentityDataChanged() const { return _identityDataChanged; } // has the identity data changed since the last time sendIdentityPacket() was called + void markIdentityDataChanged() { + _identityDataChanged = true; + _identityUpdatedAt = usecTimestampNow(); + } signals: void displayNameChanged(); - + public slots: void sendAvatarDataPacket(); void sendIdentityPacket(); @@ -778,6 +781,9 @@ protected: quint64 _audioLoudnessChanged { 0 }; float _audioAverageLoudness { 0.0f }; + bool _identityDataChanged { false }; + quint64 _identityUpdatedAt { 0 }; + private: friend void avatarStateFromFrame(const QByteArray& frameData, AvatarData* _avatar); static QUrl _defaultFullAvatarModelUrl; diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index e944c7c887..21ea8081c7 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -57,7 +57,7 @@ public slots: protected slots: void sessionUUIDChanged(const QUuid& sessionUUID, const QUuid& oldUUID); - virtual void processAvatarDataPacket(QSharedPointer message, SharedNodePointer sendingNode); + void processAvatarDataPacket(QSharedPointer message, SharedNodePointer sendingNode); void processAvatarIdentityPacket(QSharedPointer message, SharedNodePointer sendingNode); void processKillAvatar(QSharedPointer message, SharedNodePointer sendingNode); void processExitingSpaceBubble(QSharedPointer message, SharedNodePointer sendingNode); diff --git a/libraries/avatars/src/ScriptAvatarData.cpp b/libraries/avatars/src/ScriptAvatarData.cpp index f579eb9763..01d7f293d8 100644 --- a/libraries/avatars/src/ScriptAvatarData.cpp +++ b/libraries/avatars/src/ScriptAvatarData.cpp @@ -210,6 +210,13 @@ QVector ScriptAvatarData::getJointRotations() const { return QVector(); } } +QVector ScriptAvatarData::getJointTranslations() const { + if (AvatarSharedPointer sharedAvatarData = _avatarData.lock()) { + return sharedAvatarData->getJointTranslations(); + } else { + return QVector(); + } +} bool ScriptAvatarData::isJointDataValid(const QString& name) const { if (AvatarSharedPointer sharedAvatarData = _avatarData.lock()) { return sharedAvatarData->isJointDataValid(name); diff --git a/libraries/avatars/src/ScriptAvatarData.h b/libraries/avatars/src/ScriptAvatarData.h index 683306e847..d763b6e97a 100644 --- a/libraries/avatars/src/ScriptAvatarData.h +++ b/libraries/avatars/src/ScriptAvatarData.h @@ -106,6 +106,7 @@ public: Q_INVOKABLE glm::quat getJointRotation(const QString& name) const; Q_INVOKABLE glm::vec3 getJointTranslation(const QString& name) const; Q_INVOKABLE QVector getJointRotations() const; + Q_INVOKABLE QVector getJointTranslations() const; Q_INVOKABLE bool isJointDataValid(const QString& name) const; Q_INVOKABLE int getJointIndex(const QString& name) const; Q_INVOKABLE QStringList getJointNames() const; diff --git a/libraries/controllers/CMakeLists.txt b/libraries/controllers/CMakeLists.txt index 384218691a..bf226f2647 100644 --- a/libraries/controllers/CMakeLists.txt +++ b/libraries/controllers/CMakeLists.txt @@ -10,4 +10,4 @@ GroupSources("src/controllers") add_dependency_external_projects(glm) find_package(GLM REQUIRED) -target_include_directories(${TARGET_NAME} PUBLIC ${GLM_INCLUDE_DIRS}) +target_include_directories(${TARGET_NAME} PUBLIC ${GLM_INCLUDE_DIRS} "${CMAKE_BINARY_DIR}/includes") diff --git a/libraries/controllers/src/controllers/Actions.cpp b/libraries/controllers/src/controllers/Actions.cpp index 300fa684d9..62a10c851f 100644 --- a/libraries/controllers/src/controllers/Actions.cpp +++ b/libraries/controllers/src/controllers/Actions.cpp @@ -53,6 +53,9 @@ namespace controller { makePosePair(Action::RIGHT_HAND, "RightHand"), makePosePair(Action::LEFT_FOOT, "LeftFoot"), makePosePair(Action::RIGHT_FOOT, "RightFoot"), + makePosePair(Action::HIPS, "Hips"), + makePosePair(Action::SPINE2, "Spine2"), + makePosePair(Action::HEAD, "Head"), makeButtonPair(Action::LEFT_HAND_CLICK, "LeftHandClick"), makeButtonPair(Action::RIGHT_HAND_CLICK, "RightHandClick"), diff --git a/libraries/controllers/src/controllers/Actions.h b/libraries/controllers/src/controllers/Actions.h index edf3dee07a..534f01d865 100644 --- a/libraries/controllers/src/controllers/Actions.h +++ b/libraries/controllers/src/controllers/Actions.h @@ -44,6 +44,9 @@ enum class Action { RIGHT_HAND, LEFT_FOOT, RIGHT_FOOT, + HIPS, + SPINE2, + HEAD, LEFT_HAND_CLICK, RIGHT_HAND_CLICK, diff --git a/libraries/controllers/src/controllers/Input.h b/libraries/controllers/src/controllers/Input.h index 9c7f09d526..65c78cd6ea 100644 --- a/libraries/controllers/src/controllers/Input.h +++ b/libraries/controllers/src/controllers/Input.h @@ -16,9 +16,15 @@ namespace controller { struct InputCalibrationData { - glm::mat4 sensorToWorldMat; - glm::mat4 avatarMat; - glm::mat4 hmdSensorMat; + glm::mat4 sensorToWorldMat; // sensor to world + glm::mat4 avatarMat; // avatar to world + glm::mat4 hmdSensorMat; // hmd pos and orientation in sensor space + glm::mat4 defaultCenterEyeMat; // default pose for the center of the eyes in avatar space. + glm::mat4 defaultHeadMat; // default pose for head joint in avatar space + glm::mat4 defaultSpine2; // default pose for spine2 joint in avatar space + glm::mat4 defaultHips; // default pose for hips joint in avatar space + glm::mat4 defaultLeftFoot; // default pose for leftFoot joint in avatar space + glm::mat4 defaultRightFoot; // default pose for leftFoot joint in avatar space }; enum class ChannelType { diff --git a/libraries/controllers/src/controllers/InputRecorder.cpp b/libraries/controllers/src/controllers/InputRecorder.cpp new file mode 100644 index 0000000000..2d2cd40739 --- /dev/null +++ b/libraries/controllers/src/controllers/InputRecorder.cpp @@ -0,0 +1,290 @@ +// +// Created by Dante Ruiz 2017/04/16 +// 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 "InputRecorder.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +QString SAVE_DIRECTORY = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/" + BuildInfo::MODIFIED_ORGANIZATION + "/" + BuildInfo::INTERFACE_NAME + "/hifi-input-recordings/"; +QString FILE_PREFIX_NAME = "input-recording-"; +QString COMPRESS_EXTENSION = ".tar.gz"; +namespace controller { + + QJsonObject poseToJsonObject(const Pose pose) { + QJsonObject newPose; + + QJsonArray translation; + translation.append(pose.translation.x); + translation.append(pose.translation.y); + translation.append(pose.translation.z); + + QJsonArray rotation; + rotation.append(pose.rotation.x); + rotation.append(pose.rotation.y); + rotation.append(pose.rotation.z); + rotation.append(pose.rotation.w); + + QJsonArray velocity; + velocity.append(pose.velocity.x); + velocity.append(pose.velocity.y); + velocity.append(pose.velocity.z); + + QJsonArray angularVelocity; + angularVelocity.append(pose.angularVelocity.x); + angularVelocity.append(pose.angularVelocity.y); + angularVelocity.append(pose.angularVelocity.z); + + newPose["translation"] = translation; + newPose["rotation"] = rotation; + newPose["velocity"] = velocity; + newPose["angularVelocity"] = angularVelocity; + newPose["valid"] = pose.valid; + + return newPose; + } + + Pose jsonObjectToPose(const QJsonObject object) { + Pose pose; + QJsonArray translation = object["translation"].toArray(); + QJsonArray rotation = object["rotation"].toArray(); + QJsonArray velocity = object["velocity"].toArray(); + QJsonArray angularVelocity = object["angularVelocity"].toArray(); + + pose.valid = object["valid"].toBool(); + + pose.translation.x = translation[0].toDouble(); + pose.translation.y = translation[1].toDouble(); + pose.translation.z = translation[2].toDouble(); + + pose.rotation.x = rotation[0].toDouble(); + pose.rotation.y = rotation[1].toDouble(); + pose.rotation.z = rotation[2].toDouble(); + pose.rotation.w = rotation[3].toDouble(); + + pose.velocity.x = velocity[0].toDouble(); + pose.velocity.y = velocity[1].toDouble(); + pose.velocity.z = velocity[2].toDouble(); + + pose.angularVelocity.x = angularVelocity[0].toDouble(); + pose.angularVelocity.y = angularVelocity[1].toDouble(); + pose.angularVelocity.z = angularVelocity[2].toDouble(); + + return pose; + } + + + void exportToFile(QJsonObject& object) { + if (!QDir(SAVE_DIRECTORY).exists()) { + QDir().mkdir(SAVE_DIRECTORY); + } + + QString timeStamp = QDateTime::currentDateTime().toString(Qt::ISODate); + timeStamp.replace(":", "-"); + QString fileName = SAVE_DIRECTORY + FILE_PREFIX_NAME + timeStamp + COMPRESS_EXTENSION; + qDebug() << fileName; + QFile saveFile (fileName); + if (!saveFile.open(QIODevice::WriteOnly)) { + qWarning() << "could not open file: " << fileName; + return; + } + QJsonDocument saveData(object); + QByteArray compressedData = qCompress(saveData.toJson(QJsonDocument::Compact)); + saveFile.write(compressedData); + } + + QJsonObject openFile(const QString& file, bool& status) { + QJsonObject object; + QFile openFile(file); + if (!openFile.open(QIODevice::ReadOnly)) { + qWarning() << "could not open file: " << file; + status = false; + return object; + } + QByteArray compressedData = qUncompress(openFile.readAll()); + QJsonDocument jsonDoc = QJsonDocument::fromJson(compressedData); + object = jsonDoc.object(); + status = true; + return object; + } + + InputRecorder::InputRecorder() {} + + InputRecorder::~InputRecorder() {} + + InputRecorder* InputRecorder::getInstance() { + static InputRecorder inputRecorder; + return &inputRecorder; + } + + QString InputRecorder::getSaveDirectory() { + return SAVE_DIRECTORY; + } + + void InputRecorder::startRecording() { + _recording = true; + _playback = false; + _framesRecorded = 0; + _poseStateList.clear(); + _actionStateList.clear(); + } + + void InputRecorder::saveRecording() { + QJsonObject data; + data["frameCount"] = _framesRecorded; + + QJsonArray actionArrayList; + QJsonArray poseArrayList; + for(const ActionStates actionState: _actionStateList) { + QJsonArray actionArray; + for (const float value: actionState) { + actionArray.append(value); + } + actionArrayList.append(actionArray); + } + + for (const PoseStates poseState: _poseStateList) { + QJsonArray poseArray; + for (const Pose pose: poseState) { + poseArray.append(poseToJsonObject(pose)); + } + poseArrayList.append(poseArray); + } + + data["actionList"] = actionArrayList; + data["poseList"] = poseArrayList; + exportToFile(data); + } + + void InputRecorder::loadRecording(const QString& path) { + _recording = false; + _playback = false; + _loading = true; + _playCount = 0; + resetFrame(); + _poseStateList.clear(); + _actionStateList.clear(); + QString filePath = path; + filePath.remove(0,8); + QFileInfo info(filePath); + QString extension = info.suffix(); + if (extension != "gz") { + qWarning() << "can not load file with exentsion of " << extension; + return; + } + bool success = false; + QJsonObject data = openFile(info.absoluteFilePath(), success); + if (success) { + _framesRecorded = data["frameCount"].toInt(); + QJsonArray actionArrayList = data["actionList"].toArray(); + QJsonArray poseArrayList = data["poseList"].toArray(); + + for (int actionIndex = 0; actionIndex < actionArrayList.size(); actionIndex++) { + QJsonArray actionState = actionArrayList[actionIndex].toArray(); + for (int index = 0; index < actionState.size(); index++) { + _currentFrameActions[index] = actionState[index].toInt(); + } + _actionStateList.push_back(_currentFrameActions); + _currentFrameActions = ActionStates(toInt(Action::NUM_ACTIONS)); + } + + for (int poseIndex = 0; poseIndex < poseArrayList.size(); poseIndex++) { + QJsonArray poseState = poseArrayList[poseIndex].toArray(); + for (int index = 0; index < poseState.size(); index++) { + _currentFramePoses[index] = jsonObjectToPose(poseState[index].toObject()); + } + _poseStateList.push_back(_currentFramePoses); + _currentFramePoses = PoseStates(toInt(Action::NUM_ACTIONS)); + } + } + + _loading = false; + } + + void InputRecorder::stopRecording() { + _recording = false; + } + + void InputRecorder::startPlayback() { + _playback = true; + _recording = false; + _playCount = 0; + } + + void InputRecorder::stopPlayback() { + _playback = false; + _playCount = 0; + } + + void InputRecorder::setActionState(controller::Action action, float value) { + if (_recording) { + _currentFrameActions[toInt(action)] += value; + } + } + + void InputRecorder::setActionState(controller::Action action, const controller::Pose pose) { + if (_recording) { + _currentFramePoses[toInt(action)] = pose; + } + } + + void InputRecorder::resetFrame() { + if (_recording) { + for(auto& channel : _currentFramePoses) { + channel = Pose(); + } + + for(auto& channel : _currentFrameActions) { + channel = 0.0f; + } + } + } + + float InputRecorder::getActionState(controller::Action action) { + if (_actionStateList.size() > 0 ) { + return _actionStateList[_playCount][toInt(action)]; + } + + return 0.0f; + } + + controller::Pose InputRecorder::getPoseState(controller::Action action) { + if (_poseStateList.size() > 0) { + return _poseStateList[_playCount][toInt(action)]; + } + + return Pose(); + } + + void InputRecorder::frameTick() { + if (_recording) { + _framesRecorded++; + _poseStateList.push_back(_currentFramePoses); + _actionStateList.push_back(_currentFrameActions); + } + + if (_playback) { + _playCount++; + if (_playCount == _framesRecorded) { + _playCount = 0; + } + } + } +} diff --git a/libraries/controllers/src/controllers/InputRecorder.h b/libraries/controllers/src/controllers/InputRecorder.h new file mode 100644 index 0000000000..d1cc9a32eb --- /dev/null +++ b/libraries/controllers/src/controllers/InputRecorder.h @@ -0,0 +1,62 @@ +// +// Created by Dante Ruiz on 2017/04/16 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_InputRecorder_h +#define hifi_InputRecorder_h + +#include +#include +#include + +#include + +#include "Pose.h" +#include "Actions.h" + +namespace controller { + class InputRecorder { + public: + using PoseStates = std::vector; + using ActionStates = std::vector; + + InputRecorder(); + ~InputRecorder(); + + static InputRecorder* getInstance(); + + void saveRecording(); + void loadRecording(const QString& path); + void startRecording(); + void startPlayback(); + void stopPlayback(); + void stopRecording(); + void toggleRecording() { _recording = !_recording; } + void togglePlayback() { _playback = !_playback; } + void resetFrame(); + bool isRecording() { return _recording; } + bool isPlayingback() { return (_playback && !_loading); } + void setActionState(controller::Action action, float value); + void setActionState(controller::Action action, const controller::Pose pose); + float getActionState(controller::Action action); + controller::Pose getPoseState(controller::Action action); + QString getSaveDirectory(); + void frameTick(); + private: + bool _recording { false }; + bool _playback { false }; + bool _loading { false }; + std::vector _poseStateList = std::vector(); + std::vector _actionStateList = std::vector(); + PoseStates _currentFramePoses = PoseStates(toInt(Action::NUM_ACTIONS)); + ActionStates _currentFrameActions = ActionStates(toInt(Action::NUM_ACTIONS)); + + int _framesRecorded { 0 }; + int _playCount { 0 }; + }; +} +#endif diff --git a/libraries/controllers/src/controllers/ScriptingInterface.cpp b/libraries/controllers/src/controllers/ScriptingInterface.cpp index d32acb3d82..16db22401f 100644 --- a/libraries/controllers/src/controllers/ScriptingInterface.cpp +++ b/libraries/controllers/src/controllers/ScriptingInterface.cpp @@ -23,6 +23,7 @@ #include "impl/MappingBuilderProxy.h" #include "Logging.h" #include "InputDevice.h" +#include "InputRecorder.h" static QRegularExpression SANITIZE_NAME_EXPRESSION{ "[\\(\\)\\.\\s]" }; @@ -154,6 +155,41 @@ namespace controller { return DependencyManager::get()->triggerHapticPulse(strength, SHORT_HAPTIC_DURATION_MS, hand); } + void ScriptingInterface::startInputRecording() { + InputRecorder* inputRecorder = InputRecorder::getInstance(); + inputRecorder->startRecording(); + } + + void ScriptingInterface::stopInputRecording() { + InputRecorder* inputRecorder = InputRecorder::getInstance(); + inputRecorder->stopRecording(); + } + + void ScriptingInterface::startInputPlayback() { + InputRecorder* inputRecorder = InputRecorder::getInstance(); + inputRecorder->startPlayback(); + } + + void ScriptingInterface::stopInputPlayback() { + InputRecorder* inputRecorder = InputRecorder::getInstance(); + inputRecorder->stopPlayback(); + } + + void ScriptingInterface::saveInputRecording() { + InputRecorder* inputRecorder = InputRecorder::getInstance(); + inputRecorder->saveRecording(); + } + + void ScriptingInterface::loadInputRecording(const QString& file) { + InputRecorder* inputRecorder = InputRecorder::getInstance(); + inputRecorder->loadRecording(file); + } + + QString ScriptingInterface::getInputRecorderSaveDirectory() { + InputRecorder* inputRecorder = InputRecorder::getInstance(); + return inputRecorder->getSaveDirectory(); + } + bool ScriptingInterface::triggerHapticPulseOnDevice(unsigned int device, float strength, float duration, controller::Hand hand) const { return DependencyManager::get()->triggerHapticPulseOnDevice(device, strength, duration, hand); } diff --git a/libraries/controllers/src/controllers/ScriptingInterface.h b/libraries/controllers/src/controllers/ScriptingInterface.h index b47a6fea31..2c60ca25f5 100644 --- a/libraries/controllers/src/controllers/ScriptingInterface.h +++ b/libraries/controllers/src/controllers/ScriptingInterface.h @@ -99,6 +99,13 @@ namespace controller { Q_INVOKABLE const QVariantMap& getHardware() { return _hardware; } Q_INVOKABLE const QVariantMap& getActions() { return _actions; } Q_INVOKABLE const QVariantMap& getStandard() { return _standard; } + Q_INVOKABLE void startInputRecording(); + Q_INVOKABLE void stopInputRecording(); + Q_INVOKABLE void startInputPlayback(); + Q_INVOKABLE void stopInputPlayback(); + Q_INVOKABLE void saveInputRecording(); + Q_INVOKABLE void loadInputRecording(const QString& file); + Q_INVOKABLE QString getInputRecorderSaveDirectory(); bool isMouseCaptured() const { return _mouseCaptured; } bool isTouchCaptured() const { return _touchCaptured; } diff --git a/libraries/controllers/src/controllers/StandardController.cpp b/libraries/controllers/src/controllers/StandardController.cpp index cc90ee7b49..d8c98eb63b 100644 --- a/libraries/controllers/src/controllers/StandardController.cpp +++ b/libraries/controllers/src/controllers/StandardController.cpp @@ -104,6 +104,9 @@ Input::NamedVector StandardController::getAvailableInputs() const { makePair(RIGHT_HAND, "RightHand"), makePair(LEFT_FOOT, "LeftFoot"), makePair(RIGHT_FOOT, "RightFoot"), + makePair(HIPS, "Hips"), + makePair(SPINE2, "Spine2"), + makePair(HEAD, "Head"), // Aliases, PlayStation style names makePair(LB, "L1"), diff --git a/libraries/controllers/src/controllers/UserInputMapper.cpp b/libraries/controllers/src/controllers/UserInputMapper.cpp index fe50f023c3..71b052bfe4 100755 --- a/libraries/controllers/src/controllers/UserInputMapper.cpp +++ b/libraries/controllers/src/controllers/UserInputMapper.cpp @@ -22,7 +22,7 @@ #include "StandardController.h" #include "StateController.h" - +#include "InputRecorder.h" #include "Logging.h" #include "impl/conditionals/AndConditional.h" @@ -243,10 +243,11 @@ void fixBisectedAxis(float& full, float& negative, float& positive) { void UserInputMapper::update(float deltaTime) { Locker locker(_lock); - + InputRecorder* inputRecorder = InputRecorder::getInstance(); static uint64_t updateCount = 0; ++updateCount; + inputRecorder->resetFrame(); // Reset the axis state for next loop for (auto& channel : _actionStates) { channel = 0.0f; @@ -298,6 +299,7 @@ void UserInputMapper::update(float deltaTime) { emit inputEvent(input.id, value); } } + inputRecorder->frameTick(); } Input::NamedVector UserInputMapper::getAvailableInputs(uint16 deviceID) const { diff --git a/libraries/controllers/src/controllers/impl/endpoints/ActionEndpoint.cpp b/libraries/controllers/src/controllers/impl/endpoints/ActionEndpoint.cpp index b671d8e93c..6c14533f02 100644 --- a/libraries/controllers/src/controllers/impl/endpoints/ActionEndpoint.cpp +++ b/libraries/controllers/src/controllers/impl/endpoints/ActionEndpoint.cpp @@ -11,19 +11,32 @@ #include #include "../../UserInputMapper.h" +#include "../../InputRecorder.h" using namespace controller; void ActionEndpoint::apply(float newValue, const Pointer& source) { + InputRecorder* inputRecorder = InputRecorder::getInstance(); + if(inputRecorder->isPlayingback()) { + newValue = inputRecorder->getActionState(Action(_input.getChannel())); + } + _currentValue += newValue; if (_input != Input::INVALID_INPUT) { auto userInputMapper = DependencyManager::get(); userInputMapper->deltaActionState(Action(_input.getChannel()), newValue); } + inputRecorder->setActionState(Action(_input.getChannel()), newValue); } void ActionEndpoint::apply(const Pose& value, const Pointer& source) { _currentPose = value; + InputRecorder* inputRecorder = InputRecorder::getInstance(); + inputRecorder->setActionState(Action(_input.getChannel()), _currentPose); + if (inputRecorder->isPlayingback()) { + _currentPose = inputRecorder->getPoseState(Action(_input.getChannel())); + } + if (!_currentPose.isValid()) { return; } diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index f5d335adea..306db98b35 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -354,12 +354,11 @@ void OpenGLDisplayPlugin::customizeContext() { } if ((image.width() > 0) && (image.height() > 0)) { - cursorData.texture.reset( - gpu::Texture::createStrict( + cursorData.texture = gpu::Texture::createStrict( gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, - gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR)); cursorData.texture->setSource("cursor texture"); auto usage = gpu::Texture::Usage::Builder().withColor().withAlpha(); cursorData.texture->setUsage(usage.build()); diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 4c9e53c037..cab96c258b 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -295,12 +295,11 @@ void HmdDisplayPlugin::internalPresent() { image = image.mirrored(); image = image.convertToFormat(QImage::Format_RGBA8888); if (!_previewTexture) { - _previewTexture.reset( - gpu::Texture::createStrict( + _previewTexture = gpu::Texture::createStrict( gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, - gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR)); _previewTexture->setSource("HMD Preview Texture"); _previewTexture->setUsage(gpu::Texture::Usage::Builder().withColor().build()); _previewTexture->setStoredMipFormat(gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA)); diff --git a/libraries/entities-renderer/CMakeLists.txt b/libraries/entities-renderer/CMakeLists.txt index 0063f4a701..8ef28bbc7b 100644 --- a/libraries/entities-renderer/CMakeLists.txt +++ b/libraries/entities-renderer/CMakeLists.txt @@ -1,7 +1,7 @@ set(TARGET_NAME entities-renderer) AUTOSCRIBE_SHADER_LIB(gpu model procedural render render-utils) setup_hifi_library(Widgets Network Script) -link_hifi_libraries(shared gpu procedural model model-networking script-engine render render-utils) +link_hifi_libraries(shared gpu procedural model model-networking script-engine render render-utils image) target_bullet() diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 63684dcf0f..1de476c825 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -495,7 +495,7 @@ bool EntityTreeRenderer::applySkyboxAndHasAmbient() { bool isAmbientSet = false; if (_pendingAmbientTexture && !_ambientTexture) { - _ambientTexture = textureCache->getTexture(_ambientTextureURL, NetworkTexture::CUBE_TEXTURE); + _ambientTexture = textureCache->getTexture(_ambientTextureURL, image::TextureUsage::CUBE_TEXTURE); } if (_ambientTexture && _ambientTexture->isLoaded()) { _pendingAmbientTexture = false; @@ -512,7 +512,7 @@ bool EntityTreeRenderer::applySkyboxAndHasAmbient() { if (_pendingSkyboxTexture && (!_skyboxTexture || (_skyboxTexture->getURL() != _skyboxTextureURL))) { - _skyboxTexture = textureCache->getTexture(_skyboxTextureURL, NetworkTexture::CUBE_TEXTURE); + _skyboxTexture = textureCache->getTexture(_skyboxTextureURL, image::TextureUsage::CUBE_TEXTURE); } if (_skyboxTexture && _skyboxTexture->isLoaded()) { _pendingSkyboxTexture = false; @@ -1015,11 +1015,11 @@ void EntityTreeRenderer::addEntityToScene(EntityItemPointer entity) { } -void EntityTreeRenderer::entityScriptChanging(const EntityItemID& entityID, const bool reload) { +void EntityTreeRenderer::entityScriptChanging(const EntityItemID& entityID, bool reload) { checkAndCallPreload(entityID, reload, true); } -void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const bool reload, const bool unloadFirst) { +void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, bool reload, bool unloadFirst) { if (_tree && !_shuttingDown) { EntityItemPointer entity = getTree()->findEntityByEntityItemID(entityID); if (!entity) { @@ -1027,11 +1027,11 @@ void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const } bool shouldLoad = entity->shouldPreloadScript() && _entitiesScriptEngine; QString scriptUrl = entity->getScript(); - if (shouldLoad && (unloadFirst || scriptUrl.isEmpty())) { + if ((shouldLoad && unloadFirst) || scriptUrl.isEmpty()) { _entitiesScriptEngine->unloadEntityScript(entityID); entity->scriptHasUnloaded(); } - if (shouldLoad && !scriptUrl.isEmpty()) { + if (shouldLoad) { scriptUrl = ResourceManager::normalizeURL(scriptUrl); _entitiesScriptEngine->loadEntityScript(entityID, scriptUrl, reload); entity->scriptHasPreloaded(); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index ec9f707962..f4717dca51 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -152,7 +152,7 @@ private: bool applySkyboxAndHasAmbient(); bool applyLayeredZones(); - void checkAndCallPreload(const EntityItemID& entityID, const bool reload = false, const bool unloadFirst = false); + void checkAndCallPreload(const EntityItemID& entityID, bool reload = false, bool unloadFirst = false); QList _releasedModels; RayToEntityIntersectionResult findRayIntersectionWorker(const PickRay& ray, Octree::lockType lockType, diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 109c4cbfe9..0d286c46eb 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -216,7 +216,7 @@ void RenderableWebEntityItem::render(RenderArgs* args) { if (!_texture) { auto webSurface = _webSurface; - _texture = gpu::TexturePointer(gpu::Texture::createExternal(OffscreenQmlSurface::getDiscardLambda())); + _texture = gpu::Texture::createExternal(OffscreenQmlSurface::getDiscardLambda()); _texture->setSource(__FUNCTION__); } OffscreenQmlSurface::TextureAndFence newTextureAndFence; diff --git a/libraries/entities/src/EntityActionInterface.cpp b/libraries/entities/src/EntityActionInterface.cpp deleted file mode 100644 index 23e6fc0202..0000000000 --- a/libraries/entities/src/EntityActionInterface.cpp +++ /dev/null @@ -1,332 +0,0 @@ -// -// EntityActionInterface.cpp -// libraries/entities/src -// -// Created by Seth Alves on 2015-6-4 -// 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 -// - - - -/* - - - - - +-----------------------+ +-------------------+ +------------------------------+ - | | | | | | - | EntityActionInterface | | btActionInterface | | EntityActionFactoryInterface | - | (entities) | | (bullet) | | (entities) | - +-----------------------+ +-------------------+ +------------------------------+ - | | | | | - +----+ +--+ +----------+ | | - | | | | | - +-------------------+ +--------------+ +------------------------+ +-------------------------+ - | | | | | | | | - | AssignmentAction | | ObjectAction | | InterfaceActionFactory | | AssignmentActionFactory | - |(assignment client)| | (physics) | | (interface) | | (assignment client) | - +-------------------+ +--------------+ +------------------------+ +-------------------------+ - | - | - | - +--------------------+ - | | - | ObjectActionSpring | - | (physics) | - +--------------------+ - - - - -An action is a callback which is registered with bullet. An action is called-back every physics -simulation step and can do whatever it wants with the various datastructures it has available. An -action, for example, can pull an EntityItem toward a point as if that EntityItem were connected to that -point by a spring. - -In this system, an action is a property of an EntityItem (rather, an EntityItem has a property which -encodes a list of actions). Each action has a type and some arguments. Actions can be created by a -script or when receiving information via an EntityTree data-stream (either over the network or from an -svo file). - -In the interface, if an EntityItem has actions, this EntityItem will have pointers to ObjectAction -subclass (like ObjectActionSpring) instantiations. Code in the entities library affects an action-object -via the EntityActionInterface (which knows nothing about bullet). When the ObjectAction subclass -instance is created, it is registered as an action with bullet. Bullet will call into code in this -instance with the btActionInterface every physics-simulation step. - -Because the action can exist next to the interface's EntityTree or the entity-server's EntityTree, -parallel versions of the factories and actions are needed. - -In an entity-server, any type of action is instantiated as an AssignmentAction. This action isn't called -by bullet (which isn't part of an assignment-client). It does nothing but remember its type and its -arguments. This may change as we try to make the entity-server's simple physics simulation better, but -right now the AssignmentAction class is a place-holder. - -The action-objects are instantiated by singleton (dependecy) subclasses of EntityActionFactoryInterface. -In the interface, the subclass is an InterfaceActionFactory and it will produce things like -ObjectActionSpring. In an entity-server the subclass is an AssignmentActionFactory and it always -produces AssignmentActions. - -Depending on the action's type, it will have various arguments. When a script changes an argument of an -action, the argument-holding member-variables of ObjectActionSpring (in this example) are updated and -also serialized into _actionData in the EntityItem. Each subclass of ObjectAction knows how to serialize -and deserialize its own arguments. _actionData is what gets sent over the wire or saved in an svo file. -When a packet-reader receives data for _actionData, it will save it in the EntityItem; this causes the -deserializer in the ObjectAction subclass to be called with the new data, thereby updating its argument -variables. These argument variables are used by the code which is run when bullet does a callback. - - - */ - -#include "EntityItem.h" - -#include "EntityActionInterface.h" - - -EntityActionType EntityActionInterface::actionTypeFromString(QString actionTypeString) { - QString normalizedActionTypeString = actionTypeString.toLower().remove('-').remove('_'); - if (normalizedActionTypeString == "none") { - return ACTION_TYPE_NONE; - } - if (normalizedActionTypeString == "offset") { - return ACTION_TYPE_OFFSET; - } - if (normalizedActionTypeString == "spring") { - return ACTION_TYPE_SPRING; - } - if (normalizedActionTypeString == "hold") { - return ACTION_TYPE_HOLD; - } - if (normalizedActionTypeString == "traveloriented") { - return ACTION_TYPE_TRAVEL_ORIENTED; - } - - qCDebug(entities) << "Warning -- EntityActionInterface::actionTypeFromString got unknown action-type name" << actionTypeString; - return ACTION_TYPE_NONE; -} - -QString EntityActionInterface::actionTypeToString(EntityActionType actionType) { - switch(actionType) { - case ACTION_TYPE_NONE: - return "none"; - case ACTION_TYPE_OFFSET: - return "offset"; - case ACTION_TYPE_SPRING: - return "spring"; - case ACTION_TYPE_HOLD: - return "hold"; - case ACTION_TYPE_TRAVEL_ORIENTED: - return "travel-oriented"; - } - assert(false); - return "none"; -} - -glm::vec3 EntityActionInterface::extractVec3Argument(QString objectName, QVariantMap arguments, - QString argumentName, bool& ok, bool required) { - if (!arguments.contains(argumentName)) { - if (required) { - qCDebug(entities) << objectName << "requires argument:" << argumentName; - } - ok = false; - return glm::vec3(0.0f); - } - - QVariant resultV = arguments[argumentName]; - if (resultV.type() != (QVariant::Type) QMetaType::QVariantMap) { - qCDebug(entities) << objectName << "argument" << argumentName << "must be a map"; - ok = false; - return glm::vec3(0.0f); - } - - QVariantMap resultVM = resultV.toMap(); - if (!resultVM.contains("x") || !resultVM.contains("y") || !resultVM.contains("z")) { - qCDebug(entities) << objectName << "argument" << argumentName << "must be a map with keys: x, y, z"; - ok = false; - return glm::vec3(0.0f); - } - - QVariant xV = resultVM["x"]; - QVariant yV = resultVM["y"]; - QVariant zV = resultVM["z"]; - - bool xOk = true; - bool yOk = true; - bool zOk = true; - float x = xV.toFloat(&xOk); - float y = yV.toFloat(&yOk); - float z = zV.toFloat(&zOk); - if (!xOk || !yOk || !zOk) { - qCDebug(entities) << objectName << "argument" << argumentName << "must be a map with keys: x, y, and z of type float."; - ok = false; - return glm::vec3(0.0f); - } - - if (x != x || y != y || z != z) { - // at least one of the values is NaN - ok = false; - return glm::vec3(0.0f); - } - - return glm::vec3(x, y, z); -} - -glm::quat EntityActionInterface::extractQuatArgument(QString objectName, QVariantMap arguments, - QString argumentName, bool& ok, bool required) { - if (!arguments.contains(argumentName)) { - if (required) { - qCDebug(entities) << objectName << "requires argument:" << argumentName; - } - ok = false; - return glm::quat(); - } - - QVariant resultV = arguments[argumentName]; - if (resultV.type() != (QVariant::Type) QMetaType::QVariantMap) { - qCDebug(entities) << objectName << "argument" << argumentName << "must be a map, not" << resultV.typeName(); - ok = false; - return glm::quat(); - } - - QVariantMap resultVM = resultV.toMap(); - if (!resultVM.contains("x") || !resultVM.contains("y") || !resultVM.contains("z") || !resultVM.contains("w")) { - qCDebug(entities) << objectName << "argument" << argumentName << "must be a map with keys: x, y, z, and w"; - ok = false; - return glm::quat(); - } - - QVariant xV = resultVM["x"]; - QVariant yV = resultVM["y"]; - QVariant zV = resultVM["z"]; - QVariant wV = resultVM["w"]; - - bool xOk = true; - bool yOk = true; - bool zOk = true; - bool wOk = true; - float x = xV.toFloat(&xOk); - float y = yV.toFloat(&yOk); - float z = zV.toFloat(&zOk); - float w = wV.toFloat(&wOk); - if (!xOk || !yOk || !zOk || !wOk) { - qCDebug(entities) << objectName << "argument" << argumentName - << "must be a map with keys: x, y, z, and w of type float."; - ok = false; - return glm::quat(); - } - - if (x != x || y != y || z != z || w != w) { - // at least one of the components is NaN! - ok = false; - return glm::quat(); - } - - return glm::normalize(glm::quat(w, x, y, z)); -} - -float EntityActionInterface::extractFloatArgument(QString objectName, QVariantMap arguments, - QString argumentName, bool& ok, bool required) { - if (!arguments.contains(argumentName)) { - if (required) { - qCDebug(entities) << objectName << "requires argument:" << argumentName; - } - ok = false; - return 0.0f; - } - - QVariant variant = arguments[argumentName]; - bool variantOk = true; - float value = variant.toFloat(&variantOk); - - if (!variantOk || std::isnan(value)) { - ok = false; - return 0.0f; - } - - return value; -} - -int EntityActionInterface::extractIntegerArgument(QString objectName, QVariantMap arguments, - QString argumentName, bool& ok, bool required) { - if (!arguments.contains(argumentName)) { - if (required) { - qCDebug(entities) << objectName << "requires argument:" << argumentName; - } - ok = false; - return 0.0f; - } - - QVariant variant = arguments[argumentName]; - bool variantOk = true; - int value = variant.toInt(&variantOk); - - if (!variantOk) { - ok = false; - return 0; - } - - return value; -} - -QString EntityActionInterface::extractStringArgument(QString objectName, QVariantMap arguments, - QString argumentName, bool& ok, bool required) { - if (!arguments.contains(argumentName)) { - if (required) { - qCDebug(entities) << objectName << "requires argument:" << argumentName; - } - ok = false; - return ""; - } - return arguments[argumentName].toString(); -} - -bool EntityActionInterface::extractBooleanArgument(QString objectName, QVariantMap arguments, - QString argumentName, bool& ok, bool required) { - if (!arguments.contains(argumentName)) { - if (required) { - qCDebug(entities) << objectName << "requires argument:" << argumentName; - } - ok = false; - return false; - } - return arguments[argumentName].toBool(); -} - - - -QDataStream& operator<<(QDataStream& stream, const EntityActionType& entityActionType) -{ - return stream << (quint16)entityActionType; -} - -QDataStream& operator>>(QDataStream& stream, EntityActionType& entityActionType) -{ - quint16 actionTypeAsInt; - stream >> actionTypeAsInt; - entityActionType = (EntityActionType)actionTypeAsInt; - return stream; -} - -QString serializedActionsToDebugString(QByteArray data) { - if (data.size() == 0) { - return QString(); - } - QVector serializedActions; - QDataStream serializedActionsStream(data); - serializedActionsStream >> serializedActions; - - QString result; - foreach(QByteArray serializedAction, serializedActions) { - QDataStream serializedActionStream(serializedAction); - EntityActionType actionType; - QUuid actionID; - serializedActionStream >> actionType; - serializedActionStream >> actionID; - result += EntityActionInterface::actionTypeToString(actionType) + "-" + actionID.toString() + " "; - } - - return result; -} diff --git a/libraries/entities/src/EntityActionFactoryInterface.h b/libraries/entities/src/EntityDynamicFactoryInterface.h similarity index 56% rename from libraries/entities/src/EntityActionFactoryInterface.h rename to libraries/entities/src/EntityDynamicFactoryInterface.h index adff1a53ba..7d44b0a5e9 100644 --- a/libraries/entities/src/EntityActionFactoryInterface.h +++ b/libraries/entities/src/EntityDynamicFactoryInterface.h @@ -1,5 +1,5 @@ // -// EntityActionFactoryInterface.cpp +// EntityDynamicFactoryInterface.cpp // libraries/entities/src // // Created by Seth Alves on 2015-6-2 @@ -9,26 +9,26 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#ifndef hifi_EntityActionFactoryInterface_h -#define hifi_EntityActionFactoryInterface_h +#ifndef hifi_EntityDynamicFactoryInterface_h +#define hifi_EntityDynamicFactoryInterface_h #include -#include "EntityActionInterface.h" +#include "EntityDynamicInterface.h" -class EntityActionFactoryInterface : public QObject, public Dependency { +class EntityDynamicFactoryInterface : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY public: - EntityActionFactoryInterface() { } - virtual ~EntityActionFactoryInterface() { } - virtual EntityActionPointer factory(EntityActionType type, + EntityDynamicFactoryInterface() { } + virtual ~EntityDynamicFactoryInterface() { } + virtual EntityDynamicPointer factory(EntityDynamicType type, const QUuid& id, EntityItemPointer ownerEntity, QVariantMap arguments) { assert(false); return nullptr; } - virtual EntityActionPointer factoryBA(EntityItemPointer ownerEntity, + virtual EntityDynamicPointer factoryBA(EntityItemPointer ownerEntity, QByteArray data) { assert(false); return nullptr; } }; -#endif // hifi_EntityActionFactoryInterface_h +#endif // hifi_EntityDynamicFactoryInterface_h diff --git a/libraries/entities/src/EntityDynamicInterface.cpp b/libraries/entities/src/EntityDynamicInterface.cpp new file mode 100644 index 0000000000..2ab9a60397 --- /dev/null +++ b/libraries/entities/src/EntityDynamicInterface.cpp @@ -0,0 +1,347 @@ +// +// EntityDynamicInterface.cpp +// libraries/entities/src +// +// Created by Seth Alves on 2015-6-4 +// 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 +// + + + +/* + + +-------------------------+ +--------------------------------+ + | | | | + | EntityDynamicsInterface | | EntityDynamicsFactoryInterface | + | (entities) | | (entities) | + +-------------------------+ +--------------------------------+ + | | | | + +----+ +--+ | | + | | | | + +---------------------+ +----------------+ +--------------------------+ +---------------------------+ + | | | | | | | | + | AssignmentDynamics | | ObjectDynamics | | InterfaceDynamicsFactory | | AssignmentDynamicsFactory | + |(assignment client) | | (physics) | | (interface) | | (assignment client) | + +---------------------+ +----------------+ +--------------------------+ +---------------------------+ + | | + | | + +---------------------+ | | + | | | | + | btActionInterface | | | + | (bullet) | | | + +---------------------+ | | + | | | + +--------------+ +------------------+ +-------------------+ + | | | | | | + | ObjectAction | | ObjectConstraint | | btTypedConstraint | + | (physics) | | (physics) --|--->| (bullet) | + +--------------+ +------------------+ +-------------------+ + | | + | | + +--------------------+ +-----------------------+ + | | | | + | ObjectActionSpring | | ObjectConstraintHinge | + | (physics) | | (physics) | + +--------------------+ +-----------------------+ + + + +A dynamic is a callback which is registered with bullet. A dynamic is called-back every physics +simulation step and can do whatever it wants with the various datastructures it has available. An +dynamic, for example, can pull an EntityItem toward a point as if that EntityItem were connected to that +point by a spring. + +In this system, a dynamic is a property of an EntityItem (rather, an EntityItem has a property which +encodes a list of dynamics). Each dynamic has a type and some arguments. Dynamics can be created by a +script or when receiving information via an EntityTree data-stream (either over the network or from an +svo file). + +In the interface, if an EntityItem has dynamics, this EntityItem will have pointers to ObjectDynamic +subclass (like ObjectDynamicSpring) instantiations. Code in the entities library affects a dynamic-object +via the EntityDynamicInterface (which knows nothing about bullet). When the ObjectDynamic subclass +instance is created, it is registered as a dynamic with bullet. Bullet will call into code in this +instance with the btDynamicInterface every physics-simulation step. + +Because the dynamic can exist next to the interface's EntityTree or the entity-server's EntityTree, +parallel versions of the factories and dynamics are needed. + +In an entity-server, any type of dynamic is instantiated as an AssignmentDynamic. This dynamic isn't called +by bullet (which isn't part of an assignment-client). It does nothing but remember its type and its +arguments. This may change as we try to make the entity-server's simple physics simulation better, but +right now the AssignmentDynamic class is a place-holder. + +The dynamic-objects are instantiated by singleton (dependecy) subclasses of EntityDynamicFactoryInterface. +In the interface, the subclass is an InterfaceDynamicFactory and it will produce things like +ObjectDynamicSpring. In an entity-server the subclass is an AssignmentDynamicFactory and it always +produces AssignmentDynamics. + +Depending on the dynamic's type, it will have various arguments. When a script changes an argument of an +dynamic, the argument-holding member-variables of ObjectDynamicSpring (in this example) are updated and +also serialized into _dynamicData in the EntityItem. Each subclass of ObjectDynamic knows how to serialize +and deserialize its own arguments. _dynamicData is what gets sent over the wire or saved in an svo file. +When a packet-reader receives data for _dynamicData, it will save it in the EntityItem; this causes the +deserializer in the ObjectDynamic subclass to be called with the new data, thereby updating its argument +variables. These argument variables are used by the code which is run when bullet does a callback. + + +*/ + +#include "EntityItem.h" + +#include "EntityDynamicInterface.h" + + +EntityDynamicType EntityDynamicInterface::dynamicTypeFromString(QString dynamicTypeString) { + QString normalizedDynamicTypeString = dynamicTypeString.toLower().remove('-').remove('_'); + if (normalizedDynamicTypeString == "none") { + return DYNAMIC_TYPE_NONE; + } + if (normalizedDynamicTypeString == "offset") { + return DYNAMIC_TYPE_OFFSET; + } + if (normalizedDynamicTypeString == "spring") { + return DYNAMIC_TYPE_SPRING; + } + if (normalizedDynamicTypeString == "hold") { + return DYNAMIC_TYPE_HOLD; + } + if (normalizedDynamicTypeString == "traveloriented") { + return DYNAMIC_TYPE_TRAVEL_ORIENTED; + } + if (normalizedDynamicTypeString == "hinge") { + return DYNAMIC_TYPE_HINGE; + } + if (normalizedDynamicTypeString == "fargrab") { + return DYNAMIC_TYPE_FAR_GRAB; + } + + qCDebug(entities) << "Warning -- EntityDynamicInterface::dynamicTypeFromString got unknown dynamic-type name" + << dynamicTypeString; + return DYNAMIC_TYPE_NONE; +} + +QString EntityDynamicInterface::dynamicTypeToString(EntityDynamicType dynamicType) { + switch(dynamicType) { + case DYNAMIC_TYPE_NONE: + return "none"; + case DYNAMIC_TYPE_OFFSET: + return "offset"; + case DYNAMIC_TYPE_SPRING: + return "spring"; + case DYNAMIC_TYPE_HOLD: + return "hold"; + case DYNAMIC_TYPE_TRAVEL_ORIENTED: + return "travel-oriented"; + case DYNAMIC_TYPE_HINGE: + return "hinge"; + case DYNAMIC_TYPE_FAR_GRAB: + return "far-grab"; + } + assert(false); + return "none"; +} + +glm::vec3 EntityDynamicInterface::extractVec3Argument(QString objectName, QVariantMap arguments, + QString argumentName, bool& ok, bool required) { + if (!arguments.contains(argumentName)) { + if (required) { + qCDebug(entities) << objectName << "requires argument:" << argumentName; + } + ok = false; + return glm::vec3(0.0f); + } + + QVariant resultV = arguments[argumentName]; + if (resultV.type() != (QVariant::Type) QMetaType::QVariantMap) { + qCDebug(entities) << objectName << "argument" << argumentName << "must be a map"; + ok = false; + return glm::vec3(0.0f); + } + + QVariantMap resultVM = resultV.toMap(); + if (!resultVM.contains("x") || !resultVM.contains("y") || !resultVM.contains("z")) { + qCDebug(entities) << objectName << "argument" << argumentName << "must be a map with keys: x, y, z"; + ok = false; + return glm::vec3(0.0f); + } + + QVariant xV = resultVM["x"]; + QVariant yV = resultVM["y"]; + QVariant zV = resultVM["z"]; + + bool xOk = true; + bool yOk = true; + bool zOk = true; + float x = xV.toFloat(&xOk); + float y = yV.toFloat(&yOk); + float z = zV.toFloat(&zOk); + if (!xOk || !yOk || !zOk) { + qCDebug(entities) << objectName << "argument" << argumentName << "must be a map with keys: x, y, and z of type float."; + ok = false; + return glm::vec3(0.0f); + } + + if (x != x || y != y || z != z) { + // at least one of the values is NaN + ok = false; + return glm::vec3(0.0f); + } + + return glm::vec3(x, y, z); +} + +glm::quat EntityDynamicInterface::extractQuatArgument(QString objectName, QVariantMap arguments, + QString argumentName, bool& ok, bool required) { + if (!arguments.contains(argumentName)) { + if (required) { + qCDebug(entities) << objectName << "requires argument:" << argumentName; + } + ok = false; + return glm::quat(); + } + + QVariant resultV = arguments[argumentName]; + if (resultV.type() != (QVariant::Type) QMetaType::QVariantMap) { + qCDebug(entities) << objectName << "argument" << argumentName << "must be a map, not" << resultV.typeName(); + ok = false; + return glm::quat(); + } + + QVariantMap resultVM = resultV.toMap(); + if (!resultVM.contains("x") || !resultVM.contains("y") || !resultVM.contains("z") || !resultVM.contains("w")) { + qCDebug(entities) << objectName << "argument" << argumentName << "must be a map with keys: x, y, z, and w"; + ok = false; + return glm::quat(); + } + + QVariant xV = resultVM["x"]; + QVariant yV = resultVM["y"]; + QVariant zV = resultVM["z"]; + QVariant wV = resultVM["w"]; + + bool xOk = true; + bool yOk = true; + bool zOk = true; + bool wOk = true; + float x = xV.toFloat(&xOk); + float y = yV.toFloat(&yOk); + float z = zV.toFloat(&zOk); + float w = wV.toFloat(&wOk); + if (!xOk || !yOk || !zOk || !wOk) { + qCDebug(entities) << objectName << "argument" << argumentName + << "must be a map with keys: x, y, z, and w of type float."; + ok = false; + return glm::quat(); + } + + if (x != x || y != y || z != z || w != w) { + // at least one of the components is NaN! + ok = false; + return glm::quat(); + } + + return glm::normalize(glm::quat(w, x, y, z)); +} + +float EntityDynamicInterface::extractFloatArgument(QString objectName, QVariantMap arguments, + QString argumentName, bool& ok, bool required) { + if (!arguments.contains(argumentName)) { + if (required) { + qCDebug(entities) << objectName << "requires argument:" << argumentName; + } + ok = false; + return 0.0f; + } + + QVariant variant = arguments[argumentName]; + bool variantOk = true; + float value = variant.toFloat(&variantOk); + + if (!variantOk || std::isnan(value)) { + ok = false; + return 0.0f; + } + + return value; +} + +int EntityDynamicInterface::extractIntegerArgument(QString objectName, QVariantMap arguments, + QString argumentName, bool& ok, bool required) { + if (!arguments.contains(argumentName)) { + if (required) { + qCDebug(entities) << objectName << "requires argument:" << argumentName; + } + ok = false; + return 0.0f; + } + + QVariant variant = arguments[argumentName]; + bool variantOk = true; + int value = variant.toInt(&variantOk); + + if (!variantOk) { + ok = false; + return 0; + } + + return value; +} + +QString EntityDynamicInterface::extractStringArgument(QString objectName, QVariantMap arguments, + QString argumentName, bool& ok, bool required) { + if (!arguments.contains(argumentName)) { + if (required) { + qCDebug(entities) << objectName << "requires argument:" << argumentName; + } + ok = false; + return ""; + } + return arguments[argumentName].toString(); +} + +bool EntityDynamicInterface::extractBooleanArgument(QString objectName, QVariantMap arguments, + QString argumentName, bool& ok, bool required) { + if (!arguments.contains(argumentName)) { + if (required) { + qCDebug(entities) << objectName << "requires argument:" << argumentName; + } + ok = false; + return false; + } + return arguments[argumentName].toBool(); +} + +QDataStream& operator<<(QDataStream& stream, const EntityDynamicType& entityDynamicType) { + return stream << (quint16)entityDynamicType; +} + +QDataStream& operator>>(QDataStream& stream, EntityDynamicType& entityDynamicType) { + quint16 dynamicTypeAsInt; + stream >> dynamicTypeAsInt; + entityDynamicType = (EntityDynamicType)dynamicTypeAsInt; + return stream; +} + +QString serializedDynamicsToDebugString(QByteArray data) { + if (data.size() == 0) { + return QString(); + } + QVector serializedDynamics; + QDataStream serializedDynamicsStream(data); + serializedDynamicsStream >> serializedDynamics; + + QString result; + foreach(QByteArray serializedDynamic, serializedDynamics) { + QDataStream serializedDynamicStream(serializedDynamic); + EntityDynamicType dynamicType; + QUuid dynamicID; + serializedDynamicStream >> dynamicType; + serializedDynamicStream >> dynamicID; + result += EntityDynamicInterface::dynamicTypeToString(dynamicType) + "-" + dynamicID.toString() + " "; + } + + return result; +} diff --git a/libraries/entities/src/EntityActionInterface.h b/libraries/entities/src/EntityDynamicInterface.h similarity index 68% rename from libraries/entities/src/EntityActionInterface.h rename to libraries/entities/src/EntityDynamicInterface.h index d9a901f1f6..93d9ffa43e 100644 --- a/libraries/entities/src/EntityActionInterface.h +++ b/libraries/entities/src/EntityDynamicInterface.h @@ -1,5 +1,5 @@ // -// EntityActionInterface.h +// EntityDynamicInterface.h // libraries/entities/src // // Created by Seth Alves on 2015-6-2 @@ -9,8 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#ifndef hifi_EntityActionInterface_h -#define hifi_EntityActionInterface_h +#ifndef hifi_EntityDynamicInterface_h +#define hifi_EntityDynamicInterface_h #include #include @@ -23,22 +23,27 @@ using EntityItemWeakPointer = std::weak_ptr; class EntitySimulation; using EntitySimulationPointer = std::shared_ptr; -enum EntityActionType { - // keep these synchronized with actionTypeFromString and actionTypeToString - ACTION_TYPE_NONE = 0, - ACTION_TYPE_OFFSET = 1000, - ACTION_TYPE_SPRING = 2000, - ACTION_TYPE_HOLD = 3000, - ACTION_TYPE_TRAVEL_ORIENTED = 4000 +enum EntityDynamicType { + // keep these synchronized with dynamicTypeFromString and dynamicTypeToString + DYNAMIC_TYPE_NONE = 0, + DYNAMIC_TYPE_OFFSET = 1000, + DYNAMIC_TYPE_SPRING = 2000, + DYNAMIC_TYPE_HOLD = 3000, + DYNAMIC_TYPE_TRAVEL_ORIENTED = 4000, + DYNAMIC_TYPE_HINGE = 5000, + DYNAMIC_TYPE_FAR_GRAB = 6000 }; -class EntityActionInterface { +class EntityDynamicInterface { public: - EntityActionInterface(EntityActionType type, const QUuid& id) : _id(id), _type(type) { } - virtual ~EntityActionInterface() { } + EntityDynamicInterface(EntityDynamicType type, const QUuid& id) : _id(id), _type(type) { } + virtual ~EntityDynamicInterface() { } const QUuid& getID() const { return _id; } - EntityActionType getType() const { return _type; } + EntityDynamicType getType() const { return _type; } + virtual bool isAction() const { return false; } + virtual bool isConstraint() const { return false; } + virtual bool isReadyForAdd() const { return true; } bool isActive() { return _active; } @@ -51,8 +56,8 @@ public: virtual QByteArray serialize() const = 0; virtual void deserialize(QByteArray serializedArguments) = 0; - static EntityActionType actionTypeFromString(QString actionTypeString); - static QString actionTypeToString(EntityActionType actionType); + static EntityDynamicType dynamicTypeFromString(QString dynamicTypeString); + static QString dynamicTypeToString(EntityDynamicType dynamicType); virtual bool lifetimeIsOver() { return false; } virtual quint64 getExpires() { return 0; } @@ -82,26 +87,26 @@ public: protected: virtual glm::vec3 getPosition() = 0; - virtual void setPosition(glm::vec3 position) = 0; + // virtual void setPosition(glm::vec3 position) = 0; virtual glm::quat getRotation() = 0; - virtual void setRotation(glm::quat rotation) = 0; + // virtual void setRotation(glm::quat rotation) = 0; virtual glm::vec3 getLinearVelocity() = 0; virtual void setLinearVelocity(glm::vec3 linearVelocity) = 0; virtual glm::vec3 getAngularVelocity() = 0; virtual void setAngularVelocity(glm::vec3 angularVelocity) = 0; QUuid _id; - EntityActionType _type; + EntityDynamicType _type; bool _active { false }; - bool _isMine { false }; // did this interface create / edit this action? + bool _isMine { false }; // did this interface create / edit this dynamic? }; -typedef std::shared_ptr EntityActionPointer; +typedef std::shared_ptr EntityDynamicPointer; -QDataStream& operator<<(QDataStream& stream, const EntityActionType& entityActionType); -QDataStream& operator>>(QDataStream& stream, EntityActionType& entityActionType); +QDataStream& operator<<(QDataStream& stream, const EntityDynamicType& entityDynamicType); +QDataStream& operator>>(QDataStream& stream, EntityDynamicType& entityDynamicType); -QString serializedActionsToDebugString(QByteArray data); +QString serializedDynamicsToDebugString(QByteArray data); -#endif // hifi_EntityActionInterface_h +#endif // hifi_EntityDynamicInterface_h diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 3f732e26cb..a6de541958 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -30,7 +30,7 @@ #include "EntitiesLogging.h" #include "EntityTree.h" #include "EntitySimulation.h" -#include "EntityActionFactoryInterface.h" +#include "EntityDynamicFactoryInterface.h" int EntityItem::_maxActionsDataSize = 800; @@ -280,7 +280,7 @@ OctreeElement::AppendState EntityItem::appendEntityData(OctreePacketData* packet APPEND_ENTITY_PROPERTY(PROP_COLLISION_SOUND_URL, getCollisionSoundURL()); APPEND_ENTITY_PROPERTY(PROP_HREF, getHref()); APPEND_ENTITY_PROPERTY(PROP_DESCRIPTION, getDescription()); - APPEND_ENTITY_PROPERTY(PROP_ACTION_DATA, getActionData()); + APPEND_ENTITY_PROPERTY(PROP_ACTION_DATA, getDynamicData()); // convert AVATAR_SELF_ID to actual sessionUUID. QUuid actualParentID = getParentID(); @@ -821,7 +821,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef READ_ENTITY_PROPERTY(PROP_COLLISION_SOUND_URL, QString, setCollisionSoundURL); READ_ENTITY_PROPERTY(PROP_HREF, QString, setHref); READ_ENTITY_PROPERTY(PROP_DESCRIPTION, QString, setDescription); - READ_ENTITY_PROPERTY(PROP_ACTION_DATA, QByteArray, setActionData); + READ_ENTITY_PROPERTY(PROP_ACTION_DATA, QByteArray, setDynamicData); { // parentID and parentJointIndex are also protected by simulation ownership bool oldOverwrite = overwriteLocalData; @@ -1251,7 +1251,7 @@ EntityItemProperties EntityItem::getProperties(EntityPropertyFlags desiredProper COPY_ENTITY_PROPERTY_TO_PROPERTIES(name, getName); COPY_ENTITY_PROPERTY_TO_PROPERTIES(href, getHref); COPY_ENTITY_PROPERTY_TO_PROPERTIES(description, getDescription); - COPY_ENTITY_PROPERTY_TO_PROPERTIES(actionData, getActionData); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(actionData, getDynamicData); COPY_ENTITY_PROPERTY_TO_PROPERTIES(parentID, getParentID); COPY_ENTITY_PROPERTY_TO_PROPERTIES(parentJointIndex, getParentJointIndex); COPY_ENTITY_PROPERTY_TO_PROPERTIES(queryAACube, getQueryAACube); @@ -1358,7 +1358,7 @@ bool EntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(name, setName); SET_ENTITY_PROPERTY_FROM_PROPERTIES(href, setHref); SET_ENTITY_PROPERTY_FROM_PROPERTIES(description, setDescription); - SET_ENTITY_PROPERTY_FROM_PROPERTIES(actionData, setActionData); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(actionData, setDynamicData); SET_ENTITY_PROPERTY_FROM_PROPERTIES(parentID, updateParentID); SET_ENTITY_PROPERTY_FROM_PROPERTIES(parentJointIndex, setParentJointIndex); SET_ENTITY_PROPERTY_FROM_PROPERTIES(queryAACube, setQueryAACube); @@ -1872,10 +1872,20 @@ void EntityItem::computeCollisionGroupAndFinalMask(int16_t& group, int16_t& mask iAmHoldingThis = true; } // also, don't bootstrap our own avatar with a hold action - QList holdActions = getActionsOfType(ACTION_TYPE_HOLD); - QList::const_iterator i = holdActions.begin(); + QList holdActions = getActionsOfType(DYNAMIC_TYPE_HOLD); + QList::const_iterator i = holdActions.begin(); while (i != holdActions.end()) { - EntityActionPointer action = *i; + EntityDynamicPointer action = *i; + if (action->isMine()) { + iAmHoldingThis = true; + break; + } + i++; + } + QList farGrabActions = getActionsOfType(DYNAMIC_TYPE_FAR_GRAB); + i = farGrabActions.begin(); + while (i != farGrabActions.end()) { + EntityDynamicPointer action = *i; if (action->isMine()) { iAmHoldingThis = true; break; @@ -1941,18 +1951,18 @@ void EntityItem::rememberHasSimulationOwnershipBid() const { QString EntityItem::actionsToDebugString() { QString result; QVector serializedActions; - QHash::const_iterator i = _objectActions.begin(); + QHash::const_iterator i = _objectActions.begin(); while (i != _objectActions.end()) { const QUuid id = i.key(); - EntityActionPointer action = _objectActions[id]; - EntityActionType actionType = action->getType(); + EntityDynamicPointer action = _objectActions[id]; + EntityDynamicType actionType = action->getType(); result += QString("") + actionType + ":" + action->getID().toString() + " "; i++; } return result; } -bool EntityItem::addAction(EntitySimulationPointer simulation, EntityActionPointer action) { +bool EntityItem::addAction(EntitySimulationPointer simulation, EntityDynamicPointer action) { bool result; withWriteLock([&] { checkWaitingToRemove(simulation); @@ -1960,7 +1970,7 @@ bool EntityItem::addAction(EntitySimulationPointer simulation, EntityActionPoint result = addActionInternal(simulation, action); if (result) { action->setIsMine(true); - _actionDataDirty = true; + _dynamicDataDirty = true; } else { removeActionInternal(action->getID()); } @@ -1969,7 +1979,7 @@ bool EntityItem::addAction(EntitySimulationPointer simulation, EntityActionPoint return result; } -bool EntityItem::addActionInternal(EntitySimulationPointer simulation, EntityActionPointer action) { +bool EntityItem::addActionInternal(EntitySimulationPointer simulation, EntityDynamicPointer action) { assert(action); assert(simulation); auto actionOwnerEntity = action->getOwnerEntity().lock(); @@ -1979,7 +1989,7 @@ bool EntityItem::addActionInternal(EntitySimulationPointer simulation, EntityAct const QUuid& actionID = action->getID(); assert(!_objectActions.contains(actionID) || _objectActions[actionID] == action); _objectActions[actionID] = action; - simulation->addAction(action); + simulation->addDynamic(action); bool success; QByteArray newDataCache; @@ -2003,14 +2013,13 @@ bool EntityItem::updateAction(EntitySimulationPointer simulation, const QUuid& a return; } - EntityActionPointer action = _objectActions[actionID]; + EntityDynamicPointer action = _objectActions[actionID]; success = action->updateArguments(arguments); if (success) { action->setIsMine(true); serializeActions(success, _allActionsDataCache); _dirtyFlags |= Simulation::DIRTY_PHYSICS_ACTIVATION; - _dirtyFlags |= Simulation::DIRTY_COLLISION_GROUP; // may need to not collide with own avatar } else { qCDebug(entities) << "EntityItem::updateAction failed"; } @@ -2035,7 +2044,7 @@ bool EntityItem::removeActionInternal(const QUuid& actionID, EntitySimulationPoi simulation = entityTree ? entityTree->getSimulation() : nullptr; } - EntityActionPointer action = _objectActions[actionID]; + EntityDynamicPointer action = _objectActions[actionID]; action->setOwnerEntity(nullptr); action->setIsMine(false); @@ -2049,7 +2058,7 @@ bool EntityItem::removeActionInternal(const QUuid& actionID, EntitySimulationPoi serializeActions(success, _allActionsDataCache); _dirtyFlags |= Simulation::DIRTY_PHYSICS_ACTIVATION; _dirtyFlags |= Simulation::DIRTY_COLLISION_GROUP; // may need to not collide with own avatar - setActionDataNeedsTransmit(true); + setDynamicDataNeedsTransmit(true); return success; } return false; @@ -2057,10 +2066,10 @@ bool EntityItem::removeActionInternal(const QUuid& actionID, EntitySimulationPoi bool EntityItem::clearActions(EntitySimulationPointer simulation) { withWriteLock([&] { - QHash::iterator i = _objectActions.begin(); + QHash::iterator i = _objectActions.begin(); while (i != _objectActions.end()) { const QUuid id = i.key(); - EntityActionPointer action = _objectActions[id]; + EntityDynamicPointer action = _objectActions[id]; i = _objectActions.erase(i); action->setOwnerEntity(nullptr); action->removeFromSimulation(simulation); @@ -2101,12 +2110,12 @@ void EntityItem::deserializeActionsInternal() { serializedActionsStream >> serializedActions; } - // Keep track of which actions got added or updated by the new actionData + // Keep track of which actions got added or updated by the new dynamicData QSet updated; foreach(QByteArray serializedAction, serializedActions) { QDataStream serializedActionStream(serializedAction); - EntityActionType actionType; + EntityDynamicType actionType; QUuid actionID; serializedActionStream >> actionType; serializedActionStream >> actionID; @@ -2115,7 +2124,7 @@ void EntityItem::deserializeActionsInternal() { } if (_objectActions.contains(actionID)) { - EntityActionPointer action = _objectActions[actionID]; + EntityDynamicPointer action = _objectActions[actionID]; // TODO: make sure types match? there isn't currently a way to // change the type of an existing action. if (!action->isMine()) { @@ -2123,9 +2132,9 @@ void EntityItem::deserializeActionsInternal() { } updated << actionID; } else { - auto actionFactory = DependencyManager::get(); + auto actionFactory = DependencyManager::get(); EntityItemPointer entity = getThisPointer(); - EntityActionPointer action = actionFactory->factoryBA(entity, serializedAction); + EntityDynamicPointer action = actionFactory->factoryBA(entity, serializedAction); if (action) { entity->addActionInternal(simulation, action); updated << actionID; @@ -2140,15 +2149,15 @@ void EntityItem::deserializeActionsInternal() { } // remove any actions that weren't included in the new data. - QHash::const_iterator i = _objectActions.begin(); + QHash::const_iterator i = _objectActions.begin(); while (i != _objectActions.end()) { QUuid id = i.key(); if (!updated.contains(id)) { - EntityActionPointer action = i.value(); + EntityDynamicPointer action = i.value(); if (action->isMine()) { // we just received an update that didn't include one of our actions. tell the server about it (again). - setActionDataNeedsTransmit(true); + setDynamicDataNeedsTransmit(true); } else { // don't let someone else delete my action. _actionsToRemove << id; @@ -2167,7 +2176,7 @@ void EntityItem::deserializeActionsInternal() { } } - _actionDataDirty = true; + _dynamicDataDirty = true; return; } @@ -2179,15 +2188,15 @@ void EntityItem::checkWaitingToRemove(EntitySimulationPointer simulation) { _actionsToRemove.clear(); } -void EntityItem::setActionData(QByteArray actionData) { +void EntityItem::setDynamicData(QByteArray dynamicData) { withWriteLock([&] { - setActionDataInternal(actionData); + setDynamicDataInternal(dynamicData); }); } -void EntityItem::setActionDataInternal(QByteArray actionData) { - if (_allActionsDataCache != actionData) { - _allActionsDataCache = actionData; +void EntityItem::setDynamicDataInternal(QByteArray dynamicData) { + if (_allActionsDataCache != dynamicData) { + _allActionsDataCache = dynamicData; deserializeActionsInternal(); } checkWaitingToRemove(); @@ -2201,10 +2210,10 @@ void EntityItem::serializeActions(bool& success, QByteArray& result) const { } QVector serializedActions; - QHash::const_iterator i = _objectActions.begin(); + QHash::const_iterator i = _objectActions.begin(); while (i != _objectActions.end()) { const QUuid id = i.key(); - EntityActionPointer action = _objectActions[id]; + EntityDynamicPointer action = _objectActions[id]; QByteArray bytesForAction = action->serialize(); serializedActions << bytesForAction; i++; @@ -2224,23 +2233,23 @@ void EntityItem::serializeActions(bool& success, QByteArray& result) const { return; } -const QByteArray EntityItem::getActionDataInternal() const { - if (_actionDataDirty) { +const QByteArray EntityItem::getDynamicDataInternal() const { + if (_dynamicDataDirty) { bool success; serializeActions(success, _allActionsDataCache); if (success) { - _actionDataDirty = false; + _dynamicDataDirty = false; } } return _allActionsDataCache; } -const QByteArray EntityItem::getActionData() const { +const QByteArray EntityItem::getDynamicData() const { QByteArray result; - if (_actionDataDirty) { + if (_dynamicDataDirty) { withWriteLock([&] { - getActionDataInternal(); + getDynamicDataInternal(); result = _allActionsDataCache; }); } else { @@ -2255,9 +2264,9 @@ QVariantMap EntityItem::getActionArguments(const QUuid& actionID) const { QVariantMap result; withReadLock([&] { if (_objectActions.contains(actionID)) { - EntityActionPointer action = _objectActions[actionID]; + EntityDynamicPointer action = _objectActions[actionID]; result = action->getArguments(); - result["type"] = EntityActionInterface::actionTypeToString(action->getType()); + result["type"] = EntityDynamicInterface::dynamicTypeToString(action->getType()); } }); @@ -2265,7 +2274,7 @@ QVariantMap EntityItem::getActionArguments(const QUuid& actionID) const { } bool EntityItem::shouldSuppressLocationEdits() const { - QHash::const_iterator i = _objectActions.begin(); + QHash::const_iterator i = _objectActions.begin(); while (i != _objectActions.end()) { if (i.value()->shouldSuppressLocationEdits()) { return true; @@ -2276,12 +2285,12 @@ bool EntityItem::shouldSuppressLocationEdits() const { return false; } -QList EntityItem::getActionsOfType(EntityActionType typeToGet) const { - QList result; +QList EntityItem::getActionsOfType(EntityDynamicType typeToGet) const { + QList result; - QHash::const_iterator i = _objectActions.begin(); + QHash::const_iterator i = _objectActions.begin(); while (i != _objectActions.end()) { - EntityActionPointer action = i.value(); + EntityDynamicPointer action = i.value(); if (action->getType() == typeToGet && action->isActive()) { result += action; } diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 3f75c595a5..ff5f12b2f7 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -36,17 +36,17 @@ #include "EntityTypes.h" #include "SimulationOwner.h" #include "SimulationFlags.h" -#include "EntityActionInterface.h" +#include "EntityDynamicInterface.h" class EntitySimulation; class EntityTreeElement; class EntityTreeElementExtraEncodeData; -class EntityActionInterface; +class EntityDynamicInterface; class EntityItemProperties; class EntityTree; class btCollisionShape; typedef std::shared_ptr EntityTreePointer; -typedef std::shared_ptr EntityActionPointer; +typedef std::shared_ptr EntityDynamicPointer; typedef std::shared_ptr EntityTreeElementPointer; using EntityTreeElementExtraEncodeDataPointer = std::shared_ptr; @@ -398,22 +398,22 @@ public: void flagForMotionStateChange() { _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; } QString actionsToDebugString(); - bool addAction(EntitySimulationPointer simulation, EntityActionPointer action); + bool addAction(EntitySimulationPointer simulation, EntityDynamicPointer action); bool updateAction(EntitySimulationPointer simulation, const QUuid& actionID, const QVariantMap& arguments); bool removeAction(EntitySimulationPointer simulation, const QUuid& actionID); bool clearActions(EntitySimulationPointer simulation); - void setActionData(QByteArray actionData); - const QByteArray getActionData() const; + void setDynamicData(QByteArray dynamicData); + const QByteArray getDynamicData() const; bool hasActions() const { return !_objectActions.empty(); } QList getActionIDs() const { return _objectActions.keys(); } QVariantMap getActionArguments(const QUuid& actionID) const; void deserializeActions(); - void setActionDataDirty(bool value) const { _actionDataDirty = value; } - bool actionDataDirty() const { return _actionDataDirty; } + void setDynamicDataDirty(bool value) const { _dynamicDataDirty = value; } + bool dynamicDataDirty() const { return _dynamicDataDirty; } - void setActionDataNeedsTransmit(bool value) const { _actionDataNeedsTransmit = value; } - bool actionDataNeedsTransmit() const { return _actionDataNeedsTransmit; } + void setDynamicDataNeedsTransmit(bool value) const { _dynamicDataNeedsTransmit = value; } + bool dynamicDataNeedsTransmit() const { return _dynamicDataNeedsTransmit; } bool shouldSuppressLocationEdits() const; @@ -421,7 +421,7 @@ public: const QUuid& getSourceUUID() const { return _sourceUUID; } bool matchesSourceUUID(const QUuid& sourceUUID) const { return _sourceUUID == sourceUUID; } - QList getActionsOfType(EntityActionType typeToGet) const; + QList getActionsOfType(EntityDynamicType typeToGet) const; // these are in the frame of this object virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override { return glm::quat(); } @@ -479,8 +479,8 @@ protected: void setSimulated(bool simulated) { _simulated = simulated; } - const QByteArray getActionDataInternal() const; - void setActionDataInternal(QByteArray actionData); + const QByteArray getDynamicDataInternal() const; + void setDynamicDataInternal(QByteArray dynamicData); virtual void locationChanged(bool tellPhysics = true) override; virtual void dimensionsChanged() override; @@ -570,22 +570,22 @@ protected: void* _physicsInfo { nullptr }; // set by EntitySimulation bool _simulated; // set by EntitySimulation - bool addActionInternal(EntitySimulationPointer simulation, EntityActionPointer action); + bool addActionInternal(EntitySimulationPointer simulation, EntityDynamicPointer action); bool removeActionInternal(const QUuid& actionID, EntitySimulationPointer simulation = nullptr); void deserializeActionsInternal(); void serializeActions(bool& success, QByteArray& result) const; - QHash _objectActions; + QHash _objectActions; static int _maxActionsDataSize; mutable QByteArray _allActionsDataCache; - // when an entity-server starts up, EntityItem::setActionData is called before the entity-tree is + // when an entity-server starts up, EntityItem::setDynamicData is called before the entity-tree is // ready. This means we can't find our EntityItemPointer or add the action to the simulation. These // are used to keep track of and work around this situation. void checkWaitingToRemove(EntitySimulationPointer simulation = nullptr); mutable QSet _actionsToRemove; - mutable bool _actionDataDirty { false }; - mutable bool _actionDataNeedsTransmit { false }; + mutable bool _dynamicDataDirty { false }; + mutable bool _dynamicDataNeedsTransmit { false }; // _previouslyDeletedActions is used to avoid an action being re-added due to server round-trip lag static quint64 _rememberDeletedActionTime; mutable QHash _previouslyDeletedActions; diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 2c332e8d05..10479e931c 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -24,8 +24,8 @@ #include #include "EntitiesLogging.h" -#include "EntityActionFactoryInterface.h" -#include "EntityActionInterface.h" +#include "EntityDynamicFactoryInterface.h" +#include "EntityDynamicInterface.h" #include "EntitySimulation.h" #include "EntityTree.h" #include "LightEntityItem.h" @@ -1133,7 +1133,7 @@ QUuid EntityScriptingInterface::addAction(const QString& actionTypeString, PROFILE_RANGE(script_entities, __FUNCTION__); QUuid actionID = QUuid::createUuid(); - auto actionFactory = DependencyManager::get(); + auto actionFactory = DependencyManager::get(); bool success = false; actionWorker(entityID, [&](EntitySimulationPointer simulation, EntityItemPointer entity) { // create this action even if the entity doesn't have physics info. it will often be the @@ -1142,11 +1142,11 @@ QUuid EntityScriptingInterface::addAction(const QString& actionTypeString, // if (!entity->getPhysicsInfo()) { // return false; // } - EntityActionType actionType = EntityActionInterface::actionTypeFromString(actionTypeString); - if (actionType == ACTION_TYPE_NONE) { + EntityDynamicType dynamicType = EntityDynamicInterface::dynamicTypeFromString(actionTypeString); + if (dynamicType == DYNAMIC_TYPE_NONE) { return false; } - EntityActionPointer action = actionFactory->factory(actionType, actionID, entity, arguments); + EntityDynamicPointer action = actionFactory->factory(dynamicType, actionID, entity, arguments); if (!action) { return false; } diff --git a/libraries/entities/src/EntitySimulation.cpp b/libraries/entities/src/EntitySimulation.cpp index a29ea8e2c8..fbbc1bde71 100644 --- a/libraries/entities/src/EntitySimulation.cpp +++ b/libraries/entities/src/EntitySimulation.cpp @@ -279,25 +279,25 @@ void EntitySimulation::moveSimpleKinematics(const quint64& now) { } } -void EntitySimulation::addAction(EntityActionPointer action) { +void EntitySimulation::addDynamic(EntityDynamicPointer dynamic) { QMutexLocker lock(&_mutex); - _actionsToAdd += action; + _dynamicsToAdd += dynamic; } -void EntitySimulation::removeAction(const QUuid actionID) { +void EntitySimulation::removeDynamic(const QUuid dynamicID) { QMutexLocker lock(&_mutex); - _actionsToRemove += actionID; + _dynamicsToRemove += dynamicID; } -void EntitySimulation::removeActions(QList actionIDsToRemove) { +void EntitySimulation::removeDynamics(QList dynamicIDsToRemove) { QMutexLocker lock(&_mutex); - foreach(QUuid uuid, actionIDsToRemove) { - _actionsToRemove.insert(uuid); + foreach(QUuid uuid, dynamicIDsToRemove) { + _dynamicsToRemove.insert(uuid); } } -void EntitySimulation::applyActionChanges() { +void EntitySimulation::applyDynamicChanges() { QMutexLocker lock(&_mutex); - _actionsToAdd.clear(); - _actionsToRemove.clear(); + _dynamicsToAdd.clear(); + _dynamicsToRemove.clear(); } diff --git a/libraries/entities/src/EntitySimulation.h b/libraries/entities/src/EntitySimulation.h index f8f506ac70..84d30c495d 100644 --- a/libraries/entities/src/EntitySimulation.h +++ b/libraries/entities/src/EntitySimulation.h @@ -18,7 +18,7 @@ #include -#include "EntityActionInterface.h" +#include "EntityDynamicInterface.h" #include "EntityItem.h" #include "EntityTree.h" @@ -59,10 +59,10 @@ public: // friend class EntityTree; - virtual void addAction(EntityActionPointer action); - virtual void removeAction(const QUuid actionID); - virtual void removeActions(QList actionIDsToRemove); - virtual void applyActionChanges(); + virtual void addDynamic(EntityDynamicPointer dynamic); + virtual void removeDynamic(const QUuid dynamicID); + virtual void removeDynamics(QList dynamicIDsToRemove); + virtual void applyDynamicChanges(); /// \param entity pointer to EntityItem to be added /// \sideeffect sets relevant backpointers in entity, but maybe later when appropriate data structures are locked @@ -103,8 +103,8 @@ protected: SetOfEntities _entitiesToSort; // entities moved by simulation (and might need resort in EntityTree) SetOfEntities _simpleKinematicEntities; // entities undergoing non-colliding kinematic motion - QList _actionsToAdd; - QSet _actionsToRemove; + QList _dynamicsToAdd; + QSet _dynamicsToRemove; protected: SetOfEntities _entitiesToDelete; // entities simulation decided needed to be deleted (EntityTree will actually delete) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index f544a4e5c7..3ad5cc92a5 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -808,7 +808,7 @@ void EntityTree::fixupTerseEditLogging(EntityItemProperties& properties, QList= 0) { QByteArray value = properties.getActionData(); - QString changeHint = serializedActionsToDebugString(value); + QString changeHint = serializedDynamicsToDebugString(value); changedProperties[index] = QString("actionData:") + changeHint; } } diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp b/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp index 7e26e65e02..b0c53caa1a 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp @@ -76,10 +76,6 @@ GLenum GLTexelFormat::evalGLTexelFormatInternal(const gpu::Element& dstFormat) { } break; - case gpu::COMPRESSED_R: - result = GL_COMPRESSED_RED_RGTC1; - break; - case gpu::R11G11B10: // the type should be float result = GL_R11F_G11F_B10F; @@ -149,12 +145,6 @@ GLenum GLTexelFormat::evalGLTexelFormatInternal(const gpu::Element& dstFormat) { case gpu::SRGBA: result = GL_SRGB8; // standard 2.2 gamma correction color break; - case gpu::COMPRESSED_RGB: - result = GL_COMPRESSED_RGB; - break; - case gpu::COMPRESSED_SRGB: - result = GL_COMPRESSED_SRGB; - break; default: qCWarning(gpugllogging) << "Unknown combination of texel format"; } @@ -217,29 +207,22 @@ GLenum GLTexelFormat::evalGLTexelFormatInternal(const gpu::Element& dstFormat) { case gpu::SRGBA: result = GL_SRGB8_ALPHA8; // standard 2.2 gamma correction color break; - case gpu::COMPRESSED_RGBA: - result = GL_COMPRESSED_RGBA; - break; - case gpu::COMPRESSED_SRGBA: - result = GL_COMPRESSED_SRGB_ALPHA; - break; - // FIXME: WE will want to support this later - /* - case gpu::COMPRESSED_BC3_RGBA: - result = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT; + case gpu::COMPRESSED_BC4_RED: + result = GL_COMPRESSED_RED_RGTC1; break; - case gpu::COMPRESSED_BC3_SRGBA: + case gpu::COMPRESSED_BC1_SRGB: + result = GL_COMPRESSED_SRGB_S3TC_DXT1_EXT; + break; + case gpu::COMPRESSED_BC1_SRGBA: + result = GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT; + break; + case gpu::COMPRESSED_BC3_SRGBA: result = GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT; break; - - case gpu::COMPRESSED_BC7_RGBA: - result = GL_COMPRESSED_RGBA_BPTC_UNORM_ARB; + case gpu::COMPRESSED_BC5_XY: + result = GL_COMPRESSED_RG_RGTC2; break; - case gpu::COMPRESSED_BC7_SRGBA: - result = GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM; - break; - */ default: qCWarning(gpugllogging) << "Unknown combination of texel format"; @@ -269,10 +252,6 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E texel.internalFormat = GL_R8; break; - case gpu::COMPRESSED_R: - texel.internalFormat = GL_COMPRESSED_RED_RGTC1; - break; - case gpu::DEPTH: texel.format = GL_DEPTH_COMPONENT; texel.internalFormat = GL_DEPTH_COMPONENT32; @@ -315,12 +294,6 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E case gpu::RGBA: texel.internalFormat = GL_RGB8; break; - case gpu::COMPRESSED_RGB: - texel.internalFormat = GL_COMPRESSED_RGB; - break; - case gpu::COMPRESSED_SRGB: - texel.internalFormat = GL_COMPRESSED_SRGB; - break; default: qCWarning(gpugllogging) << "Unknown combination of texel format"; } @@ -359,30 +332,22 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E texel.internalFormat = GL_SRGB8_ALPHA8; break; - case gpu::COMPRESSED_RGBA: - texel.internalFormat = GL_COMPRESSED_RGBA; - break; - case gpu::COMPRESSED_SRGBA: - texel.internalFormat = GL_COMPRESSED_SRGB_ALPHA; + case gpu::COMPRESSED_BC4_RED: + texel.internalFormat = GL_COMPRESSED_RED_RGTC1; break; - - // FIXME: WE will want to support this later - /* - case gpu::COMPRESSED_BC3_RGBA: - texel.internalFormat = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT; + case gpu::COMPRESSED_BC1_SRGB: + texel.internalFormat = GL_COMPRESSED_SRGB_S3TC_DXT1_EXT; break; - case gpu::COMPRESSED_BC3_SRGBA: + case gpu::COMPRESSED_BC1_SRGBA: + texel.internalFormat = GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT; + break; + case gpu::COMPRESSED_BC3_SRGBA: texel.internalFormat = GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT; break; - - case gpu::COMPRESSED_BC7_RGBA: - texel.internalFormat = GL_COMPRESSED_RGBA_BPTC_UNORM_ARB; + case gpu::COMPRESSED_BC5_XY: + texel.internalFormat = GL_COMPRESSED_RG_RGTC2; break; - case gpu::COMPRESSED_BC7_SRGBA: - texel.internalFormat = GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM; - break; - */ default: qCWarning(gpugllogging) << "Unknown combination of texel format"; @@ -403,10 +368,6 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E texel.type = ELEMENT_TYPE_TO_GL[dstFormat.getType()]; switch (dstFormat.getSemantic()) { - case gpu::COMPRESSED_R: { - texel.internalFormat = GL_COMPRESSED_RED_RGTC1; - break; - } case gpu::RED: case gpu::RGB: case gpu::RGBA: @@ -564,12 +525,6 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E case gpu::SRGBA: texel.internalFormat = GL_SRGB8; // standard 2.2 gamma correction color break; - case gpu::COMPRESSED_RGB: - texel.internalFormat = GL_COMPRESSED_RGB; - break; - case gpu::COMPRESSED_SRGB: - texel.internalFormat = GL_COMPRESSED_SRGB; - break; default: qCWarning(gpugllogging) << "Unknown combination of texel format"; } @@ -646,11 +601,21 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E case gpu::SRGBA: texel.internalFormat = GL_SRGB8_ALPHA8; // standard 2.2 gamma correction color break; - case gpu::COMPRESSED_RGBA: - texel.internalFormat = GL_COMPRESSED_RGBA; + + case gpu::COMPRESSED_BC4_RED: + texel.internalFormat = GL_COMPRESSED_RED_RGTC1; break; - case gpu::COMPRESSED_SRGBA: - texel.internalFormat = GL_COMPRESSED_SRGB_ALPHA; + case gpu::COMPRESSED_BC1_SRGB: + texel.internalFormat = GL_COMPRESSED_SRGB_S3TC_DXT1_EXT; + break; + case gpu::COMPRESSED_BC1_SRGBA: + texel.internalFormat = GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT; + break; + case gpu::COMPRESSED_BC3_SRGBA: + texel.internalFormat = GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT; + break; + case gpu::COMPRESSED_BC5_XY: + texel.internalFormat = GL_COMPRESSED_RG_RGTC2; break; default: qCWarning(gpugllogging) << "Unknown combination of texel format"; diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp index 19891d3370..7f075a1698 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp @@ -120,11 +120,12 @@ void GLTexture::copyMipFaceFromTexture(uint16_t sourceMip, uint16_t targetMip, u } auto size = _gpuObject.evalMipDimensions(sourceMip); auto mipData = _gpuObject.accessStoredMipFace(sourceMip, face); + auto mipSize = _gpuObject.getStoredMipFaceSize(sourceMip, face); if (mipData) { GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), _gpuObject.getStoredMipFormat()); - copyMipFaceLinesFromTexture(targetMip, face, size, 0, texelFormat.format, texelFormat.type, mipData->readData()); + copyMipFaceLinesFromTexture(targetMip, face, size, 0, texelFormat.internalFormat, texelFormat.format, texelFormat.type, mipSize, mipData->readData()); } else { - qCDebug(gpugllogging) << "Missing mipData level=" << sourceMip << " face=" << (int)face << " for texture " << _gpuObject.source().c_str(); + qCDebug(gpugllogging) << "Missing mipData level=" << sourceMip << " face=" << (int)face << " for texture " << _gpuObject.source().c_str(); } } @@ -139,7 +140,7 @@ GLExternalTexture::~GLExternalTexture() { if (recycler) { backend->releaseExternalTexture(_id, recycler); } else { - qWarning() << "No recycler available for texture " << _id << " possible leak"; + qCWarning(gpugllogging) << "No recycler available for texture " << _id << " possible leak"; } const_cast(_id) = 0; } @@ -203,42 +204,38 @@ TransferJob::TransferJob(const GLTexture& parent, uint16_t sourceMip, uint16_t t auto transferDimensions = _parent._gpuObject.evalMipDimensions(sourceMip); GLenum format; + GLenum internalFormat; GLenum type; GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_parent._gpuObject.getTexelFormat(), _parent._gpuObject.getStoredMipFormat()); format = texelFormat.format; + internalFormat = texelFormat.internalFormat; type = texelFormat.type; - auto mipSize = _parent._gpuObject.getStoredMipFaceSize(sourceMip, face); + _transferSize = _parent._gpuObject.getStoredMipFaceSize(sourceMip, face); - - if (0 == lines) { - _transferSize = mipSize; - _bufferingLambda = [=] { - auto mipData = _parent._gpuObject.accessStoredMipFace(sourceMip, face); - _buffer.resize(_transferSize); - memcpy(&_buffer[0], mipData->readData(), _transferSize); - _bufferingCompleted = true; - }; - - } else { + // If we're copying a subsection of the mip, do additional calculations to find the size and offset of the segment + if (0 != lines) { transferDimensions.y = lines; auto dimensions = _parent._gpuObject.evalMipDimensions(sourceMip); - auto bytesPerLine = (uint32_t)mipSize / dimensions.y; - auto sourceOffset = bytesPerLine * lineOffset; + auto bytesPerLine = (uint32_t)_transferSize / dimensions.y; + _transferOffset = bytesPerLine * lineOffset; _transferSize = bytesPerLine * lines; - _bufferingLambda = [=] { - auto mipData = _parent._gpuObject.accessStoredMipFace(sourceMip, face); - _buffer.resize(_transferSize); - memcpy(&_buffer[0], mipData->readData() + sourceOffset, _transferSize); - _bufferingCompleted = true; - }; } Backend::updateTextureTransferPendingSize(0, _transferSize); + if (_transferSize > GLVariableAllocationSupport::MAX_TRANSFER_SIZE) { + qCWarning(gpugllogging) << "Transfer size of " << _transferSize << " exceeds theoretical maximum transfer size"; + } + + // Buffering can invoke disk IO, so it should be off of the main and render threads + _bufferingLambda = [=] { + _mipData = _parent._gpuObject.accessStoredMipFace(sourceMip, face)->createView(_transferSize, _transferOffset); + _bufferingCompleted = true; + }; + _transferLambda = [=] { - _parent.copyMipFaceLinesFromTexture(targetMip, face, transferDimensions, lineOffset, format, type, _buffer.data()); - std::vector emptyVector; - _buffer.swap(emptyVector); + _parent.copyMipFaceLinesFromTexture(targetMip, face, transferDimensions, lineOffset, internalFormat, format, type, _mipData->size(), _mipData->readData()); + _mipData.reset(); }; } @@ -451,10 +448,10 @@ void GLVariableAllocationSupport::updateMemoryPressure() { float pressure = (float)totalVariableMemoryAllocation / (float)allowedMemoryAllocation; auto newState = MemoryPressureState::Idle; - if (pressure > OVERSUBSCRIBED_PRESSURE_VALUE && canDemote) { - newState = MemoryPressureState::Oversubscribed; - } else if (pressure < UNDERSUBSCRIBED_PRESSURE_VALUE && unallocated != 0 && canPromote) { + if (pressure < UNDERSUBSCRIBED_PRESSURE_VALUE && (unallocated != 0 && canPromote)) { newState = MemoryPressureState::Undersubscribed; + } else if (pressure > OVERSUBSCRIBED_PRESSURE_VALUE && canDemote) { + newState = MemoryPressureState::Oversubscribed; } else if (hasTransfers) { newState = MemoryPressureState::Transfer; } @@ -532,6 +529,7 @@ void GLVariableAllocationSupport::processWorkQueues() { } if (workQueue.empty()) { + _memoryPressureState = MemoryPressureState::Idle; _memoryPressureStateStale = true; } } diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.h b/libraries/gpu-gl/src/gpu/gl/GLTexture.h index 960575827f..e0b8a63a99 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.h +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.h @@ -49,8 +49,10 @@ public: using VoidLambdaQueue = std::queue; using ThreadPointer = std::shared_ptr; const GLTexture& _parent; - // Holds the contents to transfer to the GPU in CPU memory - std::vector _buffer; + Texture::PixelsPointer _mipData; + size_t _transferOffset { 0 }; + size_t _transferSize { 0 }; + // Indicates if a transfer from backing storage to interal storage has started bool _bufferingStarted { false }; bool _bufferingCompleted { false }; @@ -78,7 +80,6 @@ public: #endif private: - size_t _transferSize { 0 }; #if THREADED_TEXTURE_BUFFERING void startBuffering(); #endif @@ -112,7 +113,7 @@ protected: static void manageMemory(); //bool canPromoteNoAllocate() const { return _allocatedMip < _populatedMip; } - bool canPromote() const { return _allocatedMip > 0; } + bool canPromote() const { return _allocatedMip > _minAllocatedMip; } bool canDemote() const { return _allocatedMip < _maxAllocatedMip; } bool hasPendingTransfers() const { return _populatedMip > _allocatedMip; } void executeNextTransfer(const TexturePointer& currentTexture); @@ -130,6 +131,9 @@ protected: // The highest (lowest resolution) mip that we will support, relative to the number // of mips in the gpu::Texture object uint16 _maxAllocatedMip { 0 }; + // The lowest (highest resolution) mip that we will support, relative to the number + // of mips in the gpu::Texture object + uint16 _minAllocatedMip { 0 }; // Contains a series of lambdas that when executed will transfer data to the GPU, modify // the _populatedMip and update the sampler in order to fully populate the allocated texture // until _populatedMip == _allocatedMip @@ -163,7 +167,7 @@ public: protected: virtual Size size() const = 0; virtual void generateMips() const = 0; - virtual void copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum format, GLenum type, const void* sourcePointer) const = 0; + virtual void copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum internalFormat, GLenum format, GLenum type, Size sourceSize, const void* sourcePointer) const = 0; virtual void copyMipFaceFromTexture(uint16_t sourceMip, uint16_t targetMip, uint8_t face) const final; GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id); @@ -177,7 +181,7 @@ public: protected: GLExternalTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id); void generateMips() const override {} - void copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum format, GLenum type, const void* sourcePointer) const override {} + void copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum internalFormat, GLenum format, GLenum type, Size sourceSize, const void* sourcePointer) const override {} Size size() const override { return 0; } }; diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h index 545279627a..19979a1778 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h +++ b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h @@ -50,7 +50,7 @@ public: protected: GL41Texture(const std::weak_ptr& backend, const Texture& texture); void generateMips() const override; - void copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum format, GLenum type, const void* sourcePointer) const override; + void copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum internalFormat, GLenum format, GLenum type, Size sourceSize, const void* sourcePointer) const override; virtual void syncSampler() const; void withPreservedTexture(std::function f) const; @@ -105,7 +105,7 @@ public: void promote() override; void demote() override; void populateTransferQueue() override; - void copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum format, GLenum type, const void* sourcePointer) const override; + void copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum internalFormat, GLenum format, GLenum type, Size sourceSize, const void* sourcePointer) const override; Size size() const override { return _size; } Size _size { 0 }; diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp index fe01284446..5db924dd5c 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp @@ -55,6 +55,18 @@ GLTexture* GL41Backend::syncGPUObject(const TexturePointer& texturePointer) { default: Q_UNREACHABLE(); } + } else { + if (texture.getUsageType() == TextureUsageType::RESOURCE) { + auto varTex = static_cast (object); + + if (varTex->_minAllocatedMip > 0) { + auto minAvailableMip = texture.minAvailableMipLevel(); + if (minAvailableMip < varTex->_minAllocatedMip) { + varTex->_minAllocatedMip = minAvailableMip; + GL41VariableAllocationTexture::_memoryPressureStateStale = true; + } + } + } } return object; @@ -69,9 +81,7 @@ GL41Texture::GL41Texture(const std::weak_ptr& backend, const Texture& GLuint GL41Texture::allocate(const Texture& texture) { GLuint result; - // FIXME technically GL 4.2, but OSX includes the ARB_texture_storage extension - glCreateTextures(getGLTextureType(texture), 1, &result); - //glGenTextures(1, &result); + glGenTextures(1, &result); return result; } @@ -94,12 +104,37 @@ void GL41Texture::generateMips() const { (void)CHECK_GL_ERROR(); } -void GL41Texture::copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum format, GLenum type, const void* sourcePointer) const { +void GL41Texture::copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum internalFormat, GLenum format, GLenum type, Size sourceSize, const void* sourcePointer) const { if (GL_TEXTURE_2D == _target) { - glTexSubImage2D(_target, mip, 0, yOffset, size.x, size.y, format, type, sourcePointer); + switch (internalFormat) { + case GL_COMPRESSED_SRGB_S3TC_DXT1_EXT: + case GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT: + case GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT: + case GL_COMPRESSED_RED_RGTC1: + case GL_COMPRESSED_RG_RGTC2: + glCompressedTexSubImage2D(_target, mip, 0, yOffset, size.x, size.y, internalFormat, + static_cast(sourceSize), sourcePointer); + break; + default: + glTexSubImage2D(_target, mip, 0, yOffset, size.x, size.y, format, type, sourcePointer); + break; + } } else if (GL_TEXTURE_CUBE_MAP == _target) { auto target = GLTexture::CUBE_FACE_LAYOUT[face]; - glTexSubImage2D(target, mip, 0, yOffset, size.x, size.y, format, type, sourcePointer); + + switch (internalFormat) { + case GL_COMPRESSED_SRGB_S3TC_DXT1_EXT: + case GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT: + case GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT: + case GL_COMPRESSED_RED_RGTC1: + case GL_COMPRESSED_RG_RGTC2: + glCompressedTexSubImage2D(target, mip, 0, yOffset, size.x, size.y, internalFormat, + static_cast(sourceSize), sourcePointer); + break; + default: + glTexSubImage2D(target, mip, 0, yOffset, size.x, size.y, format, type, sourcePointer); + break; + } } else { assert(false); } @@ -208,15 +243,20 @@ using GL41VariableAllocationTexture = GL41Backend::GL41VariableAllocationTexture GL41VariableAllocationTexture::GL41VariableAllocationTexture(const std::weak_ptr& backend, const Texture& texture) : GL41Texture(backend, texture) { auto mipLevels = texture.getNumMips(); _allocatedMip = mipLevels; + _maxAllocatedMip = _populatedMip = mipLevels; + _minAllocatedMip = texture.minAvailableMipLevel(); + uvec3 mipDimensions; - for (uint16_t mip = 0; mip < mipLevels; ++mip) { + for (uint16_t mip = _minAllocatedMip; mip < mipLevels; ++mip) { if (glm::all(glm::lessThanEqual(texture.evalMipDimensions(mip), INITIAL_MIP_TRANSFER_DIMENSIONS))) { _maxAllocatedMip = _populatedMip = mip; break; } } - uint16_t allocatedMip = _populatedMip - std::min(_populatedMip, 2); + auto targetMip = _populatedMip - std::min(_populatedMip, 2); + uint16_t allocatedMip = std::max(_minAllocatedMip, targetMip); + allocateStorage(allocatedMip); _memoryPressureStateStale = true; size_t maxFace = GLTexture::getFaceCount(_target); @@ -253,9 +293,9 @@ void GL41VariableAllocationTexture::allocateStorage(uint16 allocatedMip) { } -void GL41VariableAllocationTexture::copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum format, GLenum type, const void* sourcePointer) const { +void GL41VariableAllocationTexture::copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum internalFormat, GLenum format, GLenum type, Size sourceSize, const void* sourcePointer) const { withPreservedTexture([&] { - Parent::copyMipFaceLinesFromTexture(mip, face, size, yOffset, format, type, sourcePointer); + Parent::copyMipFaceLinesFromTexture(mip, face, size, yOffset, internalFormat, format, type, sourceSize, sourcePointer); }); } @@ -269,6 +309,10 @@ void GL41VariableAllocationTexture::syncSampler() const { void GL41VariableAllocationTexture::promote() { PROFILE_RANGE(render_gpu_gl, __FUNCTION__); Q_ASSERT(_allocatedMip > 0); + + uint16_t targetAllocatedMip = _allocatedMip - std::min(_allocatedMip, 2); + targetAllocatedMip = std::max(_minAllocatedMip, targetAllocatedMip); + GLuint oldId = _id; auto oldSize = _size; // create new texture @@ -276,11 +320,11 @@ void GL41VariableAllocationTexture::promote() { uint16_t oldAllocatedMip = _allocatedMip; // allocate storage for new level - allocateStorage(_allocatedMip - std::min(_allocatedMip, 2)); + allocateStorage(targetAllocatedMip); withPreservedTexture([&] { GLuint fbo { 0 }; - glCreateFramebuffers(1, &fbo); + glGenFramebuffers(1, &fbo); glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo); uint16_t mips = _gpuObject.getNumMips(); diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h index d80a70cfd1..dbedd81c76 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h @@ -48,7 +48,7 @@ public: protected: GL45Texture(const std::weak_ptr& backend, const Texture& texture); void generateMips() const override; - void copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum format, GLenum type, const void* sourcePointer) const override; + void copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum internalFormat, GLenum format, GLenum type, Size sourceSize, const void* sourcePointer) const override; virtual void syncSampler() const; }; diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp index 73974addff..120be923f5 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp @@ -80,6 +80,19 @@ GLTexture* GL45Backend::syncGPUObject(const TexturePointer& texturePointer) { default: Q_UNREACHABLE(); } + } else { + + if (texture.getUsageType() == TextureUsageType::RESOURCE) { + auto varTex = static_cast (object); + + if (varTex->_minAllocatedMip > 0) { + auto minAvailableMip = texture.minAvailableMipLevel(); + if (minAvailableMip < varTex->_minAllocatedMip) { + varTex->_minAllocatedMip = minAvailableMip; + GL45VariableAllocationTexture::_memoryPressureStateStale = true; + } + } + } } return object; @@ -109,6 +122,10 @@ GL45Texture::GL45Texture(const std::weak_ptr& backend, const Texture& GLuint GL45Texture::allocate(const Texture& texture) { GLuint result; glCreateTextures(getGLTextureType(texture), 1, &result); +#ifdef DEBUG + auto source = texture.source(); + glObjectLabel(GL_TEXTURE, result, source.length(), source.data()); +#endif return result; } @@ -117,17 +134,47 @@ void GL45Texture::generateMips() const { (void)CHECK_GL_ERROR(); } -void GL45Texture::copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum format, GLenum type, const void* sourcePointer) const { +void GL45Texture::copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum internalFormat, GLenum format, GLenum type, Size sourceSize, const void* sourcePointer) const { if (GL_TEXTURE_2D == _target) { - glTextureSubImage2D(_id, mip, 0, yOffset, size.x, size.y, format, type, sourcePointer); + switch (internalFormat) { + case GL_COMPRESSED_SRGB_S3TC_DXT1_EXT: + case GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT: + case GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT: + case GL_COMPRESSED_RED_RGTC1: + case GL_COMPRESSED_RG_RGTC2: + glCompressedTextureSubImage2D(_id, mip, 0, yOffset, size.x, size.y, internalFormat, + static_cast(sourceSize), sourcePointer); + break; + default: + glTextureSubImage2D(_id, mip, 0, yOffset, size.x, size.y, format, type, sourcePointer); + break; + } } else if (GL_TEXTURE_CUBE_MAP == _target) { - // DSA ARB does not work on AMD, so use EXT - // unless EXT is not available on the driver - if (glTextureSubImage2DEXT) { - auto target = GLTexture::CUBE_FACE_LAYOUT[face]; - glTextureSubImage2DEXT(_id, target, mip, 0, yOffset, size.x, size.y, format, type, sourcePointer); - } else { - glTextureSubImage3D(_id, mip, 0, yOffset, face, size.x, size.y, 1, format, type, sourcePointer); + switch (internalFormat) { + case GL_COMPRESSED_SRGB_S3TC_DXT1_EXT: + case GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT: + case GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT: + case GL_COMPRESSED_RED_RGTC1: + case GL_COMPRESSED_RG_RGTC2: + if (glCompressedTextureSubImage2DEXT) { + auto target = GLTexture::CUBE_FACE_LAYOUT[face]; + glCompressedTextureSubImage2DEXT(_id, target, mip, 0, yOffset, size.x, size.y, internalFormat, + static_cast(sourceSize), sourcePointer); + } else { + glCompressedTextureSubImage3D(_id, mip, 0, yOffset, face, size.x, size.y, 1, internalFormat, + static_cast(sourceSize), sourcePointer); + } + break; + default: + // DSA ARB does not work on AMD, so use EXT + // unless EXT is not available on the driver + if (glTextureSubImage2DEXT) { + auto target = GLTexture::CUBE_FACE_LAYOUT[face]; + glTextureSubImage2DEXT(_id, target, mip, 0, yOffset, size.x, size.y, format, type, sourcePointer); + } else { + glTextureSubImage3D(_id, mip, 0, yOffset, face, size.x, size.y, 1, format, type, sourcePointer); + } + break; } } else { Q_ASSERT(false); diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp index a453d4207d..92d820e5f0 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp @@ -43,16 +43,22 @@ using GL45ResourceTexture = GL45Backend::GL45ResourceTexture; GL45ResourceTexture::GL45ResourceTexture(const std::weak_ptr& backend, const Texture& texture) : GL45VariableAllocationTexture(backend, texture) { auto mipLevels = texture.getNumMips(); _allocatedMip = mipLevels; + _maxAllocatedMip = _populatedMip = mipLevels; + _minAllocatedMip = texture.minAvailableMipLevel(); + uvec3 mipDimensions; - for (uint16_t mip = 0; mip < mipLevels; ++mip) { + for (uint16_t mip = _minAllocatedMip; mip < mipLevels; ++mip) { if (glm::all(glm::lessThanEqual(texture.evalMipDimensions(mip), INITIAL_MIP_TRANSFER_DIMENSIONS))) { _maxAllocatedMip = _populatedMip = mip; break; } } - uint16_t allocatedMip = _populatedMip - std::min(_populatedMip, 2); + auto targetMip = _populatedMip - std::min(_populatedMip, 2); + uint16_t allocatedMip = std::max(_minAllocatedMip, targetMip); + allocateStorage(allocatedMip); + _memoryPressureStateStale = true; copyMipsFromTexture(); syncSampler(); @@ -70,6 +76,7 @@ void GL45ResourceTexture::allocateStorage(uint16 allocatedMip) { for (uint16_t mip = _allocatedMip; mip < mipLevels; ++mip) { _size += _gpuObject.evalMipSize(mip); } + Backend::updateTextureGPUMemoryUsage(0, _size); } @@ -93,13 +100,17 @@ void GL45ResourceTexture::syncSampler() const { void GL45ResourceTexture::promote() { PROFILE_RANGE(render_gpu_gl, __FUNCTION__); Q_ASSERT(_allocatedMip > 0); + + uint16_t targetAllocatedMip = _allocatedMip - std::min(_allocatedMip, 2); + targetAllocatedMip = std::max(_minAllocatedMip, targetAllocatedMip); + GLuint oldId = _id; auto oldSize = _size; // create new texture const_cast(_id) = allocate(_gpuObject); uint16_t oldAllocatedMip = _allocatedMip; // allocate storage for new level - allocateStorage(_allocatedMip - std::min(_allocatedMip, 2)); + allocateStorage(targetAllocatedMip); uint16_t mips = _gpuObject.getNumMips(); // copy pre-existing mips for (uint16_t mip = _populatedMip; mip < mips; ++mip) { diff --git a/libraries/gpu/src/gpu/Format.cpp b/libraries/gpu/src/gpu/Format.cpp index de202911e3..19d8855bd9 100644 --- a/libraries/gpu/src/gpu/Format.cpp +++ b/libraries/gpu/src/gpu/Format.cpp @@ -19,6 +19,12 @@ const Element Element::COLOR_SRGBA_32{ VEC4, NUINT8, SRGBA }; const Element Element::COLOR_BGRA_32{ VEC4, NUINT8, BGRA }; const Element Element::COLOR_SBGRA_32{ VEC4, NUINT8, SBGRA }; +const Element Element::COLOR_COMPRESSED_RED{ VEC4, NUINT8, COMPRESSED_BC4_RED }; +const Element Element::COLOR_COMPRESSED_SRGB{ VEC4, NUINT8, COMPRESSED_BC1_SRGB }; +const Element Element::COLOR_COMPRESSED_SRGBA_MASK{ VEC4, NUINT8, COMPRESSED_BC1_SRGBA }; +const Element Element::COLOR_COMPRESSED_SRGBA{ VEC4, NUINT8, COMPRESSED_BC3_SRGBA }; +const Element Element::COLOR_COMPRESSED_XY{ VEC4, NUINT8, COMPRESSED_BC5_XY }; + const Element Element::COLOR_R11G11B10{ SCALAR, FLOAT, R11G11B10 }; const Element Element::VEC4F_COLOR_RGBA{ VEC4, FLOAT, RGBA }; const Element Element::VEC2F_UV{ VEC2, FLOAT, UV }; diff --git a/libraries/gpu/src/gpu/Format.h b/libraries/gpu/src/gpu/Format.h index 58115edca0..f69e8d9386 100644 --- a/libraries/gpu/src/gpu/Format.h +++ b/libraries/gpu/src/gpu/Format.h @@ -157,20 +157,12 @@ enum Semantic { // These are generic compression format smeantic for images _FIRST_COMPRESSED, - COMPRESSED_R, - COMPRESSED_RGB, - COMPRESSED_RGBA, - - COMPRESSED_SRGB, - COMPRESSED_SRGBA, - - // FIXME: Will have to be supported later: - /*COMPRESSED_BC3_RGBA, // RGBA_S3TC_DXT5_EXT, - COMPRESSED_BC3_SRGBA, // SRGB_ALPHA_S3TC_DXT5_EXT - - COMPRESSED_BC7_RGBA, - COMPRESSED_BC7_SRGBA, */ + COMPRESSED_BC1_SRGB, + COMPRESSED_BC1_SRGBA, + COMPRESSED_BC3_SRGBA, + COMPRESSED_BC4_RED, + COMPRESSED_BC5_XY, _LAST_COMPRESSED, @@ -237,6 +229,11 @@ public: static const Element COLOR_BGRA_32; static const Element COLOR_SBGRA_32; static const Element COLOR_R11G11B10; + static const Element COLOR_COMPRESSED_RED; + static const Element COLOR_COMPRESSED_SRGB; + static const Element COLOR_COMPRESSED_SRGBA_MASK; + static const Element COLOR_COMPRESSED_SRGBA; + static const Element COLOR_COMPRESSED_XY; static const Element VEC4F_COLOR_RGBA; static const Element VEC2F_UV; static const Element VEC2F_XY; diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp index 1e65972114..3e6ed166a7 100755 --- a/libraries/gpu/src/gpu/Texture.cpp +++ b/libraries/gpu/src/gpu/Texture.cpp @@ -118,6 +118,7 @@ Texture::Size Texture::getAllowedGPUMemoryUsage() { return _allowedCPUMemoryUsage; } + void Texture::setAllowedGPUMemoryUsage(Size size) { qCDebug(gpulogging) << "New MAX texture memory " << BYTES_TO_MB(size) << " MB"; _allowedCPUMemoryUsage = size; @@ -212,8 +213,8 @@ void Texture::MemoryStorage::assignMipFaceData(uint16 level, uint8 face, const s } } -Texture* Texture::createExternal(const ExternalRecycler& recycler, const Sampler& sampler) { - Texture* tex = new Texture(TextureUsageType::EXTERNAL); +TexturePointer Texture::createExternal(const ExternalRecycler& recycler, const Sampler& sampler) { + TexturePointer tex = std::make_shared(TextureUsageType::EXTERNAL); tex->_type = TEX_2D; tex->_maxMipLevel = 0; tex->_sampler = sampler; @@ -221,36 +222,36 @@ Texture* Texture::createExternal(const ExternalRecycler& recycler, const Sampler return tex; } -Texture* Texture::createRenderBuffer(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips, const Sampler& sampler) { +TexturePointer Texture::createRenderBuffer(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips, const Sampler& sampler) { return create(TextureUsageType::RENDERBUFFER, TEX_2D, texelFormat, width, height, 1, 1, 0, numMips, sampler); } -Texture* Texture::create1D(const Element& texelFormat, uint16 width, uint16 numMips, const Sampler& sampler) { +TexturePointer Texture::create1D(const Element& texelFormat, uint16 width, uint16 numMips, const Sampler& sampler) { return create(TextureUsageType::RESOURCE, TEX_1D, texelFormat, width, 1, 1, 1, 0, numMips, sampler); } -Texture* Texture::create2D(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips, const Sampler& sampler) { +TexturePointer Texture::create2D(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips, const Sampler& sampler) { return create(TextureUsageType::RESOURCE, TEX_2D, texelFormat, width, height, 1, 1, 0, numMips, sampler); } -Texture* Texture::createStrict(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips, const Sampler& sampler) { +TexturePointer Texture::createStrict(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips, const Sampler& sampler) { return create(TextureUsageType::STRICT_RESOURCE, TEX_2D, texelFormat, width, height, 1, 1, 0, numMips, sampler); } -Texture* Texture::create3D(const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numMips, const Sampler& sampler) { +TexturePointer Texture::create3D(const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numMips, const Sampler& sampler) { return create(TextureUsageType::RESOURCE, TEX_3D, texelFormat, width, height, depth, 1, 0, numMips, sampler); } -Texture* Texture::createCube(const Element& texelFormat, uint16 width, uint16 numMips, const Sampler& sampler) { +TexturePointer Texture::createCube(const Element& texelFormat, uint16 width, uint16 numMips, const Sampler& sampler) { return create(TextureUsageType::RESOURCE, TEX_CUBE, texelFormat, width, width, 1, 1, 0, numMips, sampler); } -Texture* Texture::create(TextureUsageType usageType, Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, uint16 numMips, const Sampler& sampler) +TexturePointer Texture::create(TextureUsageType usageType, Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, uint16 numMips, const Sampler& sampler) { - Texture* tex = new Texture(usageType); + TexturePointer tex = std::make_shared(usageType); tex->_storage.reset(new MemoryStorage()); tex->_type = type; - tex->_storage->assignTexture(tex); + tex->_storage->assignTexture(tex.get()); tex->resize(type, texelFormat, width, height, depth, numSamples, numSlices, numMips); tex->_sampler = sampler; @@ -411,6 +412,7 @@ const Element& Texture::getStoredMipFormat() const { } void Texture::assignStoredMip(uint16 level, Size size, const Byte* bytes) { + // TODO Skip the extra allocation here storage::StoragePointer storage = std::make_shared(size, bytes); assignStoredMip(level, storage); } @@ -434,7 +436,7 @@ void Texture::assignStoredMip(uint16 level, storage::StoragePointer& storage) { // THen check that the mem texture passed make sense with its format Size expectedSize = evalStoredMipSize(level, getStoredMipFormat()); auto size = storage->size(); - if (storage->size() == expectedSize) { + if (storage->size() <= expectedSize) { _storage->assignMipData(level, storage); _stamp++; } else if (size > expectedSize) { @@ -461,7 +463,7 @@ void Texture::assignStoredMipFace(uint16 level, uint8 face, storage::StoragePoin // THen check that the mem texture passed make sense with its format Size expectedSize = evalStoredMipFaceSize(level, getStoredMipFormat()); auto size = storage->size(); - if (size == expectedSize) { + if (size <= expectedSize) { _storage->assignMipFaceData(level, face, storage); _stamp++; } else if (size > expectedSize) { @@ -474,6 +476,10 @@ void Texture::assignStoredMipFace(uint16 level, uint8 face, storage::StoragePoin } } +bool Texture::isStoredMipFaceAvailable(uint16 level, uint8 face) const { + return _storage->isMipAvailable(level, face); +} + void Texture::setAutoGenerateMips(bool enable) { bool changed = false; if (!_autoGenerateMips) { @@ -487,12 +493,11 @@ void Texture::setAutoGenerateMips(bool enable) { } Size Texture::getStoredMipSize(uint16 level) const { - PixelsPointer mipFace = accessStoredMipFace(level); Size size = 0; - if (mipFace && mipFace->getSize()) { - for (int face = 0; face < getNumFaces(); face++) { + for (int face = 0; face < getNumFaces(); face++) { + if (isStoredMipFaceAvailable(level, face)) { size += getStoredMipFaceSize(level, face); - } + } } return size; } @@ -752,7 +757,7 @@ bool sphericalHarmonicsFromTexture(const gpu::Texture& cubeTexture, std::vector< boffset = 0; } - auto data = cubeTexture.accessStoredMipFace(0,face)->readData(); + auto data = cubeTexture.accessStoredMipFace(0, face)->readData(); if (data == nullptr) { continue; } diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h index eab02141f0..9b23b4e695 100755 --- a/libraries/gpu/src/gpu/Texture.h +++ b/libraries/gpu/src/gpu/Texture.h @@ -28,10 +28,17 @@ namespace ktx { struct KTXDescriptor; using KTXDescriptorPointer = std::unique_ptr; struct Header; + struct KeyValue; + using KeyValues = std::list; } namespace gpu { + +const std::string SOURCE_HASH_KEY { "hifi.sourceHash" }; + +const uint8 SOURCE_HASH_BYTES = 16; + // THe spherical harmonics is a nice tool for cubemap, so if required, the irradiance SH can be automatically generated // with the cube texture class Texture; @@ -150,7 +157,7 @@ protected: Desc _desc; }; -enum class TextureUsageType { +enum class TextureUsageType : uint8 { RENDERBUFFER, // Used as attachments to a framebuffer RESOURCE, // Resource textures, like materials... subject to memory manipulation STRICT_RESOURCE, // Resource textures not subject to manipulation, like the normal fitting texture @@ -271,6 +278,7 @@ public: virtual void assignMipData(uint16 level, const storage::StoragePointer& storage) = 0; virtual void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) = 0; virtual bool isMipAvailable(uint16 level, uint8 face = 0) const = 0; + virtual uint16 minAvailableMipLevel() const { return 0; } Texture::Type getType() const { return _type; } Stamp getStamp() const { return _stamp; } @@ -308,33 +316,39 @@ public: KtxStorage(const std::string& filename); PixelsPointer getMipFace(uint16 level, uint8 face = 0) const override; Size getMipFaceSize(uint16 level, uint8 face = 0) const override; - // By convention, all mip levels and faces MUST be populated when using KTX backing - bool isMipAvailable(uint16 level, uint8 face = 0) const override { return true; } + bool isMipAvailable(uint16 level, uint8 face = 0) const override; + void assignMipData(uint16 level, const storage::StoragePointer& storage) override; + void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) override; + uint16 minAvailableMipLevel() const override; - void assignMipData(uint16 level, const storage::StoragePointer& storage) override { - throw std::runtime_error("Invalid call"); - } - - void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) override { - throw std::runtime_error("Invalid call"); - } void reset() override { } protected: + std::shared_ptr maybeOpenFile(); + + std::mutex _cacheFileCreateMutex; + std::mutex _cacheFileWriteMutex; + std::weak_ptr _cacheFile; + std::string _filename; + std::atomic _minMipLevelAvailable; + size_t _offsetToMinMipKV; + ktx::KTXDescriptorPointer _ktxDescriptor; friend class Texture; }; + uint16 minAvailableMipLevel() const { return _storage->minAvailableMipLevel(); }; + static const uint16 MAX_NUM_MIPS = 0; static const uint16 SINGLE_MIP = 1; - static Texture* create1D(const Element& texelFormat, uint16 width, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler()); - static Texture* create2D(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler()); - static Texture* create3D(const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler()); - static Texture* createCube(const Element& texelFormat, uint16 width, uint16 numMips = 1, const Sampler& sampler = Sampler()); - static Texture* createRenderBuffer(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler()); - static Texture* createStrict(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler()); - static Texture* createExternal(const ExternalRecycler& recycler, const Sampler& sampler = Sampler()); + static TexturePointer create1D(const Element& texelFormat, uint16 width, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler()); + static TexturePointer create2D(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler()); + static TexturePointer create3D(const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler()); + static TexturePointer createCube(const Element& texelFormat, uint16 width, uint16 numMips = 1, const Sampler& sampler = Sampler()); + static TexturePointer createRenderBuffer(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler()); + static TexturePointer createStrict(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler()); + static TexturePointer createExternal(const ExternalRecycler& recycler, const Sampler& sampler = Sampler()); // After the texture has been created, it should be defined bool isDefined() const { return _defined; } @@ -435,6 +449,8 @@ public: // For convenience assign a source name const std::string& source() const { return _source; } void setSource(const std::string& source) { _source = source; } + const std::string& sourceHash() const { return _sourceHash; } + void setSourceHash(const std::string& sourceHash) { _sourceHash = sourceHash; } // Potentially change the minimum mip (mostly for debugging purpose) bool setMinMip(uint16 newMinMip); @@ -467,7 +483,7 @@ public: // Access the stored mips and faces const PixelsPointer accessStoredMipFace(uint16 level, uint8 face = 0) const { return _storage->getMipFace(level, face); } - bool isStoredMipFaceAvailable(uint16 level, uint8 face = 0) const { return _storage->isMipAvailable(level, face); } + bool isStoredMipFaceAvailable(uint16 level, uint8 face = 0) const; Size getStoredMipFaceSize(uint16 level, uint8 face = 0) const { return _storage->getMipFaceSize(level, face); } Size getStoredMipSize(uint16 level) const; Size getStoredSize() const; @@ -482,6 +498,7 @@ public: // For Cube Texture, it's possible to generate the irradiance spherical harmonics and make them availalbe with the texture bool generateIrradiance(); const SHPointer& getIrradiance(uint16 slice = 0) const { return _irradiance; } + void overrideIrradiance(SHPointer irradiance) { _irradiance = irradiance; } bool isIrradianceValid() const { return _isIrradianceValid; } // Own sampler @@ -500,9 +517,12 @@ public: ExternalUpdates getUpdates() const; - // Textures can be serialized directly to ktx data file, here is how + // Serialize a texture into a KTX file static ktx::KTXUniquePointer serialize(const Texture& texture); - static Texture* unserialize(const std::string& ktxFile, TextureUsageType usageType = TextureUsageType::RESOURCE, Usage usage = Usage(), const Sampler::Desc& sampler = Sampler::Desc()); + + static TexturePointer unserialize(const std::string& ktxFile); + static TexturePointer unserialize(const std::string& ktxFile, const ktx::KTXDescriptor& descriptor); + static bool evalKTXFormat(const Element& mipFormat, const Element& texelFormat, ktx::Header& header); static bool evalTextureFormat(const ktx::Header& header, Element& mipFormat, Element& texelFormat); @@ -518,6 +538,7 @@ protected: std::weak_ptr _fallback; // Not strictly necessary, but incredibly useful for debugging std::string _source; + std::string _sourceHash; std::unique_ptr< Storage > _storage; Stamp _stamp { 0 }; @@ -552,7 +573,7 @@ protected: bool _isIrradianceValid = false; bool _defined = false; - static Texture* create(TextureUsageType usageType, Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, uint16 numMips, const Sampler& sampler); + static TexturePointer create(TextureUsageType usageType, Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, uint16 numMips, const Sampler& sampler); Size resize(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, uint16 numMips); }; diff --git a/libraries/gpu/src/gpu/Texture_ktx.cpp b/libraries/gpu/src/gpu/Texture_ktx.cpp index 28de0c70eb..efff6c7afe 100644 --- a/libraries/gpu/src/gpu/Texture_ktx.cpp +++ b/libraries/gpu/src/gpu/Texture_ktx.cpp @@ -12,43 +12,114 @@ #include "Texture.h" +#include + #include + +#include "GPULogging.h" + using namespace gpu; using PixelsPointer = Texture::PixelsPointer; using KtxStorage = Texture::KtxStorage; struct GPUKTXPayload { + using Version = uint8; + + static const std::string KEY; + static const Version CURRENT_VERSION { 1 }; + static const size_t PADDING { 2 }; + static const size_t SIZE { sizeof(Version) + sizeof(Sampler::Desc) + sizeof(uint32) + sizeof(TextureUsageType) + PADDING }; + static_assert(GPUKTXPayload::SIZE == 36, "Packing size may differ between platforms"); + static_assert(GPUKTXPayload::SIZE % 4 == 0, "GPUKTXPayload is not 4 bytes aligned"); + Sampler::Desc _samplerDesc; Texture::Usage _usage; TextureUsageType _usageType; - static std::string KEY; + Byte* serialize(Byte* data) const { + *(Version*)data = CURRENT_VERSION; + data += sizeof(Version); + + memcpy(data, &_samplerDesc, sizeof(Sampler::Desc)); + data += sizeof(Sampler::Desc); + + // We can't copy the bitset in Texture::Usage in a crossplateform manner + // So serialize it manually + *(uint32*)data = _usage._flags.to_ulong(); + data += sizeof(uint32); + + *(TextureUsageType*)data = _usageType; + data += sizeof(TextureUsageType); + + return data + PADDING; + } + + bool unserialize(const Byte* data, size_t size) { + if (size != SIZE) { + return false; + } + + Version version = *(const Version*)data; + if (version != CURRENT_VERSION) { + glm::vec4 borderColor(1.0f); + if (memcmp(&borderColor, data, sizeof(glm::vec4)) == 0) { + memcpy(this, data, sizeof(GPUKTXPayload)); + return true; + } else { + return false; + } + } + data += sizeof(Version); + + memcpy(&_samplerDesc, data, sizeof(Sampler::Desc)); + data += sizeof(Sampler::Desc); + + // We can't copy the bitset in Texture::Usage in a crossplateform manner + // So unserialize it manually + _usage = Texture::Usage(*(const uint32*)data); + data += sizeof(uint32); + + _usageType = *(const TextureUsageType*)data; + return true; + } + static bool isGPUKTX(const ktx::KeyValue& val) { return (val._key.compare(KEY) == 0); } static bool findInKeyValues(const ktx::KeyValues& keyValues, GPUKTXPayload& payload) { - auto found = std::find_if(keyValues.begin(), keyValues.end(), isGPUKTX); + auto found = std::find_if(keyValues.begin(), keyValues.end(), isGPUKTX); if (found != keyValues.end()) { - if ((*found)._value.size() == sizeof(GPUKTXPayload)) { - memcpy(&payload, (*found)._value.data(), sizeof(GPUKTXPayload)); - return true; - } + auto value = found->_value; + return payload.unserialize(value.data(), value.size()); } return false; } }; - -std::string GPUKTXPayload::KEY { "hifi.gpu" }; +const std::string GPUKTXPayload::KEY { "hifi.gpu" }; KtxStorage::KtxStorage(const std::string& filename) : _filename(filename) { { - ktx::StoragePointer storage { new storage::FileStorage(_filename.c_str()) }; + // We are doing a lot of work here just to get descriptor data + ktx::StoragePointer storage{ new storage::FileStorage(_filename.c_str()) }; auto ktxPointer = ktx::KTX::create(storage); _ktxDescriptor.reset(new ktx::KTXDescriptor(ktxPointer->toDescriptor())); + if (_ktxDescriptor->images.size() < _ktxDescriptor->header.numberOfMipmapLevels) { + qWarning() << "Bad images found in ktx"; + } + + _offsetToMinMipKV = _ktxDescriptor->getValueOffsetForKey(ktx::HIFI_MIN_POPULATED_MIP_KEY); + if (_offsetToMinMipKV) { + auto data = storage->data() + ktx::KTX_HEADER_SIZE + _offsetToMinMipKV; + _minMipLevelAvailable = *data; + } else { + // Assume all mip levels are available + _minMipLevelAvailable = 0; + } } + // now that we know the ktx, let's get the header info to configure this Texture::Storage: Format mipFormat = Format::COLOR_BGRA_32; Format texelFormat = Format::COLOR_SRGBA_32; @@ -57,6 +128,27 @@ KtxStorage::KtxStorage(const std::string& filename) : _filename(filename) { } } +std::shared_ptr KtxStorage::maybeOpenFile() { + std::shared_ptr file = _cacheFile.lock(); + if (file) { + return file; + } + + { + std::lock_guard lock{ _cacheFileCreateMutex }; + + file = _cacheFile.lock(); + if (file) { + return file; + } + + file = std::make_shared(_filename.c_str()); + _cacheFile = file; + } + + return file; +} + PixelsPointer KtxStorage::getMipFace(uint16 level, uint8 face) const { storage::StoragePointer result; auto faceOffset = _ktxDescriptor->getMipFaceTexelsOffset(level, face); @@ -71,6 +163,58 @@ Size KtxStorage::getMipFaceSize(uint16 level, uint8 face) const { return _ktxDescriptor->getMipFaceTexelsSize(level, face); } + +bool KtxStorage::isMipAvailable(uint16 level, uint8 face) const { + return level >= _minMipLevelAvailable; +} + +uint16 KtxStorage::minAvailableMipLevel() const { + return _minMipLevelAvailable; +} + +void KtxStorage::assignMipData(uint16 level, const storage::StoragePointer& storage) { + if (level != _minMipLevelAvailable - 1) { + qWarning() << "Invalid level to be stored, expected: " << (_minMipLevelAvailable - 1) << ", got: " << level << " " << _filename.c_str(); + return; + } + + if (level >= _ktxDescriptor->images.size()) { + throw std::runtime_error("Invalid level"); + } + + if (storage->size() != _ktxDescriptor->images[level]._imageSize) { + qWarning() << "Invalid image size: " << storage->size() << ", expected: " << _ktxDescriptor->images[level]._imageSize + << ", level: " << level << ", filename: " << QString::fromStdString(_filename); + return; + } + + auto file = maybeOpenFile(); + + auto imageData = file->mutableData(); + imageData += ktx::KTX_HEADER_SIZE + _ktxDescriptor->header.bytesOfKeyValueData + _ktxDescriptor->images[level]._imageOffset; + imageData += ktx::IMAGE_SIZE_WIDTH; + + { + std::lock_guard lock { _cacheFileWriteMutex }; + + if (level != _minMipLevelAvailable - 1) { + qWarning() << "Invalid level to be stored"; + return; + } + + memcpy(imageData, storage->data(), _ktxDescriptor->images[level]._imageSize); + _minMipLevelAvailable = level; + if (_offsetToMinMipKV > 0) { + auto minMipKeyData = file->mutableData() + ktx::KTX_HEADER_SIZE + _offsetToMinMipKV; + memcpy(minMipKeyData, (void*)&_minMipLevelAvailable, 1); + } + } +} + +void KtxStorage::assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) { + throw std::runtime_error("Invalid call"); +} + void Texture::setKtxBacking(const std::string& filename) { // Check the KTX file for validity before using it as backing storage { @@ -85,6 +229,7 @@ void Texture::setKtxBacking(const std::string& filename) { setStorage(newBacking); } + ktx::KTXUniquePointer Texture::serialize(const Texture& texture) { ktx::Header header; @@ -140,19 +285,21 @@ ktx::KTXUniquePointer Texture::serialize(const Texture& texture) { header.numberOfMipmapLevels = texture.getNumMips(); ktx::Images images; + uint32_t imageOffset = 0; for (uint32_t level = 0; level < header.numberOfMipmapLevels; level++) { auto mip = texture.accessStoredMipFace(level); if (mip) { if (numFaces == 1) { - images.emplace_back(ktx::Image((uint32_t)mip->getSize(), 0, mip->readData())); + images.emplace_back(ktx::Image(imageOffset, (uint32_t)mip->getSize(), 0, mip->readData())); } else { ktx::Image::FaceBytes cubeFaces(Texture::CUBE_FACE_COUNT); cubeFaces[0] = mip->readData(); for (uint32_t face = 1; face < Texture::CUBE_FACE_COUNT; face++) { cubeFaces[face] = texture.accessStoredMipFace(level, face)->readData(); } - images.emplace_back(ktx::Image((uint32_t)mip->getSize(), 0, cubeFaces)); + images.emplace_back(ktx::Image(imageOffset, (uint32_t)mip->getSize(), 0, cubeFaces)); } + imageOffset += static_cast(mip->getSize()) + ktx::IMAGE_SIZE_WIDTH; } } @@ -160,8 +307,19 @@ ktx::KTXUniquePointer Texture::serialize(const Texture& texture) { keyval._samplerDesc = texture.getSampler().getDesc(); keyval._usage = texture.getUsage(); keyval._usageType = texture.getUsageType(); + Byte keyvalPayload[GPUKTXPayload::SIZE]; + keyval.serialize(keyvalPayload); + ktx::KeyValues keyValues; - keyValues.emplace_back(ktx::KeyValue(GPUKTXPayload::KEY, sizeof(GPUKTXPayload), (ktx::Byte*) &keyval)); + keyValues.emplace_back(GPUKTXPayload::KEY, (uint32)GPUKTXPayload::SIZE, (ktx::Byte*) &keyvalPayload); + + auto hash = texture.sourceHash(); + if (!hash.empty()) { + // the sourceHash is an std::string in hex + // we use QByteArray to take the hex and turn it into the smaller binary representation (16 bytes) + auto binaryHash = QByteArray::fromHex(QByteArray::fromStdString(hash)); + keyValues.emplace_back(SOURCE_HASH_KEY, static_cast(binaryHash.size()), (ktx::Byte*) binaryHash.data()); + } auto ktxBuffer = ktx::KTX::create(header, images, keyValues); #if 0 @@ -193,13 +351,17 @@ ktx::KTXUniquePointer Texture::serialize(const Texture& texture) { return ktxBuffer; } -Texture* Texture::unserialize(const std::string& ktxfile, TextureUsageType usageType, Usage usage, const Sampler::Desc& sampler) { - std::unique_ptr ktxPointer = ktx::KTX::create(ktx::StoragePointer { new storage::FileStorage(ktxfile.c_str()) }); +TexturePointer Texture::unserialize(const std::string& ktxfile) { + std::unique_ptr ktxPointer = ktx::KTX::create(std::make_shared(ktxfile.c_str())); if (!ktxPointer) { return nullptr; } ktx::KTXDescriptor descriptor { ktxPointer->toDescriptor() }; + return unserialize(ktxfile, ktxPointer->toDescriptor()); +} + +TexturePointer Texture::unserialize(const std::string& ktxfile, const ktx::KTXDescriptor& descriptor) { const auto& header = descriptor.header; Format mipFormat = Format::COLOR_BGRA_32; @@ -225,28 +387,28 @@ Texture* Texture::unserialize(const std::string& ktxfile, TextureUsageType usage type = TEX_3D; } - - // If found, use the GPUKTXPayload gpuktxKeyValue; - bool isGPUKTXPayload = GPUKTXPayload::findInKeyValues(descriptor.keyValues, gpuktxKeyValue); + if (!GPUKTXPayload::findInKeyValues(descriptor.keyValues, gpuktxKeyValue)) { + qCWarning(gpulogging) << "Could not find GPUKTX key values."; + return TexturePointer(); + } - auto tex = Texture::create( (isGPUKTXPayload ? gpuktxKeyValue._usageType : usageType), - type, - texelFormat, - header.getPixelWidth(), - header.getPixelHeight(), - header.getPixelDepth(), - 1, // num Samples - header.getNumberOfSlices(), - header.getNumberOfLevels(), - (isGPUKTXPayload ? gpuktxKeyValue._samplerDesc : sampler)); - - tex->setUsage((isGPUKTXPayload ? gpuktxKeyValue._usage : usage)); + auto texture = create(gpuktxKeyValue._usageType, + type, + texelFormat, + header.getPixelWidth(), + header.getPixelHeight(), + header.getPixelDepth(), + 1, // num Samples + header.getNumberOfSlices(), + header.getNumberOfLevels(), + gpuktxKeyValue._samplerDesc); + texture->setUsage(gpuktxKeyValue._usage); // Assing the mips availables - tex->setStoredMipFormat(mipFormat); - tex->setKtxBacking(ktxfile); - return tex; + texture->setStoredMipFormat(mipFormat); + texture->setKtxBacking(ktxfile); + return texture; } bool Texture::evalKTXFormat(const Element& mipFormat, const Element& texelFormat, ktx::Header& header) { @@ -260,6 +422,16 @@ bool Texture::evalKTXFormat(const Element& mipFormat, const Element& texelFormat header.setUncompressed(ktx::GLType::UNSIGNED_BYTE, 1, ktx::GLFormat::RGBA, ktx::GLInternalFormat_Uncompressed::SRGB8_ALPHA8, ktx::GLBaseInternalFormat::RGBA); } else if (texelFormat == Format::COLOR_R_8 && mipFormat == Format::COLOR_R_8) { header.setUncompressed(ktx::GLType::UNSIGNED_BYTE, 1, ktx::GLFormat::RED, ktx::GLInternalFormat_Uncompressed::R8, ktx::GLBaseInternalFormat::RED); + } else if (texelFormat == Format::COLOR_COMPRESSED_SRGB && mipFormat == Format::COLOR_COMPRESSED_SRGB) { + header.setCompressed(ktx::GLInternalFormat_Compressed::COMPRESSED_SRGB_S3TC_DXT1_EXT, ktx::GLBaseInternalFormat::RGB); + } else if (texelFormat == Format::COLOR_COMPRESSED_SRGBA_MASK && mipFormat == Format::COLOR_COMPRESSED_SRGBA_MASK) { + header.setCompressed(ktx::GLInternalFormat_Compressed::COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT, ktx::GLBaseInternalFormat::RGBA); + } else if (texelFormat == Format::COLOR_COMPRESSED_SRGBA && mipFormat == Format::COLOR_COMPRESSED_SRGBA) { + header.setCompressed(ktx::GLInternalFormat_Compressed::COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT, ktx::GLBaseInternalFormat::RGBA); + } else if (texelFormat == Format::COLOR_COMPRESSED_RED && mipFormat == Format::COLOR_COMPRESSED_RED) { + header.setCompressed(ktx::GLInternalFormat_Compressed::COMPRESSED_RED_RGTC1, ktx::GLBaseInternalFormat::RED); + } else if (texelFormat == Format::COLOR_COMPRESSED_XY && mipFormat == Format::COLOR_COMPRESSED_XY) { + header.setCompressed(ktx::GLInternalFormat_Compressed::COMPRESSED_RG_RGTC2, ktx::GLBaseInternalFormat::RG); } else { return false; } @@ -295,6 +467,25 @@ bool Texture::evalTextureFormat(const ktx::Header& header, Element& mipFormat, E } else { return false; } + } else if (header.getGLFormat() == ktx::GLFormat::COMPRESSED_FORMAT && header.getGLType() == ktx::GLType::COMPRESSED_TYPE) { + if (header.getGLInternaFormat_Compressed() == ktx::GLInternalFormat_Compressed::COMPRESSED_SRGB_S3TC_DXT1_EXT) { + mipFormat = Format::COLOR_COMPRESSED_SRGB; + texelFormat = Format::COLOR_COMPRESSED_SRGB; + } else if (header.getGLInternaFormat_Compressed() == ktx::GLInternalFormat_Compressed::COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT) { + mipFormat = Format::COLOR_COMPRESSED_SRGBA_MASK; + texelFormat = Format::COLOR_COMPRESSED_SRGBA_MASK; + } else if (header.getGLInternaFormat_Compressed() == ktx::GLInternalFormat_Compressed::COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT) { + mipFormat = Format::COLOR_COMPRESSED_SRGBA; + texelFormat = Format::COLOR_COMPRESSED_SRGBA; + } else if (header.getGLInternaFormat_Compressed() == ktx::GLInternalFormat_Compressed::COMPRESSED_RED_RGTC1) { + mipFormat = Format::COLOR_COMPRESSED_RED; + texelFormat = Format::COLOR_COMPRESSED_RED; + } else if (header.getGLInternaFormat_Compressed() == ktx::GLInternalFormat_Compressed::COMPRESSED_RG_RGTC2) { + mipFormat = Format::COLOR_COMPRESSED_XY; + texelFormat = Format::COLOR_COMPRESSED_XY; + } else { + return false; + } } else { return false; } diff --git a/libraries/image/CMakeLists.txt b/libraries/image/CMakeLists.txt new file mode 100644 index 0000000000..85d3d8f1ae --- /dev/null +++ b/libraries/image/CMakeLists.txt @@ -0,0 +1,11 @@ +set(TARGET_NAME image) +setup_hifi_library() +link_hifi_libraries(shared gpu) + +target_glm() + +add_dependency_external_projects(nvtt) +find_package(NVTT REQUIRED) +target_include_directories(${TARGET_NAME} PRIVATE ${NVTT_INCLUDE_DIRS}) +target_link_libraries(${TARGET_NAME} ${NVTT_LIBRARIES}) +add_paths_to_fixup_libs(${NVTT_DLL_PATH}) diff --git a/libraries/image/src/image/Image.cpp b/libraries/image/src/image/Image.cpp new file mode 100644 index 0000000000..707a2e4496 --- /dev/null +++ b/libraries/image/src/image/Image.cpp @@ -0,0 +1,934 @@ +// +// Image.cpp +// image/src/image +// +// Created by Clement Brisset on 4/5/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 +// + +#include "Image.h" + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "ImageLogging.h" + +using namespace gpu; + +#define CPU_MIPMAPS 1 +#define COMPRESS_COLOR_TEXTURES 0 +#define COMPRESS_NORMALMAP_TEXTURES 0 // Disable Normalmap compression for now +#define COMPRESS_GRAYSCALE_TEXTURES 0 +#define COMPRESS_CUBEMAP_TEXTURES 0 // Disable Cubemap compression for now + +static const glm::uvec2 SPARSE_PAGE_SIZE(128); +static const glm::uvec2 MAX_TEXTURE_SIZE(4096); +bool DEV_DECIMATE_TEXTURES = false; +std::atomic DECIMATED_TEXTURE_COUNT{ 0 }; +std::atomic RECTIFIED_TEXTURE_COUNT{ 0 }; + +bool needsSparseRectification(const glm::uvec2& size) { + // Don't attempt to rectify small textures (textures less than the sparse page size in any dimension) + if (glm::any(glm::lessThan(size, SPARSE_PAGE_SIZE))) { + return false; + } + + // Don't rectify textures that are already an exact multiple of sparse page size + if (glm::uvec2(0) == (size % SPARSE_PAGE_SIZE)) { + return false; + } + + // Texture is not sparse compatible, but is bigger than the sparse page size in both dimensions, rectify! + return true; +} + +glm::uvec2 rectifyToSparseSize(const glm::uvec2& size) { + glm::uvec2 pages = ((size / SPARSE_PAGE_SIZE) + glm::clamp(size % SPARSE_PAGE_SIZE, glm::uvec2(0), glm::uvec2(1))); + glm::uvec2 result = pages * SPARSE_PAGE_SIZE; + return result; +} + + +namespace image { + +TextureUsage::TextureLoader TextureUsage::getTextureLoaderForType(Type type, const QVariantMap& options) { + switch (type) { + case ALBEDO_TEXTURE: + return image::TextureUsage::createAlbedoTextureFromImage; + case EMISSIVE_TEXTURE: + return image::TextureUsage::createEmissiveTextureFromImage; + case LIGHTMAP_TEXTURE: + return image::TextureUsage::createLightmapTextureFromImage; + case CUBE_TEXTURE: + if (options.value("generateIrradiance", true).toBool()) { + return image::TextureUsage::createCubeTextureFromImage; + } else { + return image::TextureUsage::createCubeTextureFromImageWithoutIrradiance; + } + case BUMP_TEXTURE: + return image::TextureUsage::createNormalTextureFromBumpImage; + case NORMAL_TEXTURE: + return image::TextureUsage::createNormalTextureFromNormalImage; + case ROUGHNESS_TEXTURE: + return image::TextureUsage::createRoughnessTextureFromImage; + case GLOSS_TEXTURE: + return image::TextureUsage::createRoughnessTextureFromGlossImage; + case SPECULAR_TEXTURE: + return image::TextureUsage::createMetallicTextureFromImage; + case STRICT_TEXTURE: + return image::TextureUsage::createStrict2DTextureFromImage; + + case DEFAULT_TEXTURE: + default: + return image::TextureUsage::create2DTextureFromImage; + } +} + +gpu::TexturePointer TextureUsage::createStrict2DTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { + return process2DTextureColorFromImage(srcImage, srcImageName, true); +} + +gpu::TexturePointer TextureUsage::create2DTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { + return process2DTextureColorFromImage(srcImage, srcImageName, false); +} + +gpu::TexturePointer TextureUsage::createAlbedoTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { + return process2DTextureColorFromImage(srcImage, srcImageName, false); +} + +gpu::TexturePointer TextureUsage::createEmissiveTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { + return process2DTextureColorFromImage(srcImage, srcImageName, false); +} + +gpu::TexturePointer TextureUsage::createLightmapTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { + return process2DTextureColorFromImage(srcImage, srcImageName, false); +} + +gpu::TexturePointer TextureUsage::createNormalTextureFromNormalImage(const QImage& srcImage, const std::string& srcImageName) { + return process2DTextureNormalMapFromImage(srcImage, srcImageName, false); +} + +gpu::TexturePointer TextureUsage::createNormalTextureFromBumpImage(const QImage& srcImage, const std::string& srcImageName) { + return process2DTextureNormalMapFromImage(srcImage, srcImageName, true); +} + +gpu::TexturePointer TextureUsage::createRoughnessTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { + return process2DTextureGrayscaleFromImage(srcImage, srcImageName, false); +} + +gpu::TexturePointer TextureUsage::createRoughnessTextureFromGlossImage(const QImage& srcImage, const std::string& srcImageName) { + return process2DTextureGrayscaleFromImage(srcImage, srcImageName, true); +} + +gpu::TexturePointer TextureUsage::createMetallicTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { + return process2DTextureGrayscaleFromImage(srcImage, srcImageName, false); +} + +gpu::TexturePointer TextureUsage::createCubeTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { + return processCubeTextureColorFromImage(srcImage, srcImageName, true); +} + +gpu::TexturePointer TextureUsage::createCubeTextureFromImageWithoutIrradiance(const QImage& srcImage, const std::string& srcImageName) { + return processCubeTextureColorFromImage(srcImage, srcImageName, false); +} + +gpu::TexturePointer processImage(const QByteArray& content, const std::string& filename, int maxNumPixels, TextureUsage::Type textureType) { + // Help the QImage loader by extracting the image file format from the url filename ext. + // Some tga are not created properly without it. + auto filenameExtension = filename.substr(filename.find_last_of('.') + 1); + QBuffer buffer; + buffer.setData(content); + QImageReader imageReader(&buffer, filenameExtension.c_str()); + QImage image; + + if (imageReader.canRead()) { + image = imageReader.read(); + } else { + // Extension could be incorrect, try to detect the format from the content + QImageReader newImageReader; + newImageReader.setDecideFormatFromContent(true); + buffer.setData(content); + newImageReader.setDevice(&buffer); + + if (newImageReader.canRead()) { + qCWarning(imagelogging) << "Image file" << filename.c_str() << "has extension" << filenameExtension.c_str() + << "but is actually a" << qPrintable(newImageReader.format()) << "(recovering)"; + image = newImageReader.read(); + } + } + + int imageWidth = image.width(); + int imageHeight = image.height(); + + // Validate that the image loaded + if (imageWidth == 0 || imageHeight == 0 || image.format() == QImage::Format_Invalid) { + QString reason(image.format() == QImage::Format_Invalid ? "(Invalid Format)" : "(Size is invalid)"); + qCWarning(imagelogging) << "Failed to load" << filename.c_str() << qPrintable(reason); + return nullptr; + } + + // Validate the image is less than _maxNumPixels, and downscale if necessary + if (imageWidth * imageHeight > maxNumPixels) { + float scaleFactor = sqrtf(maxNumPixels / (float)(imageWidth * imageHeight)); + int originalWidth = imageWidth; + int originalHeight = imageHeight; + imageWidth = (int)(scaleFactor * (float)imageWidth + 0.5f); + imageHeight = (int)(scaleFactor * (float)imageHeight + 0.5f); + QImage newImage = image.scaled(QSize(imageWidth, imageHeight), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + image.swap(newImage); + qCDebug(imagelogging).nospace() << "Downscaled " << filename.c_str() << " (" << + QSize(originalWidth, originalHeight) << " to " << + QSize(imageWidth, imageHeight) << ")"; + } + + auto loader = TextureUsage::getTextureLoaderForType(textureType); + auto texture = loader(image, filename); + + return texture; +} + + + +QImage processSourceImage(const QImage& srcImage, bool cubemap) { + PROFILE_RANGE(resource_parse, "processSourceImage"); + const glm::uvec2 srcImageSize = toGlm(srcImage.size()); + glm::uvec2 targetSize = srcImageSize; + + while (glm::any(glm::greaterThan(targetSize, MAX_TEXTURE_SIZE))) { + targetSize /= 2; + } + if (targetSize != srcImageSize) { + ++DECIMATED_TEXTURE_COUNT; + } + + if (!cubemap && needsSparseRectification(targetSize)) { + ++RECTIFIED_TEXTURE_COUNT; + targetSize = rectifyToSparseSize(targetSize); + } + + if (DEV_DECIMATE_TEXTURES && glm::all(glm::greaterThanEqual(targetSize / SPARSE_PAGE_SIZE, glm::uvec2(2)))) { + targetSize /= 2; + } + + if (targetSize != srcImageSize) { + PROFILE_RANGE(resource_parse, "processSourceImage Rectify"); + qCDebug(imagelogging) << "Resizing texture from " << srcImageSize.x << "x" << srcImageSize.y << " to " << targetSize.x << "x" << targetSize.y; + return srcImage.scaled(fromGlm(targetSize), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + } + + return srcImage; +} + +struct MyOutputHandler : public nvtt::OutputHandler { + MyOutputHandler(gpu::Texture* texture, int face) : _texture(texture), _face(face) {} + + virtual void beginImage(int size, int width, int height, int depth, int face, int miplevel) override { + _size = size; + _miplevel = miplevel; + + _data = static_cast(malloc(size)); + _current = _data; + } + virtual bool writeData(const void* data, int size) override { + assert(_current + size <= _data + _size); + memcpy(_current, data, size); + _current += size; + return true; + } + virtual void endImage() override { + if (_face >= 0) { + _texture->assignStoredMipFace(_miplevel, _face, _size, static_cast(_data)); + } else { + _texture->assignStoredMip(_miplevel, _size, static_cast(_data)); + } + free(_data); + _data = nullptr; + } + + gpu::Byte* _data{ nullptr }; + gpu::Byte* _current{ nullptr }; + gpu::Texture* _texture{ nullptr }; + int _miplevel = 0; + int _size = 0; + int _face = -1; +}; +struct MyErrorHandler : public nvtt::ErrorHandler { + virtual void error(nvtt::Error e) override { + qCWarning(imagelogging) << "Texture compression error:" << nvtt::errorString(e); + } +}; + +void generateMips(gpu::Texture* texture, QImage& image, int face = -1) { +#if CPU_MIPMAPS + PROFILE_RANGE(resource_parse, "generateMips"); + + if (image.format() != QImage::Format_ARGB32) { + image = image.convertToFormat(QImage::Format_ARGB32); + } + + const int width = image.width(), height = image.height(); + const void* data = static_cast(image.constBits()); + + nvtt::TextureType textureType = nvtt::TextureType_2D; + nvtt::InputFormat inputFormat = nvtt::InputFormat_BGRA_8UB; + nvtt::WrapMode wrapMode = nvtt::WrapMode_Repeat; + nvtt::RoundMode roundMode = nvtt::RoundMode_None; + nvtt::AlphaMode alphaMode = nvtt::AlphaMode_None; + + float inputGamma = 2.2f; + float outputGamma = 2.2f; + + nvtt::CompressionOptions compressionOptions; + compressionOptions.setQuality(nvtt::Quality_Production); + + auto mipFormat = texture->getStoredMipFormat(); + if (mipFormat == gpu::Element::COLOR_COMPRESSED_SRGB) { + compressionOptions.setFormat(nvtt::Format_BC1); + } else if (mipFormat == gpu::Element::COLOR_COMPRESSED_SRGBA_MASK) { + alphaMode = nvtt::AlphaMode_Transparency; + compressionOptions.setFormat(nvtt::Format_BC1a); + } else if (mipFormat == gpu::Element::COLOR_COMPRESSED_SRGBA) { + alphaMode = nvtt::AlphaMode_Transparency; + compressionOptions.setFormat(nvtt::Format_BC3); + } else if (mipFormat == gpu::Element::COLOR_COMPRESSED_RED) { + compressionOptions.setFormat(nvtt::Format_BC4); + } else if (mipFormat == gpu::Element::COLOR_COMPRESSED_XY) { + compressionOptions.setFormat(nvtt::Format_BC5); + } else if (mipFormat == gpu::Element::COLOR_RGBA_32) { + compressionOptions.setFormat(nvtt::Format_RGBA); + compressionOptions.setPixelType(nvtt::PixelType_UnsignedNorm); + compressionOptions.setPixelFormat(32, + 0x000000FF, + 0x0000FF00, + 0x00FF0000, + 0xFF000000); + inputGamma = 1.0f; + outputGamma = 1.0f; + } else if (mipFormat == gpu::Element::COLOR_BGRA_32) { + compressionOptions.setFormat(nvtt::Format_RGBA); + compressionOptions.setPixelType(nvtt::PixelType_UnsignedNorm); + compressionOptions.setPixelFormat(32, + 0x00FF0000, + 0x0000FF00, + 0x000000FF, + 0xFF000000); + inputGamma = 1.0f; + outputGamma = 1.0f; + } else if (mipFormat == gpu::Element::COLOR_SRGBA_32) { + compressionOptions.setFormat(nvtt::Format_RGBA); + compressionOptions.setPixelType(nvtt::PixelType_UnsignedNorm); + compressionOptions.setPixelFormat(32, + 0x000000FF, + 0x0000FF00, + 0x00FF0000, + 0xFF000000); + } else if (mipFormat == gpu::Element::COLOR_SBGRA_32) { + compressionOptions.setFormat(nvtt::Format_RGBA); + compressionOptions.setPixelType(nvtt::PixelType_UnsignedNorm); + compressionOptions.setPixelFormat(32, + 0x00FF0000, + 0x0000FF00, + 0x000000FF, + 0xFF000000); + } else if (mipFormat == gpu::Element::COLOR_R_8) { + compressionOptions.setFormat(nvtt::Format_RGB); + compressionOptions.setPixelType(nvtt::PixelType_UnsignedNorm); + compressionOptions.setPixelFormat(8, 0, 0, 0); + } else { + qCWarning(imagelogging) << "Unknown mip format"; + Q_UNREACHABLE(); + return; + } + + + nvtt::InputOptions inputOptions; + inputOptions.setTextureLayout(textureType, width, height); + inputOptions.setMipmapData(data, width, height); + + inputOptions.setFormat(inputFormat); + inputOptions.setGamma(inputGamma, outputGamma); + inputOptions.setAlphaMode(alphaMode); + inputOptions.setWrapMode(wrapMode); + inputOptions.setRoundMode(roundMode); + + inputOptions.setMipmapGeneration(true); + inputOptions.setMipmapFilter(nvtt::MipmapFilter_Box); + + nvtt::OutputOptions outputOptions; + outputOptions.setOutputHeader(false); + MyOutputHandler outputHandler(texture, face); + outputOptions.setOutputHandler(&outputHandler); + MyErrorHandler errorHandler; + outputOptions.setErrorHandler(&errorHandler); + + nvtt::Compressor compressor; + compressor.process(inputOptions, compressionOptions, outputOptions); +#else + texture->autoGenerateMips(-1); +#endif +} + +void processTextureAlpha(const QImage& srcImage, bool& validAlpha, bool& alphaAsMask) { + PROFILE_RANGE(resource_parse, "processTextureAlpha"); + validAlpha = false; + alphaAsMask = true; + const uint8 OPAQUE_ALPHA = 255; + const uint8 TRANSPARENT_ALPHA = 0; + + // Figure out if we can use a mask for alpha or not + int numOpaques = 0; + int numTranslucents = 0; + const int NUM_PIXELS = srcImage.width() * srcImage.height(); + const int MAX_TRANSLUCENT_PIXELS_FOR_ALPHAMASK = (int)(0.05f * (float)(NUM_PIXELS)); + const QRgb* data = reinterpret_cast(srcImage.constBits()); + for (int i = 0; i < NUM_PIXELS; ++i) { + auto alpha = qAlpha(data[i]); + if (alpha == OPAQUE_ALPHA) { + numOpaques++; + } else if (alpha != TRANSPARENT_ALPHA) { + if (++numTranslucents > MAX_TRANSLUCENT_PIXELS_FOR_ALPHAMASK) { + alphaAsMask = false; + break; + } + } + } + validAlpha = (numOpaques != NUM_PIXELS); +} + +gpu::TexturePointer TextureUsage::process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isStrict) { + PROFILE_RANGE(resource_parse, "process2DTextureColorFromImage"); + QImage image = processSourceImage(srcImage, false); + bool validAlpha = image.hasAlphaChannel(); + bool alphaAsMask = false; + + if (image.format() != QImage::Format_ARGB32) { + image = image.convertToFormat(QImage::Format_ARGB32); + } + + if (validAlpha) { + processTextureAlpha(image, validAlpha, alphaAsMask); + } + + gpu::TexturePointer theTexture = nullptr; + + if ((image.width() > 0) && (image.height() > 0)) { +#if CPU_MIPMAPS && COMPRESS_COLOR_TEXTURES + gpu::Element formatGPU; + if (validAlpha) { + formatGPU = alphaAsMask ? gpu::Element::COLOR_COMPRESSED_SRGBA_MASK : gpu::Element::COLOR_COMPRESSED_SRGBA; + } else { + formatGPU = gpu::Element::COLOR_COMPRESSED_SRGB; + } + gpu::Element formatMip = formatGPU; +#else + gpu::Element formatMip = gpu::Element::COLOR_SBGRA_32; + gpu::Element formatGPU = gpu::Element::COLOR_SRGBA_32; +#endif + + if (isStrict) { + theTexture = gpu::Texture::createStrict(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR)); + } else { + theTexture = gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR)); + } + theTexture->setSource(srcImageName); + auto usage = gpu::Texture::Usage::Builder().withColor(); + if (validAlpha) { + usage.withAlpha(); + if (alphaAsMask) { + usage.withAlphaMask(); + } + } + theTexture->setUsage(usage.build()); + theTexture->setStoredMipFormat(formatMip); + generateMips(theTexture.get(), image); + } + + return theTexture; +} + +int clampPixelCoordinate(int coordinate, int maxCoordinate) { + return coordinate - ((int)(coordinate < 0) * coordinate) + ((int)(coordinate > maxCoordinate) * (maxCoordinate - coordinate)); +} + +const int RGBA_MAX = 255; + +// transform -1 - 1 to 0 - 255 (from sobel value to rgb) +double mapComponent(double sobelValue) { + const double factor = RGBA_MAX / 2.0; + return (sobelValue + 1.0) * factor; +} + +QImage processBumpMap(QImage& image) { + if (image.format() != QImage::Format_Grayscale8) { + image = image.convertToFormat(QImage::Format_Grayscale8); + } + + // PR 5540 by AlessandroSigna integrated here as a specialized TextureLoader for bumpmaps + // The conversion is done using the Sobel Filter to calculate the derivatives from the grayscale image + const double pStrength = 2.0; + int width = image.width(); + int height = image.height(); + + QImage result(width, height, QImage::Format_ARGB32); + + for (int i = 0; i < width; i++) { + const int iNextClamped = clampPixelCoordinate(i + 1, width - 1); + const int iPrevClamped = clampPixelCoordinate(i - 1, width - 1); + + for (int j = 0; j < height; j++) { + const int jNextClamped = clampPixelCoordinate(j + 1, height - 1); + const int jPrevClamped = clampPixelCoordinate(j - 1, height - 1); + + // surrounding pixels + const QRgb topLeft = image.pixel(iPrevClamped, jPrevClamped); + const QRgb top = image.pixel(iPrevClamped, j); + const QRgb topRight = image.pixel(iPrevClamped, jNextClamped); + const QRgb right = image.pixel(i, jNextClamped); + const QRgb bottomRight = image.pixel(iNextClamped, jNextClamped); + const QRgb bottom = image.pixel(iNextClamped, j); + const QRgb bottomLeft = image.pixel(iNextClamped, jPrevClamped); + const QRgb left = image.pixel(i, jPrevClamped); + + // take their gray intensities + // since it's a grayscale image, the value of each component RGB is the same + const double tl = qRed(topLeft); + const double t = qRed(top); + const double tr = qRed(topRight); + const double r = qRed(right); + const double br = qRed(bottomRight); + const double b = qRed(bottom); + const double bl = qRed(bottomLeft); + const double l = qRed(left); + + // apply the sobel filter + const double dX = (tr + pStrength * r + br) - (tl + pStrength * l + bl); + const double dY = (bl + pStrength * b + br) - (tl + pStrength * t + tr); + const double dZ = RGBA_MAX / pStrength; + + glm::vec3 v(dX, dY, dZ); + glm::normalize(v); + + // convert to rgb from the value obtained computing the filter + QRgb qRgbValue = qRgba(mapComponent(v.z), mapComponent(v.y), mapComponent(v.x), 1.0); + result.setPixel(i, j, qRgbValue); + } + } + + return result; +} +gpu::TexturePointer TextureUsage::process2DTextureNormalMapFromImage(const QImage& srcImage, const std::string& srcImageName, bool isBumpMap) { + PROFILE_RANGE(resource_parse, "process2DTextureNormalMapFromImage"); + QImage image = processSourceImage(srcImage, false); + + if (isBumpMap) { + image = processBumpMap(image); + } + + // Make sure the normal map source image is ARGB32 + if (image.format() != QImage::Format_ARGB32) { + image = image.convertToFormat(QImage::Format_ARGB32); + } + + gpu::TexturePointer theTexture = nullptr; + if ((image.width() > 0) && (image.height() > 0)) { + +#if CPU_MIPMAPS && COMPRESS_NORMALMAP_TEXTURES + gpu::Element formatMip = gpu::Element::COLOR_COMPRESSED_XY; + gpu::Element formatGPU = gpu::Element::COLOR_COMPRESSED_XY; +#else + gpu::Element formatMip = gpu::Element::COLOR_RGBA_32; + gpu::Element formatGPU = gpu::Element::COLOR_RGBA_32; +#endif + + theTexture = gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR)); + theTexture->setSource(srcImageName); + theTexture->setStoredMipFormat(formatMip); + generateMips(theTexture.get(), image); + } + + return theTexture; +} + +gpu::TexturePointer TextureUsage::process2DTextureGrayscaleFromImage(const QImage& srcImage, const std::string& srcImageName, bool isInvertedPixels) { + PROFILE_RANGE(resource_parse, "process2DTextureGrayscaleFromImage"); + QImage image = processSourceImage(srcImage, false); + + if (image.format() != QImage::Format_ARGB32) { + image = image.convertToFormat(QImage::Format_ARGB32); + } + + if (isInvertedPixels) { + // Gloss turned into Rough + image.invertPixels(QImage::InvertRgba); + } + + gpu::TexturePointer theTexture = nullptr; + if ((image.width() > 0) && (image.height() > 0)) { + +#if CPU_MIPMAPS && COMPRESS_GRAYSCALE_TEXTURES + gpu::Element formatMip = gpu::Element::COLOR_COMPRESSED_RED; + gpu::Element formatGPU = gpu::Element::COLOR_COMPRESSED_RED; +#else + gpu::Element formatMip = gpu::Element::COLOR_R_8; + gpu::Element formatGPU = gpu::Element::COLOR_R_8; +#endif + + theTexture = gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR)); + theTexture->setSource(srcImageName); + theTexture->setStoredMipFormat(formatMip); + generateMips(theTexture.get(), image); + } + + return theTexture; +} + +class CubeLayout { +public: + + enum SourceProjection { + FLAT = 0, + EQUIRECTANGULAR, + }; + int _type = FLAT; + int _widthRatio = 1; + int _heightRatio = 1; + + class Face { + public: + int _x = 0; + int _y = 0; + bool _horizontalMirror = false; + bool _verticalMirror = false; + + Face() {} + Face(int x, int y, bool horizontalMirror, bool verticalMirror) : _x(x), _y(y), _horizontalMirror(horizontalMirror), _verticalMirror(verticalMirror) {} + }; + + Face _faceXPos; + Face _faceXNeg; + Face _faceYPos; + Face _faceYNeg; + Face _faceZPos; + Face _faceZNeg; + + CubeLayout(int wr, int hr, Face fXP, Face fXN, Face fYP, Face fYN, Face fZP, Face fZN) : + _type(FLAT), + _widthRatio(wr), + _heightRatio(hr), + _faceXPos(fXP), + _faceXNeg(fXN), + _faceYPos(fYP), + _faceYNeg(fYN), + _faceZPos(fZP), + _faceZNeg(fZN) {} + + CubeLayout(int wr, int hr) : + _type(EQUIRECTANGULAR), + _widthRatio(wr), + _heightRatio(hr) {} + + + static const CubeLayout CUBEMAP_LAYOUTS[]; + static const int NUM_CUBEMAP_LAYOUTS; + + static int findLayout(int width, int height) { + // Find the layout of the cubemap in the 2D image + int foundLayout = -1; + for (int i = 0; i < NUM_CUBEMAP_LAYOUTS; i++) { + if ((height * CUBEMAP_LAYOUTS[i]._widthRatio) == (width * CUBEMAP_LAYOUTS[i]._heightRatio)) { + foundLayout = i; + break; + } + } + return foundLayout; + } + + static QImage extractEquirectangularFace(const QImage& source, gpu::Texture::CubeFace face, int faceWidth) { + QImage image(faceWidth, faceWidth, source.format()); + + glm::vec2 dstInvSize(1.0f / (float)image.width(), 1.0f / (float)image.height()); + + struct CubeToXYZ { + gpu::Texture::CubeFace _face; + CubeToXYZ(gpu::Texture::CubeFace face) : _face(face) {} + + glm::vec3 xyzFrom(const glm::vec2& uv) { + auto faceDir = glm::normalize(glm::vec3(-1.0f + 2.0f * uv.x, -1.0f + 2.0f * uv.y, 1.0f)); + + switch (_face) { + case gpu::Texture::CubeFace::CUBE_FACE_BACK_POS_Z: + return glm::vec3(-faceDir.x, faceDir.y, faceDir.z); + case gpu::Texture::CubeFace::CUBE_FACE_FRONT_NEG_Z: + return glm::vec3(faceDir.x, faceDir.y, -faceDir.z); + case gpu::Texture::CubeFace::CUBE_FACE_LEFT_NEG_X: + return glm::vec3(faceDir.z, faceDir.y, faceDir.x); + case gpu::Texture::CubeFace::CUBE_FACE_RIGHT_POS_X: + return glm::vec3(-faceDir.z, faceDir.y, -faceDir.x); + case gpu::Texture::CubeFace::CUBE_FACE_BOTTOM_NEG_Y: + return glm::vec3(-faceDir.x, -faceDir.z, faceDir.y); + case gpu::Texture::CubeFace::CUBE_FACE_TOP_POS_Y: + default: + return glm::vec3(-faceDir.x, faceDir.z, -faceDir.y); + } + } + }; + CubeToXYZ cubeToXYZ(face); + + struct RectToXYZ { + RectToXYZ() {} + + glm::vec2 uvFrom(const glm::vec3& xyz) { + auto flatDir = glm::normalize(glm::vec2(xyz.x, xyz.z)); + auto uvRad = glm::vec2(atan2(flatDir.x, flatDir.y), asin(xyz.y)); + + const float LON_TO_RECT_U = 1.0f / (glm::pi()); + const float LAT_TO_RECT_V = 2.0f / glm::pi(); + return glm::vec2(0.5f * uvRad.x * LON_TO_RECT_U + 0.5f, 0.5f * uvRad.y * LAT_TO_RECT_V + 0.5f); + } + }; + RectToXYZ rectToXYZ; + + int srcFaceHeight = source.height(); + int srcFaceWidth = source.width(); + + glm::vec2 dstCoord; + glm::ivec2 srcPixel; + for (int y = 0; y < faceWidth; ++y) { + dstCoord.y = 1.0f - (y + 0.5f) * dstInvSize.y; // Fill cube face images from top to bottom + for (int x = 0; x < faceWidth; ++x) { + dstCoord.x = (x + 0.5f) * dstInvSize.x; + + auto xyzDir = cubeToXYZ.xyzFrom(dstCoord); + auto srcCoord = rectToXYZ.uvFrom(xyzDir); + + srcPixel.x = floor(srcCoord.x * srcFaceWidth); + // Flip the vertical axis to QImage going top to bottom + srcPixel.y = floor((1.0f - srcCoord.y) * srcFaceHeight); + + if (((uint32)srcPixel.x < (uint32)source.width()) && ((uint32)srcPixel.y < (uint32)source.height())) { + image.setPixel(x, y, source.pixel(QPoint(srcPixel.x, srcPixel.y))); + + // Keep for debug, this is showing the dir as a color + // glm::u8vec4 rgba((xyzDir.x + 1.0)*0.5 * 256, (xyzDir.y + 1.0)*0.5 * 256, (xyzDir.z + 1.0)*0.5 * 256, 256); + // unsigned int val = 0xff000000 | (rgba.r) | (rgba.g << 8) | (rgba.b << 16); + // image.setPixel(x, y, val); + } + } + } + return image; + } +}; + +const CubeLayout CubeLayout::CUBEMAP_LAYOUTS[] = { + + // Here is the expected layout for the faces in an image with the 2/1 aspect ratio: + // THis is detected as an Equirectangular projection + // WIDTH + // <---------------------------> + // ^ +------+------+------+------+ + // H | | | | | + // E | | | | | + // I | | | | | + // G +------+------+------+------+ + // H | | | | | + // T | | | | | + // | | | | | | + // v +------+------+------+------+ + // + // FaceWidth = width = height / 6 + { 2, 1 }, + + // Here is the expected layout for the faces in an image with the 1/6 aspect ratio: + // + // WIDTH + // <------> + // ^ +------+ + // | | | + // | | +X | + // | | | + // H +------+ + // E | | + // I | -X | + // G | | + // H +------+ + // T | | + // | | +Y | + // | | | + // | +------+ + // | | | + // | | -Y | + // | | | + // H +------+ + // E | | + // I | +Z | + // G | | + // H +------+ + // T | | + // | | -Z | + // | | | + // V +------+ + // + // FaceWidth = width = height / 6 + { 1, 6, + { 0, 0, true, false }, + { 0, 1, true, false }, + { 0, 2, false, true }, + { 0, 3, false, true }, + { 0, 4, true, false }, + { 0, 5, true, false } + }, + + // Here is the expected layout for the faces in an image with the 3/4 aspect ratio: + // + // <-----------WIDTH-----------> + // ^ +------+------+------+------+ + // | | | | | | + // | | | +Y | | | + // | | | | | | + // H +------+------+------+------+ + // E | | | | | + // I | -X | -Z | +X | +Z | + // G | | | | | + // H +------+------+------+------+ + // T | | | | | + // | | | -Y | | | + // | | | | | | + // V +------+------+------+------+ + // + // FaceWidth = width / 4 = height / 3 + { 4, 3, + { 2, 1, true, false }, + { 0, 1, true, false }, + { 1, 0, false, true }, + { 1, 2, false, true }, + { 3, 1, true, false }, + { 1, 1, true, false } + }, + + // Here is the expected layout for the faces in an image with the 4/3 aspect ratio: + // + // <-------WIDTH--------> + // ^ +------+------+------+ + // | | | | | + // | | | +Y | | + // | | | | | + // H +------+------+------+ + // E | | | | + // I | -X | -Z | +X | + // G | | | | + // H +------+------+------+ + // T | | | | + // | | | -Y | | + // | | | | | + // | +------+------+------+ + // | | | | | + // | | | +Z! | | <+Z is upside down! + // | | | | | + // V +------+------+------+ + // + // FaceWidth = width / 3 = height / 4 + { 3, 4, + { 2, 1, true, false }, + { 0, 1, true, false }, + { 1, 0, false, true }, + { 1, 2, false, true }, + { 1, 3, false, true }, + { 1, 1, true, false } + } +}; +const int CubeLayout::NUM_CUBEMAP_LAYOUTS = sizeof(CubeLayout::CUBEMAP_LAYOUTS) / sizeof(CubeLayout); + +gpu::TexturePointer TextureUsage::processCubeTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool generateIrradiance) { + PROFILE_RANGE(resource_parse, "processCubeTextureColorFromImage"); + + gpu::TexturePointer theTexture = nullptr; + if ((srcImage.width() > 0) && (srcImage.height() > 0)) { + QImage image = processSourceImage(srcImage, true); + if (image.format() != QImage::Format_ARGB32) { + image = image.convertToFormat(QImage::Format_ARGB32); + } + +#if CPU_MIPMAPS && COMPRESS_CUBEMAP_TEXTURES + gpu::Element formatMip = gpu::Element::COLOR_COMPRESSED_SRGBA; + gpu::Element formatGPU = gpu::Element::COLOR_COMPRESSED_SRGBA; +#else + gpu::Element formatMip = gpu::Element::COLOR_SRGBA_32; + gpu::Element formatGPU = gpu::Element::COLOR_SRGBA_32; +#endif + + // Find the layout of the cubemap in the 2D image + // Use the original image size since processSourceImage may have altered the size / aspect ratio + int foundLayout = CubeLayout::findLayout(srcImage.width(), srcImage.height()); + + std::vector faces; + // If found, go extract the faces as separate images + if (foundLayout >= 0) { + auto& layout = CubeLayout::CUBEMAP_LAYOUTS[foundLayout]; + if (layout._type == CubeLayout::FLAT) { + int faceWidth = image.width() / layout._widthRatio; + + faces.push_back(image.copy(QRect(layout._faceXPos._x * faceWidth, layout._faceXPos._y * faceWidth, faceWidth, faceWidth)).mirrored(layout._faceXPos._horizontalMirror, layout._faceXPos._verticalMirror)); + faces.push_back(image.copy(QRect(layout._faceXNeg._x * faceWidth, layout._faceXNeg._y * faceWidth, faceWidth, faceWidth)).mirrored(layout._faceXNeg._horizontalMirror, layout._faceXNeg._verticalMirror)); + faces.push_back(image.copy(QRect(layout._faceYPos._x * faceWidth, layout._faceYPos._y * faceWidth, faceWidth, faceWidth)).mirrored(layout._faceYPos._horizontalMirror, layout._faceYPos._verticalMirror)); + faces.push_back(image.copy(QRect(layout._faceYNeg._x * faceWidth, layout._faceYNeg._y * faceWidth, faceWidth, faceWidth)).mirrored(layout._faceYNeg._horizontalMirror, layout._faceYNeg._verticalMirror)); + faces.push_back(image.copy(QRect(layout._faceZPos._x * faceWidth, layout._faceZPos._y * faceWidth, faceWidth, faceWidth)).mirrored(layout._faceZPos._horizontalMirror, layout._faceZPos._verticalMirror)); + faces.push_back(image.copy(QRect(layout._faceZNeg._x * faceWidth, layout._faceZNeg._y * faceWidth, faceWidth, faceWidth)).mirrored(layout._faceZNeg._horizontalMirror, layout._faceZNeg._verticalMirror)); + } else if (layout._type == CubeLayout::EQUIRECTANGULAR) { + // THe face width is estimated from the input image + const int EQUIRECT_FACE_RATIO_TO_WIDTH = 4; + const int EQUIRECT_MAX_FACE_WIDTH = 2048; + int faceWidth = std::min(image.width() / EQUIRECT_FACE_RATIO_TO_WIDTH, EQUIRECT_MAX_FACE_WIDTH); + for (int face = gpu::Texture::CUBE_FACE_RIGHT_POS_X; face < gpu::Texture::NUM_CUBE_FACES; face++) { + QImage faceImage = CubeLayout::extractEquirectangularFace(image, (gpu::Texture::CubeFace) face, faceWidth); + faces.push_back(faceImage); + } + } + } else { + qCDebug(imagelogging) << "Failed to find a known cube map layout from this image:" << QString(srcImageName.c_str()); + return nullptr; + } + + // If the 6 faces have been created go on and define the true Texture + if (faces.size() == gpu::Texture::NUM_FACES_PER_TYPE[gpu::Texture::TEX_CUBE]) { + theTexture = gpu::Texture::createCube(formatGPU, faces[0].width(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP)); + theTexture->setSource(srcImageName); + theTexture->setStoredMipFormat(formatMip); + + for (uint8 face = 0; face < faces.size(); ++face) { + generateMips(theTexture.get(), faces[face], face); + } + + // Generate irradiance while we are at it + if (generateIrradiance) { + PROFILE_RANGE(resource_parse, "generateIrradiance"); + auto irradianceTexture = gpu::Texture::createCube(gpu::Element::COLOR_SRGBA_32, faces[0].width(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP)); + irradianceTexture->setSource(srcImageName); + irradianceTexture->setStoredMipFormat(gpu::Element::COLOR_SBGRA_32); + for (uint8 face = 0; face < faces.size(); ++face) { + irradianceTexture->assignStoredMipFace(0, face, faces[face].byteCount(), faces[face].constBits()); + } + + irradianceTexture->generateIrradiance(); + + auto irradiance = irradianceTexture->getIrradiance(); + theTexture->overrideIrradiance(irradiance); + } + } + } + + return theTexture; +} + +} // namespace image diff --git a/libraries/image/src/image/Image.h b/libraries/image/src/image/Image.h new file mode 100644 index 0000000000..3e5aa868d2 --- /dev/null +++ b/libraries/image/src/image/Image.h @@ -0,0 +1,70 @@ +// +// Image.h +// image/src/image +// +// Created by Clement Brisset on 4/5/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 +// + +#ifndef hifi_image_Image_h +#define hifi_image_Image_h + +#include + +#include + +class QByteArray; +class QImage; + +namespace image { + +namespace TextureUsage { + +enum Type { + DEFAULT_TEXTURE, + STRICT_TEXTURE, + ALBEDO_TEXTURE, + NORMAL_TEXTURE, + BUMP_TEXTURE, + SPECULAR_TEXTURE, + METALLIC_TEXTURE = SPECULAR_TEXTURE, // for now spec and metallic texture are the same, converted to grey + ROUGHNESS_TEXTURE, + GLOSS_TEXTURE, + EMISSIVE_TEXTURE, + CUBE_TEXTURE, + OCCLUSION_TEXTURE, + SCATTERING_TEXTURE = OCCLUSION_TEXTURE, + LIGHTMAP_TEXTURE +}; + +using TextureLoader = std::function; +TextureLoader getTextureLoaderForType(Type type, const QVariantMap& options = QVariantMap()); + +gpu::TexturePointer create2DTextureFromImage(const QImage& image, const std::string& srcImageName); +gpu::TexturePointer createStrict2DTextureFromImage(const QImage& image, const std::string& srcImageName); +gpu::TexturePointer createAlbedoTextureFromImage(const QImage& image, const std::string& srcImageName); +gpu::TexturePointer createEmissiveTextureFromImage(const QImage& image, const std::string& srcImageName); +gpu::TexturePointer createNormalTextureFromNormalImage(const QImage& image, const std::string& srcImageName); +gpu::TexturePointer createNormalTextureFromBumpImage(const QImage& image, const std::string& srcImageName); +gpu::TexturePointer createRoughnessTextureFromImage(const QImage& image, const std::string& srcImageName); +gpu::TexturePointer createRoughnessTextureFromGlossImage(const QImage& image, const std::string& srcImageName); +gpu::TexturePointer createMetallicTextureFromImage(const QImage& image, const std::string& srcImageName); +gpu::TexturePointer createCubeTextureFromImage(const QImage& image, const std::string& srcImageName); +gpu::TexturePointer createCubeTextureFromImageWithoutIrradiance(const QImage& image, const std::string& srcImageName); +gpu::TexturePointer createLightmapTextureFromImage(const QImage& image, const std::string& srcImageName); + +gpu::TexturePointer process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isStrict); +gpu::TexturePointer process2DTextureNormalMapFromImage(const QImage& srcImage, const std::string& srcImageName, bool isBumpMap); +gpu::TexturePointer process2DTextureGrayscaleFromImage(const QImage& srcImage, const std::string& srcImageName, bool isInvertedPixels); +gpu::TexturePointer processCubeTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool generateIrradiance); + +} // namespace TextureUsage + +gpu::TexturePointer processImage(const QByteArray& content, const std::string& url, int maxNumPixels, TextureUsage::Type textureType); + +} // namespace image + +#endif // hifi_image_Image_h diff --git a/libraries/image/src/image/ImageLogging.cpp b/libraries/image/src/image/ImageLogging.cpp new file mode 100644 index 0000000000..1df8820f93 --- /dev/null +++ b/libraries/image/src/image/ImageLogging.cpp @@ -0,0 +1,14 @@ +// +// ImageLogging.cpp +// image/src/image +// +// Created by Clement Brisset on 4/5/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 +// + +#include "ImageLogging.h" + +Q_LOGGING_CATEGORY(imagelogging, "hifi.image") \ No newline at end of file diff --git a/libraries/image/src/image/ImageLogging.h b/libraries/image/src/image/ImageLogging.h new file mode 100644 index 0000000000..668d8b9ff9 --- /dev/null +++ b/libraries/image/src/image/ImageLogging.h @@ -0,0 +1,14 @@ +// +// ImageLogging.h +// image/src/image +// +// Created by Clement Brisset on 4/5/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 +// + +#include + +Q_DECLARE_LOGGING_CATEGORY(imagelogging) diff --git a/libraries/ktx/src/ktx/KTX.cpp b/libraries/ktx/src/ktx/KTX.cpp index 6fca39788b..38bb91e5c2 100644 --- a/libraries/ktx/src/ktx/KTX.cpp +++ b/libraries/ktx/src/ktx/KTX.cpp @@ -12,6 +12,7 @@ #include "KTX.h" #include //min max and more +#include using namespace ktx; @@ -34,30 +35,80 @@ uint32_t Header::evalMaxDimension() const { return std::max(getPixelWidth(), std::max(getPixelHeight(), getPixelDepth())); } -uint32_t Header::evalPixelWidth(uint32_t level) const { - return std::max(getPixelWidth() >> level, 1U); +uint32_t Header::evalPixelOrBlockWidth(uint32_t level) const { + auto pixelWidth = std::max(getPixelWidth() >> level, 1U); + if (getGLType() == GLType::COMPRESSED_TYPE) { + return (pixelWidth + 3) / 4; + } else { + return pixelWidth; + } } -uint32_t Header::evalPixelHeight(uint32_t level) const { - return std::max(getPixelHeight() >> level, 1U); +uint32_t Header::evalPixelOrBlockHeight(uint32_t level) const { + auto pixelWidth = std::max(getPixelHeight() >> level, 1U); + if (getGLType() == GLType::COMPRESSED_TYPE) { + auto format = getGLInternaFormat_Compressed(); + switch (format) { + case GLInternalFormat_Compressed::COMPRESSED_SRGB_S3TC_DXT1_EXT: // BC1 + case GLInternalFormat_Compressed::COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT: // BC1A + case GLInternalFormat_Compressed::COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT: // BC3 + case GLInternalFormat_Compressed::COMPRESSED_RED_RGTC1: // BC4 + case GLInternalFormat_Compressed::COMPRESSED_RG_RGTC2: // BC5 + return (pixelWidth + 3) / 4; + default: + throw std::runtime_error("Unknown format"); + } + } else { + return pixelWidth; + } } -uint32_t Header::evalPixelDepth(uint32_t level) const { +uint32_t Header::evalPixelOrBlockDepth(uint32_t level) const { return std::max(getPixelDepth() >> level, 1U); } -size_t Header::evalPixelSize() const { - return glTypeSize; // Really we should generate the size from the FOrmat etc +size_t Header::evalPixelOrBlockSize() const { + if (getGLType() == GLType::COMPRESSED_TYPE) { + auto format = getGLInternaFormat_Compressed(); + if (format == GLInternalFormat_Compressed::COMPRESSED_SRGB_S3TC_DXT1_EXT) { + return 8; + } else if (format == GLInternalFormat_Compressed::COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT) { + return 8; + } else if (format == GLInternalFormat_Compressed::COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT) { + return 16; + } else if (format == GLInternalFormat_Compressed::COMPRESSED_RED_RGTC1) { + return 8; + } else if (format == GLInternalFormat_Compressed::COMPRESSED_RG_RGTC2) { + return 16; + } + } else { + auto baseFormat = getGLBaseInternalFormat(); + if (baseFormat == GLBaseInternalFormat::RED) { + return 1; + } else if (baseFormat == GLBaseInternalFormat::RG) { + return 2; + } else if (baseFormat == GLBaseInternalFormat::RGB) { + return 3; + } else if (baseFormat == GLBaseInternalFormat::RGBA) { + return 4; + } + } + + qWarning() << "Unknown ktx format: " << glFormat << " " << glBaseInternalFormat << " " << glInternalFormat; + return 0; } size_t Header::evalRowSize(uint32_t level) const { - auto pixWidth = evalPixelWidth(level); - auto pixSize = evalPixelSize(); + auto pixWidth = evalPixelOrBlockWidth(level); + auto pixSize = evalPixelOrBlockSize(); + if (pixSize == 0) { + return 0; + } auto netSize = pixWidth * pixSize; auto padding = evalPadding(netSize); return netSize + padding; } size_t Header::evalFaceSize(uint32_t level) const { - auto pixHeight = evalPixelHeight(level); - auto pixDepth = evalPixelDepth(level); + auto pixHeight = evalPixelOrBlockHeight(level); + auto pixDepth = evalPixelOrBlockDepth(level); auto rowSize = evalRowSize(level); return pixDepth * pixHeight * rowSize; } @@ -71,6 +122,47 @@ size_t Header::evalImageSize(uint32_t level) const { } +size_t KTXDescriptor::getValueOffsetForKey(const std::string& key) const { + size_t offset { 0 }; + for (auto& kv : keyValues) { + if (kv._key == key) { + return offset + ktx::KV_SIZE_WIDTH + kv._key.size() + 1; + } + offset += kv.serializedByteSize(); + } + return 0; +} + +ImageDescriptors Header::generateImageDescriptors() const { + ImageDescriptors descriptors; + + size_t imageOffset = 0; + for (uint32_t level = 0; level < numberOfMipmapLevels; ++level) { + auto imageSize = static_cast(evalImageSize(level)); + if (imageSize == 0) { + return ImageDescriptors(); + } + ImageHeader header { + numberOfFaces == NUM_CUBEMAPFACES, + imageOffset, + imageSize, + 0 + }; + + imageOffset += (imageSize * numberOfFaces) + ktx::IMAGE_SIZE_WIDTH; + + ImageHeader::FaceOffsets offsets; + // TODO Add correct face offsets + for (uint32_t i = 0; i < numberOfFaces; ++i) { + offsets.push_back(0); + } + descriptors.push_back(ImageDescriptor(header, offsets)); + } + + return descriptors; +} + + KeyValue::KeyValue(const std::string& key, uint32_t valueByteSize, const Byte* value) : _byteSize((uint32_t) key.size() + 1 + valueByteSize), // keyString size + '\0' ending char + the value size _key(key), @@ -209,4 +301,4 @@ KTXDescriptor KTX::toDescriptor() const { KTX::KTX(const StoragePointer& storage, const Header& header, const KeyValues& keyValues, const Images& images) : _header(header), _storage(storage), _keyValues(keyValues), _images(images) { -} \ No newline at end of file +} diff --git a/libraries/ktx/src/ktx/KTX.h b/libraries/ktx/src/ktx/KTX.h index f09986991a..e8fa019a07 100644 --- a/libraries/ktx/src/ktx/KTX.h +++ b/libraries/ktx/src/ktx/KTX.h @@ -71,6 +71,8 @@ end namespace ktx { const uint32_t PACKING_SIZE { sizeof(uint32_t) }; + const std::string HIFI_MIN_POPULATED_MIP_KEY{ "hifi.minMip" }; + using Byte = uint8_t; enum class GLType : uint32_t { @@ -101,8 +103,6 @@ namespace ktx { UNSIGNED_INT_10F_11F_11F_REV = 0x8C3B, UNSIGNED_INT_5_9_9_9_REV = 0x8C3E, FLOAT_32_UNSIGNED_INT_24_8_REV = 0x8DAD, - - NUM_GLTYPES = 25, }; enum class GLFormat : uint32_t { @@ -130,8 +130,6 @@ namespace ktx { RGBA_INTEGER = 0x8D99, BGR_INTEGER = 0x8D9A, BGRA_INTEGER = 0x8D9B, - - NUM_GLFORMATS = 20, }; enum class GLInternalFormat_Uncompressed : uint32_t { @@ -232,8 +230,6 @@ namespace ktx { STENCIL_INDEX4 = 0x8D47, STENCIL_INDEX8 = 0x8D48, STENCIL_INDEX16 = 0x8D49, - - NUM_UNCOMPRESSED_GLINTERNALFORMATS = 74, }; enum class GLInternalFormat_Compressed : uint32_t { @@ -246,6 +242,11 @@ namespace ktx { COMPRESSED_SRGB = 0x8C48, COMPRESSED_SRGB_ALPHA = 0x8C49, + COMPRESSED_SRGB_S3TC_DXT1_EXT = 0x8C4C, + COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT = 0x8C4D, + COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT = 0x8C4E, + COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT = 0x8C4F, + COMPRESSED_RED_RGTC1 = 0x8DBB, COMPRESSED_SIGNED_RED_RGTC1 = 0x8DBC, COMPRESSED_RG_RGTC2 = 0x8DBD, @@ -267,8 +268,6 @@ namespace ktx { COMPRESSED_SIGNED_R11_EAC = 0x9271, COMPRESSED_RG11_EAC = 0x9272, COMPRESSED_SIGNED_RG11_EAC = 0x9273, - - NUM_COMPRESSED_GLINTERNALFORMATS = 24, }; enum class GLBaseInternalFormat : uint32_t { @@ -280,8 +279,6 @@ namespace ktx { RGB = 0x1907, RGBA = 0x1908, STENCIL_INDEX = 0x1901, - - NUM_GLBASEINTERNALFORMATS = 7, }; enum CubeMapFace { @@ -297,6 +294,11 @@ namespace ktx { using Storage = storage::Storage; using StoragePointer = std::shared_ptr; + struct ImageDescriptor; + using ImageDescriptors = std::vector; + + bool checkIdentifier(const Byte* identifier); + // Header struct Header { static const size_t IDENTIFIER_LENGTH = 12; @@ -335,11 +337,11 @@ namespace ktx { uint32_t getNumberOfLevels() const { return (numberOfMipmapLevels ? numberOfMipmapLevels : 1); } uint32_t evalMaxDimension() const; - uint32_t evalPixelWidth(uint32_t level) const; - uint32_t evalPixelHeight(uint32_t level) const; - uint32_t evalPixelDepth(uint32_t level) const; + uint32_t evalPixelOrBlockWidth(uint32_t level) const; + uint32_t evalPixelOrBlockHeight(uint32_t level) const; + uint32_t evalPixelOrBlockDepth(uint32_t level) const; - size_t evalPixelSize() const; + size_t evalPixelOrBlockSize() const; size_t evalRowSize(uint32_t level) const; size_t evalFaceSize(uint32_t level) const; size_t evalImageSize(uint32_t level) const; @@ -383,7 +385,12 @@ namespace ktx { void setCube(uint32_t width, uint32_t height) { setDimensions(width, height, 0, 0, NUM_CUBEMAPFACES); } void setCubeArray(uint32_t width, uint32_t height, uint32_t numSlices) { setDimensions(width, height, 0, (numSlices > 0 ? numSlices : 1), NUM_CUBEMAPFACES); } + ImageDescriptors generateImageDescriptors() const; }; + static const size_t KTX_HEADER_SIZE = 64; + static_assert(sizeof(Header) == KTX_HEADER_SIZE, "KTX Header size is static and should not change from the spec"); + static const size_t KV_SIZE_WIDTH = 4; // Number of bytes for keyAndValueByteSize + static const size_t IMAGE_SIZE_WIDTH = 4; // Number of bytes for imageSize // Key Values struct KeyValue { @@ -410,12 +417,17 @@ namespace ktx { struct ImageHeader { using FaceOffsets = std::vector; using FaceBytes = std::vector; + + // This is the byte offset from the _start_ of the image region. For example, level 0 + // will have a byte offset of 0. const uint32_t _numFaces; + const size_t _imageOffset; const uint32_t _imageSize; const uint32_t _faceSize; const uint32_t _padding; - ImageHeader(bool cube, uint32_t imageSize, uint32_t padding) : + ImageHeader(bool cube, size_t imageOffset, uint32_t imageSize, uint32_t padding) : _numFaces(cube ? NUM_CUBEMAPFACES : 1), + _imageOffset(imageOffset), _imageSize(imageSize * _numFaces), _faceSize(imageSize), _padding(padding) { @@ -424,22 +436,22 @@ namespace ktx { struct Image; + // Image without the image data itself struct ImageDescriptor : public ImageHeader { const FaceOffsets _faceOffsets; ImageDescriptor(const ImageHeader& header, const FaceOffsets& offsets) : ImageHeader(header), _faceOffsets(offsets) {} Image toImage(const ktx::StoragePointer& storage) const; }; - using ImageDescriptors = std::vector; - + // Image with the image data itself struct Image : public ImageHeader { FaceBytes _faceBytes; Image(const ImageHeader& header, const FaceBytes& faces) : ImageHeader(header), _faceBytes(faces) {} - Image(uint32_t imageSize, uint32_t padding, const Byte* bytes) : - ImageHeader(false, imageSize, padding), + Image(size_t imageOffset, uint32_t imageSize, uint32_t padding, const Byte* bytes) : + ImageHeader(false, imageOffset, imageSize, padding), _faceBytes(1, bytes) {} - Image(uint32_t pageSize, uint32_t padding, const FaceBytes& cubeFaceBytes) : - ImageHeader(true, pageSize, padding) + Image(size_t imageOffset, uint32_t pageSize, uint32_t padding, const FaceBytes& cubeFaceBytes) : + ImageHeader(true, imageOffset, pageSize, padding) { if (cubeFaceBytes.size() == NUM_CUBEMAPFACES) { _faceBytes = cubeFaceBytes; @@ -462,6 +474,7 @@ namespace ktx { const ImageDescriptors images; size_t getMipFaceTexelsSize(uint16_t mip = 0, uint8_t face = 0) const; size_t getMipFaceTexelsOffset(uint16_t mip = 0, uint8_t face = 0) const; + size_t getValueOffsetForKey(const std::string& key) const; }; class KTX { @@ -476,6 +489,7 @@ namespace ktx { // This path allocate the Storage where to store header, keyvalues and copy mips // Then COPY all the data static std::unique_ptr create(const Header& header, const Images& images, const KeyValues& keyValues = KeyValues()); + static std::unique_ptr createBare(const Header& header, const KeyValues& keyValues = KeyValues()); // Instead of creating a full Copy of the src data in a KTX object, the write serialization can be performed with the // following two functions @@ -489,10 +503,14 @@ namespace ktx { // // This is exactly what is done in the create function static size_t evalStorageSize(const Header& header, const Images& images, const KeyValues& keyValues = KeyValues()); + static size_t evalStorageSize(const Header& header, const ImageDescriptors& images, const KeyValues& keyValues = KeyValues()); static size_t write(Byte* destBytes, size_t destByteSize, const Header& header, const Images& images, const KeyValues& keyValues = KeyValues()); + static size_t writeWithoutImages(Byte* destBytes, size_t destByteSize, const Header& header, const ImageDescriptors& descriptors, const KeyValues& keyValues = KeyValues()); static size_t writeKeyValues(Byte* destBytes, size_t destByteSize, const KeyValues& keyValues); static Images writeImages(Byte* destBytes, size_t destByteSize, const Images& images); + void writeMipData(uint16_t level, const Byte* sourceBytes, size_t source_size); + // Parse a block of memory and create a KTX object from it static std::unique_ptr create(const StoragePointer& src); diff --git a/libraries/ktx/src/ktx/Reader.cpp b/libraries/ktx/src/ktx/Reader.cpp index bf72faeba5..b22f262e85 100644 --- a/libraries/ktx/src/ktx/Reader.cpp +++ b/libraries/ktx/src/ktx/Reader.cpp @@ -144,6 +144,7 @@ namespace ktx { while ((currentPtr - srcBytes) + sizeof(uint32_t) <= (srcSize)) { // Grab the imageSize coming up + uint32_t imageOffset = currentPtr - srcBytes; size_t imageSize = *reinterpret_cast(currentPtr); currentPtr += sizeof(uint32_t); @@ -158,10 +159,10 @@ namespace ktx { faces[face] = currentPtr; currentPtr += faceSize; } - images.emplace_back(Image((uint32_t) faceSize, padding, faces)); + images.emplace_back(Image(imageOffset, (uint32_t) faceSize, padding, faces)); currentPtr += padding; } else { - images.emplace_back(Image((uint32_t) imageSize, padding, currentPtr)); + images.emplace_back(Image(imageOffset, (uint32_t) imageSize, padding, currentPtr)); currentPtr += imageSize + padding; } } else { diff --git a/libraries/ktx/src/ktx/Writer.cpp b/libraries/ktx/src/ktx/Writer.cpp index 25b363d31b..4226b8fa84 100644 --- a/libraries/ktx/src/ktx/Writer.cpp +++ b/libraries/ktx/src/ktx/Writer.cpp @@ -40,6 +40,24 @@ namespace ktx { return create(storagePointer); } + std::unique_ptr KTX::createBare(const Header& header, const KeyValues& keyValues) { + auto descriptors = header.generateImageDescriptors(); + + Byte minMip = header.numberOfMipmapLevels; + auto newKeyValues = keyValues; + newKeyValues.emplace_back(KeyValue(HIFI_MIN_POPULATED_MIP_KEY, sizeof(Byte), &minMip)); + + StoragePointer storagePointer; + { + auto storageSize = ktx::KTX::evalStorageSize(header, descriptors, newKeyValues); + auto memoryStorage = new storage::MemoryStorage(storageSize); + qDebug() << "Memory storage size is: " << storageSize; + ktx::KTX::writeWithoutImages(memoryStorage->data(), memoryStorage->size(), header, descriptors, newKeyValues); + storagePointer.reset(memoryStorage); + } + return create(storagePointer); + } + size_t KTX::evalStorageSize(const Header& header, const Images& images, const KeyValues& keyValues) { size_t storageSize = sizeof(Header); @@ -59,6 +77,25 @@ namespace ktx { return storageSize; } + size_t KTX::evalStorageSize(const Header& header, const ImageDescriptors& imageDescriptors, const KeyValues& keyValues) { + size_t storageSize = sizeof(Header); + + if (!keyValues.empty()) { + size_t keyValuesSize = KeyValue::serializedKeyValuesByteSize(keyValues); + storageSize += keyValuesSize; + } + + auto numMips = header.getNumberOfLevels(); + for (uint32_t l = 0; l < numMips; l++) { + if (imageDescriptors.size() > l) { + storageSize += sizeof(uint32_t); + storageSize += imageDescriptors[l]._imageSize; + storageSize += Header::evalPadding(imageDescriptors[l]._imageSize); + } + } + return storageSize; + } + size_t KTX::write(Byte* destBytes, size_t destByteSize, const Header& header, const Images& srcImages, const KeyValues& keyValues) { // Check again that we have enough destination capacity if (!destBytes || (destByteSize < evalStorageSize(header, srcImages, keyValues))) { @@ -87,6 +124,43 @@ namespace ktx { return destByteSize; } + size_t KTX::writeWithoutImages(Byte* destBytes, size_t destByteSize, const Header& header, const ImageDescriptors& descriptors, const KeyValues& keyValues) { + // Check again that we have enough destination capacity + if (!destBytes || (destByteSize < evalStorageSize(header, descriptors, keyValues))) { + return 0; + } + + auto currentDestPtr = destBytes; + // Header + auto destHeader = reinterpret_cast(currentDestPtr); + memcpy(currentDestPtr, &header, sizeof(Header)); + currentDestPtr += sizeof(Header); + + + // KeyValues + if (!keyValues.empty()) { + destHeader->bytesOfKeyValueData = (uint32_t) writeKeyValues(currentDestPtr, destByteSize - sizeof(Header), keyValues); + } else { + // Make sure the header contains the right bytesOfKeyValueData size + destHeader->bytesOfKeyValueData = 0; + } + currentDestPtr += destHeader->bytesOfKeyValueData; + + for (size_t i = 0; i < descriptors.size(); ++i) { + auto ptr = reinterpret_cast(currentDestPtr); + *ptr = descriptors[i]._imageSize; + ptr++; +#ifdef DEBUG + for (size_t k = 0; k < descriptors[i]._imageSize/4; k++) { + *(ptr + k) = 0xFFFFFFFF; + } +#endif + currentDestPtr += descriptors[i]._imageSize + sizeof(uint32_t); + } + + return destByteSize; + } + uint32_t KeyValue::writeSerializedKeyAndValue(Byte* destBytes, uint32_t destByteSize, const KeyValue& keyval) { uint32_t keyvalSize = keyval.serializedByteSize(); if (keyvalSize > destByteSize) { @@ -134,6 +208,7 @@ namespace ktx { for (uint32_t l = 0; l < srcImages.size(); l++) { if (currentDataSize + sizeof(uint32_t) < allocatedImagesDataSize) { + uint32_t imageOffset = currentPtr - destBytes; size_t imageSize = srcImages[l]._imageSize; *(reinterpret_cast (currentPtr)) = (uint32_t) imageSize; currentPtr += sizeof(uint32_t); @@ -146,7 +221,7 @@ namespace ktx { // Single face vs cubes if (srcImages[l]._numFaces == 1) { memcpy(currentPtr, srcImages[l]._faceBytes[0], imageSize); - destImages.emplace_back(Image((uint32_t) imageSize, padding, currentPtr)); + destImages.emplace_back(Image(imageOffset, (uint32_t) imageSize, padding, currentPtr)); currentPtr += imageSize; } else { Image::FaceBytes faceBytes(NUM_CUBEMAPFACES); @@ -156,7 +231,7 @@ namespace ktx { faceBytes[face] = currentPtr; currentPtr += faceSize; } - destImages.emplace_back(Image(faceSize, padding, faceBytes)); + destImages.emplace_back(Image(imageOffset, faceSize, padding, faceBytes)); } currentPtr += padding; @@ -168,4 +243,11 @@ namespace ktx { return destImages; } + void KTX::writeMipData(uint16_t level, const Byte* sourceBytes, size_t sourceSize) { + Q_ASSERT(level > 0); + Q_ASSERT(level < _images.size()); + Q_ASSERT(sourceSize == _images[level]._imageSize); + + //memcpy(reinterpret_cast(_images[level]._faceBytes[0]), sourceBytes, sourceSize); + } } diff --git a/libraries/model-networking/CMakeLists.txt b/libraries/model-networking/CMakeLists.txt index 00aa17ff57..f7175bc533 100644 --- a/libraries/model-networking/CMakeLists.txt +++ b/libraries/model-networking/CMakeLists.txt @@ -1,4 +1,4 @@ set(TARGET_NAME model-networking) setup_hifi_library() -link_hifi_libraries(shared networking model fbx ktx) +link_hifi_libraries(shared networking model fbx ktx image) diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index a5df41e944..623832aaa8 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -489,7 +489,7 @@ QUrl NetworkMaterial::getTextureUrl(const QUrl& baseUrl, const FBXTexture& textu } model::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& baseUrl, const FBXTexture& fbxTexture, - TextureType type, MapChannel channel) { + image::TextureUsage::Type type, MapChannel channel) { const auto url = getTextureUrl(baseUrl, fbxTexture); const auto texture = DependencyManager::get()->getTexture(url, type, fbxTexture.content, fbxTexture.maxNumPixels); _textures[channel] = Texture { fbxTexture.name, texture }; @@ -503,7 +503,7 @@ model::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& baseUrl, c return map; } -model::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& url, TextureType type, MapChannel channel) { +model::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& url, image::TextureUsage::Type type, MapChannel channel) { const auto texture = DependencyManager::get()->getTexture(url, type); _textures[channel].texture = texture; @@ -518,7 +518,7 @@ NetworkMaterial::NetworkMaterial(const FBXMaterial& material, const QUrl& textur { _textures = Textures(MapChannel::NUM_MAP_CHANNELS); if (!material.albedoTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.albedoTexture, NetworkTexture::ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP); + auto map = fetchTextureMap(textureBaseUrl, material.albedoTexture, image::TextureUsage::ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP); _albedoTransform = material.albedoTexture.transform; map->setTextureTransform(_albedoTransform); @@ -535,45 +535,45 @@ NetworkMaterial::NetworkMaterial(const FBXMaterial& material, const QUrl& textur if (!material.normalTexture.filename.isEmpty()) { - auto type = (material.normalTexture.isBumpmap ? NetworkTexture::BUMP_TEXTURE : NetworkTexture::NORMAL_TEXTURE); + auto type = (material.normalTexture.isBumpmap ? image::TextureUsage::BUMP_TEXTURE : image::TextureUsage::NORMAL_TEXTURE); auto map = fetchTextureMap(textureBaseUrl, material.normalTexture, type, MapChannel::NORMAL_MAP); setTextureMap(MapChannel::NORMAL_MAP, map); } if (!material.roughnessTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.roughnessTexture, NetworkTexture::ROUGHNESS_TEXTURE, MapChannel::ROUGHNESS_MAP); + auto map = fetchTextureMap(textureBaseUrl, material.roughnessTexture, image::TextureUsage::ROUGHNESS_TEXTURE, MapChannel::ROUGHNESS_MAP); setTextureMap(MapChannel::ROUGHNESS_MAP, map); } else if (!material.glossTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.glossTexture, NetworkTexture::GLOSS_TEXTURE, MapChannel::ROUGHNESS_MAP); + auto map = fetchTextureMap(textureBaseUrl, material.glossTexture, image::TextureUsage::GLOSS_TEXTURE, MapChannel::ROUGHNESS_MAP); setTextureMap(MapChannel::ROUGHNESS_MAP, map); } if (!material.metallicTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.metallicTexture, NetworkTexture::METALLIC_TEXTURE, MapChannel::METALLIC_MAP); + auto map = fetchTextureMap(textureBaseUrl, material.metallicTexture, image::TextureUsage::METALLIC_TEXTURE, MapChannel::METALLIC_MAP); setTextureMap(MapChannel::METALLIC_MAP, map); } else if (!material.specularTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.specularTexture, NetworkTexture::SPECULAR_TEXTURE, MapChannel::METALLIC_MAP); + auto map = fetchTextureMap(textureBaseUrl, material.specularTexture, image::TextureUsage::SPECULAR_TEXTURE, MapChannel::METALLIC_MAP); setTextureMap(MapChannel::METALLIC_MAP, map); } if (!material.occlusionTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.occlusionTexture, NetworkTexture::OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP); + auto map = fetchTextureMap(textureBaseUrl, material.occlusionTexture, image::TextureUsage::OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP); map->setTextureTransform(material.occlusionTexture.transform); setTextureMap(MapChannel::OCCLUSION_MAP, map); } if (!material.emissiveTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.emissiveTexture, NetworkTexture::EMISSIVE_TEXTURE, MapChannel::EMISSIVE_MAP); + auto map = fetchTextureMap(textureBaseUrl, material.emissiveTexture, image::TextureUsage::EMISSIVE_TEXTURE, MapChannel::EMISSIVE_MAP); setTextureMap(MapChannel::EMISSIVE_MAP, map); } if (!material.scatteringTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.scatteringTexture, NetworkTexture::SCATTERING_TEXTURE, MapChannel::SCATTERING_MAP); + auto map = fetchTextureMap(textureBaseUrl, material.scatteringTexture, image::TextureUsage::SCATTERING_TEXTURE, MapChannel::SCATTERING_MAP); setTextureMap(MapChannel::SCATTERING_MAP, map); } if (!material.lightmapTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.lightmapTexture, NetworkTexture::LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP); + auto map = fetchTextureMap(textureBaseUrl, material.lightmapTexture, image::TextureUsage::LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP); _lightmapTransform = material.lightmapTexture.transform; _lightmapParams = material.lightmapParams; map->setTextureTransform(_lightmapTransform); @@ -596,7 +596,7 @@ void NetworkMaterial::setTextures(const QVariantMap& textureMap) { if (!albedoName.isEmpty()) { auto url = textureMap.contains(albedoName) ? textureMap[albedoName].toUrl() : QUrl(); - auto map = fetchTextureMap(url, NetworkTexture::ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP); + auto map = fetchTextureMap(url, image::TextureUsage::ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP); map->setTextureTransform(_albedoTransform); // when reassigning the albedo texture we also check for the alpha channel used as opacity map->setUseAlphaChannel(true); @@ -605,45 +605,45 @@ void NetworkMaterial::setTextures(const QVariantMap& textureMap) { if (!normalName.isEmpty()) { auto url = textureMap.contains(normalName) ? textureMap[normalName].toUrl() : QUrl(); - auto map = fetchTextureMap(url, NetworkTexture::NORMAL_TEXTURE, MapChannel::NORMAL_MAP); + auto map = fetchTextureMap(url, image::TextureUsage::NORMAL_TEXTURE, MapChannel::NORMAL_MAP); setTextureMap(MapChannel::NORMAL_MAP, map); } if (!roughnessName.isEmpty()) { auto url = textureMap.contains(roughnessName) ? textureMap[roughnessName].toUrl() : QUrl(); // FIXME: If passing a gloss map instead of a roughmap how do we know? - auto map = fetchTextureMap(url, NetworkTexture::ROUGHNESS_TEXTURE, MapChannel::ROUGHNESS_MAP); + auto map = fetchTextureMap(url, image::TextureUsage::ROUGHNESS_TEXTURE, MapChannel::ROUGHNESS_MAP); setTextureMap(MapChannel::ROUGHNESS_MAP, map); } if (!metallicName.isEmpty()) { auto url = textureMap.contains(metallicName) ? textureMap[metallicName].toUrl() : QUrl(); // FIXME: If passing a specular map instead of a metallic how do we know? - auto map = fetchTextureMap(url, NetworkTexture::METALLIC_TEXTURE, MapChannel::METALLIC_MAP); + auto map = fetchTextureMap(url, image::TextureUsage::METALLIC_TEXTURE, MapChannel::METALLIC_MAP); setTextureMap(MapChannel::METALLIC_MAP, map); } if (!occlusionName.isEmpty()) { auto url = textureMap.contains(occlusionName) ? textureMap[occlusionName].toUrl() : QUrl(); - auto map = fetchTextureMap(url, NetworkTexture::OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP); + auto map = fetchTextureMap(url, image::TextureUsage::OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP); setTextureMap(MapChannel::OCCLUSION_MAP, map); } if (!emissiveName.isEmpty()) { auto url = textureMap.contains(emissiveName) ? textureMap[emissiveName].toUrl() : QUrl(); - auto map = fetchTextureMap(url, NetworkTexture::EMISSIVE_TEXTURE, MapChannel::EMISSIVE_MAP); + auto map = fetchTextureMap(url, image::TextureUsage::EMISSIVE_TEXTURE, MapChannel::EMISSIVE_MAP); setTextureMap(MapChannel::EMISSIVE_MAP, map); } if (!scatteringName.isEmpty()) { auto url = textureMap.contains(scatteringName) ? textureMap[scatteringName].toUrl() : QUrl(); - auto map = fetchTextureMap(url, NetworkTexture::SCATTERING_TEXTURE, MapChannel::SCATTERING_MAP); + auto map = fetchTextureMap(url, image::TextureUsage::SCATTERING_TEXTURE, MapChannel::SCATTERING_MAP); setTextureMap(MapChannel::SCATTERING_MAP, map); } if (!lightmapName.isEmpty()) { auto url = textureMap.contains(lightmapName) ? textureMap[lightmapName].toUrl() : QUrl(); - auto map = fetchTextureMap(url, NetworkTexture::LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP); + auto map = fetchTextureMap(url, image::TextureUsage::LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP); map->setTextureTransform(_lightmapTransform); map->setLightmapOffsetScale(_lightmapParams.x, _lightmapParams.y); setTextureMap(MapChannel::LIGHTMAP_MAP, map); diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h index 967897477d..6a1cc4c466 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.h +++ b/libraries/model-networking/src/model-networking/ModelCache.h @@ -112,6 +112,8 @@ public: void setResource(GeometryResource::Pointer resource); QUrl getURL() const { return (bool)_resource ? _resource->getURL() : QUrl(); } + int getResourceDownloadAttempts() { return _resource ? _resource->getDownloadAttempts() : 0; } + int getResourceDownloadAttemptsRemaining() { return _resource ? _resource->getDownloadAttemptsRemaining() : 0; } private: void startWatching(); @@ -180,13 +182,11 @@ protected: const bool& isOriginal() const { return _isOriginal; } private: - using TextureType = NetworkTexture::Type; - // Helpers for the ctors QUrl getTextureUrl(const QUrl& baseUrl, const FBXTexture& fbxTexture); model::TextureMapPointer fetchTextureMap(const QUrl& baseUrl, const FBXTexture& fbxTexture, - TextureType type, MapChannel channel); - model::TextureMapPointer fetchTextureMap(const QUrl& url, TextureType type, MapChannel channel); + image::TextureUsage::Type type, MapChannel channel); + model::TextureMapPointer fetchTextureMap(const QUrl& url, image::TextureUsage::Type type, MapChannel channel); Transform _albedoTransform; Transform _lightmapTransform; diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp index 98b03eba1e..be3bfcc0e9 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.cpp +++ b/libraries/model-networking/src/model-networking/TextureCache.cpp @@ -13,11 +13,12 @@ #include -#include -#include +#include +#include #include #include -#include +#include +#include #if DEBUG_DUMP_TEXTURE_LOADS #include @@ -29,13 +30,15 @@ #include -#include +#include #include #include #include +#include +#include "NetworkLogging.h" #include "ModelNetworkingLogging.h" #include #include @@ -47,20 +50,13 @@ Q_LOGGING_CATEGORY(trace_resource_parse_image_ktx, "trace.resource.parse.image.k const std::string TextureCache::KTX_DIRNAME { "ktx_cache" }; const std::string TextureCache::KTX_EXT { "ktx" }; +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 + TextureCache::TextureCache() : _ktxCache(KTX_DIRNAME, KTX_EXT) { setUnusedResourceCacheSize(0); setObjectName("TextureCache"); - - // Expose enum Type to JS/QML via properties - // Despite being one-off, this should be fine, because TextureCache is a SINGLETON_DEPENDENCY - QObject* type = new QObject(this); - type->setObjectName("TextureType"); - setProperty("Type", QVariant::fromValue(type)); - auto metaEnum = QMetaEnum::fromType(); - for (int i = 0; i < metaEnum.keyCount(); ++i) { - type->setProperty(metaEnum.key(i), metaEnum.value(i)); - } } TextureCache::~TextureCache() { @@ -117,7 +113,7 @@ const gpu::TexturePointer& TextureCache::getPermutationNormalTexture() { data[i + 2] = ((randvec.z + 1.0f) / 2.0f) * 255.0f; } - _permutationNormalTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB), 256, 2)); + _permutationNormalTexture = gpu::Texture::create2D(gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB), 256, 2); _permutationNormalTexture->setStoredMipFormat(_permutationNormalTexture->getTexelFormat()); _permutationNormalTexture->assignStoredMip(0, sizeof(data), data); } @@ -131,7 +127,7 @@ const unsigned char OPAQUE_BLACK[] = { 0x00, 0x00, 0x00, 0xFF }; const gpu::TexturePointer& TextureCache::getWhiteTexture() { if (!_whiteTexture) { - _whiteTexture = gpu::TexturePointer(gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1)); + _whiteTexture = gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1); _whiteTexture->setSource("TextureCache::_whiteTexture"); _whiteTexture->setStoredMipFormat(_whiteTexture->getTexelFormat()); _whiteTexture->assignStoredMip(0, sizeof(OPAQUE_WHITE), OPAQUE_WHITE); @@ -141,7 +137,7 @@ const gpu::TexturePointer& TextureCache::getWhiteTexture() { const gpu::TexturePointer& TextureCache::getGrayTexture() { if (!_grayTexture) { - _grayTexture = gpu::TexturePointer(gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1)); + _grayTexture = gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1); _grayTexture->setSource("TextureCache::_grayTexture"); _grayTexture->setStoredMipFormat(_grayTexture->getTexelFormat()); _grayTexture->assignStoredMip(0, sizeof(OPAQUE_GRAY), OPAQUE_GRAY); @@ -151,7 +147,7 @@ const gpu::TexturePointer& TextureCache::getGrayTexture() { const gpu::TexturePointer& TextureCache::getBlueTexture() { if (!_blueTexture) { - _blueTexture = gpu::TexturePointer(gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1)); + _blueTexture = gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1); _blueTexture->setSource("TextureCache::_blueTexture"); _blueTexture->setStoredMipFormat(_blueTexture->getTexelFormat()); _blueTexture->assignStoredMip(0, sizeof(OPAQUE_BLUE), OPAQUE_BLUE); @@ -161,7 +157,7 @@ const gpu::TexturePointer& TextureCache::getBlueTexture() { const gpu::TexturePointer& TextureCache::getBlackTexture() { if (!_blackTexture) { - _blackTexture = gpu::TexturePointer(gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1)); + _blackTexture = gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1); _blackTexture->setSource("TextureCache::_blackTexture"); _blackTexture->setStoredMipFormat(_blackTexture->getTexelFormat()); _blackTexture->assignStoredMip(0, sizeof(OPAQUE_BLACK), OPAQUE_BLACK); @@ -172,18 +168,18 @@ const gpu::TexturePointer& TextureCache::getBlackTexture() { /// Extra data for creating textures. class TextureExtra { public: - NetworkTexture::Type type; + image::TextureUsage::Type type; const QByteArray& content; int maxNumPixels; }; ScriptableResource* TextureCache::prefetch(const QUrl& url, int type, int maxNumPixels) { auto byteArray = QByteArray(); - TextureExtra extra = { (Type)type, byteArray, maxNumPixels }; + TextureExtra extra = { (image::TextureUsage::Type)type, byteArray, maxNumPixels }; return ResourceCache::prefetch(url, &extra); } -NetworkTexturePointer TextureCache::getTexture(const QUrl& url, Type type, const QByteArray& content, int maxNumPixels) { +NetworkTexturePointer TextureCache::getTexture(const QUrl& url, image::TextureUsage::Type type, const QByteArray& content, int maxNumPixels) { TextureExtra extra = { type, content, maxNumPixels }; return ResourceCache::getResource(url, QUrl(), &extra).staticCast(); } @@ -216,8 +212,7 @@ gpu::TexturePointer TextureCache::cacheTextureByHash(const std::string& hash, co return result; } - -gpu::TexturePointer getFallbackTextureForType(NetworkTexture::Type type) { +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 @@ -226,121 +221,64 @@ gpu::TexturePointer getFallbackTextureForType(NetworkTexture::Type type) { return result; } switch (type) { - case NetworkTexture::DEFAULT_TEXTURE: - case NetworkTexture::ALBEDO_TEXTURE: - case NetworkTexture::ROUGHNESS_TEXTURE: - case NetworkTexture::OCCLUSION_TEXTURE: + case image::TextureUsage::DEFAULT_TEXTURE: + case image::TextureUsage::ALBEDO_TEXTURE: + case image::TextureUsage::ROUGHNESS_TEXTURE: + case image::TextureUsage::OCCLUSION_TEXTURE: result = textureCache->getWhiteTexture(); break; - case NetworkTexture::NORMAL_TEXTURE: + case image::TextureUsage::NORMAL_TEXTURE: result = textureCache->getBlueTexture(); break; - case NetworkTexture::EMISSIVE_TEXTURE: - case NetworkTexture::LIGHTMAP_TEXTURE: + case image::TextureUsage::EMISSIVE_TEXTURE: + case image::TextureUsage::LIGHTMAP_TEXTURE: result = textureCache->getBlackTexture(); break; - case NetworkTexture::BUMP_TEXTURE: - case NetworkTexture::SPECULAR_TEXTURE: - case NetworkTexture::GLOSS_TEXTURE: - case NetworkTexture::CUBE_TEXTURE: - case NetworkTexture::CUSTOM_TEXTURE: - case NetworkTexture::STRICT_TEXTURE: + case image::TextureUsage::BUMP_TEXTURE: + case image::TextureUsage::SPECULAR_TEXTURE: + case image::TextureUsage::GLOSS_TEXTURE: + case image::TextureUsage::CUBE_TEXTURE: + case image::TextureUsage::STRICT_TEXTURE: default: break; } return result; } - -NetworkTexture::TextureLoaderFunc getTextureLoaderForType(NetworkTexture::Type type, - const QVariantMap& options = QVariantMap()) { - using Type = NetworkTexture; - - switch (type) { - case Type::ALBEDO_TEXTURE: { - return model::TextureUsage::createAlbedoTextureFromImage; - break; - } - case Type::EMISSIVE_TEXTURE: { - return model::TextureUsage::createEmissiveTextureFromImage; - break; - } - case Type::LIGHTMAP_TEXTURE: { - return model::TextureUsage::createLightmapTextureFromImage; - break; - } - case Type::CUBE_TEXTURE: { - if (options.value("generateIrradiance", true).toBool()) { - return model::TextureUsage::createCubeTextureFromImage; - } else { - return model::TextureUsage::createCubeTextureFromImageWithoutIrradiance; - } - break; - } - case Type::BUMP_TEXTURE: { - return model::TextureUsage::createNormalTextureFromBumpImage; - break; - } - case Type::NORMAL_TEXTURE: { - return model::TextureUsage::createNormalTextureFromNormalImage; - break; - } - case Type::ROUGHNESS_TEXTURE: { - return model::TextureUsage::createRoughnessTextureFromImage; - break; - } - case Type::GLOSS_TEXTURE: { - return model::TextureUsage::createRoughnessTextureFromGlossImage; - break; - } - case Type::SPECULAR_TEXTURE: { - return model::TextureUsage::createMetallicTextureFromImage; - break; - } - case Type::STRICT_TEXTURE: { - return model::TextureUsage::createStrict2DTextureFromImage; - break; - } - case Type::CUSTOM_TEXTURE: { - Q_ASSERT(false); - return NetworkTexture::TextureLoaderFunc(); - break; - } - - case Type::DEFAULT_TEXTURE: - default: { - return model::TextureUsage::create2DTextureFromImage; - break; - } - } -} - /// Returns a texture version of an image file -gpu::TexturePointer TextureCache::getImageTexture(const QString& path, Type type, QVariantMap options) { +gpu::TexturePointer TextureCache::getImageTexture(const QString& path, image::TextureUsage::Type type, QVariantMap options) { QImage image = QImage(path); - auto loader = getTextureLoaderForType(type, options); + auto loader = image::TextureUsage::getTextureLoaderForType(type, options); return gpu::TexturePointer(loader(image, QUrl::fromLocalFile(path).fileName().toStdString())); } QSharedPointer TextureCache::createResource(const QUrl& url, const QSharedPointer& fallback, const void* extra) { const TextureExtra* textureExtra = static_cast(extra); - auto type = textureExtra ? textureExtra->type : Type::DEFAULT_TEXTURE; + auto type = textureExtra ? textureExtra->type : image::TextureUsage::DEFAULT_TEXTURE; auto content = textureExtra ? textureExtra->content : QByteArray(); auto maxNumPixels = textureExtra ? textureExtra->maxNumPixels : ABSOLUTE_MAX_TEXTURE_NUM_PIXELS; NetworkTexture* texture = new NetworkTexture(url, type, content, maxNumPixels); return QSharedPointer(texture, &Resource::deleter); } -NetworkTexture::NetworkTexture(const QUrl& url, Type type, const QByteArray& content, int maxNumPixels) : +NetworkTexture::NetworkTexture(const QUrl& url, image::TextureUsage::Type type, const QByteArray& content, int maxNumPixels) : Resource(url), _type(type), + _sourceIsKTX(url.path().endsWith(".ktx")), _maxNumPixels(maxNumPixels) { _textureSource = std::make_shared(); + _lowestRequestedMipLevel = 0; + + if (type == image::TextureUsage::CUBE_TEXTURE) { + setLoadPriority(this, SKYBOX_LOAD_PRIORITY); + } else if (_sourceIsKTX) { + setLoadPriority(this, HIGH_MIPS_LOAD_PRIORITY); + } if (!url.isValid()) { _loaded = true; @@ -353,13 +291,6 @@ NetworkTexture::NetworkTexture(const QUrl& url, Type type, const QByteArray& con } } -NetworkTexture::TextureLoaderFunc NetworkTexture::getTextureLoader() const { - if (_type == CUSTOM_TEXTURE) { - return _textureLoader; - } - return getTextureLoaderForType(_type); -} - void NetworkTexture::setImage(gpu::TexturePointer texture, int originalWidth, int originalHeight) { _originalWidth = originalWidth; @@ -384,107 +315,363 @@ void NetworkTexture::setImage(gpu::TexturePointer texture, int originalWidth, } gpu::TexturePointer NetworkTexture::getFallbackTexture() const { - if (_type == CUSTOM_TEXTURE) { - return gpu::TexturePointer(); - } return getFallbackTextureForType(_type); } -class Reader : public QRunnable { -public: - Reader(const QWeakPointer& resource, const QUrl& url); - void run() override final; - virtual void read() = 0; - -protected: - QWeakPointer _resource; - QUrl _url; -}; - -class ImageReader : public Reader { +class ImageReader : public QRunnable { public: ImageReader(const QWeakPointer& resource, const QUrl& url, - const QByteArray& data, const std::string& hash, int maxNumPixels); - void read() override final; + const QByteArray& data, int maxNumPixels); + void run() override final; + void read(); private: static void listSupportedImageFormats(); + QWeakPointer _resource; + QUrl _url; QByteArray _content; - std::string _hash; int _maxNumPixels; }; +const uint16_t NetworkTexture::NULL_MIP_LEVEL = std::numeric_limits::max(); +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() } }); + + if (_ktxResourceState == PENDING_INITIAL_LOAD) { + _ktxResourceState = LOADING_INITIAL_DATA; + + // Add a fragment to the base url so we can identify the section of the ktx being requested when debugging + // The actual requested url is _activeUrl and will not contain the fragment + _url.setFragment("head"); + _ktxHeaderRequest = ResourceManager::createResourceRequest(this, _activeUrl); + + if (!_ktxHeaderRequest) { + qCDebug(networking).noquote() << "Failed to get request for" << _url.toDisplayString(); + + PROFILE_ASYNC_END(resource, "Resource:" + getType(), QString::number(_requestID)); + return; + } + + ByteRange range; + range.fromInclusive = 0; + range.toExclusive = 1000; + _ktxHeaderRequest->setByteRange(range); + + emit loading(); + + connect(_ktxHeaderRequest, &ResourceRequest::progress, this, &NetworkTexture::ktxHeaderRequestProgress); + connect(_ktxHeaderRequest, &ResourceRequest::finished, this, &NetworkTexture::ktxHeaderRequestFinished); + + _bytesReceived = _bytesTotal = _bytes = 0; + + _ktxHeaderRequest->send(); + + startMipRangeRequest(NULL_MIP_LEVEL, NULL_MIP_LEVEL); + } else if (_ktxResourceState == PENDING_MIP_REQUEST) { + if (_lowestKnownPopulatedMip > 0) { + _ktxResourceState = REQUESTING_MIP; + + // Add a fragment to the base url so we can identify the section of the ktx being requested when debugging + // The actual requested url is _activeUrl and will not contain the fragment + uint16_t nextMip = _lowestKnownPopulatedMip - 1; + _url.setFragment(QString::number(nextMip)); + startMipRangeRequest(nextMip, nextMip); + } + } else { + qWarning(networking) << "NetworkTexture::makeRequest() called while not in a valid state: " << _ktxResourceState; + } + +} + +void NetworkTexture::startRequestForNextMipLevel() { + if (_lowestKnownPopulatedMip == 0) { + qWarning(networking) << "Requesting next mip level but all have been fulfilled: " << _lowestKnownPopulatedMip + << " " << _textureSource->getGPUTexture()->minAvailableMipLevel() << " " << _url; + return; + } + + if (_ktxResourceState == WAITING_FOR_MIP_REQUEST) { + _ktxResourceState = PENDING_MIP_REQUEST; + + init(); + float priority = -(float)_originalKtxDescriptor->header.numberOfMipmapLevels + (float)_lowestKnownPopulatedMip; + setLoadPriority(this, priority); + _url.setFragment(QString::number(_lowestKnownPopulatedMip - 1)); + TextureCache::attemptRequest(_self); + } +} + +// Load mips in the range [low, high] (inclusive) +void NetworkTexture::startMipRangeRequest(uint16_t low, uint16_t high) { + if (_ktxMipRequest) { + return; + } + + bool isHighMipRequest = low == NULL_MIP_LEVEL && high == NULL_MIP_LEVEL; + + _ktxMipRequest = ResourceManager::createResourceRequest(this, _activeUrl); + + if (!_ktxMipRequest) { + qCWarning(networking).noquote() << "Failed to get request for" << _url.toDisplayString(); + + PROFILE_ASYNC_END(resource, "Resource:" + getType(), QString::number(_requestID)); + return; + } + + _ktxMipLevelRangeInFlight = { low, high }; + if (isHighMipRequest) { + static const int HIGH_MIP_MAX_SIZE = 5516; + // This is a special case where we load the high 7 mips + ByteRange range; + range.fromInclusive = -HIGH_MIP_MAX_SIZE; + _ktxMipRequest->setByteRange(range); + } else { + ByteRange range; + range.fromInclusive = ktx::KTX_HEADER_SIZE + _originalKtxDescriptor->header.bytesOfKeyValueData + + _originalKtxDescriptor->images[low]._imageOffset + ktx::IMAGE_SIZE_WIDTH; + range.toExclusive = ktx::KTX_HEADER_SIZE + _originalKtxDescriptor->header.bytesOfKeyValueData + + _originalKtxDescriptor->images[high + 1]._imageOffset; + _ktxMipRequest->setByteRange(range); + } + + connect(_ktxMipRequest, &ResourceRequest::progress, this, &NetworkTexture::ktxMipRequestProgress); + connect(_ktxMipRequest, &ResourceRequest::finished, this, &NetworkTexture::ktxMipRequestFinished); + + _ktxMipRequest->send(); +} + + +void NetworkTexture::ktxHeaderRequestFinished() { + Q_ASSERT(_ktxResourceState == LOADING_INITIAL_DATA); + + _ktxHeaderRequestFinished = true; + maybeHandleFinishedInitialLoad(); +} + +void NetworkTexture::ktxMipRequestFinished() { + Q_ASSERT(_ktxResourceState == LOADING_INITIAL_DATA || _ktxResourceState == REQUESTING_MIP); + + if (_ktxResourceState == LOADING_INITIAL_DATA) { + _ktxHighMipRequestFinished = true; + maybeHandleFinishedInitialLoad(); + } else if (_ktxResourceState == REQUESTING_MIP) { + Q_ASSERT(_ktxMipLevelRangeInFlight.first != NULL_MIP_LEVEL); + TextureCache::requestCompleted(_self); + + if (_ktxMipRequest->getResult() == ResourceRequest::Success) { + Q_ASSERT(_ktxMipLevelRangeInFlight.second - _ktxMipLevelRangeInFlight.first == 0); + + auto texture = _textureSource->getGPUTexture(); + if (texture) { + texture->assignStoredMip(_ktxMipLevelRangeInFlight.first, + _ktxMipRequest->getData().size(), reinterpret_cast(_ktxMipRequest->getData().data())); + _lowestKnownPopulatedMip = _textureSource->getGPUTexture()->minAvailableMipLevel(); + } + else { + qWarning(networking) << "Trying to update mips but texture is null"; + } + finishedLoading(true); + _ktxResourceState = WAITING_FOR_MIP_REQUEST; + } + else { + finishedLoading(false); + if (handleFailedRequest(_ktxMipRequest->getResult())) { + _ktxResourceState = PENDING_MIP_REQUEST; + } + else { + qWarning(networking) << "Failed to load mip: " << _url; + _ktxResourceState = FAILED_TO_LOAD; + } + } + + _ktxMipRequest->deleteLater(); + _ktxMipRequest = nullptr; + + if (_ktxResourceState == WAITING_FOR_MIP_REQUEST && _lowestRequestedMipLevel < _lowestKnownPopulatedMip) { + startRequestForNextMipLevel(); + } + } + else { + qWarning() << "Mip request finished in an unexpected state: " << _ktxResourceState; + } +} + +// This is called when the header or top mips have been loaded +void NetworkTexture::maybeHandleFinishedInitialLoad() { + Q_ASSERT(_ktxResourceState == LOADING_INITIAL_DATA); + + if (_ktxHeaderRequestFinished && _ktxHighMipRequestFinished) { + + TextureCache::requestCompleted(_self); + + if (_ktxHeaderRequest->getResult() != ResourceRequest::Success || _ktxMipRequest->getResult() != ResourceRequest::Success) { + if (handleFailedRequest(_ktxMipRequest->getResult())) { + _ktxResourceState = PENDING_INITIAL_LOAD; + } + else { + _ktxResourceState = FAILED_TO_LOAD; + } + + _ktxHeaderRequest->deleteLater(); + _ktxHeaderRequest = nullptr; + _ktxMipRequest->deleteLater(); + _ktxMipRequest = nullptr; + } else { + // create ktx... + auto ktxHeaderData = _ktxHeaderRequest->getData(); + auto ktxHighMipData = _ktxMipRequest->getData(); + + auto header = reinterpret_cast(ktxHeaderData.data()); + + if (!ktx::checkIdentifier(header->identifier)) { + qWarning() << "Cannot load " << _url << ", invalid header identifier"; + _ktxResourceState = FAILED_TO_LOAD; + finishedLoading(false); + return; + } + + auto kvSize = header->bytesOfKeyValueData; + if (kvSize > (ktxHeaderData.size() - ktx::KTX_HEADER_SIZE)) { + qWarning() << "Cannot load " << _url << ", did not receive all kv data with initial request"; + _ktxResourceState = FAILED_TO_LOAD; + finishedLoading(false); + return; + } + + auto keyValues = ktx::KTX::parseKeyValues(header->bytesOfKeyValueData, reinterpret_cast(ktxHeaderData.data()) + ktx::KTX_HEADER_SIZE); + + auto imageDescriptors = header->generateImageDescriptors(); + if (imageDescriptors.size() == 0) { + qWarning(networking) << "Failed to process ktx file " << _url; + _ktxResourceState = FAILED_TO_LOAD; + finishedLoading(false); + } + _originalKtxDescriptor.reset(new ktx::KTXDescriptor(*header, keyValues, imageDescriptors)); + + // Create bare ktx in memory + auto found = std::find_if(keyValues.begin(), keyValues.end(), [](const ktx::KeyValue& val) -> bool { + return val._key.compare(gpu::SOURCE_HASH_KEY) == 0; + }); + std::string filename; + std::string hash; + if (found == keyValues.end() || found->_value.size() != gpu::SOURCE_HASH_BYTES) { + qWarning("Invalid source hash key found, bailing"); + _ktxResourceState = FAILED_TO_LOAD; + finishedLoading(false); + return; + } else { + // at this point the source hash is in binary 16-byte form + // and we need it in a hexadecimal string + auto binaryHash = QByteArray(reinterpret_cast(found->_value.data()), gpu::SOURCE_HASH_BYTES); + hash = filename = binaryHash.toHex().toStdString(); + } + + auto textureCache = DependencyManager::get(); + + gpu::TexturePointer texture = textureCache->getTextureByHash(hash); + + if (!texture) { + KTXFilePointer ktxFile = textureCache->_ktxCache.getFile(hash); + if (ktxFile) { + texture = gpu::Texture::unserialize(ktxFile->getFilepath()); + if (texture) { + texture = textureCache->cacheTextureByHash(hash, texture); + } + } + } + + if (!texture) { + + auto memKtx = ktx::KTX::createBare(*header, keyValues); + if (!memKtx) { + qWarning() << " Ktx could not be created, bailing"; + finishedLoading(false); + return; + } + + // Move ktx to file + const char* data = reinterpret_cast(memKtx->_storage->data()); + size_t length = memKtx->_storage->size(); + KTXFilePointer file; + auto& ktxCache = textureCache->_ktxCache; + if (!memKtx || !(file = ktxCache.writeFile(data, KTXCache::Metadata(filename, length)))) { + qCWarning(modelnetworking) << _url << " failed to write cache file"; + _ktxResourceState = FAILED_TO_LOAD; + finishedLoading(false); + return; + } else { + _file = file; + } + + auto newKtxDescriptor = memKtx->toDescriptor(); + + texture = gpu::Texture::unserialize(_file->getFilepath(), newKtxDescriptor); + texture->setKtxBacking(file->getFilepath()); + texture->setSource(filename); + + auto& images = _originalKtxDescriptor->images; + size_t imageSizeRemaining = ktxHighMipData.size(); + uint8_t* ktxData = reinterpret_cast(ktxHighMipData.data()); + ktxData += ktxHighMipData.size(); + // TODO Move image offset calculation to ktx ImageDescriptor + for (int level = static_cast(images.size()) - 1; level >= 0; --level) { + auto& image = images[level]; + if (image._imageSize > imageSizeRemaining) { + break; + } + ktxData -= image._imageSize; + texture->assignStoredMip(static_cast(level), image._imageSize, ktxData); + ktxData -= ktx::IMAGE_SIZE_WIDTH; + imageSizeRemaining -= (image._imageSize + ktx::IMAGE_SIZE_WIDTH); + } + + // 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(filename, texture); + } + + _lowestKnownPopulatedMip = texture->minAvailableMipLevel(); + + _ktxResourceState = WAITING_FOR_MIP_REQUEST; + setImage(texture, header->getPixelWidth(), header->getPixelHeight()); + + _ktxHeaderRequest->deleteLater(); + _ktxHeaderRequest = nullptr; + _ktxMipRequest->deleteLater(); + _ktxMipRequest = nullptr; + } + startRequestForNextMipLevel(); + } +} + void NetworkTexture::downloadFinished(const QByteArray& data) { loadContent(data); } void NetworkTexture::loadContent(const QByteArray& content) { - // Hash the source image to for KTX caching - std::string hash; - { - QCryptographicHash hasher(QCryptographicHash::Md5); - hasher.addData(content); - hash = hasher.result().toHex().toStdString(); - } - - auto textureCache = static_cast(_cache.data()); - - if (textureCache != nullptr) { - // If we already have a live texture with the same hash, use it - auto texture = textureCache->getTextureByHash(hash); - - // If there is no live texture, check if there's an existing KTX file - if (!texture) { - KTXFilePointer ktxFile = textureCache->_ktxCache.getFile(hash); - if (ktxFile) { - texture.reset(gpu::Texture::unserialize(ktxFile->getFilepath())); - if (texture) { - texture = textureCache->cacheTextureByHash(hash, texture); - } - } - } - - // If we found the texture either because it's in use or via KTX deserialization, - // set the image and return immediately. - if (texture) { - setImage(texture, texture->getWidth(), texture->getHeight()); - return; - } - } - - // We failed to find an existing live or KTX texture, so trigger an image reader - QThreadPool::globalInstance()->start(new ImageReader(_self, _url, content, hash, _maxNumPixels)); -} - -Reader::Reader(const QWeakPointer& resource, const QUrl& url) : - _resource(resource), _url(url) { - DependencyManager::get()->incrementStat("PendingProcessing"); -} - -void Reader::run() { - PROFILE_RANGE_EX(resource_parse_image, __FUNCTION__, 0xffff0000, 0, { { "url", _url.toString() } }); - DependencyManager::get()->decrementStat("PendingProcessing"); - CounterStat counter("Processing"); - - auto originalPriority = QThread::currentThread()->priority(); - if (originalPriority == QThread::InheritPriority) { - originalPriority = QThread::NormalPriority; - } - QThread::currentThread()->setPriority(QThread::LowPriority); - Finally restorePriority([originalPriority]{ QThread::currentThread()->setPriority(originalPriority); }); - - if (!_resource.data()) { - qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; + if (_sourceIsKTX) { + assert(false); return; } - read(); + QThreadPool::globalInstance()->start(new ImageReader(_self, _url, content, _maxNumPixels)); } -ImageReader::ImageReader(const QWeakPointer& resource, const QUrl& url, - const QByteArray& data, const std::string& hash, int maxNumPixels) : - Reader(resource, url), _content(data), _hash(hash), _maxNumPixels(maxNumPixels) { +ImageReader::ImageReader(const QWeakPointer& resource, const QUrl& url, const QByteArray& data, int maxNumPixels) : + _resource(resource), + _url(url), + _content(data), + _maxNumPixels(maxNumPixels) +{ + DependencyManager::get()->incrementStat("PendingProcessing"); listSupportedImageFormats(); #if DEBUG_DUMP_TEXTURE_LOADS @@ -515,89 +702,111 @@ void ImageReader::listSupportedImageFormats() { }); } -void ImageReader::read() { - // Help the QImage loader by extracting the image file format from the url filename ext. - // Some tga are not created properly without it. - auto filename = _url.fileName().toStdString(); - auto filenameExtension = filename.substr(filename.find_last_of('.') + 1); - QImage image = QImage::fromData(_content, filenameExtension.c_str()); - int imageWidth = image.width(); - int imageHeight = image.height(); +void ImageReader::run() { + PROFILE_RANGE_EX(resource_parse_image, __FUNCTION__, 0xffff0000, 0, { { "url", _url.toString() } }); + DependencyManager::get()->decrementStat("PendingProcessing"); + CounterStat counter("Processing"); - // Validate that the image loaded - if (imageWidth == 0 || imageHeight == 0 || image.format() == QImage::Format_Invalid) { - QString reason(filenameExtension.empty() ? "" : "(no file extension)"); - qCWarning(modelnetworking) << "Failed to load" << _url << reason; + auto originalPriority = QThread::currentThread()->priority(); + if (originalPriority == QThread::InheritPriority) { + originalPriority = QThread::NormalPriority; + } + QThread::currentThread()->setPriority(QThread::LowPriority); + Finally restorePriority([originalPriority] { QThread::currentThread()->setPriority(originalPriority); }); + + read(); +} + +void ImageReader::read() { + auto resource = _resource.lock(); // to ensure the resource is still needed + if (!resource) { + qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; return; } + auto networkTexture = resource.staticCast(); - // Validate the image is less than _maxNumPixels, and downscale if necessary - if (imageWidth * imageHeight > _maxNumPixels) { - float scaleFactor = sqrtf(_maxNumPixels / (float)(imageWidth * imageHeight)); - int originalWidth = imageWidth; - int originalHeight = imageHeight; - imageWidth = (int)(scaleFactor * (float)imageWidth + 0.5f); - imageHeight = (int)(scaleFactor * (float)imageHeight + 0.5f); - QImage newImage = image.scaled(QSize(imageWidth, imageHeight), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - image.swap(newImage); - qCDebug(modelnetworking).nospace() << "Downscaled " << _url << " (" << - QSize(originalWidth, originalHeight) << " to " << - QSize(imageWidth, imageHeight) << ")"; + // Hash the source image to for KTX caching + std::string hash; + { + QCryptographicHash hasher(QCryptographicHash::Md5); + hasher.addData(_content); + hash = hasher.result().toHex().toStdString(); } - gpu::TexturePointer texture = nullptr; + // Maybe load from cache + auto textureCache = DependencyManager::get(); + if (textureCache) { + // If we already have a live texture with the same hash, use it + auto texture = textureCache->getTextureByHash(hash); + + // If there is no live texture, check if there's an existing KTX file + if (!texture) { + KTXFilePointer ktxFile = textureCache->_ktxCache.getFile(hash); + if (ktxFile) { + texture = gpu::Texture::unserialize(ktxFile->getFilepath()); + if (texture) { + texture = textureCache->cacheTextureByHash(hash, texture); + } + } + } + + // 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", + Q_ARG(gpu::TexturePointer, texture), + Q_ARG(int, texture->getWidth()), + Q_ARG(int, texture->getHeight())); + return; + } + } + + // Proccess new texture + gpu::TexturePointer texture; { - auto resource = _resource.lock(); // to ensure the resource is still needed - if (!resource) { - qCDebug(modelnetworking) << _url << "loading stopped; resource out of scope"; + PROFILE_RANGE_EX(resource_parse_image_raw, __FUNCTION__, 0xffff0000, 0); + texture = image::processImage(_content, _url.toString().toStdString(), _maxNumPixels, networkTexture->getTextureType()); + + if (!texture) { + qCWarning(modelnetworking) << "Could not process:" << _url; + QMetaObject::invokeMethod(resource.data(), "setImage", + Q_ARG(gpu::TexturePointer, texture), + Q_ARG(int, 0), + Q_ARG(int, 0)); return; } - auto url = _url.toString().toStdString(); + texture->setSourceHash(hash); + texture->setFallbackTexture(networkTexture->getFallbackTexture()); + } - PROFILE_RANGE_EX(resource_parse_image_raw, __FUNCTION__, 0xffff0000, 0); - // Load the image into a gpu::Texture - auto networkTexture = resource.staticCast(); - texture.reset(networkTexture->getTextureLoader()(image, url)); - texture->setSource(url); - if (texture) { - texture->setFallbackTexture(networkTexture->getFallbackTexture()); - } - - auto textureCache = DependencyManager::get(); - // Save the image into a KTXFile + // Save the image into a KTXFile + if (texture && textureCache) { auto memKtx = gpu::Texture::serialize(*texture); - if (!memKtx) { - qCWarning(modelnetworking) << "Unable to serialize texture to KTX " << _url; - } - if (memKtx && textureCache) { + // Move the texture into a memory mapped file + if (memKtx) { const char* data = reinterpret_cast(memKtx->_storage->data()); size_t length = memKtx->_storage->size(); - KTXFilePointer file; auto& ktxCache = textureCache->_ktxCache; - if (!memKtx || !(file = ktxCache.writeFile(data, KTXCache::Metadata(_hash, length)))) { + networkTexture->_file = ktxCache.writeFile(data, KTXCache::Metadata(hash, length)); + if (!networkTexture->_file) { qCWarning(modelnetworking) << _url << "file cache failed"; } else { - resource.staticCast()->_file = file; - texture->setKtxBacking(file->getFilepath()); + texture->setKtxBacking(networkTexture->_file->getFilepath()); } + } else { + 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 // 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 - if (textureCache) { - texture = textureCache->cacheTextureByHash(_hash, texture); - } + texture = textureCache->cacheTextureByHash(hash, texture); } - auto resource = _resource.lock(); // to ensure the resource is still needed - if (resource) { - QMetaObject::invokeMethod(resource.data(), "setImage", - Q_ARG(gpu::TexturePointer, texture), - Q_ARG(int, imageWidth), Q_ARG(int, imageHeight)); - } else { - qCDebug(modelnetworking) << _url << "loading stopped; resource out of scope"; - } + QMetaObject::invokeMethod(resource.data(), "setImage", + Q_ARG(gpu::TexturePointer, texture), + Q_ARG(int, texture->getWidth()), + Q_ARG(int, texture->getHeight())); } diff --git a/libraries/model-networking/src/model-networking/TextureCache.h b/libraries/model-networking/src/model-networking/TextureCache.h index 6005cc1226..1e61b9ecee 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.h +++ b/libraries/model-networking/src/model-networking/TextureCache.h @@ -22,6 +22,8 @@ #include #include #include +#include +#include #include "KTXCache.h" @@ -43,29 +45,7 @@ class NetworkTexture : public Resource, public Texture { Q_OBJECT public: - enum Type { - DEFAULT_TEXTURE, - STRICT_TEXTURE, - ALBEDO_TEXTURE, - NORMAL_TEXTURE, - BUMP_TEXTURE, - SPECULAR_TEXTURE, - METALLIC_TEXTURE = SPECULAR_TEXTURE, // for now spec and metallic texture are the same, converted to grey - ROUGHNESS_TEXTURE, - GLOSS_TEXTURE, - EMISSIVE_TEXTURE, - CUBE_TEXTURE, - OCCLUSION_TEXTURE, - SCATTERING_TEXTURE = OCCLUSION_TEXTURE, - LIGHTMAP_TEXTURE, - CUSTOM_TEXTURE - }; - Q_ENUM(Type) - - typedef gpu::Texture* TextureLoader(const QImage& image, const std::string& srcImageName); - using TextureLoaderFunc = std::function; - - NetworkTexture(const QUrl& url, Type type, const QByteArray& content, int maxNumPixels); + NetworkTexture(const QUrl& url, image::TextureUsage::Type type, const QByteArray& content, int maxNumPixels); QString getType() const override { return "NetworkTexture"; } @@ -73,15 +53,23 @@ public: int getOriginalHeight() const { return _originalHeight; } int getWidth() const { return _width; } int getHeight() const { return _height; } - Type getTextureType() const { return _type; } + image::TextureUsage::Type getTextureType() const { return _type; } - TextureLoaderFunc getTextureLoader() const; gpu::TexturePointer getFallbackTexture() const; signals: void networkTextureCreated(const QWeakPointer& self); +public slots: + void ktxHeaderRequestProgress(uint64_t bytesReceived, uint64_t bytesTotal) { } + void ktxHeaderRequestFinished(); + + void ktxMipRequestProgress(uint64_t bytesReceived, uint64_t bytesTotal) { } + void ktxMipRequestFinished(); + protected: + void makeRequest() override; + virtual bool isCacheable() const override { return _loaded; } virtual void downloadFinished(const QByteArray& data) override; @@ -89,13 +77,51 @@ protected: Q_INVOKABLE void loadContent(const QByteArray& content); Q_INVOKABLE void setImage(gpu::TexturePointer texture, int originalWidth, int originalHeight); + void startRequestForNextMipLevel(); + + void startMipRangeRequest(uint16_t low, uint16_t high); + void maybeHandleFinishedInitialLoad(); + private: friend class KTXReader; friend class ImageReader; - Type _type; - TextureLoaderFunc _textureLoader { [](const QImage&, const std::string&){ return nullptr; } }; + image::TextureUsage::Type _type; + + static const uint16_t NULL_MIP_LEVEL; + enum KTXResourceState { + PENDING_INITIAL_LOAD = 0, + LOADING_INITIAL_DATA, // Loading KTX Header + Low Resolution Mips + WAITING_FOR_MIP_REQUEST, // Waiting for the gpu layer to report that it needs higher resolution mips + PENDING_MIP_REQUEST, // We have added ourselves to the ResourceCache queue + REQUESTING_MIP, // We have a mip in flight + FAILED_TO_LOAD + }; + + bool _sourceIsKTX { false }; + KTXResourceState _ktxResourceState { PENDING_INITIAL_LOAD }; + + // TODO Can this be removed? KTXFilePointer _file; + + // The current mips that are currently being requested w/ _ktxMipRequest + std::pair _ktxMipLevelRangeInFlight{ NULL_MIP_LEVEL, NULL_MIP_LEVEL }; + + ResourceRequest* _ktxHeaderRequest { nullptr }; + ResourceRequest* _ktxMipRequest { nullptr }; + bool _ktxHeaderRequestFinished{ false }; + bool _ktxHighMipRequestFinished{ false }; + + uint16_t _lowestRequestedMipLevel { NULL_MIP_LEVEL }; + uint16_t _lowestKnownPopulatedMip { NULL_MIP_LEVEL }; + + // This is a copy of the original KTX descriptor from the source url. + // We need this because the KTX that will be cached will likely include extra data + // in its key/value data, and so will not match up with the original, causing + // mip offsets to change. + ktx::KTXDescriptorPointer _originalKtxDescriptor; + + int _originalWidth { 0 }; int _originalHeight { 0 }; int _width { 0 }; @@ -110,8 +136,6 @@ class TextureCache : public ResourceCache, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY - using Type = NetworkTexture::Type; - public: /// Returns the ID of the permutation/normal texture used for Perlin noise shader programs. This texture /// has two lines: the first, a set of random numbers in [0, 255] to be used as permutation offsets, and @@ -131,10 +155,10 @@ public: const gpu::TexturePointer& getBlackTexture(); /// Returns a texture version of an image file - static gpu::TexturePointer getImageTexture(const QString& path, Type type = Type::DEFAULT_TEXTURE, QVariantMap options = QVariantMap()); + static gpu::TexturePointer getImageTexture(const QString& path, image::TextureUsage::Type type = image::TextureUsage::DEFAULT_TEXTURE, QVariantMap options = QVariantMap()); /// Loads a texture from the specified URL. - NetworkTexturePointer getTexture(const QUrl& url, Type type = Type::DEFAULT_TEXTURE, + NetworkTexturePointer getTexture(const QUrl& url, image::TextureUsage::Type type = image::TextureUsage::DEFAULT_TEXTURE, const QByteArray& content = QByteArray(), int maxNumPixels = ABSOLUTE_MAX_TEXTURE_NUM_PIXELS); diff --git a/libraries/model/CMakeLists.txt b/libraries/model/CMakeLists.txt index 021aa3d027..da85b6aa3d 100755 --- a/libraries/model/CMakeLists.txt +++ b/libraries/model/CMakeLists.txt @@ -1,5 +1,4 @@ set(TARGET_NAME model) AUTOSCRIBE_SHADER_LIB(gpu model) setup_hifi_library() -link_hifi_libraries(shared ktx gpu) - +link_hifi_libraries(shared ktx gpu image) \ No newline at end of file diff --git a/libraries/model/src/model/TextureMap.cpp b/libraries/model/src/model/TextureMap.cpp index e619a2d70f..b308dd72f8 100755 --- a/libraries/model/src/model/TextureMap.cpp +++ b/libraries/model/src/model/TextureMap.cpp @@ -10,81 +10,9 @@ // #include "TextureMap.h" -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "ModelLogging.h" using namespace model; using namespace gpu; -// FIXME: Declare this to enable compression -//#define COMPRESS_TEXTURES -static const uvec2 SPARSE_PAGE_SIZE(128); -static const uvec2 MAX_TEXTURE_SIZE(4096); -bool DEV_DECIMATE_TEXTURES = false; - -bool needsSparseRectification(const uvec2& size) { - // Don't attempt to rectify small textures (textures less than the sparse page size in any dimension) - if (glm::any(glm::lessThan(size, SPARSE_PAGE_SIZE))) { - return false; - } - - // Don't rectify textures that are already an exact multiple of sparse page size - if (uvec2(0) == (size % SPARSE_PAGE_SIZE)) { - return false; - } - - // Texture is not sparse compatible, but is bigger than the sparse page size in both dimensions, rectify! - return true; -} - -uvec2 rectifyToSparseSize(const uvec2& size) { - uvec2 pages = ((size / SPARSE_PAGE_SIZE) + glm::clamp(size % SPARSE_PAGE_SIZE, uvec2(0), uvec2(1))); - uvec2 result = pages * SPARSE_PAGE_SIZE; - return result; -} - -std::atomic DECIMATED_TEXTURE_COUNT { 0 }; -std::atomic RECTIFIED_TEXTURE_COUNT { 0 }; - -QImage processSourceImage(const QImage& srcImage, bool cubemap) { - PROFILE_RANGE(resource_parse, "processSourceImage"); - const uvec2 srcImageSize = toGlm(srcImage.size()); - uvec2 targetSize = srcImageSize; - - while (glm::any(glm::greaterThan(targetSize, MAX_TEXTURE_SIZE))) { - targetSize /= 2; - } - if (targetSize != srcImageSize) { - ++DECIMATED_TEXTURE_COUNT; - } - - if (!cubemap && needsSparseRectification(targetSize)) { - ++RECTIFIED_TEXTURE_COUNT; - targetSize = rectifyToSparseSize(targetSize); - } - - if (DEV_DECIMATE_TEXTURES && glm::all(glm::greaterThanEqual(targetSize / SPARSE_PAGE_SIZE, uvec2(2)))) { - targetSize /= 2; - } - - if (targetSize != srcImageSize) { - PROFILE_RANGE(resource_parse, "processSourceImage Rectify"); - qCDebug(modelLog) << "Resizing texture from " << srcImageSize.x << "x" << srcImageSize.y << " to " << targetSize.x << "x" << targetSize.y; - return srcImage.scaled(fromGlm(targetSize), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - } - - return srcImage; -} - void TextureMap::setTextureSource(TextureSourcePointer& textureSource) { _textureSource = textureSource; } @@ -113,758 +41,3 @@ void TextureMap::setLightmapOffsetScale(float offset, float scale) { _lightmapOffsetScale.x = offset; _lightmapOffsetScale.y = scale; } - -const QImage TextureUsage::process2DImageColor(const QImage& srcImage, bool& validAlpha, bool& alphaAsMask) { - PROFILE_RANGE(resource_parse, "process2DImageColor"); - QImage image = processSourceImage(srcImage, false); - validAlpha = false; - alphaAsMask = true; - const uint8 OPAQUE_ALPHA = 255; - const uint8 TRANSPARENT_ALPHA = 0; - if (image.hasAlphaChannel()) { - if (image.format() != QImage::Format_ARGB32) { - image = image.convertToFormat(QImage::Format_ARGB32); - } - - // Figure out if we can use a mask for alpha or not - int numOpaques = 0; - int numTranslucents = 0; - const int NUM_PIXELS = image.width() * image.height(); - const int MAX_TRANSLUCENT_PIXELS_FOR_ALPHAMASK = (int)(0.05f * (float)(NUM_PIXELS)); - const QRgb* data = reinterpret_cast(image.constBits()); - for (int i = 0; i < NUM_PIXELS; ++i) { - auto alpha = qAlpha(data[i]); - if (alpha == OPAQUE_ALPHA) { - numOpaques++; - } else if (alpha != TRANSPARENT_ALPHA) { - if (++numTranslucents > MAX_TRANSLUCENT_PIXELS_FOR_ALPHAMASK) { - alphaAsMask = false; - break; - } - } - } - validAlpha = (numOpaques != NUM_PIXELS); - } - - // Force all the color images to be rgba32bits - if (image.format() != QImage::Format_ARGB32) { - image = image.convertToFormat(QImage::Format_ARGB32); - } - - return image; -} - -void TextureUsage::defineColorTexelFormats(gpu::Element& formatGPU, gpu::Element& formatMip, -const QImage& image, bool isLinear, bool doCompress) { - -#ifdef COMPRESS_TEXTURES -#else - doCompress = false; -#endif - - if (image.hasAlphaChannel()) { - gpu::Semantic gpuSemantic; - gpu::Semantic mipSemantic; - if (isLinear) { - mipSemantic = gpu::BGRA; - if (doCompress) { - gpuSemantic = gpu::COMPRESSED_RGBA; - } else { - gpuSemantic = gpu::RGBA; - } - } else { - mipSemantic = gpu::SBGRA; - if (doCompress) { - gpuSemantic = gpu::COMPRESSED_SRGBA; - } else { - gpuSemantic = gpu::SRGBA; - } - } - formatGPU = gpu::Element(gpu::VEC4, gpu::NUINT8, gpuSemantic); - formatMip = gpu::Element(gpu::VEC4, gpu::NUINT8, mipSemantic); - } else { - gpu::Semantic gpuSemantic; - gpu::Semantic mipSemantic; - if (isLinear) { - mipSemantic = gpu::RGB; - if (doCompress) { - gpuSemantic = gpu::COMPRESSED_RGB; - } else { - gpuSemantic = gpu::RGB; - } - } else { - mipSemantic = gpu::SRGB; - if (doCompress) { - gpuSemantic = gpu::COMPRESSED_SRGB; - } else { - gpuSemantic = gpu::SRGB; - } - } - formatGPU = gpu::Element(gpu::VEC3, gpu::NUINT8, gpuSemantic); - formatMip = gpu::Element(gpu::VEC3, gpu::NUINT8, mipSemantic); - } -} - -#define CPU_MIPMAPS 1 - -void generateMips(gpu::Texture* texture, QImage& image, bool fastResize) { -#if CPU_MIPMAPS - PROFILE_RANGE(resource_parse, "generateMips"); - auto numMips = texture->getNumMips(); - for (uint16 level = 1; level < numMips; ++level) { - QSize mipSize(texture->evalMipWidth(level), texture->evalMipHeight(level)); - if (fastResize) { - image = image.scaled(mipSize); - texture->assignStoredMip(level, image.byteCount(), image.constBits()); - } else { - QImage mipImage = image.scaled(mipSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - texture->assignStoredMip(level, mipImage.byteCount(), mipImage.constBits()); - } - } - -#else - texture->autoGenerateMips(-1); -#endif -} - -void generateFaceMips(gpu::Texture* texture, QImage& image, uint8 face) { -#if CPU_MIPMAPS - PROFILE_RANGE(resource_parse, "generateFaceMips"); - auto numMips = texture->getNumMips(); - for (uint16 level = 1; level < numMips; ++level) { - QSize mipSize(texture->evalMipWidth(level), texture->evalMipHeight(level)); - QImage mipImage = image.scaled(mipSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - texture->assignStoredMipFace(level, face, mipImage.byteCount(), mipImage.constBits()); - } -#else - texture->autoGenerateMips(-1); -#endif -} - -gpu::Texture* TextureUsage::process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips, bool isStrict) { - PROFILE_RANGE(resource_parse, "process2DTextureColorFromImage"); - bool validAlpha = false; - bool alphaAsMask = true; - QImage image = process2DImageColor(srcImage, validAlpha, alphaAsMask); - - gpu::Texture* theTexture = nullptr; - - if ((image.width() > 0) && (image.height() > 0)) { - gpu::Element formatGPU; - gpu::Element formatMip; - defineColorTexelFormats(formatGPU, formatMip, image, isLinear, doCompress); - - if (isStrict) { - theTexture = (gpu::Texture::createStrict(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); - } else { - theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); - } - theTexture->setSource(srcImageName); - auto usage = gpu::Texture::Usage::Builder().withColor(); - if (validAlpha) { - usage.withAlpha(); - if (alphaAsMask) { - usage.withAlphaMask(); - } - } - theTexture->setUsage(usage.build()); - theTexture->setStoredMipFormat(formatMip); - theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); - - if (generateMips) { - ::generateMips(theTexture, image, false); - } - theTexture->setSource(srcImageName); - } - - return theTexture; -} - -gpu::Texture* TextureUsage::createStrict2DTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { - return process2DTextureColorFromImage(srcImage, srcImageName, false, false, true, true); -} - -gpu::Texture* TextureUsage::create2DTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { - return process2DTextureColorFromImage(srcImage, srcImageName, false, false, true); -} - -gpu::Texture* TextureUsage::createAlbedoTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { - return process2DTextureColorFromImage(srcImage, srcImageName, false, true, true); -} - -gpu::Texture* TextureUsage::createEmissiveTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { - return process2DTextureColorFromImage(srcImage, srcImageName, false, true, true); -} - -gpu::Texture* TextureUsage::createLightmapTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { - return process2DTextureColorFromImage(srcImage, srcImageName, false, true, true); -} - - -gpu::Texture* TextureUsage::createNormalTextureFromNormalImage(const QImage& srcImage, const std::string& srcImageName) { - PROFILE_RANGE(resource_parse, "createNormalTextureFromNormalImage"); - QImage image = processSourceImage(srcImage, false); - - // Make sure the normal map source image is ARGB32 - if (image.format() != QImage::Format_ARGB32) { - image = image.convertToFormat(QImage::Format_ARGB32); - } - - - gpu::Texture* theTexture = nullptr; - if ((image.width() > 0) && (image.height() > 0)) { - - gpu::Element formatMip = gpu::Element::COLOR_BGRA_32; - gpu::Element formatGPU = gpu::Element::COLOR_RGBA_32; - - theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); - theTexture->setSource(srcImageName); - theTexture->setStoredMipFormat(formatMip); - theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); - generateMips(theTexture, image, true); - - theTexture->setSource(srcImageName); - } - - return theTexture; -} - -int clampPixelCoordinate(int coordinate, int maxCoordinate) { - return coordinate - ((int)(coordinate < 0) * coordinate) + ((int)(coordinate > maxCoordinate) * (maxCoordinate - coordinate)); -} - -const int RGBA_MAX = 255; - -// transform -1 - 1 to 0 - 255 (from sobel value to rgb) -double mapComponent(double sobelValue) { - const double factor = RGBA_MAX / 2.0; - return (sobelValue + 1.0) * factor; -} - -gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcImage, const std::string& srcImageName) { - PROFILE_RANGE(resource_parse, "createNormalTextureFromBumpImage"); - QImage image = processSourceImage(srcImage, false); - - if (image.format() != QImage::Format_Grayscale8) { - image = image.convertToFormat(QImage::Format_Grayscale8); - } - - // PR 5540 by AlessandroSigna integrated here as a specialized TextureLoader for bumpmaps - // The conversion is done using the Sobel Filter to calculate the derivatives from the grayscale image - const double pStrength = 2.0; - int width = image.width(); - int height = image.height(); - - QImage result(width, height, QImage::Format_ARGB32); - - for (int i = 0; i < width; i++) { - const int iNextClamped = clampPixelCoordinate(i + 1, width - 1); - const int iPrevClamped = clampPixelCoordinate(i - 1, width - 1); - - for (int j = 0; j < height; j++) { - const int jNextClamped = clampPixelCoordinate(j + 1, height - 1); - const int jPrevClamped = clampPixelCoordinate(j - 1, height - 1); - - // surrounding pixels - const QRgb topLeft = image.pixel(iPrevClamped, jPrevClamped); - const QRgb top = image.pixel(iPrevClamped, j); - const QRgb topRight = image.pixel(iPrevClamped, jNextClamped); - const QRgb right = image.pixel(i, jNextClamped); - const QRgb bottomRight = image.pixel(iNextClamped, jNextClamped); - const QRgb bottom = image.pixel(iNextClamped, j); - const QRgb bottomLeft = image.pixel(iNextClamped, jPrevClamped); - const QRgb left = image.pixel(i, jPrevClamped); - - // take their gray intensities - // since it's a grayscale image, the value of each component RGB is the same - const double tl = qRed(topLeft); - const double t = qRed(top); - const double tr = qRed(topRight); - const double r = qRed(right); - const double br = qRed(bottomRight); - const double b = qRed(bottom); - const double bl = qRed(bottomLeft); - const double l = qRed(left); - - // apply the sobel filter - const double dX = (tr + pStrength * r + br) - (tl + pStrength * l + bl); - const double dY = (bl + pStrength * b + br) - (tl + pStrength * t + tr); - const double dZ = RGBA_MAX / pStrength; - - glm::vec3 v(dX, dY, dZ); - glm::normalize(v); - - // convert to rgb from the value obtained computing the filter - QRgb qRgbValue = qRgba(mapComponent(v.z), mapComponent(v.y), mapComponent(v.x), 1.0); - result.setPixel(i, j, qRgbValue); - } - } - - gpu::Texture* theTexture = nullptr; - if ((result.width() > 0) && (result.height() > 0)) { - - gpu::Element formatMip = gpu::Element::COLOR_BGRA_32; - gpu::Element formatGPU = gpu::Element::COLOR_RGBA_32; - - - theTexture = (gpu::Texture::create2D(formatGPU, result.width(), result.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); - theTexture->setSource(srcImageName); - theTexture->setStoredMipFormat(formatMip); - theTexture->assignStoredMip(0, result.byteCount(), result.constBits()); - generateMips(theTexture, result, true); - - theTexture->setSource(srcImageName); - } - - return theTexture; -} - -gpu::Texture* TextureUsage::createRoughnessTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { - PROFILE_RANGE(resource_parse, "createRoughnessTextureFromImage"); - QImage image = processSourceImage(srcImage, false); - if (!image.hasAlphaChannel()) { - if (image.format() != QImage::Format_RGB888) { - image = image.convertToFormat(QImage::Format_RGB888); - } - } else { - if (image.format() != QImage::Format_RGBA8888) { - image = image.convertToFormat(QImage::Format_RGBA8888); - } - } - - image = image.convertToFormat(QImage::Format_Grayscale8); - - gpu::Texture* theTexture = nullptr; - if ((image.width() > 0) && (image.height() > 0)) { -#ifdef COMPRESS_TEXTURES - gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::COMPRESSED_R); -#else - gpu::Element formatGPU = gpu::Element::COLOR_R_8; -#endif - gpu::Element formatMip = gpu::Element::COLOR_R_8; - - theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); - theTexture->setSource(srcImageName); - theTexture->setStoredMipFormat(formatMip); - theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); - generateMips(theTexture, image, true); - - theTexture->setSource(srcImageName); - } - - return theTexture; -} - -gpu::Texture* TextureUsage::createRoughnessTextureFromGlossImage(const QImage& srcImage, const std::string& srcImageName) { - PROFILE_RANGE(resource_parse, "createRoughnessTextureFromGlossImage"); - QImage image = processSourceImage(srcImage, false); - if (!image.hasAlphaChannel()) { - if (image.format() != QImage::Format_RGB888) { - image = image.convertToFormat(QImage::Format_RGB888); - } - } else { - if (image.format() != QImage::Format_RGBA8888) { - image = image.convertToFormat(QImage::Format_RGBA8888); - } - } - - // Gloss turned into Rough - image.invertPixels(QImage::InvertRgba); - - image = image.convertToFormat(QImage::Format_Grayscale8); - - gpu::Texture* theTexture = nullptr; - if ((image.width() > 0) && (image.height() > 0)) { - -#ifdef COMPRESS_TEXTURES - gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::COMPRESSED_R); -#else - gpu::Element formatGPU = gpu::Element::COLOR_R_8; -#endif - gpu::Element formatMip = gpu::Element::COLOR_R_8; - - theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); - theTexture->setSource(srcImageName); - theTexture->setStoredMipFormat(formatMip); - theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); - generateMips(theTexture, image, true); - - theTexture->setSource(srcImageName); - } - - return theTexture; -} - -gpu::Texture* TextureUsage::createMetallicTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { - PROFILE_RANGE(resource_parse, "createMetallicTextureFromImage"); - QImage image = processSourceImage(srcImage, false); - if (!image.hasAlphaChannel()) { - if (image.format() != QImage::Format_RGB888) { - image = image.convertToFormat(QImage::Format_RGB888); - } - } else { - if (image.format() != QImage::Format_RGBA8888) { - image = image.convertToFormat(QImage::Format_RGBA8888); - } - } - - image = image.convertToFormat(QImage::Format_Grayscale8); - - gpu::Texture* theTexture = nullptr; - if ((image.width() > 0) && (image.height() > 0)) { - -#ifdef COMPRESS_TEXTURES - gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::COMPRESSED_R); -#else - gpu::Element formatGPU = gpu::Element::COLOR_R_8; -#endif - gpu::Element formatMip = gpu::Element::COLOR_R_8; - - theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); - theTexture->setSource(srcImageName); - theTexture->setStoredMipFormat(formatMip); - theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); - generateMips(theTexture, image, true); - - theTexture->setSource(srcImageName); - } - - return theTexture; -} - -class CubeLayout { -public: - - enum SourceProjection { - FLAT = 0, - EQUIRECTANGULAR, - }; - int _type = FLAT; - int _widthRatio = 1; - int _heightRatio = 1; - - class Face { - public: - int _x = 0; - int _y = 0; - bool _horizontalMirror = false; - bool _verticalMirror = false; - - Face() {} - Face(int x, int y, bool horizontalMirror, bool verticalMirror) : _x(x), _y(y), _horizontalMirror(horizontalMirror), _verticalMirror(verticalMirror) {} - }; - - Face _faceXPos; - Face _faceXNeg; - Face _faceYPos; - Face _faceYNeg; - Face _faceZPos; - Face _faceZNeg; - - CubeLayout(int wr, int hr, Face fXP, Face fXN, Face fYP, Face fYN, Face fZP, Face fZN) : - _type(FLAT), - _widthRatio(wr), - _heightRatio(hr), - _faceXPos(fXP), - _faceXNeg(fXN), - _faceYPos(fYP), - _faceYNeg(fYN), - _faceZPos(fZP), - _faceZNeg(fZN) {} - - CubeLayout(int wr, int hr) : - _type(EQUIRECTANGULAR), - _widthRatio(wr), - _heightRatio(hr) {} - - - static const CubeLayout CUBEMAP_LAYOUTS[]; - static const int NUM_CUBEMAP_LAYOUTS; - - static int findLayout(int width, int height) { - // Find the layout of the cubemap in the 2D image - int foundLayout = -1; - for (int i = 0; i < NUM_CUBEMAP_LAYOUTS; i++) { - if ((height * CUBEMAP_LAYOUTS[i]._widthRatio) == (width * CUBEMAP_LAYOUTS[i]._heightRatio)) { - foundLayout = i; - break; - } - } - return foundLayout; - } - - static QImage extractEquirectangularFace(const QImage& source, gpu::Texture::CubeFace face, int faceWidth) { - QImage image(faceWidth, faceWidth, source.format()); - - glm::vec2 dstInvSize(1.0f / (float)image.width(), 1.0f / (float)image.height()); - - struct CubeToXYZ { - gpu::Texture::CubeFace _face; - CubeToXYZ(gpu::Texture::CubeFace face) : _face(face) {} - - glm::vec3 xyzFrom(const glm::vec2& uv) { - auto faceDir = glm::normalize(glm::vec3(-1.0f + 2.0f * uv.x, -1.0f + 2.0f * uv.y, 1.0f)); - - switch (_face) { - case gpu::Texture::CubeFace::CUBE_FACE_BACK_POS_Z: - return glm::vec3(-faceDir.x, faceDir.y, faceDir.z); - case gpu::Texture::CubeFace::CUBE_FACE_FRONT_NEG_Z: - return glm::vec3(faceDir.x, faceDir.y, -faceDir.z); - case gpu::Texture::CubeFace::CUBE_FACE_LEFT_NEG_X: - return glm::vec3(faceDir.z, faceDir.y, faceDir.x); - case gpu::Texture::CubeFace::CUBE_FACE_RIGHT_POS_X: - return glm::vec3(-faceDir.z, faceDir.y, -faceDir.x); - case gpu::Texture::CubeFace::CUBE_FACE_BOTTOM_NEG_Y: - return glm::vec3(-faceDir.x, -faceDir.z, faceDir.y); - case gpu::Texture::CubeFace::CUBE_FACE_TOP_POS_Y: - default: - return glm::vec3(-faceDir.x, faceDir.z, -faceDir.y); - } - } - }; - CubeToXYZ cubeToXYZ(face); - - struct RectToXYZ { - RectToXYZ() {} - - glm::vec2 uvFrom(const glm::vec3& xyz) { - auto flatDir = glm::normalize(glm::vec2(xyz.x, xyz.z)); - auto uvRad = glm::vec2(atan2(flatDir.x, flatDir.y), asin(xyz.y)); - - const float LON_TO_RECT_U = 1.0f / (glm::pi()); - const float LAT_TO_RECT_V = 2.0f / glm::pi(); - return glm::vec2(0.5f * uvRad.x * LON_TO_RECT_U + 0.5f, 0.5f * uvRad.y * LAT_TO_RECT_V + 0.5f); - } - }; - RectToXYZ rectToXYZ; - - int srcFaceHeight = source.height(); - int srcFaceWidth = source.width(); - - glm::vec2 dstCoord; - glm::ivec2 srcPixel; - for (int y = 0; y < faceWidth; ++y) { - dstCoord.y = 1.0f - (y + 0.5f) * dstInvSize.y; // Fill cube face images from top to bottom - for (int x = 0; x < faceWidth; ++x) { - dstCoord.x = (x + 0.5f) * dstInvSize.x; - - auto xyzDir = cubeToXYZ.xyzFrom(dstCoord); - auto srcCoord = rectToXYZ.uvFrom(xyzDir); - - srcPixel.x = floor(srcCoord.x * srcFaceWidth); - // Flip the vertical axis to QImage going top to bottom - srcPixel.y = floor((1.0f - srcCoord.y) * srcFaceHeight); - - if (((uint32) srcPixel.x < (uint32) source.width()) && ((uint32) srcPixel.y < (uint32) source.height())) { - image.setPixel(x, y, source.pixel(QPoint(srcPixel.x, srcPixel.y))); - - // Keep for debug, this is showing the dir as a color - // glm::u8vec4 rgba((xyzDir.x + 1.0)*0.5 * 256, (xyzDir.y + 1.0)*0.5 * 256, (xyzDir.z + 1.0)*0.5 * 256, 256); - // unsigned int val = 0xff000000 | (rgba.r) | (rgba.g << 8) | (rgba.b << 16); - // image.setPixel(x, y, val); - } - } - } - return image; - } -}; - -const CubeLayout CubeLayout::CUBEMAP_LAYOUTS[] = { - - // Here is the expected layout for the faces in an image with the 2/1 aspect ratio: - // THis is detected as an Equirectangular projection - // WIDTH - // <---------------------------> - // ^ +------+------+------+------+ - // H | | | | | - // E | | | | | - // I | | | | | - // G +------+------+------+------+ - // H | | | | | - // T | | | | | - // | | | | | | - // v +------+------+------+------+ - // - // FaceWidth = width = height / 6 - { 2, 1 }, - - // Here is the expected layout for the faces in an image with the 1/6 aspect ratio: - // - // WIDTH - // <------> - // ^ +------+ - // | | | - // | | +X | - // | | | - // H +------+ - // E | | - // I | -X | - // G | | - // H +------+ - // T | | - // | | +Y | - // | | | - // | +------+ - // | | | - // | | -Y | - // | | | - // H +------+ - // E | | - // I | +Z | - // G | | - // H +------+ - // T | | - // | | -Z | - // | | | - // V +------+ - // - // FaceWidth = width = height / 6 - { 1, 6, - { 0, 0, true, false }, - { 0, 1, true, false }, - { 0, 2, false, true }, - { 0, 3, false, true }, - { 0, 4, true, false }, - { 0, 5, true, false } - }, - - // Here is the expected layout for the faces in an image with the 3/4 aspect ratio: - // - // <-----------WIDTH-----------> - // ^ +------+------+------+------+ - // | | | | | | - // | | | +Y | | | - // | | | | | | - // H +------+------+------+------+ - // E | | | | | - // I | -X | -Z | +X | +Z | - // G | | | | | - // H +------+------+------+------+ - // T | | | | | - // | | | -Y | | | - // | | | | | | - // V +------+------+------+------+ - // - // FaceWidth = width / 4 = height / 3 - { 4, 3, - { 2, 1, true, false }, - { 0, 1, true, false }, - { 1, 0, false, true }, - { 1, 2, false, true }, - { 3, 1, true, false }, - { 1, 1, true, false } - }, - - // Here is the expected layout for the faces in an image with the 4/3 aspect ratio: - // - // <-------WIDTH--------> - // ^ +------+------+------+ - // | | | | | - // | | | +Y | | - // | | | | | - // H +------+------+------+ - // E | | | | - // I | -X | -Z | +X | - // G | | | | - // H +------+------+------+ - // T | | | | - // | | | -Y | | - // | | | | | - // | +------+------+------+ - // | | | | | - // | | | +Z! | | <+Z is upside down! - // | | | | | - // V +------+------+------+ - // - // FaceWidth = width / 3 = height / 4 - { 3, 4, - { 2, 1, true, false }, - { 0, 1, true, false }, - { 1, 0, false, true }, - { 1, 2, false, true }, - { 1, 3, false, true }, - { 1, 1, true, false } - } -}; -const int CubeLayout::NUM_CUBEMAP_LAYOUTS = sizeof(CubeLayout::CUBEMAP_LAYOUTS) / sizeof(CubeLayout); - -gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips, bool generateIrradiance) { - PROFILE_RANGE(resource_parse, "processCubeTextureColorFromImage"); - - gpu::Texture* theTexture = nullptr; - if ((srcImage.width() > 0) && (srcImage.height() > 0)) { - QImage image = processSourceImage(srcImage, true); - if (image.format() != QImage::Format_ARGB32) { - image = image.convertToFormat(QImage::Format_ARGB32); - } - - gpu::Element formatGPU; - gpu::Element formatMip; - defineColorTexelFormats(formatGPU, formatMip, image, isLinear, doCompress); - - // Find the layout of the cubemap in the 2D image - // Use the original image size since processSourceImage may have altered the size / aspect ratio - int foundLayout = CubeLayout::findLayout(srcImage.width(), srcImage.height()); - - std::vector faces; - // If found, go extract the faces as separate images - if (foundLayout >= 0) { - auto& layout = CubeLayout::CUBEMAP_LAYOUTS[foundLayout]; - if (layout._type == CubeLayout::FLAT) { - int faceWidth = image.width() / layout._widthRatio; - - faces.push_back(image.copy(QRect(layout._faceXPos._x * faceWidth, layout._faceXPos._y * faceWidth, faceWidth, faceWidth)).mirrored(layout._faceXPos._horizontalMirror, layout._faceXPos._verticalMirror)); - faces.push_back(image.copy(QRect(layout._faceXNeg._x * faceWidth, layout._faceXNeg._y * faceWidth, faceWidth, faceWidth)).mirrored(layout._faceXNeg._horizontalMirror, layout._faceXNeg._verticalMirror)); - faces.push_back(image.copy(QRect(layout._faceYPos._x * faceWidth, layout._faceYPos._y * faceWidth, faceWidth, faceWidth)).mirrored(layout._faceYPos._horizontalMirror, layout._faceYPos._verticalMirror)); - faces.push_back(image.copy(QRect(layout._faceYNeg._x * faceWidth, layout._faceYNeg._y * faceWidth, faceWidth, faceWidth)).mirrored(layout._faceYNeg._horizontalMirror, layout._faceYNeg._verticalMirror)); - faces.push_back(image.copy(QRect(layout._faceZPos._x * faceWidth, layout._faceZPos._y * faceWidth, faceWidth, faceWidth)).mirrored(layout._faceZPos._horizontalMirror, layout._faceZPos._verticalMirror)); - faces.push_back(image.copy(QRect(layout._faceZNeg._x * faceWidth, layout._faceZNeg._y * faceWidth, faceWidth, faceWidth)).mirrored(layout._faceZNeg._horizontalMirror, layout._faceZNeg._verticalMirror)); - } else if (layout._type == CubeLayout::EQUIRECTANGULAR) { - // THe face width is estimated from the input image - const int EQUIRECT_FACE_RATIO_TO_WIDTH = 4; - const int EQUIRECT_MAX_FACE_WIDTH = 2048; - int faceWidth = std::min(image.width() / EQUIRECT_FACE_RATIO_TO_WIDTH, EQUIRECT_MAX_FACE_WIDTH); - for (int face = gpu::Texture::CUBE_FACE_RIGHT_POS_X; face < gpu::Texture::NUM_CUBE_FACES; face++) { - QImage faceImage = CubeLayout::extractEquirectangularFace(image, (gpu::Texture::CubeFace) face, faceWidth); - faces.push_back(faceImage); - } - } - } else { - qCDebug(modelLog) << "Failed to find a known cube map layout from this image:" << QString(srcImageName.c_str()); - return nullptr; - } - - // If the 6 faces have been created go on and define the true Texture - if (faces.size() == gpu::Texture::NUM_FACES_PER_TYPE[gpu::Texture::TEX_CUBE]) { - theTexture = gpu::Texture::createCube(formatGPU, faces[0].width(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP)); - theTexture->setSource(srcImageName); - theTexture->setStoredMipFormat(formatMip); - int f = 0; - for (auto& face : faces) { - theTexture->assignStoredMipFace(0, f, face.byteCount(), face.constBits()); - if (generateMips) { - generateFaceMips(theTexture, face, f); - } - f++; - } - - // Generate irradiance while we are at it - if (generateIrradiance) { - PROFILE_RANGE(resource_parse, "generateIrradiance"); - theTexture->generateIrradiance(); - } - - theTexture->setSource(srcImageName); - } - } - - return theTexture; -} - -gpu::Texture* TextureUsage::createCubeTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { - return processCubeTextureColorFromImage(srcImage, srcImageName, false, true, true, true); -} - -gpu::Texture* TextureUsage::createCubeTextureFromImageWithoutIrradiance(const QImage& srcImage, const std::string& srcImageName) { - return processCubeTextureColorFromImage(srcImage, srcImageName, false, true, true, false); -} diff --git a/libraries/model/src/model/TextureMap.h b/libraries/model/src/model/TextureMap.h index a4bb861502..1785d44730 100755 --- a/libraries/model/src/model/TextureMap.h +++ b/libraries/model/src/model/TextureMap.h @@ -13,48 +13,10 @@ #include "gpu/Texture.h" -#include "Material.h" #include "Transform.h" -#include - -class QImage; - namespace model { -typedef glm::vec3 Color; - -class TextureUsage { -public: - gpu::Texture::Type _type{ gpu::Texture::TEX_2D }; - Material::MapFlags _materialUsage{ MaterialKey::ALBEDO_MAP }; - - int _environmentUsage = 0; - - static gpu::Texture* create2DTextureFromImage(const QImage& image, const std::string& srcImageName); - static gpu::Texture* createStrict2DTextureFromImage(const QImage& image, const std::string& srcImageName); - static gpu::Texture* createAlbedoTextureFromImage(const QImage& image, const std::string& srcImageName); - static gpu::Texture* createEmissiveTextureFromImage(const QImage& image, const std::string& srcImageName); - static gpu::Texture* createNormalTextureFromNormalImage(const QImage& image, const std::string& srcImageName); - static gpu::Texture* createNormalTextureFromBumpImage(const QImage& image, const std::string& srcImageName); - static gpu::Texture* createRoughnessTextureFromImage(const QImage& image, const std::string& srcImageName); - static gpu::Texture* createRoughnessTextureFromGlossImage(const QImage& image, const std::string& srcImageName); - static gpu::Texture* createMetallicTextureFromImage(const QImage& image, const std::string& srcImageName); - static gpu::Texture* createCubeTextureFromImage(const QImage& image, const std::string& srcImageName); - static gpu::Texture* createCubeTextureFromImageWithoutIrradiance(const QImage& image, const std::string& srcImageName); - static gpu::Texture* createLightmapTextureFromImage(const QImage& image, const std::string& srcImageName); - - - static const QImage process2DImageColor(const QImage& srcImage, bool& validAlpha, bool& alphaAsMask); - static void defineColorTexelFormats(gpu::Element& formatGPU, gpu::Element& formatMip, - const QImage& srcImage, bool isLinear, bool doCompress); - static gpu::Texture* process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips, bool isStrict = false); - static gpu::Texture* processCubeTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips, bool generateIrradiance); - -}; - - - class TextureMap { public: TextureMap() {} diff --git a/libraries/networking/src/AssetClient.cpp b/libraries/networking/src/AssetClient.cpp index 37b1af0996..054557e920 100644 --- a/libraries/networking/src/AssetClient.cpp +++ b/libraries/networking/src/AssetClient.cpp @@ -67,7 +67,6 @@ void AssetClient::init() { } } - void AssetClient::cacheInfoRequest(QObject* reciever, QString slot) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "cacheInfoRequest", Qt::QueuedConnection, @@ -182,8 +181,8 @@ RenameMappingRequest* AssetClient::createRenameMappingRequest(const AssetPath& o return request; } -AssetRequest* AssetClient::createRequest(const AssetHash& hash) { - auto request = new AssetRequest(hash); +AssetRequest* AssetClient::createRequest(const AssetHash& hash, const ByteRange& byteRange) { + auto request = new AssetRequest(hash, byteRange); // Move to the AssetClient thread in case we are not currently on that thread (which will usually be the case) request->moveToThread(thread()); @@ -349,18 +348,19 @@ void AssetClient::handleAssetGetReply(QSharedPointer message, S // Store message in case we need to disconnect from it later. callbacks.message = message; + + auto weakNode = senderNode.toWeakRef(); + connect(message.data(), &ReceivedMessage::progress, this, [this, weakNode, messageID, length](qint64 size) { + handleProgressCallback(weakNode, messageID, size, length); + }); + connect(message.data(), &ReceivedMessage::completed, this, [this, weakNode, messageID]() { + handleCompleteCallback(weakNode, messageID); + }); + if (message->isComplete()) { + disconnect(message.data(), nullptr, this, nullptr); callbacks.completeCallback(true, error, message->readAll()); messageCallbackMap.erase(requestIt); - } else { - auto weakNode = senderNode.toWeakRef(); - - connect(message.data(), &ReceivedMessage::progress, this, [this, weakNode, messageID, length](qint64 size) { - handleProgressCallback(weakNode, messageID, size, length); - }); - connect(message.data(), &ReceivedMessage::completed, this, [this, weakNode, messageID]() { - handleCompleteCallback(weakNode, messageID); - }); } } diff --git a/libraries/networking/src/AssetClient.h b/libraries/networking/src/AssetClient.h index c0d58cd8e6..6f9cc3cd31 100644 --- a/libraries/networking/src/AssetClient.h +++ b/libraries/networking/src/AssetClient.h @@ -21,6 +21,7 @@ #include #include "AssetUtils.h" +#include "ByteRange.h" #include "ClientServerUtils.h" #include "LimitedNodeList.h" #include "Node.h" @@ -55,7 +56,7 @@ public: Q_INVOKABLE DeleteMappingsRequest* createDeleteMappingsRequest(const AssetPathList& paths); Q_INVOKABLE SetMappingRequest* createSetMappingRequest(const AssetPath& path, const AssetHash& hash); Q_INVOKABLE RenameMappingRequest* createRenameMappingRequest(const AssetPath& oldPath, const AssetPath& newPath); - Q_INVOKABLE AssetRequest* createRequest(const AssetHash& hash); + Q_INVOKABLE AssetRequest* createRequest(const AssetHash& hash, const ByteRange& byteRange = ByteRange()); Q_INVOKABLE AssetUpload* createUpload(const QString& filename); Q_INVOKABLE AssetUpload* createUpload(const QByteArray& data); diff --git a/libraries/networking/src/AssetRequest.cpp b/libraries/networking/src/AssetRequest.cpp index 8d663933ca..920c7ae036 100644 --- a/libraries/networking/src/AssetRequest.cpp +++ b/libraries/networking/src/AssetRequest.cpp @@ -23,10 +23,12 @@ static int requestID = 0; -AssetRequest::AssetRequest(const QString& hash) : +AssetRequest::AssetRequest(const QString& hash, const ByteRange& byteRange) : _requestID(++requestID), - _hash(hash) + _hash(hash), + _byteRange(byteRange) { + } AssetRequest::~AssetRequest() { @@ -34,9 +36,6 @@ AssetRequest::~AssetRequest() { if (_assetRequestID) { assetClient->cancelGetAssetRequest(_assetRequestID); } - if (_assetInfoRequestID) { - assetClient->cancelGetAssetInfoRequest(_assetInfoRequestID); - } } void AssetRequest::start() { @@ -62,108 +61,76 @@ void AssetRequest::start() { // Try to load from cache _data = loadFromCache(getUrl()); if (!_data.isNull()) { - _info.hash = _hash; - _info.size = _data.size(); _error = NoError; _state = Finished; emit finished(this); return; } - - _state = WaitingForInfo; - + + _state = WaitingForData; + auto assetClient = DependencyManager::get(); - _assetInfoRequestID = assetClient->getAssetInfo(_hash, - [this](bool responseReceived, AssetServerError serverError, AssetInfo info) { + auto that = QPointer(this); // Used to track the request's lifetime + auto hash = _hash; - _assetInfoRequestID = INVALID_MESSAGE_ID; + _assetRequestID = assetClient->getAsset(_hash, _byteRange.fromInclusive, _byteRange.toExclusive, + [this, that, hash](bool responseReceived, AssetServerError serverError, const QByteArray& data) { - _info = info; + if (!that) { + qCWarning(asset_client) << "Got reply for dead asset request " << hash << "- error code" << _error; + // If the request is dead, return + return; + } + _assetRequestID = INVALID_MESSAGE_ID; if (!responseReceived) { _error = NetworkError; } else if (serverError != AssetServerError::NoError) { - switch(serverError) { + switch (serverError) { case AssetServerError::AssetNotFound: _error = NotFound; break; + case AssetServerError::InvalidByteRange: + _error = InvalidByteRange; + break; default: _error = UnknownError; break; } - } + } else { + if (_byteRange.isSet()) { + // we had a byte range, the size of the data does not match what we expect, so we return an error + if (data.size() != _byteRange.size()) { + _error = SizeVerificationFailed; + } + } else if (hashData(data).toHex() != _hash) { + // the hash of the received data does not match what we expect, so we return an error + _error = HashVerificationFailed; + } + if (_error == NoError) { + _data = data; + _totalReceived += data.size(); + emit progress(_totalReceived, data.size()); + + if (!_byteRange.isSet()) { + saveToCache(getUrl(), data); + } + } + } + if (_error != NoError) { - qCWarning(asset_client) << "Got error retrieving asset info for" << _hash; - _state = Finished; - emit finished(this); - + qCWarning(asset_client) << "Got error retrieving asset" << _hash << "- error code" << _error; + } + + _state = Finished; + emit finished(this); + }, [this, that](qint64 totalReceived, qint64 total) { + if (!that) { + // If the request is dead, return return; } - - _state = WaitingForData; - _data.resize(info.size); - - qCDebug(asset_client) << "Got size of " << _hash << " : " << info.size << " bytes"; - - int start = 0, end = _info.size; - - auto assetClient = DependencyManager::get(); - auto that = QPointer(this); // Used to track the request's lifetime - auto hash = _hash; - _assetRequestID = assetClient->getAsset(_hash, start, end, - [this, that, hash, start, end](bool responseReceived, AssetServerError serverError, const QByteArray& data) { - if (!that) { - qCWarning(asset_client) << "Got reply for dead asset request " << hash << "- error code" << _error; - // If the request is dead, return - return; - } - _assetRequestID = INVALID_MESSAGE_ID; - - if (!responseReceived) { - _error = NetworkError; - } else if (serverError != AssetServerError::NoError) { - switch (serverError) { - case AssetServerError::AssetNotFound: - _error = NotFound; - break; - case AssetServerError::InvalidByteRange: - _error = InvalidByteRange; - break; - default: - _error = UnknownError; - break; - } - } else { - Q_ASSERT(data.size() == (end - start)); - - // we need to check the hash of the received data to make sure it matches what we expect - if (hashData(data).toHex() == _hash) { - memcpy(_data.data() + start, data.constData(), data.size()); - _totalReceived += data.size(); - emit progress(_totalReceived, _info.size); - - saveToCache(getUrl(), data); - } else { - // hash doesn't match - we have an error - _error = HashVerificationFailed; - } - - } - - if (_error != NoError) { - qCWarning(asset_client) << "Got error retrieving asset" << _hash << "- error code" << _error; - } - - _state = Finished; - emit finished(this); - }, [this, that](qint64 totalReceived, qint64 total) { - if (!that) { - // If the request is dead, return - return; - } - emit progress(totalReceived, total); - }); + emit progress(totalReceived, total); }); } diff --git a/libraries/networking/src/AssetRequest.h b/libraries/networking/src/AssetRequest.h index 1632a55336..b808ae0ca6 100644 --- a/libraries/networking/src/AssetRequest.h +++ b/libraries/networking/src/AssetRequest.h @@ -17,15 +17,15 @@ #include #include "AssetClient.h" - #include "AssetUtils.h" +#include "ByteRange.h" + class AssetRequest : public QObject { Q_OBJECT public: enum State { NotStarted = 0, - WaitingForInfo, WaitingForData, Finished }; @@ -36,11 +36,12 @@ public: InvalidByteRange, InvalidHash, HashVerificationFailed, + SizeVerificationFailed, NetworkError, UnknownError }; - AssetRequest(const QString& hash); + AssetRequest(const QString& hash, const ByteRange& byteRange = ByteRange()); virtual ~AssetRequest() override; Q_INVOKABLE void start(); @@ -59,13 +60,12 @@ private: int _requestID; State _state = NotStarted; Error _error = NoError; - AssetInfo _info; uint64_t _totalReceived { 0 }; QString _hash; QByteArray _data; int _numPendingRequests { 0 }; MessageID _assetRequestID { INVALID_MESSAGE_ID }; - MessageID _assetInfoRequestID { INVALID_MESSAGE_ID }; + const ByteRange _byteRange; }; #endif diff --git a/libraries/networking/src/AssetResourceRequest.cpp b/libraries/networking/src/AssetResourceRequest.cpp index 540fb4767f..092e0ccb3d 100644 --- a/libraries/networking/src/AssetResourceRequest.cpp +++ b/libraries/networking/src/AssetResourceRequest.cpp @@ -114,7 +114,7 @@ void AssetResourceRequest::requestMappingForPath(const AssetPath& path) { void AssetResourceRequest::requestHash(const AssetHash& hash) { // Make request to atp auto assetClient = DependencyManager::get(); - _assetRequest = assetClient->createRequest(hash); + _assetRequest = assetClient->createRequest(hash, _byteRange); connect(_assetRequest, &AssetRequest::progress, this, &AssetResourceRequest::onDownloadProgress); connect(_assetRequest, &AssetRequest::finished, this, [this](AssetRequest* req) { diff --git a/libraries/networking/src/ByteRange.h b/libraries/networking/src/ByteRange.h new file mode 100644 index 0000000000..6fd3559154 --- /dev/null +++ b/libraries/networking/src/ByteRange.h @@ -0,0 +1,53 @@ +// +// ByteRange.h +// libraries/networking/src +// +// Created by Stephen Birarda on 4/17/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 +// + +#ifndef hifi_ByteRange_h +#define hifi_ByteRange_h + +struct ByteRange { + int64_t fromInclusive { 0 }; + int64_t toExclusive { 0 }; + + bool isSet() const { return fromInclusive < 0 || fromInclusive < toExclusive; } + int64_t size() const { return toExclusive - fromInclusive; } + + // byte ranges are invalid if: + // (1) the toExclusive of the range is negative + // (2) the toExclusive of the range is less than the fromInclusive, and isn't zero + // (3) the fromExclusive of the range is negative, and the toExclusive isn't zero + bool isValid() { + return toExclusive >= 0 + && (toExclusive >= fromInclusive || toExclusive == 0) + && (fromInclusive >= 0 || toExclusive == 0); + } + + void fixupRange(int64_t fileSize) { + if (!isSet()) { + // if the byte range is not set, force it to be from 0 to the end of the file + fromInclusive = 0; + toExclusive = fileSize; + } + + if (fromInclusive > 0 && toExclusive == 0) { + // we have a left side of the range that is non-zero + // if the RHS of the range is zero, set it to the end of the file now + toExclusive = fileSize; + } else if (-fromInclusive >= fileSize) { + // we have a negative range that is equal or greater than the full size of the file + // so we just set this to be a range across the entire file, from 0 + fromInclusive = 0; + toExclusive = fileSize; + } + } +}; + + +#endif // hifi_ByteRange_h diff --git a/libraries/networking/src/FileResourceRequest.cpp b/libraries/networking/src/FileResourceRequest.cpp index 58a2074103..1e549e5fa3 100644 --- a/libraries/networking/src/FileResourceRequest.cpp +++ b/libraries/networking/src/FileResourceRequest.cpp @@ -11,6 +11,8 @@ #include "FileResourceRequest.h" +#include + #include void FileResourceRequest::doSend() { @@ -21,17 +23,39 @@ void FileResourceRequest::doSend() { if (filename.isEmpty()) { filename = _url.toString(); } - - QFile file(filename); - if (file.exists()) { - if (file.open(QFile::ReadOnly)) { - _data = file.readAll(); - _result = ResourceRequest::Success; - } else { - _result = ResourceRequest::AccessDenied; - } + + if (!_byteRange.isValid()) { + _result = ResourceRequest::InvalidByteRange; } else { - _result = ResourceRequest::NotFound; + QFile file(filename); + if (file.exists()) { + if (file.open(QFile::ReadOnly)) { + + if (file.size() < _byteRange.fromInclusive || file.size() < _byteRange.toExclusive) { + _result = ResourceRequest::InvalidByteRange; + } else { + // fix it up based on the known size of the file + _byteRange.fixupRange(file.size()); + + if (_byteRange.fromInclusive >= 0) { + // this is a positive byte range, simply skip to that part of the file and read from there + file.seek(_byteRange.fromInclusive); + _data = file.read(_byteRange.size()); + } else { + // this is a negative byte range, we'll need to grab data from the end of the file first + file.seek(file.size() + _byteRange.fromInclusive); + _data = file.read(_byteRange.size()); + } + + _result = ResourceRequest::Success; + } + + } else { + _result = ResourceRequest::AccessDenied; + } + } else { + _result = ResourceRequest::NotFound; + } } _state = Finished; diff --git a/libraries/networking/src/HTTPResourceRequest.cpp b/libraries/networking/src/HTTPResourceRequest.cpp index 85da5de5b8..c6a4b93e51 100644 --- a/libraries/networking/src/HTTPResourceRequest.cpp +++ b/libraries/networking/src/HTTPResourceRequest.cpp @@ -59,6 +59,18 @@ void HTTPResourceRequest::doSend() { networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); } + if (_byteRange.isSet()) { + QString byteRange; + if (_byteRange.fromInclusive < 0) { + byteRange = QString("bytes=%1").arg(_byteRange.fromInclusive); + } else { + // HTTP byte ranges are inclusive on the `to` end: [from, to] + byteRange = QString("bytes=%1-%2").arg(_byteRange.fromInclusive).arg(_byteRange.toExclusive - 1); + } + networkRequest.setRawHeader("Range", byteRange.toLatin1()); + } + networkRequest.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, false); + _reply = NetworkAccessManager::getInstance().get(networkRequest); connect(_reply, &QNetworkReply::finished, this, &HTTPResourceRequest::onRequestFinished); @@ -72,12 +84,60 @@ void HTTPResourceRequest::onRequestFinished() { Q_ASSERT(_reply); cleanupTimer(); - + + // Content-Range headers have the form: + // + // Content-Range: -/ + // Content-Range: -/* + // Content-Range: */ + // + auto parseContentRangeHeader = [](QString contentRangeHeader) -> std::pair { + auto unitRangeParts = contentRangeHeader.split(' '); + if (unitRangeParts.size() != 2) { + return { false, 0 }; + } + + auto rangeSizeParts = unitRangeParts[1].split('/'); + if (rangeSizeParts.size() != 2) { + return { false, 0 }; + } + + auto sizeStr = rangeSizeParts[1]; + if (sizeStr == "*") { + return { true, 0 }; + } else { + bool ok; + auto size = sizeStr.toLong(&ok); + return { ok, size }; + } + }; + switch(_reply->error()) { case QNetworkReply::NoError: _data = _reply->readAll(); _loadedFromCache = _reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool(); _result = Success; + + if (_byteRange.isSet()) { + auto statusCode = _reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (statusCode == 206) { + _rangeRequestSuccessful = true; + auto contentRangeHeader = _reply->rawHeader("Content-Range"); + bool success; + uint64_t size; + std::tie(success, size) = parseContentRangeHeader(contentRangeHeader); + if (success) { + _totalSizeOfResource = size; + } else { + qWarning(networking) << "Error parsing content-range header: " << contentRangeHeader; + _totalSizeOfResource = 0; + } + } else { + _rangeRequestSuccessful = false; + _totalSizeOfResource = _data.size(); + } + } + break; case QNetworkReply::TimeoutError: @@ -130,6 +190,7 @@ void HTTPResourceRequest::onDownloadProgress(qint64 bytesReceived, qint64 bytesT } void HTTPResourceRequest::onTimeout() { + qDebug() << "Timeout: " << _url << ":" << _reply->isFinished(); Q_ASSERT(_state == InProgress); _reply->disconnect(this); _reply->abort(); diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 459382c5bf..8feb695c79 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -644,8 +644,6 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t } } - - std::unique_ptr LimitedNodeList::constructPingPacket(PingType_t pingType) { int packetSize = sizeof(PingType_t) + sizeof(quint64); diff --git a/libraries/networking/src/NetworkAccessManager.cpp b/libraries/networking/src/NetworkAccessManager.cpp index 73096825e0..fd356c3e94 100644 --- a/libraries/networking/src/NetworkAccessManager.cpp +++ b/libraries/networking/src/NetworkAccessManager.cpp @@ -13,6 +13,7 @@ #include "AtpReply.h" #include "NetworkAccessManager.h" +#include QThreadStorage networkAccessManagers; diff --git a/libraries/networking/src/Node.cpp b/libraries/networking/src/Node.cpp index 033f4bbaa8..60227eeaa1 100644 --- a/libraries/networking/src/Node.cpp +++ b/libraries/networking/src/Node.cpp @@ -56,7 +56,6 @@ Node::Node(const QUuid& uuid, NodeType_t type, const HifiSockAddr& publicSocket, NetworkPeer(uuid, publicSocket, localSocket, parent), _type(type), _connectionSecret(connectionSecret), - _isAlive(true), _pingMs(-1), // "Uninitialized" _clockSkewUsec(0), _mutex(), diff --git a/libraries/networking/src/Node.h b/libraries/networking/src/Node.h index 28afb8b943..d1bbffd817 100644 --- a/libraries/networking/src/Node.h +++ b/libraries/networking/src/Node.h @@ -54,9 +54,6 @@ public: NodeData* getLinkedData() const { return _linkedData.get(); } void setLinkedData(std::unique_ptr linkedData) { _linkedData = std::move(linkedData); } - bool isAlive() const { return _isAlive; } - void setAlive(bool isAlive) { _isAlive = isAlive; } - int getPingMs() const { return _pingMs; } void setPingMs(int pingMs) { _pingMs = pingMs; } @@ -92,7 +89,6 @@ private: QUuid _connectionSecret; std::unique_ptr _linkedData; - bool _isAlive; int _pingMs; qint64 _clockSkewUsec; QMutex _mutex; diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 7147682d48..868128f093 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -257,14 +257,14 @@ void NodeList::reset() { _avatarGainMap.clear(); _avatarGainMapLock.unlock(); - // refresh the owner UUID to the NULL UUID - setSessionUUID(QUuid()); - if (sender() != &_domainHandler) { // clear the domain connection information, unless they're the ones that asked us to reset _domainHandler.softReset(); } + // refresh the owner UUID to the NULL UUID + setSessionUUID(QUuid()); + // if we setup the DTLS socket, also disconnect from the DTLS socket readyRead() so it can handle handshaking if (_dtlsSocket) { disconnect(_dtlsSocket, 0, this, 0); diff --git a/libraries/networking/src/ReceivedMessage.cpp b/libraries/networking/src/ReceivedMessage.cpp index 02cb58fb2d..2c5a11334b 100644 --- a/libraries/networking/src/ReceivedMessage.cpp +++ b/libraries/networking/src/ReceivedMessage.cpp @@ -26,8 +26,7 @@ ReceivedMessage::ReceivedMessage(const NLPacketList& packetList) _sourceID(packetList.getSourceID()), _packetType(packetList.getType()), _packetVersion(packetList.getVersion()), - _senderSockAddr(packetList.getSenderSockAddr()), - _isComplete(true) + _senderSockAddr(packetList.getSenderSockAddr()) { } diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index 4031ff8bf7..8d4edab2d5 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -474,8 +474,9 @@ int ResourceCache::getLoadingRequestCount() { bool ResourceCache::attemptRequest(QSharedPointer resource) { Q_ASSERT(!resource.isNull()); - auto sharedItems = DependencyManager::get(); + + auto sharedItems = DependencyManager::get(); if (_requestsActive >= _requestLimit) { // wait until a slot becomes available sharedItems->appendPendingRequest(resource); @@ -490,6 +491,7 @@ bool ResourceCache::attemptRequest(QSharedPointer resource) { void ResourceCache::requestCompleted(QWeakPointer resource) { auto sharedItems = DependencyManager::get(); + sharedItems->removeRequest(resource); --_requestsActive; @@ -553,6 +555,10 @@ void Resource::clearLoadPriority(const QPointer& owner) { } float Resource::getLoadPriority() { + if (_loadPriorities.size() == 0) { + return 0; + } + float highestPriority = -FLT_MAX; for (QHash, float>::iterator it = _loadPriorities.begin(); it != _loadPriorities.end(); ) { if (it.key().isNull()) { @@ -621,8 +627,6 @@ void Resource::init() { } } -const int MAX_ATTEMPTS = 8; - void Resource::attemptRequest() { _startedLoading = true; @@ -637,12 +641,12 @@ void Resource::attemptRequest() { void Resource::finishedLoading(bool success) { if (success) { qCDebug(networking).noquote() << "Finished loading:" << _url.toDisplayString(); + _loadPriorities.clear(); _loaded = true; } else { qCDebug(networking).noquote() << "Failed to load:" << _url.toDisplayString(); _failedToLoad = true; } - _loadPriorities.clear(); emit finished(success); } @@ -676,6 +680,8 @@ void Resource::makeRequest() { return; } + _request->setByteRange(_requestByteRange); + qCDebug(resourceLog).noquote() << "Starting request for:" << _url.toDisplayString(); emit loading(); @@ -722,34 +728,7 @@ void Resource::handleReplyFinished() { emit loaded(data); downloadFinished(data); } else { - switch (result) { - case ResourceRequest::Result::Timeout: { - qCDebug(networking) << "Timed out loading" << _url << "received" << _bytesReceived << "total" << _bytesTotal; - // Fall through to other cases - } - case ResourceRequest::Result::ServerUnavailable: { - // retry with increasing delays - const int BASE_DELAY_MS = 1000; - if (_attempts++ < MAX_ATTEMPTS) { - auto waitTime = BASE_DELAY_MS * (int)pow(2.0, _attempts); - - qCDebug(networking).noquote() << "Server unavailable for" << _url << "- may retry in" << waitTime << "ms" - << "if resource is still needed"; - - QTimer::singleShot(waitTime, this, &Resource::attemptRequest); - break; - } - // fall through to final failure - } - default: { - qCDebug(networking) << "Error loading " << _url; - auto error = (result == ResourceRequest::Timeout) ? QNetworkReply::TimeoutError - : QNetworkReply::UnknownNetworkError; - emit failed(error); - finishedLoading(false); - break; - } - } + handleFailedRequest(result); } _request->disconnect(this); @@ -757,6 +736,45 @@ void Resource::handleReplyFinished() { _request = nullptr; } +bool Resource::handleFailedRequest(ResourceRequest::Result result) { + bool willRetry = false; + switch (result) { + case ResourceRequest::Result::Timeout: { + qCDebug(networking) << "Timed out loading" << _url << "received" << _bytesReceived << "total" << _bytesTotal; + // Fall through to other cases + } + case ResourceRequest::Result::ServerUnavailable: { + _attempts++; + _attemptsRemaining--; + + qCDebug(networking) << "Retryable error while loading" << _url << "attempt:" << _attempts << "attemptsRemaining:" << _attemptsRemaining; + + // retry with increasing delays + const int BASE_DELAY_MS = 1000; + if (_attempts < MAX_ATTEMPTS) { + auto waitTime = BASE_DELAY_MS * (int)pow(2.0, _attempts); + qCDebug(networking).noquote() << "Server unavailable for" << _url << "- may retry in" << waitTime << "ms" + << "if resource is still needed"; + QTimer::singleShot(waitTime, this, &Resource::attemptRequest); + willRetry = true; + break; + } + // fall through to final failure + } + default: { + _attemptsRemaining = 0; + qCDebug(networking) << "Error loading " << _url << "attempt:" << _attempts << "attemptsRemaining:" << _attemptsRemaining; + auto error = (result == ResourceRequest::Timeout) ? QNetworkReply::TimeoutError + : QNetworkReply::UnknownNetworkError; + emit failed(error); + willRetry = false; + finishedLoading(false); + break; + } + } + return willRetry; +} + uint qHash(const QPointer& value, uint seed) { return qHash(value.data(), seed); } diff --git a/libraries/networking/src/ResourceCache.h b/libraries/networking/src/ResourceCache.h index 53ccd2c386..d4c7d63ee5 100644 --- a/libraries/networking/src/ResourceCache.h +++ b/libraries/networking/src/ResourceCache.h @@ -395,6 +395,9 @@ public: const QUrl& getURL() const { return _url; } + unsigned int getDownloadAttempts() { return _attempts; } + unsigned int getDownloadAttemptsRemaining() { return _attemptsRemaining; } + signals: /// Fired when the resource begins downloading. void loading(); @@ -424,6 +427,11 @@ protected slots: protected: virtual void init(); + /// Called by ResourceCache to begin loading this Resource. + /// This method can be overriden to provide custom request functionality. If this is done, + /// downloadFinished and ResourceCache::requestCompleted must be called. + virtual void makeRequest(); + /// Checks whether the resource is cacheable. virtual bool isCacheable() const { return true; } @@ -440,16 +448,27 @@ protected: Q_INVOKABLE void allReferencesCleared(); + /// Return true if the resource will be retried + bool handleFailedRequest(ResourceRequest::Result result); + QUrl _url; QUrl _activeUrl; + ByteRange _requestByteRange; bool _startedLoading = false; bool _failedToLoad = false; bool _loaded = false; QHash, float> _loadPriorities; QWeakPointer _self; QPointer _cache; - -private slots: + + qint64 _bytesReceived{ 0 }; + qint64 _bytesTotal{ 0 }; + qint64 _bytes{ 0 }; + + int _requestID; + ResourceRequest* _request{ nullptr }; + +public slots: void handleDownloadProgress(uint64_t bytesReceived, uint64_t bytesTotal); void handleReplyFinished(); @@ -459,21 +478,17 @@ private: void setLRUKey(int lruKey) { _lruKey = lruKey; } - void makeRequest(); void retry(); void reinsert(); bool isInScript() const { return _isInScript; } void setInScript(bool isInScript) { _isInScript = isInScript; } - int _requestID; - ResourceRequest* _request{ nullptr }; int _lruKey{ 0 }; QTimer* _replyTimer{ nullptr }; - qint64 _bytesReceived{ 0 }; - qint64 _bytesTotal{ 0 }; - qint64 _bytes{ 0 }; - int _attempts{ 0 }; + unsigned int _attempts{ 0 }; + static const int MAX_ATTEMPTS = 8; + unsigned int _attemptsRemaining { MAX_ATTEMPTS }; bool _isInScript{ false }; }; diff --git a/libraries/networking/src/ResourceManager.h b/libraries/networking/src/ResourceManager.h index 162892abaf..d193c39cae 100644 --- a/libraries/networking/src/ResourceManager.h +++ b/libraries/networking/src/ResourceManager.h @@ -26,6 +26,7 @@ const QString URL_SCHEME_ATP = "atp"; class ResourceManager { public: + static void setUrlPrefixOverride(const QString& prefix, const QString& replacement); static QString normalizeURL(const QString& urlString); static QUrl normalizeURL(const QUrl& url); diff --git a/libraries/networking/src/ResourceRequest.h b/libraries/networking/src/ResourceRequest.h index 7588fca046..ef40cb3455 100644 --- a/libraries/networking/src/ResourceRequest.h +++ b/libraries/networking/src/ResourceRequest.h @@ -17,6 +17,8 @@ #include +#include "ByteRange.h" + class ResourceRequest : public QObject { Q_OBJECT public: @@ -35,6 +37,7 @@ public: Timeout, ServerUnavailable, AccessDenied, + InvalidByteRange, InvalidURL, NotFound }; @@ -46,8 +49,11 @@ public: QString getResultString() const; QUrl getUrl() const { return _url; } bool loadedFromCache() const { return _loadedFromCache; } + bool getRangeRequestSuccessful() const { return _rangeRequestSuccessful; } + bool getTotalSizeOfResource() const { return _totalSizeOfResource; } void setCacheEnabled(bool value) { _cacheEnabled = value; } + void setByteRange(ByteRange byteRange) { _byteRange = byteRange; } public slots: void send(); @@ -65,6 +71,9 @@ protected: QByteArray _data; bool _cacheEnabled { true }; bool _loadedFromCache { false }; + ByteRange _byteRange; + bool _rangeRequestSuccessful { false }; + uint64_t _totalSizeOfResource { 0 }; }; #endif diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp index 9c29e87f16..ff69363570 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp @@ -13,28 +13,28 @@ #include "UserActivityLogger.h" void UserActivityLoggerScriptingInterface::enabledEdit() { - logAction("enabled_edit"); + doLogAction("enabled_edit"); } void UserActivityLoggerScriptingInterface::openedTablet(bool visibleToOthers) { - logAction("opened_tablet", { { "visible_to_others", visibleToOthers } }); + doLogAction("opened_tablet", { { "visible_to_others", visibleToOthers } }); } void UserActivityLoggerScriptingInterface::closedTablet() { - logAction("closed_tablet"); + doLogAction("closed_tablet"); } void UserActivityLoggerScriptingInterface::openedMarketplace() { - logAction("opened_marketplace"); + doLogAction("opened_marketplace"); } void UserActivityLoggerScriptingInterface::toggledAway(bool isAway) { - logAction("toggled_away", { { "is_away", isAway } }); + doLogAction("toggled_away", { { "is_away", isAway } }); } void UserActivityLoggerScriptingInterface::tutorialProgress( QString stepName, int stepNumber, float secondsToComplete, float tutorialElapsedTime, QString tutorialRunID, int tutorialVersion, QString controllerType) { - logAction("tutorial_progress", { + doLogAction("tutorial_progress", { { "tutorial_run_id", tutorialRunID }, { "tutorial_version", tutorialVersion }, { "step", stepName }, @@ -52,11 +52,11 @@ void UserActivityLoggerScriptingInterface::palAction(QString action, QString tar if (target.length() > 0) { payload["target"] = target; } - logAction("pal_activity", payload); + doLogAction("pal_activity", payload); } void UserActivityLoggerScriptingInterface::palOpened(float secondsOpened) { - logAction("pal_opened", { + doLogAction("pal_opened", { { "seconds_opened", secondsOpened } }); } @@ -68,10 +68,14 @@ void UserActivityLoggerScriptingInterface::makeUserConnection(QString otherID, b if (detailsString.length() > 0) { payload["details"] = detailsString; } - logAction("makeUserConnection", payload); + doLogAction("makeUserConnection", payload); } -void UserActivityLoggerScriptingInterface::logAction(QString action, QJsonObject details) { +void UserActivityLoggerScriptingInterface::logAction(QString action, QVariantMap details) { + doLogAction(action, QJsonObject::fromVariantMap(details)); +} + +void UserActivityLoggerScriptingInterface::doLogAction(QString action, QJsonObject details) { QMetaObject::invokeMethod(&UserActivityLogger::getInstance(), "logAction", Q_ARG(QString, action), Q_ARG(QJsonObject, details)); diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.h b/libraries/networking/src/UserActivityLoggerScriptingInterface.h index b68c7beb95..b141e930f2 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.h +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.h @@ -29,9 +29,10 @@ public: float tutorialElapsedTime, QString tutorialRunID = "", int tutorialVersion = 0, QString controllerType = ""); Q_INVOKABLE void palAction(QString action, QString target); Q_INVOKABLE void palOpened(float secondsOpen); - Q_INVOKABLE void makeUserConnection(QString otherUser, bool success, QString details=""); + Q_INVOKABLE void makeUserConnection(QString otherUser, bool success, QString details = ""); + Q_INVOKABLE void logAction(QString action, QVariantMap details = QVariantMap{}); private: - void logAction(QString action, QJsonObject details = {}); + void doLogAction(QString action, QJsonObject details = {}); }; #endif // hifi_UserActivityLoggerScriptingInterface_h diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 246821908a..adaa7a848c 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -39,7 +39,7 @@ const QSet NON_SOURCED_PACKETS = QSet() << PacketType::ICEServerPeerInformation << PacketType::ICEServerQuery << PacketType::ICEServerHeartbeat << PacketType::ICEServerHeartbeatACK << PacketType::ICEPing << PacketType::ICEPingReply << PacketType::ICEServerHeartbeatDenied << PacketType::AssignmentClientStatus << PacketType::StopNode - << PacketType::DomainServerRemovedNode << PacketType::UsernameFromIDReply; + << PacketType::DomainServerRemovedNode << PacketType::UsernameFromIDReply << PacketType::OctreeFileReplacement; PacketVersion versionForPacketType(PacketType packetType) { switch (packetType) { @@ -49,14 +49,14 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityEdit: case PacketType::EntityData: case PacketType::EntityPhysics: - return VERSION_ENTITIES_ZONE_FILTERS; + return VERSION_ENTITIES_HINGE_CONSTRAINT; case PacketType::EntityQuery: return static_cast(EntityQueryPacketVersion::JSONFilterWithFamilyTree); case PacketType::AvatarIdentity: case PacketType::AvatarData: case PacketType::BulkAvatarData: case PacketType::KillAvatar: - return static_cast(AvatarMixerPacketVersion::StickAndBallDefaultAvatar); + return static_cast(AvatarMixerPacketVersion::IdentityPacketsIncludeUpdateTime); case PacketType::MessagesData: return static_cast(MessageDataVersion::TextOrBinaryData); case PacketType::ICEServerHeartbeat: @@ -64,7 +64,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::AssetGetInfo: case PacketType::AssetGet: case PacketType::AssetUpload: - return static_cast(AssetServerPacketVersion::VegasCongestionControl); + return static_cast(AssetServerPacketVersion::RangeRequestSupport); case PacketType::NodeIgnoreRequest: return 18; // Introduction of node ignore request (which replaced an unused packet tpye) diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 03a773f24f..f803b83887 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -113,7 +113,8 @@ public: EntityPhysics, EntityServerScriptLog, AdjustAvatarSorting, - LAST_PACKET_TYPE = AdjustAvatarSorting + OctreeFileReplacement, + LAST_PACKET_TYPE = OctreeFileReplacement }; }; @@ -206,6 +207,7 @@ const PacketVersion VERSION_ENTITIES_LAST_EDITED_BY = 65; const PacketVersion VERSION_ENTITIES_SERVER_SCRIPTS = 66; const PacketVersion VERSION_ENTITIES_PHYSICS_PACKET = 67; const PacketVersion VERSION_ENTITIES_ZONE_FILTERS = 68; +const PacketVersion VERSION_ENTITIES_HINGE_CONSTRAINT = 69; enum class EntityQueryPacketVersion: PacketVersion { JSONFilter = 18, @@ -213,7 +215,8 @@ enum class EntityQueryPacketVersion: PacketVersion { }; enum class AssetServerPacketVersion: PacketVersion { - VegasCongestionControl = 19 + VegasCongestionControl = 19, + RangeRequestSupport }; enum class AvatarMixerPacketVersion : PacketVersion { @@ -229,7 +232,8 @@ enum class AvatarMixerPacketVersion : PacketVersion { ImmediateSessionDisplayNameUpdates, VariableAvatarData, AvatarAsChildFixes, - StickAndBallDefaultAvatar + StickAndBallDefaultAvatar, + IdentityPacketsIncludeUpdateTime }; enum class DomainConnectRequestVersion : PacketVersion { diff --git a/libraries/octree/src/OctreePersistThread.cpp b/libraries/octree/src/OctreePersistThread.cpp index 7034790eaf..ea6bd28fc4 100644 --- a/libraries/octree/src/OctreePersistThread.cpp +++ b/libraries/octree/src/OctreePersistThread.cpp @@ -33,6 +33,7 @@ #include "OctreePersistThread.h" const int OctreePersistThread::DEFAULT_PERSIST_INTERVAL = 1000 * 30; // every 30 seconds +const QString OctreePersistThread::REPLACEMENT_FILE_EXTENSION = ".replace"; OctreePersistThread::OctreePersistThread(OctreePointer tree, const QString& filename, const QString& backupDirectory, int persistInterval, bool wantBackup, const QJsonObject& settings, bool debugTimestampNow, @@ -131,10 +132,47 @@ quint64 OctreePersistThread::getMostRecentBackupTimeInUsecs(const QString& forma return mostRecentBackupInUsecs; } +void OctreePersistThread::possiblyReplaceContent() { + // before we load the normal file, check if there's a pending replacement file + auto replacementFileName = _filename + REPLACEMENT_FILE_EXTENSION; + + QFile replacementFile { replacementFileName }; + if (replacementFile.exists()) { + // we have a replacement file to process + qDebug() << "Replacing models file with" << replacementFileName; + + // first take the current models file and move it to a different filename, appended with the timestamp + QFile currentFile { _filename }; + if (currentFile.exists()) { + static const QString FILENAME_TIMESTAMP_FORMAT = "yyyyMMdd-hhmmss"; + auto backupFileName = _filename + ".backup." + QDateTime::currentDateTime().toString(FILENAME_TIMESTAMP_FORMAT); + + if (currentFile.rename(backupFileName)) { + qDebug() << "Moved previous models file to" << backupFileName; + } else { + qWarning() << "Could not backup previous models file to" << backupFileName << "- removing replacement models file"; + + if (!replacementFile.remove()) { + qWarning() << "Could not remove replacement models file from" << replacementFileName + << "- replacement will be re-attempted on next server restart"; + return; + } + } + } + + // rename the replacement file to match what the persist thread is just about to read + if (!replacementFile.rename(_filename)) { + qWarning() << "Could not replace models file with" << replacementFileName << "- starting with empty models file"; + } + } +} + bool OctreePersistThread::process() { if (!_initialLoadComplete) { + possiblyReplaceContent(); + quint64 loadStarted = usecTimestampNow(); qCDebug(octree) << "loading Octrees from file: " << _filename << "..."; diff --git a/libraries/octree/src/OctreePersistThread.h b/libraries/octree/src/OctreePersistThread.h index 927304e862..2441223467 100644 --- a/libraries/octree/src/OctreePersistThread.h +++ b/libraries/octree/src/OctreePersistThread.h @@ -32,6 +32,7 @@ public: }; static const int DEFAULT_PERSIST_INTERVAL; + static const QString REPLACEMENT_FILE_EXTENSION; OctreePersistThread(OctreePointer tree, const QString& filename, const QString& backupDirectory, int persistInterval = DEFAULT_PERSIST_INTERVAL, bool wantBackup = false, @@ -60,6 +61,7 @@ protected: bool getMostRecentBackup(const QString& format, QString& mostRecentBackupFileName, QDateTime& mostRecentBackupTime); quint64 getMostRecentBackupTimeInUsecs(const QString& format); void parseSettings(const QJsonObject& settings); + void possiblyReplaceContent(); private: OctreePointer _tree; diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index d383f4c199..0c804fb5b7 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -94,7 +94,7 @@ void EntityMotionState::updateServerPhysicsVariables() { _serverPosition = localTransform.getTranslation(); _serverRotation = localTransform.getRotation(); _serverAcceleration = _entity->getAcceleration(); - _serverActionData = _entity->getActionData(); + _serverActionData = _entity->getDynamicData(); } void EntityMotionState::handleDeactivation() { @@ -309,7 +309,7 @@ bool EntityMotionState::isCandidateForOwnership() const { assert(entityTreeIsLocked()); return _outgoingPriority != 0 || Physics::getSessionUUID() == _entity->getSimulatorID() - || _entity->actionDataNeedsTransmit(); + || _entity->dynamicDataNeedsTransmit(); } bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { @@ -335,7 +335,7 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { _serverAcceleration = Vectors::ZERO; _serverAngularVelocity = worldVelocityToLocal.transform(bulletToGLM(_body->getAngularVelocity())); _lastStep = simulationStep; - _serverActionData = _entity->getActionData(); + _serverActionData = _entity->getDynamicData(); _numInactiveUpdates = 1; return false; } @@ -387,7 +387,7 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { } } - if (_entity->actionDataNeedsTransmit()) { + if (_entity->dynamicDataNeedsTransmit()) { _outgoingPriority = _entity->hasActions() ? SCRIPT_GRAB_SIMULATION_PRIORITY : SCRIPT_POKE_SIMULATION_PRIORITY; return true; } @@ -474,7 +474,7 @@ bool EntityMotionState::shouldSendUpdate(uint32_t simulationStep) { return false; } - if (_entity->actionDataNeedsTransmit()) { + if (_entity->dynamicDataNeedsTransmit()) { return true; } @@ -551,7 +551,7 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ _serverPosition = localTransform.getTranslation(); _serverRotation = localTransform.getRotation(); _serverAcceleration = _entity->getAcceleration(); - _serverActionData = _entity->getActionData(); + _serverActionData = _entity->getDynamicData(); EntityItemProperties properties; @@ -562,8 +562,8 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ properties.setVelocity(_serverVelocity); properties.setAcceleration(_serverAcceleration); properties.setAngularVelocity(_serverAngularVelocity); - if (_entity->actionDataNeedsTransmit()) { - _entity->setActionDataNeedsTransmit(false); + if (_entity->dynamicDataNeedsTransmit()) { + _entity->setDynamicDataNeedsTransmit(false); properties.setActionData(_serverActionData); } diff --git a/libraries/physics/src/ObjectAction.cpp b/libraries/physics/src/ObjectAction.cpp index 95448ad029..5f5f763ca6 100644 --- a/libraries/physics/src/ObjectAction.cpp +++ b/libraries/physics/src/ObjectAction.cpp @@ -16,13 +16,10 @@ #include "PhysicsLogging.h" -ObjectAction::ObjectAction(EntityActionType type, const QUuid& id, EntityItemPointer ownerEntity) : +ObjectAction::ObjectAction(EntityDynamicType type, const QUuid& id, EntityItemPointer ownerEntity) : btActionInterface(), - EntityActionInterface(type, id), - _ownerEntity(ownerEntity) { -} - -ObjectAction::~ObjectAction() { + ObjectDynamic(type, id, ownerEntity) +{ } void ObjectAction::updateAction(btCollisionWorld* collisionWorld, btScalar deltaTimeStep) { @@ -35,7 +32,7 @@ void ObjectAction::updateAction(btCollisionWorld* collisionWorld, btScalar delta }); if (!ownerEntity) { - qCDebug(physics) << "warning -- action with no entity removing self from btCollisionWorld."; + qCDebug(physics) << "warning -- action [" << _tag << "] with no entity removing self from btCollisionWorld."; btDynamicsWorld* dynamicsWorld = static_cast(collisionWorld); if (dynamicsWorld) { dynamicsWorld->removeAction(this); @@ -64,240 +61,5 @@ void ObjectAction::updateAction(btCollisionWorld* collisionWorld, btScalar delta updateActionWorker(deltaTimeStep); } -qint64 ObjectAction::getEntityServerClockSkew() const { - auto nodeList = DependencyManager::get(); - - auto ownerEntity = _ownerEntity.lock(); - if (!ownerEntity) { - return 0; - } - - const QUuid& entityServerNodeID = ownerEntity->getSourceUUID(); - auto entityServerNode = nodeList->nodeWithUUID(entityServerNodeID); - if (entityServerNode) { - return entityServerNode->getClockSkewUsec(); - } - return 0; -} - -bool ObjectAction::updateArguments(QVariantMap arguments) { - bool somethingChanged = false; - - withWriteLock([&]{ - quint64 previousExpires = _expires; - QString previousTag = _tag; - - bool ttlSet = true; - float ttl = EntityActionInterface::extractFloatArgument("action", arguments, "ttl", ttlSet, false); - if (ttlSet) { - quint64 now = usecTimestampNow(); - _expires = now + (quint64)(ttl * USECS_PER_SECOND); - } else { - _expires = 0; - } - - bool tagSet = true; - QString tag = EntityActionInterface::extractStringArgument("action", arguments, "tag", tagSet, false); - if (tagSet) { - _tag = tag; - } else { - tag = ""; - } - - if (previousExpires != _expires || previousTag != _tag) { - somethingChanged = true; - } - }); - - return somethingChanged; -} - -QVariantMap ObjectAction::getArguments() { - QVariantMap arguments; - withReadLock([&]{ - if (_expires == 0) { - arguments["ttl"] = 0.0f; - } else { - quint64 now = usecTimestampNow(); - arguments["ttl"] = (float)(_expires - now) / (float)USECS_PER_SECOND; - } - arguments["tag"] = _tag; - - EntityItemPointer entity = _ownerEntity.lock(); - if (entity) { - ObjectMotionState* motionState = static_cast(entity->getPhysicsInfo()); - if (motionState) { - arguments["::active"] = motionState->isActive(); - arguments["::motion-type"] = motionTypeToString(motionState->getMotionType()); - } else { - arguments["::no-motion-state"] = true; - } - } - arguments["isMine"] = isMine(); - }); - return arguments; -} - - void ObjectAction::debugDraw(btIDebugDraw* debugDrawer) { } - -void ObjectAction::removeFromSimulation(EntitySimulationPointer simulation) const { - QUuid myID; - withReadLock([&]{ - myID = _id; - }); - simulation->removeAction(myID); -} - -btRigidBody* ObjectAction::getRigidBody() { - ObjectMotionState* motionState = nullptr; - withReadLock([&]{ - auto ownerEntity = _ownerEntity.lock(); - if (!ownerEntity) { - return; - } - void* physicsInfo = ownerEntity->getPhysicsInfo(); - if (!physicsInfo) { - return; - } - motionState = static_cast(physicsInfo); - }); - if (motionState) { - return motionState->getRigidBody(); - } - return nullptr; -} - -glm::vec3 ObjectAction::getPosition() { - auto rigidBody = getRigidBody(); - if (!rigidBody) { - return glm::vec3(0.0f); - } - return bulletToGLM(rigidBody->getCenterOfMassPosition()); -} - -void ObjectAction::setPosition(glm::vec3 position) { - auto rigidBody = getRigidBody(); - if (!rigidBody) { - return; - } - // XXX - // void setWorldTransform (const btTransform &worldTrans) - assert(false); - rigidBody->activate(); -} - -glm::quat ObjectAction::getRotation() { - auto rigidBody = getRigidBody(); - if (!rigidBody) { - return glm::quat(0.0f, 0.0f, 0.0f, 1.0f); - } - return bulletToGLM(rigidBody->getOrientation()); -} - -void ObjectAction::setRotation(glm::quat rotation) { - auto rigidBody = getRigidBody(); - if (!rigidBody) { - return; - } - // XXX - // void setWorldTransform (const btTransform &worldTrans) - assert(false); - rigidBody->activate(); -} - -glm::vec3 ObjectAction::getLinearVelocity() { - auto rigidBody = getRigidBody(); - if (!rigidBody) { - return glm::vec3(0.0f); - } - return bulletToGLM(rigidBody->getLinearVelocity()); -} - -void ObjectAction::setLinearVelocity(glm::vec3 linearVelocity) { - auto rigidBody = getRigidBody(); - if (!rigidBody) { - return; - } - rigidBody->setLinearVelocity(glmToBullet(glm::vec3(0.0f))); - rigidBody->activate(); -} - -glm::vec3 ObjectAction::getAngularVelocity() { - auto rigidBody = getRigidBody(); - if (!rigidBody) { - return glm::vec3(0.0f); - } - return bulletToGLM(rigidBody->getAngularVelocity()); -} - -void ObjectAction::setAngularVelocity(glm::vec3 angularVelocity) { - auto rigidBody = getRigidBody(); - if (!rigidBody) { - return; - } - rigidBody->setAngularVelocity(glmToBullet(angularVelocity)); - rigidBody->activate(); -} - -void ObjectAction::activateBody(bool forceActivation) { - auto rigidBody = getRigidBody(); - if (rigidBody) { - rigidBody->activate(forceActivation); - } else { - qCDebug(physics) << "ObjectAction::activateBody -- no rigid body" << (void*)rigidBody; - } -} - -void ObjectAction::forceBodyNonStatic() { - auto ownerEntity = _ownerEntity.lock(); - if (!ownerEntity) { - return; - } - void* physicsInfo = ownerEntity->getPhysicsInfo(); - ObjectMotionState* motionState = static_cast(physicsInfo); - if (motionState && motionState->getMotionType() == MOTION_TYPE_STATIC) { - ownerEntity->flagForMotionStateChange(); - } -} - -bool ObjectAction::lifetimeIsOver() { - if (_expires == 0) { - return false; - } - - quint64 now = usecTimestampNow(); - if (now >= _expires) { - return true; - } - return false; -} - -quint64 ObjectAction::localTimeToServerTime(quint64 timeValue) const { - // 0 indicates no expiration - if (timeValue == 0) { - return 0; - } - - qint64 serverClockSkew = getEntityServerClockSkew(); - if (serverClockSkew < 0 && timeValue <= (quint64)(-serverClockSkew)) { - return 1; // non-zero but long-expired value to avoid negative roll-over - } - - return timeValue + serverClockSkew; -} - -quint64 ObjectAction::serverTimeToLocalTime(quint64 timeValue) const { - // 0 indicates no expiration - if (timeValue == 0) { - return 0; - } - - qint64 serverClockSkew = getEntityServerClockSkew(); - if (serverClockSkew > 0 && timeValue <= (quint64)serverClockSkew) { - return 1; // non-zero but long-expired value to avoid negative roll-over - } - - return timeValue - serverClockSkew; -} diff --git a/libraries/physics/src/ObjectAction.h b/libraries/physics/src/ObjectAction.h index 43330269ac..fb141a4620 100644 --- a/libraries/physics/src/ObjectAction.h +++ b/libraries/physics/src/ObjectAction.h @@ -14,27 +14,15 @@ #define hifi_ObjectAction_h #include - #include +#include "ObjectDynamic.h" -#include - -#include "ObjectMotionState.h" -#include "BulletUtil.h" -#include "EntityActionInterface.h" - - -class ObjectAction : public btActionInterface, public EntityActionInterface, public ReadWriteLockable { +class ObjectAction : public btActionInterface, public ObjectDynamic { public: - ObjectAction(EntityActionType type, const QUuid& id, EntityItemPointer ownerEntity); - virtual ~ObjectAction(); + ObjectAction(EntityDynamicType type, const QUuid& id, EntityItemPointer ownerEntity); + virtual ~ObjectAction() {} - virtual void removeFromSimulation(EntitySimulationPointer simulation) const override; - virtual EntityItemWeakPointer getOwnerEntity() const override { return _ownerEntity; } - virtual void setOwnerEntity(const EntityItemPointer ownerEntity) override { _ownerEntity = ownerEntity; } - - virtual bool updateArguments(QVariantMap arguments) override; - virtual QVariantMap getArguments() override; + virtual bool isAction() const override { return true; } // this is called from updateAction and should be overridden by subclasses virtual void updateActionWorker(float deltaTimeStep) = 0; @@ -42,35 +30,6 @@ public: // these are from btActionInterface virtual void updateAction(btCollisionWorld* collisionWorld, btScalar deltaTimeStep) override; virtual void debugDraw(btIDebugDraw* debugDrawer) override; - - virtual QByteArray serialize() const override = 0; - virtual void deserialize(QByteArray serializedArguments) override = 0; - - virtual bool lifetimeIsOver() override; - virtual quint64 getExpires() override { return _expires; } - -protected: - quint64 localTimeToServerTime(quint64 timeValue) const; - quint64 serverTimeToLocalTime(quint64 timeValue) const; - - virtual btRigidBody* getRigidBody(); - virtual glm::vec3 getPosition() override; - virtual void setPosition(glm::vec3 position) override; - virtual glm::quat getRotation() override; - virtual void setRotation(glm::quat rotation) override; - virtual glm::vec3 getLinearVelocity() override; - virtual void setLinearVelocity(glm::vec3 linearVelocity) override; - virtual glm::vec3 getAngularVelocity() override; - virtual void setAngularVelocity(glm::vec3 angularVelocity) override; - virtual void activateBody(bool forceActivation = false); - virtual void forceBodyNonStatic(); - - EntityItemWeakPointer _ownerEntity; - QString _tag; - quint64 _expires { 0 }; // in seconds since epoch - -private: - qint64 getEntityServerClockSkew() const; }; #endif // hifi_ObjectAction_h diff --git a/libraries/physics/src/ObjectActionOffset.cpp b/libraries/physics/src/ObjectActionOffset.cpp index f23b3985de..c1fb397e19 100644 --- a/libraries/physics/src/ObjectActionOffset.cpp +++ b/libraries/physics/src/ObjectActionOffset.cpp @@ -19,7 +19,7 @@ const uint16_t ObjectActionOffset::offsetVersion = 1; ObjectActionOffset::ObjectActionOffset(const QUuid& id, EntityItemPointer ownerEntity) : - ObjectAction(ACTION_TYPE_OFFSET, id, ownerEntity), + ObjectAction(DYNAMIC_TYPE_OFFSET, id, ownerEntity), _pointToOffsetFrom(0.0f), _linearDistance(0.0f), _linearTimeScale(FLT_MAX), @@ -88,26 +88,26 @@ bool ObjectActionOffset::updateArguments(QVariantMap arguments) { float linearDistance; bool needUpdate = false; - bool somethingChanged = ObjectAction::updateArguments(arguments); + bool somethingChanged = ObjectDynamic::updateArguments(arguments); withReadLock([&]{ bool ok = true; pointToOffsetFrom = - EntityActionInterface::extractVec3Argument("offset action", arguments, "pointToOffsetFrom", ok, true); + EntityDynamicInterface::extractVec3Argument("offset action", arguments, "pointToOffsetFrom", ok, true); if (!ok) { pointToOffsetFrom = _pointToOffsetFrom; } ok = true; linearTimeScale = - EntityActionInterface::extractFloatArgument("offset action", arguments, "linearTimeScale", ok, false); + EntityDynamicInterface::extractFloatArgument("offset action", arguments, "linearTimeScale", ok, false); if (!ok) { linearTimeScale = _linearTimeScale; } ok = true; linearDistance = - EntityActionInterface::extractFloatArgument("offset action", arguments, "linearDistance", ok, false); + EntityDynamicInterface::extractFloatArgument("offset action", arguments, "linearDistance", ok, false); if (!ok) { linearDistance = _linearDistance; } @@ -132,8 +132,8 @@ bool ObjectActionOffset::updateArguments(QVariantMap arguments) { auto ownerEntity = _ownerEntity.lock(); if (ownerEntity) { - ownerEntity->setActionDataDirty(true); - ownerEntity->setActionDataNeedsTransmit(true); + ownerEntity->setDynamicDataDirty(true); + ownerEntity->setDynamicDataNeedsTransmit(true); } }); activateBody(); @@ -143,7 +143,7 @@ bool ObjectActionOffset::updateArguments(QVariantMap arguments) { } QVariantMap ObjectActionOffset::getArguments() { - QVariantMap arguments = ObjectAction::getArguments(); + QVariantMap arguments = ObjectDynamic::getArguments(); withReadLock([&] { arguments["pointToOffsetFrom"] = glmToQMap(_pointToOffsetFrom); arguments["linearTimeScale"] = _linearTimeScale; @@ -155,7 +155,7 @@ QVariantMap ObjectActionOffset::getArguments() { QByteArray ObjectActionOffset::serialize() const { QByteArray ba; QDataStream dataStream(&ba, QIODevice::WriteOnly); - dataStream << ACTION_TYPE_OFFSET; + dataStream << DYNAMIC_TYPE_OFFSET; dataStream << getID(); dataStream << ObjectActionOffset::offsetVersion; @@ -174,7 +174,7 @@ QByteArray ObjectActionOffset::serialize() const { void ObjectActionOffset::deserialize(QByteArray serializedArguments) { QDataStream dataStream(serializedArguments); - EntityActionType type; + EntityDynamicType type; dataStream >> type; assert(type == getType()); diff --git a/libraries/physics/src/ObjectActionSpring.cpp b/libraries/physics/src/ObjectActionSpring.cpp index b22b3c3368..df7e5f87a3 100644 --- a/libraries/physics/src/ObjectActionSpring.cpp +++ b/libraries/physics/src/ObjectActionSpring.cpp @@ -21,7 +21,7 @@ const uint16_t ObjectActionSpring::springVersion = 1; ObjectActionSpring::ObjectActionSpring(const QUuid& id, EntityItemPointer ownerEntity) : - ObjectAction(ACTION_TYPE_SPRING, id, ownerEntity), + ObjectAction(DYNAMIC_TYPE_SPRING, id, ownerEntity), _positionalTarget(glm::vec3(0.0f)), _desiredPositionalTarget(glm::vec3(0.0f)), _linearTimeScale(FLT_MAX), @@ -64,11 +64,12 @@ bool ObjectActionSpring::prepareForSpringUpdate(btScalar deltaTimeStep) { bool valid = false; int springCount = 0; - QList springDerivedActions; - springDerivedActions.append(ownerEntity->getActionsOfType(ACTION_TYPE_SPRING)); - springDerivedActions.append(ownerEntity->getActionsOfType(ACTION_TYPE_HOLD)); + QList springDerivedActions; + springDerivedActions.append(ownerEntity->getActionsOfType(DYNAMIC_TYPE_SPRING)); + springDerivedActions.append(ownerEntity->getActionsOfType(DYNAMIC_TYPE_FAR_GRAB)); + springDerivedActions.append(ownerEntity->getActionsOfType(DYNAMIC_TYPE_HOLD)); - foreach (EntityActionPointer action, springDerivedActions) { + foreach (EntityDynamicPointer action, springDerivedActions) { std::shared_ptr springAction = std::static_pointer_cast(action); glm::quat rotationForAction; glm::vec3 positionForAction; @@ -190,29 +191,29 @@ bool ObjectActionSpring::updateArguments(QVariantMap arguments) { float angularTimeScale; bool needUpdate = false; - bool somethingChanged = ObjectAction::updateArguments(arguments); + bool somethingChanged = ObjectDynamic::updateArguments(arguments); withReadLock([&]{ // targets are required, spring-constants are optional bool ok = true; - positionalTarget = EntityActionInterface::extractVec3Argument("spring action", arguments, "targetPosition", ok, false); + positionalTarget = EntityDynamicInterface::extractVec3Argument("spring action", arguments, "targetPosition", ok, false); if (!ok) { positionalTarget = _desiredPositionalTarget; } ok = true; - linearTimeScale = EntityActionInterface::extractFloatArgument("spring action", arguments, "linearTimeScale", ok, false); + linearTimeScale = EntityDynamicInterface::extractFloatArgument("spring action", arguments, "linearTimeScale", ok, false); if (!ok || linearTimeScale <= 0.0f) { linearTimeScale = _linearTimeScale; } ok = true; - rotationalTarget = EntityActionInterface::extractQuatArgument("spring action", arguments, "targetRotation", ok, false); + rotationalTarget = EntityDynamicInterface::extractQuatArgument("spring action", arguments, "targetRotation", ok, false); if (!ok) { rotationalTarget = _desiredRotationalTarget; } ok = true; angularTimeScale = - EntityActionInterface::extractFloatArgument("spring action", arguments, "angularTimeScale", ok, false); + EntityDynamicInterface::extractFloatArgument("spring action", arguments, "angularTimeScale", ok, false); if (!ok) { angularTimeScale = _angularTimeScale; } @@ -237,8 +238,8 @@ bool ObjectActionSpring::updateArguments(QVariantMap arguments) { auto ownerEntity = _ownerEntity.lock(); if (ownerEntity) { - ownerEntity->setActionDataDirty(true); - ownerEntity->setActionDataNeedsTransmit(true); + ownerEntity->setDynamicDataDirty(true); + ownerEntity->setDynamicDataNeedsTransmit(true); } }); activateBody(); @@ -248,7 +249,7 @@ bool ObjectActionSpring::updateArguments(QVariantMap arguments) { } QVariantMap ObjectActionSpring::getArguments() { - QVariantMap arguments = ObjectAction::getArguments(); + QVariantMap arguments = ObjectDynamic::getArguments(); withReadLock([&] { arguments["linearTimeScale"] = _linearTimeScale; arguments["targetPosition"] = glmToQMap(_desiredPositionalTarget); @@ -259,14 +260,7 @@ QVariantMap ObjectActionSpring::getArguments() { return arguments; } -QByteArray ObjectActionSpring::serialize() const { - QByteArray serializedActionArguments; - QDataStream dataStream(&serializedActionArguments, QIODevice::WriteOnly); - - dataStream << ACTION_TYPE_SPRING; - dataStream << getID(); - dataStream << ObjectActionSpring::springVersion; - +void ObjectActionSpring::serializeParameters(QDataStream& dataStream) const { withReadLock([&] { dataStream << _desiredPositionalTarget; dataStream << _linearTimeScale; @@ -277,28 +271,22 @@ QByteArray ObjectActionSpring::serialize() const { dataStream << localTimeToServerTime(_expires); dataStream << _tag; }); +} + +QByteArray ObjectActionSpring::serialize() const { + QByteArray serializedActionArguments; + QDataStream dataStream(&serializedActionArguments, QIODevice::WriteOnly); + + dataStream << DYNAMIC_TYPE_SPRING; + dataStream << getID(); + dataStream << ObjectActionSpring::springVersion; + + serializeParameters(dataStream); return serializedActionArguments; } -void ObjectActionSpring::deserialize(QByteArray serializedArguments) { - QDataStream dataStream(serializedArguments); - - EntityActionType type; - dataStream >> type; - assert(type == getType()); - - QUuid id; - dataStream >> id; - assert(id == getID()); - - uint16_t serializationVersion; - dataStream >> serializationVersion; - if (serializationVersion != ObjectActionSpring::springVersion) { - assert(false); - return; - } - +void ObjectActionSpring::deserializeParameters(QByteArray serializedArguments, QDataStream& dataStream) { withWriteLock([&] { dataStream >> _desiredPositionalTarget; dataStream >> _linearTimeScale; @@ -317,3 +305,24 @@ void ObjectActionSpring::deserialize(QByteArray serializedArguments) { _active = true; }); } + +void ObjectActionSpring::deserialize(QByteArray serializedArguments) { + QDataStream dataStream(serializedArguments); + + EntityDynamicType type; + dataStream >> type; + assert(type == getType()); + + QUuid id; + dataStream >> id; + assert(id == getID()); + + uint16_t serializationVersion; + dataStream >> serializationVersion; + if (serializationVersion != ObjectActionSpring::springVersion) { + assert(false); + return; + } + + deserializeParameters(serializedArguments, dataStream); +} diff --git a/libraries/physics/src/ObjectActionSpring.h b/libraries/physics/src/ObjectActionSpring.h index 498bb6c1f5..de9562d3fa 100644 --- a/libraries/physics/src/ObjectActionSpring.h +++ b/libraries/physics/src/ObjectActionSpring.h @@ -47,6 +47,9 @@ protected: glm::vec3 _angularVelocityTarget; virtual bool prepareForSpringUpdate(btScalar deltaTimeStep); + + void serializeParameters(QDataStream& dataStream) const; + void deserializeParameters(QByteArray serializedArguments, QDataStream& dataStream); }; #endif // hifi_ObjectActionSpring_h diff --git a/libraries/physics/src/ObjectActionTravelOriented.cpp b/libraries/physics/src/ObjectActionTravelOriented.cpp index 8f6d45c6f1..8ab24511d7 100644 --- a/libraries/physics/src/ObjectActionTravelOriented.cpp +++ b/libraries/physics/src/ObjectActionTravelOriented.cpp @@ -19,7 +19,7 @@ const uint16_t ObjectActionTravelOriented::actionVersion = 1; ObjectActionTravelOriented::ObjectActionTravelOriented(const QUuid& id, EntityItemPointer ownerEntity) : - ObjectAction(ACTION_TYPE_TRAVEL_ORIENTED, id, ownerEntity) { + ObjectAction(DYNAMIC_TYPE_TRAVEL_ORIENTED, id, ownerEntity) { #if WANT_DEBUG qCDebug(physics) << "ObjectActionTravelOriented::ObjectActionTravelOriented"; #endif @@ -106,16 +106,16 @@ bool ObjectActionTravelOriented::updateArguments(QVariantMap arguments) { float angularTimeScale; bool needUpdate = false; - bool somethingChanged = ObjectAction::updateArguments(arguments); + bool somethingChanged = ObjectDynamic::updateArguments(arguments); withReadLock([&]{ bool ok = true; - forward = EntityActionInterface::extractVec3Argument("travel oriented action", arguments, "forward", ok, true); + forward = EntityDynamicInterface::extractVec3Argument("travel oriented action", arguments, "forward", ok, true); if (!ok) { forward = _forward; } ok = true; angularTimeScale = - EntityActionInterface::extractFloatArgument("travel oriented action", arguments, "angularTimeScale", ok, false); + EntityDynamicInterface::extractFloatArgument("travel oriented action", arguments, "angularTimeScale", ok, false); if (!ok) { angularTimeScale = _angularTimeScale; } @@ -136,8 +136,8 @@ bool ObjectActionTravelOriented::updateArguments(QVariantMap arguments) { auto ownerEntity = _ownerEntity.lock(); if (ownerEntity) { - ownerEntity->setActionDataDirty(true); - ownerEntity->setActionDataNeedsTransmit(true); + ownerEntity->setDynamicDataDirty(true); + ownerEntity->setDynamicDataNeedsTransmit(true); } }); activateBody(); @@ -147,7 +147,7 @@ bool ObjectActionTravelOriented::updateArguments(QVariantMap arguments) { } QVariantMap ObjectActionTravelOriented::getArguments() { - QVariantMap arguments = ObjectAction::getArguments(); + QVariantMap arguments = ObjectDynamic::getArguments(); withReadLock([&] { arguments["forward"] = glmToQMap(_forward); arguments["angularTimeScale"] = _angularTimeScale; @@ -159,7 +159,7 @@ QByteArray ObjectActionTravelOriented::serialize() const { QByteArray serializedActionArguments; QDataStream dataStream(&serializedActionArguments, QIODevice::WriteOnly); - dataStream << ACTION_TYPE_TRAVEL_ORIENTED; + dataStream << DYNAMIC_TYPE_TRAVEL_ORIENTED; dataStream << getID(); dataStream << ObjectActionTravelOriented::actionVersion; @@ -177,7 +177,7 @@ QByteArray ObjectActionTravelOriented::serialize() const { void ObjectActionTravelOriented::deserialize(QByteArray serializedArguments) { QDataStream dataStream(serializedArguments); - EntityActionType type; + EntityDynamicType type; dataStream >> type; assert(type == getType()); diff --git a/libraries/physics/src/ObjectConstraint.cpp b/libraries/physics/src/ObjectConstraint.cpp new file mode 100644 index 0000000000..54fd4777e0 --- /dev/null +++ b/libraries/physics/src/ObjectConstraint.cpp @@ -0,0 +1,25 @@ +// +// ObjectConstraint.cpp +// libraries/physcis/src +// +// Created by Seth Alves 2015-6-2 +// 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 +// + +#include "EntitySimulation.h" + +#include "ObjectConstraint.h" + +#include "PhysicsLogging.h" + +ObjectConstraint::ObjectConstraint(EntityDynamicType type, const QUuid& id, EntityItemPointer ownerEntity) : + ObjectDynamic(type, id, ownerEntity) +{ +} + +void ObjectConstraint::invalidate() { + _constraint = nullptr; +} diff --git a/libraries/physics/src/ObjectConstraint.h b/libraries/physics/src/ObjectConstraint.h new file mode 100644 index 0000000000..711daea812 --- /dev/null +++ b/libraries/physics/src/ObjectConstraint.h @@ -0,0 +1,35 @@ +// +// ObjectConstraint.h +// libraries/physcis/src +// +// Created by Seth Alves 2017-4-11 +// 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 +// +// http://bulletphysics.org/Bullet/BulletFull/classbtConstraintInterface.html + +#ifndef hifi_ObjectConstraint_h +#define hifi_ObjectConstraint_h + +#include +#include +#include "ObjectDynamic.h" + +class ObjectConstraint : public ObjectDynamic +{ +public: + ObjectConstraint(EntityDynamicType type, const QUuid& id, EntityItemPointer ownerEntity); + virtual ~ObjectConstraint() {} + + virtual btTypedConstraint* getConstraint() = 0; + + virtual bool isConstraint() const override { return true; } + virtual void invalidate() override; + +protected: + btTypedConstraint* _constraint { nullptr }; +}; + +#endif // hifi_ObjectConstraint_h diff --git a/libraries/physics/src/ObjectConstraintHinge.cpp b/libraries/physics/src/ObjectConstraintHinge.cpp new file mode 100644 index 0000000000..6c55d9c5dd --- /dev/null +++ b/libraries/physics/src/ObjectConstraintHinge.cpp @@ -0,0 +1,363 @@ +// +// ObjectConstraintHinge.cpp +// libraries/physics/src +// +// Created by Seth Alves 2017-4-11 +// 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 "QVariantGLM.h" + +#include "EntityTree.h" +#include "ObjectConstraintHinge.h" +#include "PhysicsLogging.h" + + +const uint16_t ObjectConstraintHinge::constraintVersion = 1; + + +ObjectConstraintHinge::ObjectConstraintHinge(const QUuid& id, EntityItemPointer ownerEntity) : + ObjectConstraint(DYNAMIC_TYPE_HINGE, id, ownerEntity), + _pivotInA(glm::vec3(0.0f)), + _axisInA(glm::vec3(0.0f)) +{ + #if WANT_DEBUG + qCDebug(physics) << "ObjectConstraintHinge::ObjectConstraintHinge"; + #endif +} + +ObjectConstraintHinge::~ObjectConstraintHinge() { + #if WANT_DEBUG + qCDebug(physics) << "ObjectConstraintHinge::~ObjectConstraintHinge"; + #endif +} + +QList ObjectConstraintHinge::getRigidBodies() { + QList result; + result += getRigidBody(); + QUuid otherEntityID; + withReadLock([&]{ + otherEntityID = _otherEntityID; + }); + if (!otherEntityID.isNull()) { + result += getOtherRigidBody(otherEntityID); + } + return result; +} + +void ObjectConstraintHinge::updateHinge() { + btHingeConstraint* constraint { nullptr }; + float low; + float high; + float softness; + float biasFactor; + float relaxationFactor; + float motorVelocity; + + withReadLock([&]{ + constraint = static_cast(_constraint); + low = _low; + high = _high; + softness = _softness; + biasFactor = _biasFactor; + relaxationFactor = _relaxationFactor; + motorVelocity = _motorVelocity; + }); + + if (!constraint) { + return; + } + + constraint->setLimit(low, high, softness, biasFactor, relaxationFactor); + if (motorVelocity != 0.0f) { + constraint->setMotorTargetVelocity(motorVelocity); + constraint->enableMotor(true); + } else { + constraint->enableMotor(false); + } +} + + +btTypedConstraint* ObjectConstraintHinge::getConstraint() { + btHingeConstraint* constraint { nullptr }; + QUuid otherEntityID; + glm::vec3 pivotInA; + glm::vec3 axisInA; + glm::vec3 pivotInB; + glm::vec3 axisInB; + + withReadLock([&]{ + constraint = static_cast(_constraint); + pivotInA = _pivotInA; + axisInA = _axisInA; + otherEntityID = _otherEntityID; + pivotInB = _pivotInB; + axisInB = _axisInB; + }); + if (constraint) { + return constraint; + } + + btRigidBody* rigidBodyA = getRigidBody(); + if (!rigidBodyA) { + qCDebug(physics) << "ObjectConstraintHinge::getConstraint -- no rigidBodyA"; + return nullptr; + } + + if (!otherEntityID.isNull()) { + // This hinge is between two entities... find the other rigid body. + btRigidBody* rigidBodyB = getOtherRigidBody(otherEntityID); + if (!rigidBodyB) { + return nullptr; + } + constraint = new btHingeConstraint(*rigidBodyA, *rigidBodyB, + glmToBullet(pivotInA), glmToBullet(pivotInB), + glmToBullet(axisInA), glmToBullet(axisInB), + true); // useReferenceFrameA + + } else { + // This hinge is between an entity and the world-frame. + constraint = new btHingeConstraint(*rigidBodyA, + glmToBullet(pivotInA), glmToBullet(axisInA), + true); // useReferenceFrameA + } + + withWriteLock([&]{ + _constraint = constraint; + }); + + // if we don't wake up rigidBodyA, we may not send the dynamicData property over the network + forceBodyNonStatic(); + activateBody(); + + updateHinge(); + + return constraint; +} + + +bool ObjectConstraintHinge::updateArguments(QVariantMap arguments) { + glm::vec3 pivotInA; + glm::vec3 axisInA; + QUuid otherEntityID; + glm::vec3 pivotInB; + glm::vec3 axisInB; + float low; + float high; + float softness; + float biasFactor; + float relaxationFactor; + float motorVelocity; + + bool needUpdate = false; + bool somethingChanged = ObjectDynamic::updateArguments(arguments); + withReadLock([&]{ + bool ok = true; + pivotInA = EntityDynamicInterface::extractVec3Argument("hinge constraint", arguments, "pivot", ok, false); + if (!ok) { + pivotInA = _pivotInA; + } + + ok = true; + axisInA = EntityDynamicInterface::extractVec3Argument("hinge constraint", arguments, "axis", ok, false); + if (!ok) { + axisInA = _axisInA; + } + + ok = true; + otherEntityID = QUuid(EntityDynamicInterface::extractStringArgument("hinge constraint", + arguments, "otherEntityID", ok, false)); + if (!ok) { + otherEntityID = _otherEntityID; + } + + ok = true; + pivotInB = EntityDynamicInterface::extractVec3Argument("hinge constraint", arguments, "otherPivot", ok, false); + if (!ok) { + pivotInB = _pivotInB; + } + + ok = true; + axisInB = EntityDynamicInterface::extractVec3Argument("hinge constraint", arguments, "otherAxis", ok, false); + if (!ok) { + axisInB = _axisInB; + } + + ok = true; + low = EntityDynamicInterface::extractFloatArgument("hinge constraint", arguments, "low", ok, false); + if (!ok) { + low = _low; + } + + ok = true; + high = EntityDynamicInterface::extractFloatArgument("hinge constraint", arguments, "high", ok, false); + if (!ok) { + high = _high; + } + + ok = true; + softness = EntityDynamicInterface::extractFloatArgument("hinge constraint", arguments, "softness", ok, false); + if (!ok) { + softness = _softness; + } + + ok = true; + biasFactor = EntityDynamicInterface::extractFloatArgument("hinge constraint", arguments, "biasFactor", ok, false); + if (!ok) { + biasFactor = _biasFactor; + } + + ok = true; + relaxationFactor = EntityDynamicInterface::extractFloatArgument("hinge constraint", arguments, + "relaxationFactor", ok, false); + if (!ok) { + relaxationFactor = _relaxationFactor; + } + + ok = true; + motorVelocity = EntityDynamicInterface::extractFloatArgument("hinge constraint", arguments, + "motorVelocity", ok, false); + if (!ok) { + motorVelocity = _motorVelocity; + } + + if (somethingChanged || + pivotInA != _pivotInA || + axisInA != _axisInA || + otherEntityID != _otherEntityID || + pivotInB != _pivotInB || + axisInB != _axisInB || + low != _low || + high != _high || + softness != _softness || + biasFactor != _biasFactor || + relaxationFactor != _relaxationFactor || + motorVelocity != _motorVelocity) { + // something changed + needUpdate = true; + } + }); + + if (needUpdate) { + withWriteLock([&] { + _pivotInA = pivotInA; + _axisInA = axisInA; + _otherEntityID = otherEntityID; + _pivotInB = pivotInB; + _axisInB = axisInB; + _low = low; + _high = high; + _softness = softness; + _biasFactor = biasFactor; + _relaxationFactor = relaxationFactor; + _motorVelocity = motorVelocity; + + _active = true; + + auto ownerEntity = _ownerEntity.lock(); + if (ownerEntity) { + ownerEntity->setDynamicDataDirty(true); + ownerEntity->setDynamicDataNeedsTransmit(true); + } + }); + + updateHinge(); + } + + return true; +} + +QVariantMap ObjectConstraintHinge::getArguments() { + QVariantMap arguments = ObjectDynamic::getArguments(); + withReadLock([&] { + if (_constraint) { + arguments["pivot"] = glmToQMap(_pivotInA); + arguments["axis"] = glmToQMap(_axisInA); + arguments["otherEntityID"] = _otherEntityID; + arguments["otherPivot"] = glmToQMap(_pivotInB); + arguments["otherAxis"] = glmToQMap(_axisInB); + arguments["low"] = _low; + arguments["high"] = _high; + arguments["softness"] = _softness; + arguments["biasFactor"] = _biasFactor; + arguments["relaxationFactor"] = _relaxationFactor; + arguments["motorVelocity"] = _motorVelocity; + arguments["angle"] = static_cast(_constraint)->getHingeAngle(); // [-PI,PI] + } + }); + return arguments; +} + +QByteArray ObjectConstraintHinge::serialize() const { + QByteArray serializedConstraintArguments; + QDataStream dataStream(&serializedConstraintArguments, QIODevice::WriteOnly); + + dataStream << DYNAMIC_TYPE_HINGE; + dataStream << getID(); + dataStream << ObjectConstraintHinge::constraintVersion; + + withReadLock([&] { + dataStream << _pivotInA; + dataStream << _axisInA; + dataStream << _otherEntityID; + dataStream << _pivotInB; + dataStream << _axisInB; + dataStream << _low; + dataStream << _high; + dataStream << _softness; + dataStream << _biasFactor; + dataStream << _relaxationFactor; + + dataStream << localTimeToServerTime(_expires); + dataStream << _tag; + + dataStream << _motorVelocity; + }); + + return serializedConstraintArguments; +} + +void ObjectConstraintHinge::deserialize(QByteArray serializedArguments) { + QDataStream dataStream(serializedArguments); + + EntityDynamicType type; + dataStream >> type; + assert(type == getType()); + + QUuid id; + dataStream >> id; + assert(id == getID()); + + uint16_t serializationVersion; + dataStream >> serializationVersion; + if (serializationVersion != ObjectConstraintHinge::constraintVersion) { + assert(false); + return; + } + + withWriteLock([&] { + dataStream >> _pivotInA; + dataStream >> _axisInA; + dataStream >> _otherEntityID; + dataStream >> _pivotInB; + dataStream >> _axisInB; + dataStream >> _low; + dataStream >> _high; + dataStream >> _softness; + dataStream >> _biasFactor; + dataStream >> _relaxationFactor; + + quint64 serverExpires; + dataStream >> serverExpires; + _expires = serverTimeToLocalTime(serverExpires); + + dataStream >> _tag; + + dataStream >> _motorVelocity; + + _active = true; + }); +} diff --git a/libraries/physics/src/ObjectConstraintHinge.h b/libraries/physics/src/ObjectConstraintHinge.h new file mode 100644 index 0000000000..7d2cac7511 --- /dev/null +++ b/libraries/physics/src/ObjectConstraintHinge.h @@ -0,0 +1,53 @@ +// +// ObjectConstraintHinge.h +// libraries/physics/src +// +// Created by Seth Alves 2017-4-11 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ObjectConstraintHinge_h +#define hifi_ObjectConstraintHinge_h + +#include "ObjectConstraint.h" + +// http://bulletphysics.org/Bullet/BulletFull/classbtHingeConstraint.html + +class ObjectConstraintHinge : public ObjectConstraint { +public: + ObjectConstraintHinge(const QUuid& id, EntityItemPointer ownerEntity); + virtual ~ObjectConstraintHinge(); + + virtual bool updateArguments(QVariantMap arguments) override; + virtual QVariantMap getArguments() override; + + virtual QByteArray serialize() const override; + virtual void deserialize(QByteArray serializedArguments) override; + + virtual QList getRigidBodies() override; + virtual btTypedConstraint* getConstraint() override; + +protected: + static const uint16_t constraintVersion; + + void updateHinge(); + + glm::vec3 _pivotInA; + glm::vec3 _axisInA; + + EntityItemID _otherEntityID; + glm::vec3 _pivotInB; + glm::vec3 _axisInB; + + float _low { -2.0f * PI }; + float _high { 2.0f * PI }; + float _softness { 0.9f }; + float _biasFactor { 0.3f }; + float _relaxationFactor { 1.0f }; + float _motorVelocity { 0.0f }; +}; + +#endif // hifi_ObjectConstraintHinge_h diff --git a/libraries/physics/src/ObjectDynamic.cpp b/libraries/physics/src/ObjectDynamic.cpp new file mode 100644 index 0000000000..3cb9f5b405 --- /dev/null +++ b/libraries/physics/src/ObjectDynamic.cpp @@ -0,0 +1,276 @@ +// +// ObjectDynamic.cpp +// libraries/physcis/src +// +// Created by Seth Alves 2015-6-2 +// 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 +// + +#include "EntitySimulation.h" + +#include "ObjectDynamic.h" + +#include "PhysicsLogging.h" + + +ObjectDynamic::ObjectDynamic(EntityDynamicType type, const QUuid& id, EntityItemPointer ownerEntity) : + EntityDynamicInterface(type, id), + _ownerEntity(ownerEntity) { +} + +ObjectDynamic::~ObjectDynamic() { +} + +qint64 ObjectDynamic::getEntityServerClockSkew() const { + auto nodeList = DependencyManager::get(); + + auto ownerEntity = _ownerEntity.lock(); + if (!ownerEntity) { + return 0; + } + + const QUuid& entityServerNodeID = ownerEntity->getSourceUUID(); + auto entityServerNode = nodeList->nodeWithUUID(entityServerNodeID); + if (entityServerNode) { + return entityServerNode->getClockSkewUsec(); + } + return 0; +} + +bool ObjectDynamic::updateArguments(QVariantMap arguments) { + bool somethingChanged = false; + + withWriteLock([&]{ + quint64 previousExpires = _expires; + QString previousTag = _tag; + + bool ttlSet = true; + float ttl = EntityDynamicInterface::extractFloatArgument("dynamic", arguments, "ttl", ttlSet, false); + if (ttlSet) { + quint64 now = usecTimestampNow(); + _expires = now + (quint64)(ttl * USECS_PER_SECOND); + } else { + _expires = 0; + } + + bool tagSet = true; + QString tag = EntityDynamicInterface::extractStringArgument("dynamic", arguments, "tag", tagSet, false); + if (tagSet) { + _tag = tag; + } else { + tag = ""; + } + + if (previousExpires != _expires || previousTag != _tag) { + somethingChanged = true; + } + }); + + return somethingChanged; +} + +QVariantMap ObjectDynamic::getArguments() { + QVariantMap arguments; + withReadLock([&]{ + if (_expires == 0) { + arguments["ttl"] = 0.0f; + } else { + quint64 now = usecTimestampNow(); + arguments["ttl"] = (float)(_expires - now) / (float)USECS_PER_SECOND; + } + arguments["tag"] = _tag; + + EntityItemPointer entity = _ownerEntity.lock(); + if (entity) { + ObjectMotionState* motionState = static_cast(entity->getPhysicsInfo()); + if (motionState) { + arguments["::active"] = motionState->isActive(); + arguments["::motion-type"] = motionTypeToString(motionState->getMotionType()); + } else { + arguments["::no-motion-state"] = true; + } + } + arguments["isMine"] = isMine(); + }); + return arguments; +} + +void ObjectDynamic::removeFromSimulation(EntitySimulationPointer simulation) const { + QUuid myID; + withReadLock([&]{ + myID = _id; + }); + simulation->removeDynamic(myID); +} + +EntityItemPointer ObjectDynamic::getEntityByID(EntityItemID entityID) const { + EntityItemPointer ownerEntity; + withReadLock([&]{ + ownerEntity = _ownerEntity.lock(); + }); + EntityTreeElementPointer element = ownerEntity ? ownerEntity->getElement() : nullptr; + EntityTreePointer tree = element ? element->getTree() : nullptr; + if (!tree) { + return nullptr; + } + return tree->findEntityByID(entityID); +} + + +btRigidBody* ObjectDynamic::getRigidBody() { + ObjectMotionState* motionState = nullptr; + withReadLock([&]{ + auto ownerEntity = _ownerEntity.lock(); + if (!ownerEntity) { + return; + } + void* physicsInfo = ownerEntity->getPhysicsInfo(); + if (!physicsInfo) { + return; + } + motionState = static_cast(physicsInfo); + }); + if (motionState) { + return motionState->getRigidBody(); + } + return nullptr; +} + +glm::vec3 ObjectDynamic::getPosition() { + auto rigidBody = getRigidBody(); + if (!rigidBody) { + return glm::vec3(0.0f); + } + return bulletToGLM(rigidBody->getCenterOfMassPosition()); +} + +glm::quat ObjectDynamic::getRotation() { + auto rigidBody = getRigidBody(); + if (!rigidBody) { + return glm::quat(0.0f, 0.0f, 0.0f, 1.0f); + } + return bulletToGLM(rigidBody->getOrientation()); +} + +glm::vec3 ObjectDynamic::getLinearVelocity() { + auto rigidBody = getRigidBody(); + if (!rigidBody) { + return glm::vec3(0.0f); + } + return bulletToGLM(rigidBody->getLinearVelocity()); +} + +void ObjectDynamic::setLinearVelocity(glm::vec3 linearVelocity) { + auto rigidBody = getRigidBody(); + if (!rigidBody) { + return; + } + rigidBody->setLinearVelocity(glmToBullet(glm::vec3(0.0f))); + rigidBody->activate(); +} + +glm::vec3 ObjectDynamic::getAngularVelocity() { + auto rigidBody = getRigidBody(); + if (!rigidBody) { + return glm::vec3(0.0f); + } + return bulletToGLM(rigidBody->getAngularVelocity()); +} + +void ObjectDynamic::setAngularVelocity(glm::vec3 angularVelocity) { + auto rigidBody = getRigidBody(); + if (!rigidBody) { + return; + } + rigidBody->setAngularVelocity(glmToBullet(angularVelocity)); + rigidBody->activate(); +} + +void ObjectDynamic::activateBody(bool forceActivation) { + auto rigidBody = getRigidBody(); + if (rigidBody) { + rigidBody->activate(forceActivation); + } else { + qCDebug(physics) << "ObjectDynamic::activateBody -- no rigid body" << (void*)rigidBody; + } +} + +void ObjectDynamic::forceBodyNonStatic() { + auto ownerEntity = _ownerEntity.lock(); + if (!ownerEntity) { + return; + } + void* physicsInfo = ownerEntity->getPhysicsInfo(); + ObjectMotionState* motionState = static_cast(physicsInfo); + if (motionState && motionState->getMotionType() == MOTION_TYPE_STATIC) { + ownerEntity->flagForMotionStateChange(); + } +} + +bool ObjectDynamic::lifetimeIsOver() { + if (_expires == 0) { + return false; + } + + quint64 now = usecTimestampNow(); + if (now >= _expires) { + return true; + } + return false; +} + +quint64 ObjectDynamic::localTimeToServerTime(quint64 timeValue) const { + // 0 indicates no expiration + if (timeValue == 0) { + return 0; + } + + qint64 serverClockSkew = getEntityServerClockSkew(); + if (serverClockSkew < 0 && timeValue <= (quint64)(-serverClockSkew)) { + return 1; // non-zero but long-expired value to avoid negative roll-over + } + + return timeValue + serverClockSkew; +} + +quint64 ObjectDynamic::serverTimeToLocalTime(quint64 timeValue) const { + // 0 indicates no expiration + if (timeValue == 0) { + return 0; + } + + qint64 serverClockSkew = getEntityServerClockSkew(); + if (serverClockSkew > 0 && timeValue <= (quint64)serverClockSkew) { + return 1; // non-zero but long-expired value to avoid negative roll-over + } + + return timeValue - serverClockSkew; +} + +btRigidBody* ObjectDynamic::getOtherRigidBody(EntityItemID otherEntityID) { + EntityItemPointer otherEntity = getEntityByID(otherEntityID); + if (!otherEntity) { + return nullptr; + } + + void* otherPhysicsInfo = otherEntity->getPhysicsInfo(); + if (!otherPhysicsInfo) { + return nullptr; + } + + ObjectMotionState* otherMotionState = static_cast(otherPhysicsInfo); + if (!otherMotionState) { + return nullptr; + } + + return otherMotionState->getRigidBody(); +} + +QList ObjectDynamic::getRigidBodies() { + QList result; + result += getRigidBody(); + return result; +} diff --git a/libraries/physics/src/ObjectDynamic.h b/libraries/physics/src/ObjectDynamic.h new file mode 100644 index 0000000000..dcd0103a55 --- /dev/null +++ b/libraries/physics/src/ObjectDynamic.h @@ -0,0 +1,76 @@ +// +// ObjectDynamic.h +// libraries/physcis/src +// +// Created by Seth Alves 2015-6-2 +// 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 +// +// http://bulletphysics.org/Bullet/BulletFull/classbtDynamicInterface.html + +#ifndef hifi_ObjectDynamic_h +#define hifi_ObjectDynamic_h + +#include + +#include + +#include + +#include "ObjectMotionState.h" +#include "BulletUtil.h" +#include "EntityDynamicInterface.h" + + +class ObjectDynamic : public EntityDynamicInterface, public ReadWriteLockable { +public: + ObjectDynamic(EntityDynamicType type, const QUuid& id, EntityItemPointer ownerEntity); + virtual ~ObjectDynamic(); + + virtual void removeFromSimulation(EntitySimulationPointer simulation) const override; + virtual EntityItemWeakPointer getOwnerEntity() const override { return _ownerEntity; } + virtual void setOwnerEntity(const EntityItemPointer ownerEntity) override { _ownerEntity = ownerEntity; } + + virtual void invalidate() {}; + + virtual bool updateArguments(QVariantMap arguments) override; + virtual QVariantMap getArguments() override; + + + virtual QByteArray serialize() const override = 0; + virtual void deserialize(QByteArray serializedArguments) override = 0; + + virtual bool lifetimeIsOver() override; + virtual quint64 getExpires() override { return _expires; } + + virtual QList getRigidBodies(); + +protected: + quint64 localTimeToServerTime(quint64 timeValue) const; + quint64 serverTimeToLocalTime(quint64 timeValue) const; + + btRigidBody* getOtherRigidBody(EntityItemID otherEntityID); + EntityItemPointer getEntityByID(EntityItemID entityID) const; + virtual btRigidBody* getRigidBody(); + virtual glm::vec3 getPosition() override; + virtual glm::quat getRotation() override; + virtual glm::vec3 getLinearVelocity() override; + virtual void setLinearVelocity(glm::vec3 linearVelocity) override; + virtual glm::vec3 getAngularVelocity() override; + virtual void setAngularVelocity(glm::vec3 angularVelocity) override; + virtual void activateBody(bool forceActivation = false); + virtual void forceBodyNonStatic(); + + EntityItemWeakPointer _ownerEntity; + QString _tag; + quint64 _expires { 0 }; // in seconds since epoch + +private: + qint64 getEntityServerClockSkew() const; +}; + +typedef std::shared_ptr ObjectDynamicPointer; + +#endif // hifi_ObjectDynamic_h diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index 6f5b474810..5081f981d4 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -330,33 +330,41 @@ void PhysicalEntitySimulation::handleCollisionEvents(const CollisionEvents& coll } -void PhysicalEntitySimulation::addAction(EntityActionPointer action) { +void PhysicalEntitySimulation::addDynamic(EntityDynamicPointer dynamic) { if (_physicsEngine) { // FIXME put fine grain locking into _physicsEngine { QMutexLocker lock(&_mutex); - const QUuid& actionID = action->getID(); - if (_physicsEngine->getActionByID(actionID)) { - qCDebug(physics) << "warning -- PhysicalEntitySimulation::addAction -- adding an " - "action that was already in _physicsEngine"; + const QUuid& dynamicID = dynamic->getID(); + if (_physicsEngine->getDynamicByID(dynamicID)) { + qCDebug(physics) << "warning -- PhysicalEntitySimulation::addDynamic -- adding an " + "dynamic that was already in _physicsEngine"; } } - EntitySimulation::addAction(action); + EntitySimulation::addDynamic(dynamic); } } -void PhysicalEntitySimulation::applyActionChanges() { +void PhysicalEntitySimulation::applyDynamicChanges() { + QList dynamicsFailedToAdd; if (_physicsEngine) { // FIXME put fine grain locking into _physicsEngine QMutexLocker lock(&_mutex); - foreach(QUuid actionToRemove, _actionsToRemove) { - _physicsEngine->removeAction(actionToRemove); + foreach(QUuid dynamicToRemove, _dynamicsToRemove) { + _physicsEngine->removeDynamic(dynamicToRemove); } - foreach (EntityActionPointer actionToAdd, _actionsToAdd) { - if (!_actionsToRemove.contains(actionToAdd->getID())) { - _physicsEngine->addAction(actionToAdd); + foreach (EntityDynamicPointer dynamicToAdd, _dynamicsToAdd) { + if (!_dynamicsToRemove.contains(dynamicToAdd->getID())) { + if (!_physicsEngine->addDynamic(dynamicToAdd)) { + dynamicsFailedToAdd += dynamicToAdd; + } } } } - EntitySimulation::applyActionChanges(); + // applyDynamicChanges will clear _dynamicsToRemove and _dynamicsToAdd + EntitySimulation::applyDynamicChanges(); + // put back the ones that couldn't yet be added + foreach (EntityDynamicPointer dynamicFailedToAdd, dynamicsFailedToAdd) { + addDynamic(dynamicFailedToAdd); + } } diff --git a/libraries/physics/src/PhysicalEntitySimulation.h b/libraries/physics/src/PhysicalEntitySimulation.h index 5f6185add3..e0b15440bb 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.h +++ b/libraries/physics/src/PhysicalEntitySimulation.h @@ -34,8 +34,8 @@ public: void init(EntityTreePointer tree, PhysicsEnginePointer engine, EntityEditPacketSender* packetSender); - virtual void addAction(EntityActionPointer action) override; - virtual void applyActionChanges() override; + virtual void addDynamic(EntityDynamicPointer dynamic) override; + virtual void applyDynamicChanges() override; virtual void takeEntitiesToDelete(VectorOfEntities& entitiesToDelete) override; diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index a8a8e6acfd..ca6889485a 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -142,6 +142,26 @@ void PhysicsEngine::addObjectToDynamicsWorld(ObjectMotionState* motionState) { motionState->clearIncomingDirtyFlags(); } +QList PhysicsEngine::removeDynamicsForBody(btRigidBody* body) { + // remove dynamics that are attached to this body + QList removedDynamics; + QMutableSetIterator i(_objectDynamicsByBody[body]); + + while (i.hasNext()) { + QUuid dynamicID = i.next(); + if (dynamicID.isNull()) { + continue; + } + EntityDynamicPointer dynamic = _objectDynamics[dynamicID]; + if (!dynamic) { + continue; + } + removeDynamic(dynamicID); + removedDynamics += dynamic; + } + return removedDynamics; +} + void PhysicsEngine::removeObjects(const VectorOfMotionStates& objects) { // bump and prune contacts for all objects in the list for (auto object : objects) { @@ -175,6 +195,7 @@ void PhysicsEngine::removeObjects(const VectorOfMotionStates& objects) { for (auto object : objects) { btRigidBody* body = object->getRigidBody(); if (body) { + removeDynamicsForBody(body); _dynamicsWorld->removeRigidBody(body); // NOTE: setRigidBody() modifies body->m_userPointer so we should clear the MotionState's body BEFORE deleting it. @@ -191,6 +212,7 @@ void PhysicsEngine::removeObjects(const SetOfMotionStates& objects) { for (auto object : objects) { btRigidBody* body = object->getRigidBody(); if (body) { + removeDynamicsForBody(body); _dynamicsWorld->removeRigidBody(body); // NOTE: setRigidBody() modifies body->m_userPointer so we should clear the MotionState's body BEFORE deleting it. @@ -240,7 +262,6 @@ void PhysicsEngine::reinsertObject(ObjectMotionState* object) { btRigidBody* body = object->getRigidBody(); if (body) { _dynamicsWorld->removeRigidBody(body); - // add it back addObjectToDynamicsWorld(object); } @@ -548,44 +569,84 @@ void PhysicsEngine::setCharacterController(CharacterController* character) { } } -EntityActionPointer PhysicsEngine::getActionByID(const QUuid& actionID) const { - if (_objectActions.contains(actionID)) { - return _objectActions[actionID]; +EntityDynamicPointer PhysicsEngine::getDynamicByID(const QUuid& dynamicID) const { + if (_objectDynamics.contains(dynamicID)) { + return _objectDynamics[dynamicID]; } return nullptr; } -void PhysicsEngine::addAction(EntityActionPointer action) { - assert(action); - const QUuid& actionID = action->getID(); - if (_objectActions.contains(actionID)) { - if (_objectActions[actionID] == action) { +bool PhysicsEngine::addDynamic(EntityDynamicPointer dynamic) { + assert(dynamic); + + if (!dynamic->isReadyForAdd()) { + return false; + } + + const QUuid& dynamicID = dynamic->getID(); + if (_objectDynamics.contains(dynamicID)) { + if (_objectDynamics[dynamicID] == dynamic) { + return true; + } + removeDynamic(dynamic->getID()); + } + + bool success { false }; + if (dynamic->isAction()) { + ObjectAction* objectAction = static_cast(dynamic.get()); + _dynamicsWorld->addAction(objectAction); + success = true; + } else if (dynamic->isConstraint()) { + ObjectConstraint* objectConstraint = static_cast(dynamic.get()); + btTypedConstraint* constraint = objectConstraint->getConstraint(); + if (constraint) { + _dynamicsWorld->addConstraint(constraint); + success = true; + } // else perhaps not all the rigid bodies are available, yet + } + + if (success) { + _objectDynamics[dynamicID] = dynamic; + foreach(btRigidBody* rigidBody, std::static_pointer_cast(dynamic)->getRigidBodies()) { + _objectDynamicsByBody[rigidBody] += dynamic->getID(); + } + } + return success; +} + +void PhysicsEngine::removeDynamic(const QUuid dynamicID) { + if (_objectDynamics.contains(dynamicID)) { + ObjectDynamicPointer dynamic = std::static_pointer_cast(_objectDynamics[dynamicID]); + if (!dynamic) { return; } - removeAction(action->getID()); - } - - _objectActions[actionID] = action; - - // bullet needs a pointer to the action, but it doesn't use shared pointers. - // is there a way to bump the reference count? - ObjectAction* objectAction = static_cast(action.get()); - _dynamicsWorld->addAction(objectAction); -} - -void PhysicsEngine::removeAction(const QUuid actionID) { - if (_objectActions.contains(actionID)) { - EntityActionPointer action = _objectActions[actionID]; - ObjectAction* objectAction = static_cast(action.get()); - _dynamicsWorld->removeAction(objectAction); - _objectActions.remove(actionID); + QList rigidBodies = dynamic->getRigidBodies(); + if (dynamic->isAction()) { + ObjectAction* objectAction = static_cast(dynamic.get()); + _dynamicsWorld->removeAction(objectAction); + } else { + ObjectConstraint* objectConstraint = static_cast(dynamic.get()); + btTypedConstraint* constraint = objectConstraint->getConstraint(); + if (constraint) { + _dynamicsWorld->removeConstraint(constraint); + } else { + qCDebug(physics) << "PhysicsEngine::removeDynamic of constraint failed"; + } + } + _objectDynamics.remove(dynamicID); + foreach(btRigidBody* rigidBody, rigidBodies) { + _objectDynamicsByBody[rigidBody].remove(dynamic->getID()); + } + dynamic->invalidate(); } } -void PhysicsEngine::forEachAction(std::function actor) { - QHashIterator iter(_objectActions); +void PhysicsEngine::forEachDynamic(std::function actor) { + QMutableHashIterator iter(_objectDynamics); while (iter.hasNext()) { iter.next(); - actor(iter.value()); + if (iter.value()) { + actor(iter.value()); + } } } diff --git a/libraries/physics/src/PhysicsEngine.h b/libraries/physics/src/PhysicsEngine.h index b2ebe58f08..9f2f1aff5c 100644 --- a/libraries/physics/src/PhysicsEngine.h +++ b/libraries/physics/src/PhysicsEngine.h @@ -24,6 +24,7 @@ #include "ObjectMotionState.h" #include "ThreadSafeDynamicsWorld.h" #include "ObjectAction.h" +#include "ObjectConstraint.h" const float HALF_SIMULATION_EXTENT = 512.0f; // meters @@ -84,12 +85,13 @@ public: void dumpNextStats() { _dumpNextStats = true; } - EntityActionPointer getActionByID(const QUuid& actionID) const; - void addAction(EntityActionPointer action); - void removeAction(const QUuid actionID); - void forEachAction(std::function actor); + EntityDynamicPointer getDynamicByID(const QUuid& dynamicID) const; + bool addDynamic(EntityDynamicPointer dynamic); + void removeDynamic(const QUuid dynamicID); + void forEachDynamic(std::function actor); private: + QList removeDynamicsForBody(btRigidBody* body); void addObjectToDynamicsWorld(ObjectMotionState* motionState); void recursivelyHarvestPerformanceStats(CProfileIterator* profileIterator, QString contextName); @@ -110,7 +112,8 @@ private: ContactMap _contactMap; CollisionEvents _collisionEvents; - QHash _objectActions; + QHash _objectDynamics; + QHash> _objectDynamicsByBody; std::vector _activeStaticBodies; glm::vec3 _originOffset; @@ -122,6 +125,7 @@ private: bool _dumpNextStats = false; bool _hasOutgoingChanges = false; + }; typedef std::shared_ptr PhysicsEnginePointer; diff --git a/libraries/procedural/CMakeLists.txt b/libraries/procedural/CMakeLists.txt index 7145f7de5c..3ebd0f3d14 100644 --- a/libraries/procedural/CMakeLists.txt +++ b/libraries/procedural/CMakeLists.txt @@ -1,5 +1,5 @@ set(TARGET_NAME procedural) AUTOSCRIBE_SHADER_LIB(gpu model) setup_hifi_library() -link_hifi_libraries(shared gpu gpu-gl networking model model-networking) +link_hifi_libraries(shared gpu gpu-gl networking model model-networking ktx image) diff --git a/libraries/recording/src/recording/Deck.cpp b/libraries/recording/src/recording/Deck.cpp index 186516e01c..c9ac0524ad 100644 --- a/libraries/recording/src/recording/Deck.cpp +++ b/libraries/recording/src/recording/Deck.cpp @@ -166,6 +166,12 @@ void Deck::processFrames() { if (!overLimit) { auto nextFrameTime = nextClip->positionFrameTime(); nextInterval = (int)Frame::frameTimeToMilliseconds(nextFrameTime - _position); + if (nextInterval < 0) { + qCWarning(recordingLog) << "Unexpected nextInterval < 0 nextFrameTime:" << nextFrameTime + << "_position:" << _position << "-- setting nextInterval to 0"; + nextInterval = 0; + } + #ifdef WANT_RECORDING_DEBUG qCDebug(recordingLog) << "Now " << _position; qCDebug(recordingLog) << "Next frame time " << nextInterval; diff --git a/libraries/render-utils/CMakeLists.txt b/libraries/render-utils/CMakeLists.txt index 3bf389973a..454097233a 100644 --- a/libraries/render-utils/CMakeLists.txt +++ b/libraries/render-utils/CMakeLists.txt @@ -3,7 +3,7 @@ AUTOSCRIBE_SHADER_LIB(gpu model render) # pull in the resources.qrc file qt5_add_resources(QT_RESOURCES_FILE "${CMAKE_CURRENT_SOURCE_DIR}/res/fonts/fonts.qrc") setup_hifi_library(Widgets OpenGL Network Qml Quick Script) -link_hifi_libraries(shared ktx gpu model model-networking render animation fbx entities) +link_hifi_libraries(shared ktx gpu model model-networking render animation fbx entities image) if (NOT ANDROID) target_nsight() diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.cpp b/libraries/render-utils/src/AmbientOcclusionEffect.cpp index 9f4a71ef08..83753131c8 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.cpp +++ b/libraries/render-utils/src/AmbientOcclusionEffect.cpp @@ -74,11 +74,11 @@ void AmbientOcclusionFramebuffer::allocate() { auto width = _frameSize.x; auto height = _frameSize.y; - _occlusionTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _occlusionTexture = gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT)); _occlusionFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("occlusion")); _occlusionFramebuffer->setRenderBuffer(0, _occlusionTexture); - _occlusionBlurredTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _occlusionBlurredTexture = gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT)); _occlusionBlurredFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("occlusionBlurred")); _occlusionBlurredFramebuffer->setRenderBuffer(0, _occlusionBlurredTexture); } diff --git a/libraries/render-utils/src/AntialiasingEffect.cpp b/libraries/render-utils/src/AntialiasingEffect.cpp index 9a81320416..cd378d4e5b 100644 --- a/libraries/render-utils/src/AntialiasingEffect.cpp +++ b/libraries/render-utils/src/AntialiasingEffect.cpp @@ -52,7 +52,7 @@ const gpu::PipelinePointer& Antialiasing::getAntialiasingPipeline() { _antialiasingBuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("antialiasing")); auto format = gpu::Element::COLOR_SRGBA_32; // DependencyManager::get()->getLightingTexture()->getTexelFormat(); auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - _antialiasingTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(format, width, height, gpu::Texture::SINGLE_MIP, defaultSampler)); + _antialiasingTexture = gpu::Texture::createRenderBuffer(format, width, height, gpu::Texture::SINGLE_MIP, defaultSampler); _antialiasingBuffer->setRenderBuffer(0, _antialiasingTexture); } diff --git a/libraries/render-utils/src/DeferredFramebuffer.cpp b/libraries/render-utils/src/DeferredFramebuffer.cpp index 5d345f0851..64ea8f0342 100644 --- a/libraries/render-utils/src/DeferredFramebuffer.cpp +++ b/libraries/render-utils/src/DeferredFramebuffer.cpp @@ -53,9 +53,9 @@ void DeferredFramebuffer::allocate() { auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - _deferredColorTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(colorFormat, width, height, gpu::Texture::SINGLE_MIP, defaultSampler)); - _deferredNormalTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(linearFormat, width, height, gpu::Texture::SINGLE_MIP, defaultSampler)); - _deferredSpecularTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(linearFormat, width, height, gpu::Texture::SINGLE_MIP, defaultSampler)); + _deferredColorTexture = gpu::Texture::createRenderBuffer(colorFormat, width, height, gpu::Texture::SINGLE_MIP, defaultSampler); + _deferredNormalTexture = gpu::Texture::createRenderBuffer(linearFormat, width, height, gpu::Texture::SINGLE_MIP, defaultSampler); + _deferredSpecularTexture = gpu::Texture::createRenderBuffer(linearFormat, width, height, gpu::Texture::SINGLE_MIP, defaultSampler); _deferredFramebuffer->setRenderBuffer(0, _deferredColorTexture); _deferredFramebuffer->setRenderBuffer(1, _deferredNormalTexture); @@ -65,7 +65,7 @@ void DeferredFramebuffer::allocate() { auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format if (!_primaryDepthTexture) { - _primaryDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(depthFormat, width, height, gpu::Texture::SINGLE_MIP, defaultSampler)); + _primaryDepthTexture = gpu::Texture::createRenderBuffer(depthFormat, width, height, gpu::Texture::SINGLE_MIP, defaultSampler); } _deferredFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); @@ -75,7 +75,7 @@ void DeferredFramebuffer::allocate() { auto smoothSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR); - _lightingTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::R11G11B10), width, height, gpu::Texture::SINGLE_MIP, defaultSampler)); + _lightingTexture = gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::R11G11B10), width, height, gpu::Texture::SINGLE_MIP, defaultSampler); _lightingFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("lighting")); _lightingFramebuffer->setRenderBuffer(0, _lightingTexture); _lightingFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 34fd481c44..93a176f4f3 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -496,14 +496,14 @@ void PreparePrimaryFramebuffer::run(const RenderContextPointer& renderContext, g auto colorFormat = gpu::Element::COLOR_SRGBA_32; auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - auto primaryColorTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(colorFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler)); + auto primaryColorTexture = gpu::Texture::createRenderBuffer(colorFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler); _primaryFramebuffer->setRenderBuffer(0, primaryColorTexture); auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format - auto primaryDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(depthFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler)); + auto primaryDepthTexture = gpu::Texture::createRenderBuffer(depthFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler); _primaryFramebuffer->setDepthStencilBuffer(primaryDepthTexture, depthFormat); } diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 51ce0fffa7..2e08420073 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -118,7 +118,7 @@ void MeshPartPayload::drawCall(gpu::Batch& batch) const { batch.drawIndexed(gpu::TRIANGLES, _drawPart._numIndices, _drawPart._startIndex); } -void MeshPartPayload::bindMesh(gpu::Batch& batch) const { +void MeshPartPayload::bindMesh(gpu::Batch& batch) { batch.setIndexBuffer(gpu::UINT32, (_drawMesh->getIndexBuffer()._buffer), 0); batch.setInputFormat((_drawMesh->getVertexFormat())); @@ -255,7 +255,7 @@ void MeshPartPayload::bindTransform(gpu::Batch& batch, const ShapePipeline::Loca } -void MeshPartPayload::render(RenderArgs* args) const { +void MeshPartPayload::render(RenderArgs* args) { PerformanceTimer perfTimer("MeshPartPayload::render"); gpu::Batch& batch = *(args->_batch); @@ -485,7 +485,7 @@ ShapeKey ModelMeshPartPayload::getShapeKey() const { return builder.build(); } -void ModelMeshPartPayload::bindMesh(gpu::Batch& batch) const { +void ModelMeshPartPayload::bindMesh(gpu::Batch& batch) { if (!_isBlendShaped) { batch.setIndexBuffer(gpu::UINT32, (_drawMesh->getIndexBuffer()._buffer), 0); @@ -517,7 +517,7 @@ void ModelMeshPartPayload::bindTransform(gpu::Batch& batch, const ShapePipeline: batch.setModelTransform(_transform); } -float ModelMeshPartPayload::computeFadeAlpha() const { +float ModelMeshPartPayload::computeFadeAlpha() { if (_fadeState == FADE_WAITING_TO_START) { return 0.0f; } @@ -536,7 +536,7 @@ float ModelMeshPartPayload::computeFadeAlpha() const { return Interpolate::simpleNonLinearBlend(fadeAlpha); } -void ModelMeshPartPayload::render(RenderArgs* args) const { +void ModelMeshPartPayload::render(RenderArgs* args) { PerformanceTimer perfTimer("ModelMeshPartPayload::render"); if (!_model->addedToScene() || !_model->isVisible()) { @@ -544,7 +544,7 @@ void ModelMeshPartPayload::render(RenderArgs* args) const { } if (_fadeState == FADE_WAITING_TO_START) { - if (_model->isLoaded() && _model->getGeometry()->areTexturesLoaded()) { + if (_model->isLoaded()) { if (EntityItem::getEntitiesShouldFadeFunction()()) { _fadeStartTime = usecTimestampNow(); _fadeState = FADE_IN_PROGRESS; @@ -557,6 +557,11 @@ void ModelMeshPartPayload::render(RenderArgs* args) const { } } + if (_materialNeedsUpdate && _model->getGeometry()->areTexturesLoaded()) { + _model->setRenderItemsNeedUpdate(); + _materialNeedsUpdate = false; + } + if (!args) { return; } diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h index ef74011c40..11d1bbf6a7 100644 --- a/libraries/render-utils/src/MeshPartPayload.h +++ b/libraries/render-utils/src/MeshPartPayload.h @@ -46,11 +46,11 @@ public: virtual render::ItemKey getKey() const; virtual render::Item::Bound getBound() const; virtual render::ShapeKey getShapeKey() const; // shape interface - virtual void render(RenderArgs* args) const; + virtual void render(RenderArgs* args); // ModelMeshPartPayload functions to perform render void drawCall(gpu::Batch& batch) const; - virtual void bindMesh(gpu::Batch& batch) const; + virtual void bindMesh(gpu::Batch& batch); virtual void bindMaterial(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, bool enableTextures) const; virtual void bindTransform(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const; @@ -93,16 +93,16 @@ public: const Transform& boundTransform, const gpu::BufferPointer& buffer); - float computeFadeAlpha() const; + float computeFadeAlpha(); // Render Item interface render::ItemKey getKey() const override; int getLayer() const; render::ShapeKey getShapeKey() const override; // shape interface - void render(RenderArgs* args) const override; + void render(RenderArgs* args) override; // ModelMeshPartPayload functions to perform render - void bindMesh(gpu::Batch& batch) const override; + void bindMesh(gpu::Batch& batch) override; void bindTransform(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const override; void initCache(); @@ -116,11 +116,12 @@ public: int _shapeID; bool _isSkinned{ false }; - bool _isBlendShaped{ false }; + bool _isBlendShaped { false }; + bool _materialNeedsUpdate { true }; private: - mutable quint64 _fadeStartTime { 0 }; - mutable uint8_t _fadeState { FADE_WAITING_TO_START }; + quint64 _fadeStartTime { 0 }; + uint8_t _fadeState { FADE_WAITING_TO_START }; }; namespace render { diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 62e045a3c1..5899ccf6b5 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -252,6 +252,8 @@ public: void renderDebugMeshBoxes(gpu::Batch& batch); + int getResourceDownloadAttempts() { return _renderWatcher.getResourceDownloadAttempts(); } + int getResourceDownloadAttemptsRemaining() { return _renderWatcher.getResourceDownloadAttemptsRemaining(); } public slots: void loadURLFinished(bool success); diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 08d4f0fc68..313b176f19 100644 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -199,7 +199,7 @@ void RenderDeferredTask::build(JobModel& task, const render::Varying& input, ren { // Grab a texture map representing the different status icons and assign that to the drawStatsuJob auto iconMapPath = PathUtils::resourcesPath() + "icons/statusIconAtlas.svg"; - auto statusIconMap = DependencyManager::get()->getImageTexture(iconMapPath, NetworkTexture::STRICT_TEXTURE); + auto statusIconMap = DependencyManager::get()->getImageTexture(iconMapPath, image::TextureUsage::STRICT_TEXTURE); task.addJob("DrawStatus", opaques, DrawStatus(statusIconMap)); } } diff --git a/libraries/render-utils/src/RenderForwardTask.cpp b/libraries/render-utils/src/RenderForwardTask.cpp index 46a7128fee..84514eeb1a 100755 --- a/libraries/render-utils/src/RenderForwardTask.cpp +++ b/libraries/render-utils/src/RenderForwardTask.cpp @@ -75,11 +75,11 @@ void PrepareFramebuffer::run(const RenderContextPointer& renderContext, auto colorFormat = gpu::Element::COLOR_SRGBA_32; auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - auto colorTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler)); + auto colorTexture = gpu::Texture::create2D(colorFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler); _framebuffer->setRenderBuffer(0, colorTexture); auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format - auto depthTexture = gpu::TexturePointer(gpu::Texture::create2D(depthFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler)); + auto depthTexture = gpu::Texture::create2D(depthFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler); _framebuffer->setDepthStencilBuffer(depthTexture, depthFormat); } diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index c92acc11ad..40b3c85675 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -414,7 +414,7 @@ gpu::TexturePointer SubsurfaceScatteringResource::generateScatteringProfile(Rend const int PROFILE_RESOLUTION = 512; // const auto pixelFormat = gpu::Element::COLOR_SRGBA_32; const auto pixelFormat = gpu::Element::COLOR_R11G11B10; - auto profileMap = gpu::TexturePointer(gpu::Texture::createRenderBuffer(pixelFormat, PROFILE_RESOLUTION, 1, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); + auto profileMap = gpu::Texture::createRenderBuffer(pixelFormat, PROFILE_RESOLUTION, 1, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP)); profileMap->setSource("Generated Scattering Profile"); diffuseProfileGPU(profileMap, args); return profileMap; @@ -425,7 +425,7 @@ gpu::TexturePointer SubsurfaceScatteringResource::generatePreIntegratedScatterin const int TABLE_RESOLUTION = 512; // const auto pixelFormat = gpu::Element::COLOR_SRGBA_32; const auto pixelFormat = gpu::Element::COLOR_R11G11B10; - auto scatteringLUT = gpu::TexturePointer(gpu::Texture::createRenderBuffer(pixelFormat, TABLE_RESOLUTION, TABLE_RESOLUTION, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); + auto scatteringLUT = gpu::Texture::createRenderBuffer(pixelFormat, TABLE_RESOLUTION, TABLE_RESOLUTION, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP)); //diffuseScatter(scatteringLUT); scatteringLUT->setSource("Generated pre-integrated scattering"); diffuseScatterGPU(profile, scatteringLUT, args); @@ -434,7 +434,7 @@ gpu::TexturePointer SubsurfaceScatteringResource::generatePreIntegratedScatterin gpu::TexturePointer SubsurfaceScatteringResource::generateScatteringSpecularBeckmann(RenderArgs* args) { const int SPECULAR_RESOLUTION = 256; - auto beckmannMap = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, SPECULAR_RESOLUTION, SPECULAR_RESOLUTION, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); + auto beckmannMap = gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, SPECULAR_RESOLUTION, SPECULAR_RESOLUTION, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP)); beckmannMap->setSource("Generated beckmannMap"); computeSpecularBeckmannGPU(beckmannMap, args); return beckmannMap; diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index 164aca0624..ef50960b7d 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -72,19 +72,19 @@ void LinearDepthFramebuffer::allocate() { auto height = _frameSize.y; // For Linear Depth: - _linearDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RED), width, height, gpu::Texture::SINGLE_MIP, - gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _linearDepthTexture = gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RED), width, height, gpu::Texture::SINGLE_MIP, + gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT)); _linearDepthFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("linearDepth")); _linearDepthFramebuffer->setRenderBuffer(0, _linearDepthTexture); _linearDepthFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, _primaryDepthTexture->getTexelFormat()); // For Downsampling: const uint16_t HALF_LINEAR_DEPTH_MAX_MIP_LEVEL = 5; - _halfLinearDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RED), _halfFrameSize.x, _halfFrameSize.y, HALF_LINEAR_DEPTH_MAX_MIP_LEVEL, - gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _halfLinearDepthTexture = gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RED), _halfFrameSize.x, _halfFrameSize.y, HALF_LINEAR_DEPTH_MAX_MIP_LEVEL, + gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT)); - _halfNormalTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, _halfFrameSize.x, _halfFrameSize.y, gpu::Texture::SINGLE_MIP, - gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _halfNormalTexture = gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, _halfFrameSize.x, _halfFrameSize.y, gpu::Texture::SINGLE_MIP, + gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT)); _downsampleFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("halfLinearDepth")); _downsampleFramebuffer->setRenderBuffer(0, _halfLinearDepthTexture); @@ -304,15 +304,15 @@ void SurfaceGeometryFramebuffer::allocate() { auto width = _frameSize.x; auto height = _frameSize.y; - _curvatureTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _curvatureTexture = gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT)); _curvatureFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("surfaceGeometry::curvature")); _curvatureFramebuffer->setRenderBuffer(0, _curvatureTexture); - _lowCurvatureTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _lowCurvatureTexture = gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT)); _lowCurvatureFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("surfaceGeometry::lowCurvature")); _lowCurvatureFramebuffer->setRenderBuffer(0, _lowCurvatureTexture); - _blurringTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _blurringTexture = gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT)); _blurringFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("surfaceGeometry::blurring")); _blurringFramebuffer->setRenderBuffer(0, _blurringTexture); } diff --git a/libraries/render-utils/src/text/Font.cpp b/libraries/render-utils/src/text/Font.cpp index 00fcabd7da..c6a7da3a1a 100644 --- a/libraries/render-utils/src/text/Font.cpp +++ b/libraries/render-utils/src/text/Font.cpp @@ -207,8 +207,8 @@ void Font::read(QIODevice& in) { formatGPU = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA); formatMip = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::BGRA); } - _texture = gpu::TexturePointer(gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::SINGLE_MIP, - gpu::Sampler(gpu::Sampler::FILTER_MIN_POINT_MAG_LINEAR))); + _texture = gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::SINGLE_MIP, + gpu::Sampler(gpu::Sampler::FILTER_MIN_POINT_MAG_LINEAR)); _texture->setStoredMipFormat(formatMip); _texture->assignStoredMip(0, image.byteCount(), image.constBits()); } diff --git a/libraries/render/src/render/BlurTask.cpp b/libraries/render/src/render/BlurTask.cpp index 2fc7dc0ea0..0a6b3d16fc 100644 --- a/libraries/render/src/render/BlurTask.cpp +++ b/libraries/render/src/render/BlurTask.cpp @@ -108,7 +108,7 @@ bool BlurInOutResource::updateResources(const gpu::FramebufferPointer& sourceFra // _blurredFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); //} auto blurringSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT); - auto blurringTarget = gpu::TexturePointer(gpu::Texture::create2D(sourceFramebuffer->getRenderBuffer(0)->getTexelFormat(), sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), gpu::Texture::SINGLE_MIP, blurringSampler)); + auto blurringTarget = gpu::Texture::create2D(sourceFramebuffer->getRenderBuffer(0)->getTexelFormat(), sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), gpu::Texture::SINGLE_MIP, blurringSampler); _blurredFramebuffer->setRenderBuffer(0, blurringTarget); } @@ -131,7 +131,7 @@ bool BlurInOutResource::updateResources(const gpu::FramebufferPointer& sourceFra _outputFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); }*/ auto blurringSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT); - auto blurringTarget = gpu::TexturePointer(gpu::Texture::create2D(sourceFramebuffer->getRenderBuffer(0)->getTexelFormat(), sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), gpu::Texture::SINGLE_MIP, blurringSampler)); + auto blurringTarget = gpu::Texture::create2D(sourceFramebuffer->getRenderBuffer(0)->getTexelFormat(), sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), gpu::Texture::SINGLE_MIP, blurringSampler); _outputFramebuffer->setRenderBuffer(0, blurringTarget); } diff --git a/libraries/script-engine/CMakeLists.txt b/libraries/script-engine/CMakeLists.txt index b0dbbc111b..39338fd767 100644 --- a/libraries/script-engine/CMakeLists.txt +++ b/libraries/script-engine/CMakeLists.txt @@ -16,6 +16,6 @@ if (NOT ANDROID) endif () -link_hifi_libraries(shared networking octree gpu ui procedural model model-networking recording avatars fbx entities controllers animation audio physics) +link_hifi_libraries(shared networking octree gpu ui procedural model model-networking ktx recording avatars fbx entities controllers animation audio physics image) # ui includes gl, but link_hifi_libraries does not use transitive includes, so gl must be explicit -include_hifi_library_headers(gl) \ No newline at end of file +include_hifi_library_headers(gl) diff --git a/libraries/script-engine/src/AssetScriptingInterface.cpp b/libraries/script-engine/src/AssetScriptingInterface.cpp index 00fa1f3ba5..65259987c4 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.cpp +++ b/libraries/script-engine/src/AssetScriptingInterface.cpp @@ -44,7 +44,8 @@ void AssetScriptingInterface::setMapping(QString path, QString hash, QScriptValu QObject::connect(setMappingRequest, &SetMappingRequest::finished, this, [this, callback](SetMappingRequest* request) mutable { if (callback.isFunction()) { - QScriptValueList args { }; + QString error = request->getErrorString(); + QScriptValueList args { error }; callback.call(_engine->currentContext()->thisObject(), args); } request->deleteLater(); diff --git a/libraries/script-engine/src/AssetScriptingInterface.h b/libraries/script-engine/src/AssetScriptingInterface.h index d8bc319256..0238329b73 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.h +++ b/libraries/script-engine/src/AssetScriptingInterface.h @@ -72,6 +72,7 @@ public: /**jsdoc * Called when setMapping is complete * @callback Assets~setMappingCallback + * @param {string} error */ Q_INVOKABLE void setMapping(QString path, QString hash, QScriptValue callback); diff --git a/libraries/script-engine/src/Quat.cpp b/libraries/script-engine/src/Quat.cpp index 6d49ed27c1..05002dcf5d 100644 --- a/libraries/script-engine/src/Quat.cpp +++ b/libraries/script-engine/src/Quat.cpp @@ -122,3 +122,10 @@ bool Quat::equal(const glm::quat& q1, const glm::quat& q2) { return q1 == q2; } +glm::quat Quat::cancelOutRollAndPitch(const glm::quat& q) { + return ::cancelOutRollAndPitch(q); +} + +glm::quat Quat::cancelOutRoll(const glm::quat& q) { + return ::cancelOutRoll(q); +} diff --git a/libraries/script-engine/src/Quat.h b/libraries/script-engine/src/Quat.h index 8a88767a41..ee3ab9aa7c 100644 --- a/libraries/script-engine/src/Quat.h +++ b/libraries/script-engine/src/Quat.h @@ -60,6 +60,8 @@ public slots: float dot(const glm::quat& q1, const glm::quat& q2); void print(const QString& label, const glm::quat& q); bool equal(const glm::quat& q1, const glm::quat& q2); + glm::quat cancelOutRollAndPitch(const glm::quat& q); + glm::quat cancelOutRoll(const glm::quat& q); }; #endif // hifi_Quat_h diff --git a/libraries/script-engine/src/RecordingScriptingInterface.cpp b/libraries/script-engine/src/RecordingScriptingInterface.cpp index 36de1c1ef7..98838441d2 100644 --- a/libraries/script-engine/src/RecordingScriptingInterface.cpp +++ b/libraries/script-engine/src/RecordingScriptingInterface.cpp @@ -210,9 +210,11 @@ bool RecordingScriptingInterface::saveRecordingToAsset(QScriptValue getClipAtpUr } if (QThread::currentThread() != thread()) { + bool result; QMetaObject::invokeMethod(this, "saveRecordingToAsset", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, result), Q_ARG(QScriptValue, getClipAtpUrl)); - return false; + return result; } if (!_lastClip) { diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 067c7c1412..c904062507 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -43,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -678,6 +679,8 @@ void ScriptEngine::init() { registerGlobalObject("Model", new ModelScriptingInterface(this)); qScriptRegisterMetaType(this, meshToScriptValue, meshFromScriptValue); qScriptRegisterMetaType(this, meshesToScriptValue, meshesFromScriptValue); + + registerGlobalObject("UserActivityLogger", DependencyManager::get().data()); } void ScriptEngine::registerValue(const QString& valueName, QScriptValue value) { @@ -2317,6 +2320,8 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID, bool shouldR if (_entityScripts.contains(entityID)) { const EntityScriptDetails &oldDetails = _entityScripts[entityID]; + auto scriptText = oldDetails.scriptText; + if (isEntityScriptRunning(entityID)) { callEntityScriptMethod(entityID, "unload"); } @@ -2334,14 +2339,14 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID, bool shouldR newDetails.status = EntityScriptStatus::UNLOADED; newDetails.lastModified = QDateTime::currentMSecsSinceEpoch(); // keep scriptText populated for the current need to "debouce" duplicate calls to unloadEntityScript - newDetails.scriptText = oldDetails.scriptText; + newDetails.scriptText = scriptText; setEntityScriptDetails(entityID, newDetails); } stopAllTimersForEntityScript(entityID); { // FIXME: shouldn't have to do this here, but currently something seems to be firing unloads moments after firing initial load requests - processDeferredEntityLoads(oldDetails.scriptText, entityID); + processDeferredEntityLoads(scriptText, entityID); } } } diff --git a/libraries/script-engine/src/TabletScriptingInterface.cpp b/libraries/script-engine/src/TabletScriptingInterface.cpp index bffe318c11..d4eeecc82e 100644 --- a/libraries/script-engine/src/TabletScriptingInterface.cpp +++ b/libraries/script-engine/src/TabletScriptingInterface.cpp @@ -508,7 +508,7 @@ void TabletProxy::gotoWebScreen(const QString& url, const QString& injectedJavaS if (root) { removeButtonsFromHomeScreen(); - QMetaObject::invokeMethod(root, "loadSource", Q_ARG(const QVariant&, QVariant(WEB_VIEW_SOURCE_URL))); + QMetaObject::invokeMethod(root, "loadWebBase"); QMetaObject::invokeMethod(root, "setShown", Q_ARG(const QVariant&, QVariant(true))); QMetaObject::invokeMethod(root, "loadWebUrl", Q_ARG(const QVariant&, QVariant(url)), Q_ARG(const QVariant&, QVariant(injectedJavaScriptUrl))); } diff --git a/libraries/shared/src/GLMHelpers.cpp b/libraries/shared/src/GLMHelpers.cpp index ec244553f8..db42fef8bc 100644 --- a/libraries/shared/src/GLMHelpers.cpp +++ b/libraries/shared/src/GLMHelpers.cpp @@ -38,6 +38,11 @@ const quat Quaternions::X_180{ 0.0f, 1.0f, 0.0f, 0.0f }; const quat Quaternions::Y_180{ 0.0f, 0.0f, 1.0f, 0.0f }; const quat Quaternions::Z_180{ 0.0f, 0.0f, 0.0f, 1.0f }; +const mat4 Matrices::IDENTITY { glm::mat4() }; +const mat4 Matrices::X_180 { createMatFromQuatAndPos(Quaternions::X_180, Vectors::ZERO) }; +const mat4 Matrices::Y_180 { createMatFromQuatAndPos(Quaternions::Y_180, Vectors::ZERO) }; +const mat4 Matrices::Z_180 { createMatFromQuatAndPos(Quaternions::Z_180, Vectors::ZERO) }; + // Safe version of glm::mix; based on the code in Nick Bobick's article, // http://www.gamasutra.com/features/19980703/quaternions_01.htm (via Clyde, // https://github.com/threerings/clyde/blob/master/src/main/java/com/threerings/math/Quaternion.java) diff --git a/libraries/shared/src/GLMHelpers.h b/libraries/shared/src/GLMHelpers.h index deb87930fc..fd75fa416c 100644 --- a/libraries/shared/src/GLMHelpers.h +++ b/libraries/shared/src/GLMHelpers.h @@ -54,6 +54,13 @@ const glm::vec3 IDENTITY_FORWARD = glm::vec3( 0.0f, 0.0f,-1.0f); glm::quat safeMix(const glm::quat& q1, const glm::quat& q2, float alpha); +class Matrices { +public: + static const mat4 IDENTITY; + static const mat4 X_180; + static const mat4 Y_180; + static const mat4 Z_180; +}; class Quaternions { public: diff --git a/libraries/shared/src/shared/Storage.cpp b/libraries/shared/src/shared/Storage.cpp index 3c46347a49..aae1f8455f 100644 --- a/libraries/shared/src/shared/Storage.cpp +++ b/libraries/shared/src/shared/Storage.cpp @@ -68,7 +68,7 @@ StoragePointer FileStorage::create(const QString& filename, size_t size, const u } FileStorage::FileStorage(const QString& filename) : _file(filename) { - if (_file.open(QFile::ReadOnly)) { + if (_file.open(QFile::ReadWrite)) { _mapped = _file.map(0, _file.size()); if (_mapped) { _valid = true; diff --git a/libraries/shared/src/shared/Storage.h b/libraries/shared/src/shared/Storage.h index 306984040f..da5b773d52 100644 --- a/libraries/shared/src/shared/Storage.h +++ b/libraries/shared/src/shared/Storage.h @@ -20,10 +20,12 @@ namespace storage { class Storage; using StoragePointer = std::shared_ptr; + // Abstract class to represent memory that stored _somewhere_ (in system memory or in a file, for example) class Storage : public std::enable_shared_from_this { public: virtual ~Storage() {} virtual const uint8_t* data() const = 0; + virtual uint8_t* mutableData() = 0; virtual size_t size() const = 0; virtual operator bool() const { return true; } @@ -41,6 +43,7 @@ namespace storage { MemoryStorage(size_t size, const uint8_t* data = nullptr); const uint8_t* data() const override { return _data.data(); } uint8_t* data() { return _data.data(); } + uint8_t* mutableData() override { return _data.data(); } size_t size() const override { return _data.size(); } operator bool() const override { return true; } private: @@ -57,6 +60,7 @@ namespace storage { FileStorage& operator=(const FileStorage& other) = delete; const uint8_t* data() const override { return _mapped; } + uint8_t* mutableData() override { return _mapped; } size_t size() const override { return _file.size(); } operator bool() const override { return _valid; } private: @@ -69,6 +73,7 @@ namespace storage { public: ViewStorage(const storage::StoragePointer& owner, size_t size, const uint8_t* data); const uint8_t* data() const override { return _data; } + uint8_t* mutableData() override { throw std::runtime_error("Cannot modify ViewStorage"); } size_t size() const override { return _size; } operator bool() const override { return *_owner; } private: diff --git a/libraries/trackers/CMakeLists.txt b/libraries/trackers/CMakeLists.txt new file mode 100644 index 0000000000..0999a45b59 --- /dev/null +++ b/libraries/trackers/CMakeLists.txt @@ -0,0 +1,6 @@ +set(TARGET_NAME trackers) +setup_hifi_library() +GroupSources("src") +link_hifi_libraries(shared) + +target_bullet() diff --git a/interface/src/devices/DeviceTracker.cpp b/libraries/trackers/src/trackers/DeviceTracker.cpp similarity index 98% rename from interface/src/devices/DeviceTracker.cpp rename to libraries/trackers/src/trackers/DeviceTracker.cpp index 2cd4950064..93aeb607bc 100644 --- a/interface/src/devices/DeviceTracker.cpp +++ b/libraries/trackers/src/trackers/DeviceTracker.cpp @@ -1,7 +1,4 @@ // -// DeviceTracker.cpp -// interface/src/devices -// // Created by Sam Cake on 6/20/14. // Copyright 2014 High Fidelity, Inc. // diff --git a/interface/src/devices/DeviceTracker.h b/libraries/trackers/src/trackers/DeviceTracker.h similarity index 98% rename from interface/src/devices/DeviceTracker.h rename to libraries/trackers/src/trackers/DeviceTracker.h index 543e9bab1a..8a7f509cb3 100644 --- a/interface/src/devices/DeviceTracker.h +++ b/libraries/trackers/src/trackers/DeviceTracker.h @@ -1,7 +1,4 @@ // -// DeviceTracker.h -// interface/src/devices -// // Created by Sam Cake on 6/20/14. // Copyright 2014 High Fidelity, Inc. // diff --git a/interface/src/devices/EyeTracker.cpp b/libraries/trackers/src/trackers/EyeTracker.cpp similarity index 98% rename from interface/src/devices/EyeTracker.cpp rename to libraries/trackers/src/trackers/EyeTracker.cpp index 367aa52aae..8733461dbb 100644 --- a/interface/src/devices/EyeTracker.cpp +++ b/libraries/trackers/src/trackers/EyeTracker.cpp @@ -1,7 +1,4 @@ // -// EyeTracker.cpp -// interface/src/devices -// // Created by David Rowe on 27 Jul 2015. // Copyright 2015 High Fidelity, Inc. // @@ -17,8 +14,8 @@ #include -#include "InterfaceLogging.h" -#include "OctreeConstants.h" +#include "Logging.h" +#include #ifdef HAVE_IVIEWHMD char* HIGH_FIDELITY_EYE_TRACKER_CALIBRATION = "HighFidelityEyeTrackerCalibration"; @@ -115,7 +112,7 @@ void EyeTracker::processData(smi_CallbackDataStruct* data) { void EyeTracker::init() { if (_isInitialized) { - qCWarning(interfaceapp) << "Eye Tracker: Already initialized"; + qCWarning(trackers) << "Eye Tracker: Already initialized"; return; } } diff --git a/interface/src/devices/EyeTracker.h b/libraries/trackers/src/trackers/EyeTracker.h similarity index 97% rename from interface/src/devices/EyeTracker.h rename to libraries/trackers/src/trackers/EyeTracker.h index 0e760d9454..9cf35d0f2a 100644 --- a/interface/src/devices/EyeTracker.h +++ b/libraries/trackers/src/trackers/EyeTracker.h @@ -1,7 +1,4 @@ // -// EyeTracker.h -// interface/src/devices -// // Created by David Rowe on 27 Jul 2015. // Copyright 2015 High Fidelity, Inc. // diff --git a/interface/src/devices/FaceTracker.cpp b/libraries/trackers/src/trackers/FaceTracker.cpp similarity index 73% rename from interface/src/devices/FaceTracker.cpp rename to libraries/trackers/src/trackers/FaceTracker.cpp index 76a4534952..034787f19a 100644 --- a/interface/src/devices/FaceTracker.cpp +++ b/libraries/trackers/src/trackers/FaceTracker.cpp @@ -1,7 +1,4 @@ // -// FaceTracker.cpp -// interface/src/devices -// // Created by Andrzej Kapolka on 4/9/14. // Copyright 2014 High Fidelity, Inc. // @@ -9,22 +6,21 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include - -#include - #include "FaceTracker.h" -#include "InterfaceLogging.h" -#include "Menu.h" + +#include +#include +#include "Logging.h" +//#include "Menu.h" const int FPS_TIMER_DELAY = 2000; // ms const int FPS_TIMER_DURATION = 2000; // ms const float DEFAULT_EYE_DEFLECTION = 0.25f; Setting::Handle FaceTracker::_eyeDeflection("faceshiftEyeDeflection", DEFAULT_EYE_DEFLECTION); +bool FaceTracker::_isMuted { true }; void FaceTracker::init() { - _isMuted = Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking); _isInitialized = true; // FaceTracker can be used now } @@ -101,7 +97,7 @@ void FaceTracker::countFrame() { } void FaceTracker::finishFPSTimer() { - qCDebug(interfaceapp) << "Face tracker FPS =" << (float)_frameCount / ((float)FPS_TIMER_DURATION / 1000.0f); + qCDebug(trackers) << "Face tracker FPS =" << (float)_frameCount / ((float)FPS_TIMER_DURATION / 1000.0f); _isCalculatingFPS = false; } @@ -113,3 +109,25 @@ void FaceTracker::toggleMute() { void FaceTracker::setEyeDeflection(float eyeDeflection) { _eyeDeflection.set(eyeDeflection); } + +void FaceTracker::updateFakeCoefficients(float leftBlink, float rightBlink, float browUp, + float jawOpen, float mouth2, float mouth3, float mouth4, QVector& coefficients) { + const int MMMM_BLENDSHAPE = 34; + const int FUNNEL_BLENDSHAPE = 40; + const int SMILE_LEFT_BLENDSHAPE = 28; + const int SMILE_RIGHT_BLENDSHAPE = 29; + const int MAX_FAKE_BLENDSHAPE = 40; // Largest modified blendshape from above and below + + coefficients.resize(std::max((int)coefficients.size(), MAX_FAKE_BLENDSHAPE + 1)); + qFill(coefficients.begin(), coefficients.end(), 0.0f); + coefficients[_leftBlinkIndex] = leftBlink; + coefficients[_rightBlinkIndex] = rightBlink; + coefficients[_browUpCenterIndex] = browUp; + coefficients[_browUpLeftIndex] = browUp; + coefficients[_browUpRightIndex] = browUp; + coefficients[_jawOpenIndex] = jawOpen; + coefficients[SMILE_LEFT_BLENDSHAPE] = coefficients[SMILE_RIGHT_BLENDSHAPE] = mouth4; + coefficients[MMMM_BLENDSHAPE] = mouth2; + coefficients[FUNNEL_BLENDSHAPE] = mouth3; +} + diff --git a/interface/src/devices/FaceTracker.h b/libraries/trackers/src/trackers/FaceTracker.h similarity index 64% rename from interface/src/devices/FaceTracker.h rename to libraries/trackers/src/trackers/FaceTracker.h index 7126d19ca8..58d5c5e574 100644 --- a/interface/src/devices/FaceTracker.h +++ b/libraries/trackers/src/trackers/FaceTracker.h @@ -1,7 +1,4 @@ // -// FaceTracker.h -// interface/src/devices -// // Created by Andrzej Kapolka on 4/9/14. // Copyright 2014 High Fidelity, Inc. // @@ -20,7 +17,7 @@ #include -/// Base class for face trackers (Faceshift, DDE). +/// Base class for face trackers (DDE, BinaryVR). class FaceTracker : public QObject { Q_OBJECT @@ -45,12 +42,21 @@ public: const QVector& getBlendshapeCoefficients() const; float getBlendshapeCoefficient(int index) const; - bool isMuted() const { return _isMuted; } - void setIsMuted(bool isMuted) { _isMuted = isMuted; } + static bool isMuted() { return _isMuted; } + static void setIsMuted(bool isMuted) { _isMuted = isMuted; } static float getEyeDeflection() { return _eyeDeflection.get(); } static void setEyeDeflection(float eyeDeflection); + static void updateFakeCoefficients(float leftBlink, + float rightBlink, + float browUp, + float jawOpen, + float mouth2, + float mouth3, + float mouth4, + QVector& coefficients); + signals: void muteToggled(); @@ -63,7 +69,7 @@ protected: virtual ~FaceTracker() {}; bool _isInitialized = false; - bool _isMuted = true; + static bool _isMuted; glm::vec3 _headTranslation = glm::vec3(0.0f); glm::quat _headRotation = glm::quat(); @@ -84,6 +90,24 @@ private: bool _isCalculatingFPS = false; int _frameCount = 0; + // see http://support.faceshift.com/support/articles/35129-export-of-blendshapes + static const int _leftBlinkIndex = 0; + static const int _rightBlinkIndex = 1; + static const int _leftEyeOpenIndex = 8; + static const int _rightEyeOpenIndex = 9; + + // Brows + static const int _browDownLeftIndex = 14; + static const int _browDownRightIndex = 15; + static const int _browUpCenterIndex = 16; + static const int _browUpLeftIndex = 17; + static const int _browUpRightIndex = 18; + + static const int _mouthSmileLeftIndex = 28; + static const int _mouthSmileRightIndex = 29; + + static const int _jawOpenIndex = 21; + static Setting::Handle _eyeDeflection; }; diff --git a/libraries/trackers/src/trackers/Logging.cpp b/libraries/trackers/src/trackers/Logging.cpp new file mode 100644 index 0000000000..a4dcf1b711 --- /dev/null +++ b/libraries/trackers/src/trackers/Logging.cpp @@ -0,0 +1,11 @@ +// +// Created by Bradley Austin Davis on 2017/04/25 +// Copyright 2013-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 "Logging.h" + +Q_LOGGING_CATEGORY(trackers, "hifi.trackers") diff --git a/libraries/trackers/src/trackers/Logging.h b/libraries/trackers/src/trackers/Logging.h new file mode 100644 index 0000000000..554429b61d --- /dev/null +++ b/libraries/trackers/src/trackers/Logging.h @@ -0,0 +1,16 @@ +// +// Created by Bradley Austin Davis on 2017/04/25 +// Copyright 2013-2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_TrackersLogging_h +#define hifi_TrackersLogging_h + +#include + +Q_DECLARE_LOGGING_CATEGORY(trackers) + +#endif // hifi_TrackersLogging_h diff --git a/interface/src/devices/MotionTracker.cpp b/libraries/trackers/src/trackers/MotionTracker.cpp similarity index 98% rename from interface/src/devices/MotionTracker.cpp rename to libraries/trackers/src/trackers/MotionTracker.cpp index 234a8d0c0c..c6012c0422 100644 --- a/interface/src/devices/MotionTracker.cpp +++ b/libraries/trackers/src/trackers/MotionTracker.cpp @@ -1,7 +1,4 @@ // -// MotionTracker.cpp -// interface/src/devices -// // Created by Sam Cake on 6/20/14. // Copyright 2014 High Fidelity, Inc. // @@ -10,8 +7,6 @@ // #include "MotionTracker.h" -#include "GLMHelpers.h" - // glm::mult(mat43, mat43) just the composition of the 2 matrices assuming they are in fact mat44 with the last raw = { 0, 0, 0, 1 } namespace glm { diff --git a/interface/src/devices/MotionTracker.h b/libraries/trackers/src/trackers/MotionTracker.h similarity index 93% rename from interface/src/devices/MotionTracker.h rename to libraries/trackers/src/trackers/MotionTracker.h index a4b5e6735e..26c8dcea2c 100644 --- a/interface/src/devices/MotionTracker.h +++ b/libraries/trackers/src/trackers/MotionTracker.h @@ -1,7 +1,4 @@ // -// MotionTracker.h -// interface/src/devices -// // Created by Sam Cake on 6/20/14. // Copyright 2014 High Fidelity, Inc. // @@ -14,20 +11,7 @@ #include "DeviceTracker.h" -#ifdef __GNUC__ -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wsign-compare" -#endif - -#include - -#ifdef __GNUC__ -#pragma GCC diagnostic pop -#endif - - -#include -#include +#include /// Base class for device trackers. class MotionTracker : public DeviceTracker { diff --git a/libraries/ui/src/FileDialogHelper.cpp b/libraries/ui/src/FileDialogHelper.cpp index 2752de8592..6d14adf1db 100644 --- a/libraries/ui/src/FileDialogHelper.cpp +++ b/libraries/ui/src/FileDialogHelper.cpp @@ -26,6 +26,10 @@ QStringList FileDialogHelper::standardPath(StandardLocation location) { return QStandardPaths::standardLocations(static_cast(location)); } +QString FileDialogHelper::writableLocation(StandardLocation location) { + return QStandardPaths::writableLocation(static_cast(location)); +} + QString FileDialogHelper::urlToPath(const QUrl& url) { return url.toLocalFile(); } diff --git a/libraries/ui/src/FileDialogHelper.h b/libraries/ui/src/FileDialogHelper.h index 6c352ecdfc..12fd60daac 100644 --- a/libraries/ui/src/FileDialogHelper.h +++ b/libraries/ui/src/FileDialogHelper.h @@ -48,6 +48,7 @@ public: Q_INVOKABLE QUrl home(); Q_INVOKABLE QStringList standardPath(StandardLocation location); + Q_INVOKABLE QString writableLocation(StandardLocation location); Q_INVOKABLE QStringList drives(); Q_INVOKABLE QString urlToPath(const QUrl& url); Q_INVOKABLE bool urlIsDir(const QUrl& url); diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 2a8f3ec9d5..84812b4f60 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -716,6 +716,86 @@ QString OffscreenUi::getExistingDirectory(void* ignored, const QString &caption, return DependencyManager::get()->existingDirectoryDialog(caption, dir, filter, selectedFilter, options); } +class AssetDialogListener : public ModalDialogListener { + // ATP equivalent of FileDialogListener. + Q_OBJECT + + friend class OffscreenUi; + AssetDialogListener(QQuickItem* messageBox) : ModalDialogListener(messageBox) { + if (_finished) { + return; + } + connect(_dialog, SIGNAL(selectedAsset(QVariant)), this, SLOT(onSelectedAsset(QVariant))); + } + + private slots: + void onSelectedAsset(QVariant asset) { + _result = asset; + _finished = true; + disconnect(_dialog); + } +}; + + +QString OffscreenUi::assetDialog(const QVariantMap& properties) { + // ATP equivalent of fileDialog(). + QVariant buildDialogResult; + bool invokeResult; + auto tabletScriptingInterface = DependencyManager::get(); + TabletProxy* tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + if (tablet->getToolbarMode()) { + invokeResult = QMetaObject::invokeMethod(_desktop, "assetDialog", + Q_RETURN_ARG(QVariant, buildDialogResult), + Q_ARG(QVariant, QVariant::fromValue(properties))); + } else { + QQuickItem* tabletRoot = tablet->getTabletRoot(); + invokeResult = QMetaObject::invokeMethod(tabletRoot, "assetDialog", + Q_RETURN_ARG(QVariant, buildDialogResult), + Q_ARG(QVariant, QVariant::fromValue(properties))); + emit tabletScriptingInterface->tabletNotification(); + } + + if (!invokeResult) { + qWarning() << "Failed to create asset open dialog"; + return QString(); + } + + QVariant result = AssetDialogListener(qvariant_cast(buildDialogResult)).waitForResult(); + if (!result.isValid()) { + return QString(); + } + qCDebug(uiLogging) << result.toString(); + return result.toUrl().toString(); +} + +QString OffscreenUi::assetOpenDialog(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { + // ATP equivalent of fileOpenDialog(). + if (QThread::currentThread() != thread()) { + QString result; + QMetaObject::invokeMethod(this, "assetOpenDialog", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QString, result), + Q_ARG(QString, caption), + Q_ARG(QString, dir), + Q_ARG(QString, filter), + Q_ARG(QString*, selectedFilter), + Q_ARG(QFileDialog::Options, options)); + return result; + } + + // FIXME support returning the selected filter... somehow? + QVariantMap map; + map.insert("caption", caption); + map.insert("dir", dir); + map.insert("filter", filter); + map.insert("options", static_cast(options)); + return assetDialog(map); +} + +QString OffscreenUi::getOpenAssetName(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { + // ATP equivalent of getOpenFileName(). + return DependencyManager::get()->assetOpenDialog(caption, dir, filter, selectedFilter, options); +} + bool OffscreenUi::eventFilter(QObject* originalDestination, QEvent* event) { if (!filterEnabled(originalDestination, event)) { return false; diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index 5813d0bfd2..55fb8b2c3d 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -118,6 +118,8 @@ public: Q_INVOKABLE QString fileSaveDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); Q_INVOKABLE QString existingDirectoryDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + Q_INVOKABLE QString assetOpenDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + // Compatibility with QFileDialog::getOpenFileName static QString getOpenFileName(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); // Compatibility with QFileDialog::getSaveFileName @@ -125,6 +127,8 @@ public: // Compatibility with QFileDialog::getExistingDirectory static QString getExistingDirectory(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + static QString getOpenAssetName(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + Q_INVOKABLE QVariant inputDialog(const Icon icon, const QString& title, const QString& label = QString(), const QVariant& current = QVariant()); Q_INVOKABLE QVariant customInputDialog(const Icon icon, const QString& title, const QVariantMap& config); QQuickItem* createInputDialog(const Icon icon, const QString& title, const QString& label, const QVariant& current); @@ -155,6 +159,7 @@ signals: private: QString fileDialog(const QVariantMap& properties); + QString assetDialog(const QVariantMap& properties); QQuickItem* _desktop { nullptr }; QQuickItem* _toolWindow { nullptr }; diff --git a/libraries/ui/src/ui/Menu.cpp b/libraries/ui/src/ui/Menu.cpp index 50833e90fc..7511448c38 100644 --- a/libraries/ui/src/ui/Menu.cpp +++ b/libraries/ui/src/ui/Menu.cpp @@ -223,6 +223,18 @@ QAction* Menu::addCheckableActionToQMenuAndActionHash(MenuWrapper* destinationMe return action; } +QAction* Menu::addCheckableActionToQMenuAndActionHash(MenuWrapper* destinationMenu, + const QString& actionName, + const std::function& handler, + const QKeySequence& shortcut, + const bool checked, + int menuItemLocation, + const QString& grouping) { + auto action = addCheckableActionToQMenuAndActionHash(destinationMenu, actionName, shortcut, checked, nullptr, nullptr, menuItemLocation, grouping); + connect(action, &QAction::triggered, handler); + return action; +} + void Menu::removeAction(MenuWrapper* menu, const QString& actionName) { auto action = _actionHash.value(actionName); menu->removeAction(action); diff --git a/libraries/ui/src/ui/Menu.h b/libraries/ui/src/ui/Menu.h index 9839bd1eb6..25f8f74063 100644 --- a/libraries/ui/src/ui/Menu.h +++ b/libraries/ui/src/ui/Menu.h @@ -9,6 +9,8 @@ #ifndef hifi_ui_Menu_h #define hifi_ui_Menu_h +#include + #include #include #include @@ -90,6 +92,14 @@ public: int menuItemLocation = UNSPECIFIED_POSITION, const QString& grouping = QString()); + QAction* addCheckableActionToQMenuAndActionHash(MenuWrapper* destinationMenu, + const QString& actionName, + const std::function& handler, + const QKeySequence& shortcut = 0, + const bool checked = false, + int menuItemLocation = UNSPECIFIED_POSITION, + const QString& grouping = QString()); + void removeAction(MenuWrapper* menu, const QString& actionName); public slots: diff --git a/plugins/openvr/CMakeLists.txt b/plugins/openvr/CMakeLists.txt index 79bfd91068..bc62117e70 100644 --- a/plugins/openvr/CMakeLists.txt +++ b/plugins/openvr/CMakeLists.txt @@ -13,7 +13,7 @@ if (WIN32) setup_hifi_plugin(OpenGL Script Qml Widgets) link_hifi_libraries(shared gl networking controllers ui plugins display-plugins ui-plugins input-plugins script-engine - render-utils model gpu gpu-gl render model-networking fbx) + render-utils model gpu gpu-gl render model-networking fbx ktx image) include_hifi_library_headers(octree) diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 585a0d00ef..8105de7a13 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -495,7 +495,7 @@ void OpenVrDisplayPlugin::customizeContext() { _compositeInfos[0].texture = _compositeFramebuffer->getRenderBuffer(0); for (size_t i = 0; i < COMPOSITING_BUFFER_SIZE; ++i) { if (0 != i) { - _compositeInfos[i].texture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, _renderTargetSize.x, _renderTargetSize.y, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT))); + _compositeInfos[i].texture = gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, _renderTargetSize.x, _renderTargetSize.y, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT)); } _compositeInfos[i].textureID = getGLBackend()->getTextureID(_compositeInfos[i].texture); } diff --git a/script-archive/acScripts/BetterClientSimulationBotFromRecording.js b/script-archive/acScripts/BetterClientSimulationBotFromRecording.js index c276d302e1..1b555972b9 100644 --- a/script-archive/acScripts/BetterClientSimulationBotFromRecording.js +++ b/script-archive/acScripts/BetterClientSimulationBotFromRecording.js @@ -126,6 +126,8 @@ Vec3.print("RANDOM LOCATION SELECTED:", LOCATION); var playFromCurrentLocation = true; var loop = true; +// Disable the privacy bubble +Users.disableIgnoreRadius(); // Set position here if playFromCurrentLocation is true Avatar.position = LOCATION; diff --git a/script-archive/acScripts/PlayRecordingOnAC.js b/script-archive/acScripts/PlayRecordingOnAC.js index 5979913d23..00f3f71bd4 100644 --- a/script-archive/acScripts/PlayRecordingOnAC.js +++ b/script-archive/acScripts/PlayRecordingOnAC.js @@ -20,6 +20,9 @@ Avatar.orientation = Quat.fromPitchYawRollDegrees(0, 0, 0); Avatar.scale = 1.0; Agent.isAvatar = true; +// Disable the privacy bubble +Users.disableIgnoreRadius(); + Recording.loadRecording(recordingFile, function(success) { if (success) { Script.update.connect(update); diff --git a/scripts/developer/inputRecording.js b/scripts/developer/inputRecording.js new file mode 100644 index 0000000000..85bda623b3 --- /dev/null +++ b/scripts/developer/inputRecording.js @@ -0,0 +1,103 @@ +// +// Created by Dante Ruiz 2017/04/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 +// + +(function() { + var recording = false; + var onRecordingScreen = false; + var passedSaveDirectory = false; + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var button = tablet.addButton({ + text: "IRecord" + }); + function onClick() { + if (onRecordingScreen) { + tablet.gotoHomeScreen(); + onRecordingScreen = false; + } else { + tablet.loadQMLSource("InputRecorder.qml"); + onRecordingScreen = true; + } + } + + function onScreenChanged(type, url) { + onRecordingScreen = false; + passedSaveDirectory = false; + } + + button.clicked.connect(onClick); + tablet.fromQml.connect(fromQml); + tablet.screenChanged.connect(onScreenChanged); + function fromQml(message) { + switch (message.method) { + case "Start": + startRecording(); + break; + case "Stop": + stopRecording(); + break; + case "Save": + saveRecording(); + break; + case "Load": + loadRecording(message.params.file); + break; + case "playback": + startPlayback(); + break; + } + + } + + function startRecording() { + Controller.startInputRecording(); + recording = true; + } + + function stopRecording() { + Controller.stopInputRecording(); + recording = false; + } + + function saveRecording() { + Controller.saveInputRecording(); + } + + function loadRecording(file) { + Controller.loadInputRecording(file); + } + + function startPlayback() { + Controller.startInputPlayback(); + } + + function sendToQml(message) { + tablet.sendToQml(message); + } + + function update() { + + if (!passedSaveDirectory) { + var directory = Controller.getInputRecorderSaveDirectory(); + sendToQml({method: "path", params: directory}); + passedSaveDirectory = true; + } + sendToQml({method: "update", params: recording}); + } + + Script.setInterval(update, 60); + + Script.scriptEnding.connect(function () { + button.clicked.disconnect(onClick); + if (tablet) { + tablet.removeButton(button); + } + + Controller.stopInputRecording(); + }); + +}()); diff --git a/scripts/developer/tests/hipsControllerTest.js b/scripts/developer/tests/hipsControllerTest.js new file mode 100644 index 0000000000..5c6a4811e5 --- /dev/null +++ b/scripts/developer/tests/hipsControllerTest.js @@ -0,0 +1,105 @@ +// +// hipsControllerTest.js +// +// Created by Anthony Thibault on 4/24/17 +// Copyright 2017 High Fidelity, Inc. +// +// Test procedural manipulation of the Avatar hips via the controller system. +// Pull the left and right triggers on your hand controllers, you hips should begin to gyrate in an amusing mannor. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* global Xform */ +Script.include("/~/system/libraries/Xform.js"); + +var triggerPressHandled = false; +var rightTriggerPressed = false; +var leftTriggerPressed = false; + +var MAPPING_NAME = "com.highfidelity.hipsIkTest"; + +var mapping = Controller.newMapping(MAPPING_NAME); +mapping.from([Controller.Standard.RTClick]).peek().to(function (value) { + rightTriggerPressed = (value !== 0) ? true : false; +}); +mapping.from([Controller.Standard.LTClick]).peek().to(function (value) { + leftTriggerPressed = (value !== 0) ? true : false; +}); + +Controller.enableMapping(MAPPING_NAME); + +var CONTROLLER_MAPPING_NAME = "com.highfidelity.hipsIkTest.controller"; +var controllerMapping; + +var ZERO = {x: 0, y: 0, z: 0}; +var X_AXIS = {x: 1, y: 0, z: 0}; +var Y_AXIS = {x: 0, y: 1, z: 0}; +var Y_180 = {x: 0, y: 1, z: 0, w: 0}; +var Y_180_XFORM = new Xform(Y_180, {x: 0, y: 0, z: 0}); + +var hips = undefined; + +function computeCurrentXform(jointIndex) { + var currentXform = new Xform(MyAvatar.getAbsoluteJointRotationInObjectFrame(jointIndex), + MyAvatar.getAbsoluteJointTranslationInObjectFrame(jointIndex)); + return currentXform; +} + +function calibrate() { + hips = computeCurrentXform(MyAvatar.getJointIndex("Hips")); +} + +function circleOffset(radius, theta, normal) { + var pos = {x: radius * Math.cos(theta), y: radius * Math.sin(theta), z: 0}; + var lookAtRot = Quat.lookAt(normal, ZERO, X_AXIS); + return Vec3.multiplyQbyV(lookAtRot, pos); +} + +var calibrationCount = 0; + +function update(dt) { + if (rightTriggerPressed && leftTriggerPressed) { + if (!triggerPressHandled) { + triggerPressHandled = true; + if (controllerMapping) { + hips = undefined; + Controller.disableMapping(CONTROLLER_MAPPING_NAME + calibrationCount); + controllerMapping = undefined; + } else { + calibrate(); + calibrationCount++; + controllerMapping = Controller.newMapping(CONTROLLER_MAPPING_NAME + calibrationCount); + + var n = Y_AXIS; + var t = 0; + if (hips) { + controllerMapping.from(function () { + t += (1 / 60) * 4; + return { + valid: true, + translation: Vec3.sum(hips.pos, circleOffset(0.1, t, n)), + rotation: hips.rot, + velocity: ZERO, + angularVelocity: ZERO + }; + }).to(Controller.Standard.Hips); + } + Controller.enableMapping(CONTROLLER_MAPPING_NAME + calibrationCount); + } + } + } else { + triggerPressHandled = false; + } +} + +Script.update.connect(update); + +Script.scriptEnding.connect(function () { + Controller.disableMapping(MAPPING_NAME); + if (controllerMapping) { + Controller.disableMapping(CONTROLLER_MAPPING_NAME + calibrationCount); + } + Script.update.disconnect(update); +}); + diff --git a/scripts/developer/tests/hipsIkTest.js b/scripts/developer/tests/hipsIkTest.js new file mode 100644 index 0000000000..340d1ae7a0 --- /dev/null +++ b/scripts/developer/tests/hipsIkTest.js @@ -0,0 +1,118 @@ +// +// hipsIKTest.js +// +// Created by Anthony Thibault on 4/24/17 +// Copyright 2017 High Fidelity, Inc. +// +// Test procedural manipulation of the Avatar hips IK target. +// Pull the left and right triggers on your hand controllers, you hips should begin to gyrate in an amusing mannor. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* global Xform */ +Script.include("/~/system/libraries/Xform.js"); + +var calibrated = false; +var rightTriggerPressed = false; +var leftTriggerPressed = false; + +var MAPPING_NAME = "com.highfidelity.hipsIkTest"; + +var mapping = Controller.newMapping(MAPPING_NAME); +mapping.from([Controller.Standard.RTClick]).peek().to(function (value) { + rightTriggerPressed = (value !== 0) ? true : false; +}); +mapping.from([Controller.Standard.LTClick]).peek().to(function (value) { + leftTriggerPressed = (value !== 0) ? true : false; +}); + +Controller.enableMapping(MAPPING_NAME); + +var ANIM_VARS = [ + "headType", + "hipsType", + "hipsPosition", + "hipsRotation" +]; + +var ZERO = {x: 0, y: 0, z: 0}; +var X_AXIS = {x: 1, y: 0, z: 0}; +var Y_AXIS = {x: 0, y: 1, z: 0}; +var Y_180 = {x: 0, y: 1, z: 0, w: 0}; +var Y_180_XFORM = new Xform(Y_180, {x: 0, y: 0, z: 0}); + +var hips = undefined; + +function computeCurrentXform(jointIndex) { + var currentXform = new Xform(MyAvatar.getAbsoluteJointRotationInObjectFrame(jointIndex), + MyAvatar.getAbsoluteJointTranslationInObjectFrame(jointIndex)); + return Xform.mul(Y_180_XFORM, currentXform); +} + +function calibrate() { + hips = computeCurrentXform(MyAvatar.getJointIndex("Hips")); +} + +var ikTypes = { + RotationAndPosition: 0, + RotationOnly: 1, + HmdHead: 2, + HipsRelativeRotationAndPosition: 3, + Off: 4 +}; + +function circleOffset(radius, theta, normal) { + var pos = {x: radius * Math.cos(theta), y: radius * Math.sin(theta), z: 0}; + var lookAtRot = Quat.lookAt(normal, ZERO, X_AXIS); + return Vec3.multiplyQbyV(lookAtRot, pos); +} + +var handlerId; + +function update(dt) { + if (rightTriggerPressed && leftTriggerPressed) { + if (!calibrated) { + calibrate(); + calibrated = true; + + if (handlerId) { + MyAvatar.removeAnimationStateHandler(handlerId); + handlerId = undefined; + } else { + + var n = Y_AXIS; + var t = 0; + handlerId = MyAvatar.addAnimationStateHandler(function (props) { + + t += (1 / 60) * 4; + var result = {}, xform; + if (hips) { + xform = hips; + result.hipsType = ikTypes.RotationAndPosition; + result.hipsPosition = Vec3.sum(xform.pos, circleOffset(0.1, t, n)); + result.hipsRotation = xform.rot; + result.headType = ikTypes.RotationAndPosition; + } else { + result.headType = ikTypes.HmdHead; + result.hipsType = props.hipsType; + result.hipsPosition = props.hipsPosition; + result.hipsRotation = props.hipsRotation; + } + + return result; + }, ANIM_VARS); + } + } + } else { + calibrated = false; + } +} + +Script.update.connect(update); + +Script.scriptEnding.connect(function () { + Controller.disableMapping(MAPPING_NAME); + Script.update.disconnect(update); +}); + diff --git a/scripts/developer/tests/viveMotionCapture.js b/scripts/developer/tests/viveMotionCapture.js new file mode 100644 index 0000000000..e7fb8566dc --- /dev/null +++ b/scripts/developer/tests/viveMotionCapture.js @@ -0,0 +1,348 @@ +/* global Xform */ +Script.include("/~/system/libraries/Xform.js"); + +var TRACKED_OBJECT_POSES = [ + "TrackedObject00", "TrackedObject01", "TrackedObject02", "TrackedObject03", + "TrackedObject04", "TrackedObject05", "TrackedObject06", "TrackedObject07", + "TrackedObject08", "TrackedObject09", "TrackedObject10", "TrackedObject11", + "TrackedObject12", "TrackedObject13", "TrackedObject14", "TrackedObject15" +]; + +var triggerPressHandled = false; +var rightTriggerPressed = false; +var leftTriggerPressed = false; +var calibrationCount = 0; + +var TRIGGER_MAPPING_NAME = "com.highfidelity.viveMotionCapture.triggers"; +var triggerMapping = Controller.newMapping(TRIGGER_MAPPING_NAME); +triggerMapping.from([Controller.Standard.RTClick]).peek().to(function (value) { + rightTriggerPressed = (value !== 0) ? true : false; +}); +triggerMapping.from([Controller.Standard.LTClick]).peek().to(function (value) { + leftTriggerPressed = (value !== 0) ? true : false; +}); +Controller.enableMapping(TRIGGER_MAPPING_NAME); + +var CONTROLLER_MAPPING_NAME = "com.highfidelity.viveMotionCapture.controller"; +var controllerMapping; + +var head; +var leftFoot; +var rightFoot; +var hips; +var spine2; + +var FEET_ONLY = 0; +var FEET_AND_HIPS = 1; +var FEET_AND_CHEST = 2; +var FEET_HIPS_AND_CHEST = 3; +var AUTO = 4; + +var SENSOR_CONFIG_NAMES = [ + "FeetOnly", + "FeetAndHips", + "FeetAndChest", + "FeetHipsAndChest", + "Auto" +]; + +var sensorConfig = AUTO; + +var Y_180 = {x: 0, y: 1, z: 0, w: 0}; +var Y_180_XFORM = new Xform(Y_180, {x: 0, y: 0, z: 0}); + +function computeOffsetXform(defaultToReferenceXform, pose, jointIndex) { + var poseXform = new Xform(pose.rotation, pose.translation); + + var defaultJointXform = new Xform(MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame(jointIndex), + MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(jointIndex)); + + var referenceJointXform = Xform.mul(defaultToReferenceXform, defaultJointXform); + + return Xform.mul(poseXform.inv(), referenceJointXform); +} + +function computeDefaultToReferenceXform() { + var headIndex = MyAvatar.getJointIndex("Head"); + if (headIndex >= 0) { + var defaultHeadXform = new Xform(MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame(headIndex), + MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(headIndex)); + var currentHeadXform = new Xform(Quat.cancelOutRollAndPitch(MyAvatar.getAbsoluteJointRotationInObjectFrame(headIndex)), + MyAvatar.getAbsoluteJointTranslationInObjectFrame(headIndex)); + + var defaultToReferenceXform = Xform.mul(currentHeadXform, defaultHeadXform.inv()); + + return defaultToReferenceXform; + } else { + return Xform.ident(); + } +} + +function computeHeadOffsetXform() { + var leftEyeIndex = MyAvatar.getJointIndex("LeftEye"); + var rightEyeIndex = MyAvatar.getJointIndex("RightEye"); + var headIndex = MyAvatar.getJointIndex("Head"); + if (leftEyeIndex > 0 && rightEyeIndex > 0 && headIndex > 0) { + var defaultHeadXform = new Xform(MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame(headIndex), + MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(headIndex)); + var defaultLeftEyeXform = new Xform(MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame(leftEyeIndex), + MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(leftEyeIndex)); + var defaultRightEyeXform = new Xform(MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame(rightEyeIndex), + MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(rightEyeIndex)); + var defaultCenterEyePos = Vec3.multiply(0.5, Vec3.sum(defaultLeftEyeXform.pos, defaultRightEyeXform.pos)); + var defaultCenterEyeXform = new Xform(defaultLeftEyeXform.rot, defaultCenterEyePos); + + return Xform.mul(defaultCenterEyeXform.inv(), defaultHeadXform); + } else { + return undefined; + } +} + +function calibrate() { + + head = undefined; + leftFoot = undefined; + rightFoot = undefined; + hips = undefined; + spine2 = undefined; + + var defaultToReferenceXform = computeDefaultToReferenceXform(); + + var headOffsetXform = computeHeadOffsetXform(); + print("AJT: computed headOffsetXform " + (headOffsetXform ? JSON.stringify(headOffsetXform) : "undefined")); + + if (headOffsetXform) { + head = { offsetXform: headOffsetXform }; + } + + var poses = []; + if (Controller.Hardware.Vive) { + TRACKED_OBJECT_POSES.forEach(function (key) { + var channel = Controller.Hardware.Vive[key]; + var pose = Controller.getPoseValue(channel); + if (pose.valid) { + poses.push({ + channel: channel, + pose: pose, + lastestPose: pose + }); + } + }); + } + + print("AJT: calibrating, num tracked poses = " + poses.length + ", sensorConfig = " + SENSOR_CONFIG_NAMES[sensorConfig]); + + var config = sensorConfig; + + if (config === AUTO) { + if (poses.length === 2) { + config = FEET_ONLY; + } else if (poses.length === 3) { + config = FEET_AND_HIPS; + } else if (poses.length >= 4) { + config = FEET_HIPS_AND_CHEST; + } else { + print("AJT: auto config failed: poses.length = " + poses.length); + config = FEET_ONLY; + } + } + + if (poses.length >= 2) { + // sort by y + poses.sort(function(a, b) { + var ay = a.pose.translation.y; + var by = b.pose.translation.y; + return ay - by; + }); + + if (poses[0].pose.translation.x > poses[1].pose.translation.x) { + rightFoot = poses[0]; + leftFoot = poses[1]; + } else { + rightFoot = poses[1]; + leftFoot = poses[0]; + } + + // compute offsets + rightFoot.offsetXform = computeOffsetXform(defaultToReferenceXform, rightFoot.pose, MyAvatar.getJointIndex("RightFoot")); + leftFoot.offsetXform = computeOffsetXform(defaultToReferenceXform, leftFoot.pose, MyAvatar.getJointIndex("LeftFoot")); + + print("AJT: rightFoot = " + JSON.stringify(rightFoot)); + print("AJT: leftFoot = " + JSON.stringify(leftFoot)); + + if (config === FEET_ONLY) { + // we're done! + } else if (config === FEET_AND_HIPS && poses.length >= 3) { + hips = poses[2]; + } else if (config === FEET_AND_CHEST && poses.length >= 3) { + spine2 = poses[2]; + } else if (config === FEET_HIPS_AND_CHEST && poses.length >= 4) { + hips = poses[2]; + spine2 = poses[3]; + } else { + // TODO: better error messages + print("AJT: could not calibrate for sensor config " + SENSOR_CONFIG_NAMES[config] + ", please try again!"); + } + + if (hips) { + hips.offsetXform = computeOffsetXform(defaultToReferenceXform, hips.pose, MyAvatar.getJointIndex("Hips")); + print("AJT: hips = " + JSON.stringify(hips)); + } + + if (spine2) { + spine2.offsetXform = computeOffsetXform(defaultToReferenceXform, spine2.pose, MyAvatar.getJointIndex("Spine2")); + print("AJT: spine2 = " + JSON.stringify(spine2)); + } + + } else { + print("AJT: could not find two trackers, try again!"); + } +} + +var ikTypes = { + RotationAndPosition: 0, + RotationOnly: 1, + HmdHead: 2, + HipsRelativeRotationAndPosition: 3, + Off: 4 +}; + +var handlerId; + +function convertJointInfoToPose(jointInfo) { + var latestPose = jointInfo.latestPose; + var offsetXform = jointInfo.offsetXform; + var xform = Xform.mul(new Xform(latestPose.rotation, latestPose.translation), offsetXform); + return { + valid: true, + translation: xform.pos, + rotation: xform.rot, + velocity: Vec3.sum(latestPose.velocity, Vec3.cross(latestPose.angularVelocity, Vec3.subtract(xform.pos, latestPose.translation))), + angularVelocity: latestPose.angularVelocity + }; +} + +function update(dt) { + if (rightTriggerPressed && leftTriggerPressed) { + if (!triggerPressHandled) { + triggerPressHandled = true; + if (controllerMapping) { + + // go back to normal, vive pucks will be ignored. + print("AJT: UN-CALIBRATE!"); + + head = undefined; + leftFoot = undefined; + rightFoot = undefined; + hips = undefined; + spine2 = undefined; + + Controller.disableMapping(CONTROLLER_MAPPING_NAME + calibrationCount); + controllerMapping = undefined; + + } else { + print("AJT: CALIBRATE!"); + calibrate(); + calibrationCount++; + + controllerMapping = Controller.newMapping(CONTROLLER_MAPPING_NAME + calibrationCount); + + if (head) { + controllerMapping.from(function () { + var worldToAvatarXform = (new Xform(MyAvatar.orientation, MyAvatar.position)).inv(); + head.latestPose = { + valid: true, + translation: worldToAvatarXform.xformPoint(HMD.position), + rotation: Quat.multiply(worldToAvatarXform.rot, Quat.multiply(HMD.orientation, Y_180)), // postMult 180 rot flips head direction + velocity: {x: 0, y: 0, z: 0}, // TODO: currently this is unused anyway... + angularVelocity: {x: 0, y: 0, z: 0} + }; + return convertJointInfoToPose(head); + }).to(Controller.Standard.Head); + } + + if (leftFoot) { + controllerMapping.from(leftFoot.channel).to(function (pose) { + leftFoot.latestPose = pose; + }); + controllerMapping.from(function () { + return convertJointInfoToPose(leftFoot); + }).to(Controller.Standard.LeftFoot); + } + if (rightFoot) { + controllerMapping.from(rightFoot.channel).to(function (pose) { + rightFoot.latestPose = pose; + }); + controllerMapping.from(function () { + return convertJointInfoToPose(rightFoot); + }).to(Controller.Standard.RightFoot); + } + if (hips) { + controllerMapping.from(hips.channel).to(function (pose) { + hips.latestPose = pose; + }); + controllerMapping.from(function () { + return convertJointInfoToPose(hips); + }).to(Controller.Standard.Hips); + } + if (spine2) { + controllerMapping.from(spine2.channel).to(function (pose) { + spine2.latestPose = pose; + }); + controllerMapping.from(function () { + return convertJointInfoToPose(spine2); + }).to(Controller.Standard.Spine2); + } + Controller.enableMapping(CONTROLLER_MAPPING_NAME + calibrationCount); + } + } + } else { + triggerPressHandled = false; + } + + var drawMarkers = false; + if (drawMarkers) { + var RED = {x: 1, y: 0, z: 0, w: 1}; + var BLUE = {x: 0, y: 0, z: 1, w: 1}; + + if (leftFoot) { + var leftFootPose = Controller.getPoseValue(leftFoot.channel); + DebugDraw.addMyAvatarMarker("leftFootTracker", leftFootPose.rotation, leftFootPose.translation, BLUE); + } + + if (rightFoot) { + var rightFootPose = Controller.getPoseValue(rightFoot.channel); + DebugDraw.addMyAvatarMarker("rightFootTracker", rightFootPose.rotation, rightFootPose.translation, RED); + } + + if (hips) { + var hipsPose = Controller.getPoseValue(hips.channel); + DebugDraw.addMyAvatarMarker("hipsTracker", hipsPose.rotation, hipsPose.translation, GREEN); + } + } + + var drawReferencePose = false; + if (drawReferencePose) { + var GREEN = {x: 0, y: 1, z: 0, w: 1}; + var defaultToReferenceXform = computeDefaultToReferenceXform(); + var leftFootIndex = MyAvatar.getJointIndex("LeftFoot"); + if (leftFootIndex > 0) { + var defaultLeftFootXform = new Xform(MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame(leftFootIndex), + MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(leftFootIndex)); + var referenceLeftFootXform = Xform.mul(defaultToReferenceXform, defaultLeftFootXform); + DebugDraw.addMyAvatarMarker("leftFootTracker", referenceLeftFootXform.rot, referenceLeftFootXform.pos, GREEN); + } + } + +} + +Script.update.connect(update); + +Script.scriptEnding.connect(function () { + Controller.disableMapping(TRIGGER_MAPPING_NAME); + if (controllerMapping) { + Controller.disableMapping(CONTROLLER_MAPPING_NAME + calibrationCount); + } + Script.update.disconnect(update); +}); + diff --git a/scripts/developer/tests/viveTrackedObjects.js b/scripts/developer/tests/viveTrackedObjects.js index 78911538e4..4155afb82b 100644 --- a/scripts/developer/tests/viveTrackedObjects.js +++ b/scripts/developer/tests/viveTrackedObjects.js @@ -18,14 +18,14 @@ function shutdown() { }); } -var WHITE = {x: 1, y: 1, z: 1, w: 1}; +var BLUE = {x: 0, y: 0, z: 1, w: 1}; function update(dt) { if (Controller.Hardware.Vive) { TRACKED_OBJECT_POSES.forEach(function (key) { var pose = Controller.getPoseValue(Controller.Hardware.Vive[key]); if (pose.valid) { - DebugDraw.addMyAvatarMarker(key, pose.rotation, pose.translation, WHITE); + DebugDraw.addMyAvatarMarker(key, pose.rotation, pose.translation, BLUE); } else { DebugDraw.removeMyAvatarMarker(key); } diff --git a/scripts/system/app-doppleganger.js b/scripts/system/app-doppleganger.js new file mode 100644 index 0000000000..d7f85e5767 --- /dev/null +++ b/scripts/system/app-doppleganger.js @@ -0,0 +1,85 @@ +// doppleganger-app.js +// +// Created by Timothy Dedischew on 04/21/2017. +// Copyright 2017 High Fidelity, Inc. +// +// This Client script creates an instance of a Doppleganger that can be toggled on/off via tablet button. +// (for more info see doppleganger.js) +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var DopplegangerClass = Script.require('./doppleganger.js'); + +var tablet = Tablet.getTablet('com.highfidelity.interface.tablet.system'), + button = tablet.addButton({ + icon: "icons/tablet-icons/doppleganger-i.svg", + activeIcon: "icons/tablet-icons/doppleganger-a.svg", + text: 'MIRROR' + }); + +Script.scriptEnding.connect(function() { + tablet.removeButton(button); + button = null; +}); + +var doppleganger = new DopplegangerClass({ + avatar: MyAvatar, + mirrored: true, + autoUpdate: true +}); + +// hide the doppleganger if this client script is unloaded +Script.scriptEnding.connect(doppleganger, 'stop'); + +// hide the doppleganger if the user switches domains (which might place them arbitrarily far away in world space) +function onDomainChanged() { + if (doppleganger.active) { + doppleganger.stop('domain_changed'); + } +} +Window.domainChanged.connect(onDomainChanged); +Window.domainConnectionRefused.connect(onDomainChanged); +Script.scriptEnding.connect(function() { + Window.domainChanged.disconnect(onDomainChanged); + Window.domainConnectionRefused.disconnect(onDomainChanged); +}); + +// toggle on/off via tablet button +button.clicked.connect(doppleganger, 'toggle'); + +// highlight tablet button based on current doppleganger state +doppleganger.activeChanged.connect(function(active, reason) { + if (button) { + button.editProperties({ isActive: active }); + print('doppleganger.activeChanged', active, reason); + } +}); + +// alert the user if there was an error applying their skeletonModelURL +doppleganger.modelOverlayLoaded.connect(function(error, result) { + if (doppleganger.active && error) { + Window.alert('doppleganger | ' + error + '\n' + doppleganger.skeletonModelURL); + } +}); + +// add debug indicators, but only if the user has configured the settings value +if (Settings.getValue('debug.doppleganger', false)) { + DopplegangerClass.addDebugControls(doppleganger); +} + +UserActivityLogger.logAction('doppleganger_app_load'); +doppleganger.activeChanged.connect(function(active, reason) { + if (active) { + UserActivityLogger.logAction('doppleganger_enable'); + } else { + if (reason === 'stop') { + // user intentionally toggled the doppleganger + UserActivityLogger.logAction('doppleganger_disable'); + } else { + print('doppleganger stopped:', reason); + UserActivityLogger.logAction('doppleganger_autodisable', { reason: reason }); + } + } +}); diff --git a/scripts/system/assets/sounds/countdown-tick.wav b/scripts/system/assets/sounds/countdown-tick.wav new file mode 100644 index 0000000000..015e1f642e Binary files /dev/null and b/scripts/system/assets/sounds/countdown-tick.wav differ diff --git a/scripts/system/assets/sounds/finish-recording.wav b/scripts/system/assets/sounds/finish-recording.wav new file mode 100644 index 0000000000..f224049f97 Binary files /dev/null and b/scripts/system/assets/sounds/finish-recording.wav differ diff --git a/scripts/system/assets/sounds/start-recording.wav b/scripts/system/assets/sounds/start-recording.wav new file mode 100644 index 0000000000..71c69f3372 Binary files /dev/null and b/scripts/system/assets/sounds/start-recording.wav differ diff --git a/scripts/system/controllers/grab.js b/scripts/system/controllers/grab.js index 10f477b3af..811799917d 100644 --- a/scripts/system/controllers/grab.js +++ b/scripts/system/controllers/grab.js @@ -529,7 +529,7 @@ Grabber.prototype.moveEvent = function(event) { if (!this.actionID) { if (!entityIsGrabbedByOther(this.entityID)) { - this.actionID = Entities.addAction("spring", this.entityID, actionArgs); + this.actionID = Entities.addAction("far-grab", this.entityID, actionArgs); } } else { Entities.updateAction(this.entityID, this.actionID, actionArgs); diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 464101a4e3..ec70b0b1c8 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -111,12 +111,12 @@ var EQUIP_RADIUS = 0.2; // radius used for palm vs equip-hotspot for equipping. // has reached the required position, and then grow larger once the hand is close enough to equip. var EQUIP_HOTSPOT_RENDER_RADIUS = 0.0; // radius used for palm vs equip-hotspot for rendering hot-spots var MAX_EQUIP_HOTSPOT_RADIUS = 1.0; - +var MAX_FAR_TO_NEAR_EQUIP_HOTSPOT_RADIUS = 0.5; // radius used for far to near equipping object. var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position var NEAR_GRAB_RADIUS = 0.1; // radius used for palm vs object for near grabbing. var NEAR_GRAB_MAX_DISTANCE = 1.0; // you cannot grab objects that are this far away from your hand - +var FAR_TO_NEAR_GRAB_MAX_DISTANCE = 0.3; // In far to near grabbing conversion,grab the object if distancetoObject from hand is less than this. var NEAR_GRAB_PICK_RADIUS = 0.25; // radius used for search ray vs object for near grabbing. var NEAR_GRABBING_KINEMATIC = true; // force objects to be kinematic when near-grabbed @@ -155,6 +155,7 @@ var INCHES_TO_METERS = 1.0 / 39.3701; // these control how long an abandoned pointer line or action will hang around var ACTION_TTL = 15; // seconds +var ACTION_TTL_ZERO = 0; // seconds var ACTION_TTL_REFRESH = 5; var PICKS_PER_SECOND_PER_HAND = 60; var MSECS_PER_SEC = 1000.0; @@ -192,6 +193,7 @@ var FORBIDDEN_GRAB_TYPES = ["Unknown", "Light", "PolyLine", "Zone"]; var holdEnabled = true; var nearGrabEnabled = true; var farGrabEnabled = true; +var farToNearGrab = false; var myAvatarScalingEnabled = true; var objectScalingEnabled = true; var mostRecentSearchingHand = RIGHT_HAND; @@ -1330,7 +1332,7 @@ function MyController(hand) { if (this.stylus) { return; } - + var stylusProperties = { name: "stylus", url: Script.resourcesPath() + "meshes/tablet-stylus-fat.fbx", @@ -1420,6 +1422,14 @@ function MyController(hand) { } }; + // Turns off indicators used for searching. Overlay line and sphere. + this.searchIndicatorOff = function() { + this.searchSphereOff(); + if (PICK_WITH_HAND_RAY) { + this.overlayLineOff(); + } + } + this.otherGrabbingLineOn = function(avatarPosition, entityPosition, color) { if (this.otherGrabbingLine === null) { var lineProperties = { @@ -1791,6 +1801,15 @@ function MyController(hand) { } this.processStylus(); + + 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); + } else { + this.searchIndicatorOff(); + } }; this.handleLaserOnHomeButton = function(rayPickInfo) { @@ -2080,6 +2099,15 @@ function MyController(hand) { return true; }; + this.entityIsFarToNearGrabbable = function (objectPosition, handPosition, maxDistance) { + var distanceToObjectFromHand = Vec3.length(Vec3.subtract(handPosition, objectPosition)); + + if (distanceToObjectFromHand > maxDistance) { + return false; + } + return true; + }; + this.chooseNearEquipHotspots = function(candidateEntities, distance) { var equippableHotspots = flatten(candidateEntities.map(function(entityID) { return _this.collectEquipHotspots(entityID); @@ -2108,6 +2136,34 @@ function MyController(hand) { return null; } }; + + this.chooseNearEquipHotspotsForFarToNearEquip = function(candidateEntities, distance) { + var equippableHotspots = flatten(candidateEntities.map(function(entityID) { + return _this.collectEquipHotspots(entityID); + })).filter(function(hotspot) { + return (Vec3.distance(hotspot.worldPosition, getControllerWorldLocation(_this.handToController(), true).position) < + hotspot.radius + distance); + }); + return equippableHotspots; + }; + + this.chooseBestEquipHotspotForFarToNearEquip = function(candidateEntities) { + var DISTANCE = 1; + var equippableHotspots = this.chooseNearEquipHotspotsForFarToNearEquip(candidateEntities, DISTANCE); + var _this = this; + if (equippableHotspots.length > 0) { + // sort by distance + equippableHotspots.sort(function(a, b) { + var handControllerLocation = getControllerWorldLocation(_this.handToController(), true); + var aDistance = Vec3.distance(a.worldPosition, handControllerLocation.position); + var bDistance = Vec3.distance(b.worldPosition, handControllerLocation.position); + return aDistance - bDistance; + }); + return equippableHotspots[0]; + } else { + return null; + } + }; this.searchEnter = function() { mostRecentSearchingHand = this.hand; @@ -2237,15 +2293,22 @@ function MyController(hand) { return; } } - + if (isInEditMode()) { this.searchIndicatorOn(rayPickInfo.searchRay); if (this.triggerSmoothedGrab()) { - if (!this.editTriggered && rayPickInfo.entityID) { - Messages.sendLocalMessage("entityToolUpdates", JSON.stringify({ - method: "selectEntity", - entityID: rayPickInfo.entityID - })); + if (!this.editTriggered){ + if (rayPickInfo.entityID) { + Messages.sendLocalMessage("entityToolUpdates", JSON.stringify({ + method: "selectEntity", + entityID: rayPickInfo.entityID + })); + } else if (rayPickInfo.overlayID) { + Messages.sendLocalMessage("entityToolUpdates", JSON.stringify({ + method: "selectOverlay", + overlayID: rayPickInfo.overlayID + })); + } } this.editTriggered = true; } @@ -2274,7 +2337,7 @@ function MyController(hand) { if (this.getOtherHandController().state === STATE_DISTANCE_HOLDING) { this.setState(STATE_DISTANCE_ROTATING, "distance rotate '" + name + "'"); } else { - this.setState(STATE_DISTANCE_HOLDING, "distance hold '" + name + "'"); + this.setState(STATE_DISTANCE_HOLDING, "distance hold '" + name + "'"); } return; } else { @@ -2526,7 +2589,7 @@ function MyController(hand) { var timeScale = this.distanceGrabTimescale(this.mass, distanceToObject); this.linearTimeScale = timeScale; this.actionID = NULL_UUID; - this.actionID = Entities.addAction("spring", this.grabbedThingID, { + this.actionID = Entities.addAction("far-grab", this.grabbedThingID, { targetPosition: this.currentObjectPosition, linearTimeScale: timeScale, targetRotation: this.currentObjectRotation, @@ -2656,6 +2719,55 @@ function MyController(hand) { this.grabbedThingID); var distanceToObject = Vec3.length(Vec3.subtract(MyAvatar.position, this.currentObjectPosition)); + + var candidateHotSpotEntities = Entities.findEntities(controllerLocation.position,MAX_FAR_TO_NEAR_EQUIP_HOTSPOT_RADIUS); + entityPropertiesCache.addEntities(candidateHotSpotEntities); + + var potentialEquipHotspot = this.chooseBestEquipHotspotForFarToNearEquip(candidateHotSpotEntities); + if (potentialEquipHotspot && (potentialEquipHotspot.entityID == this.grabbedThingID)) { + if ((this.triggerSmoothedGrab() || this.secondarySqueezed()) && holdEnabled) { + this.grabbedHotspot = potentialEquipHotspot; + this.grabbedThingID = potentialEquipHotspot.entityID; + this.grabbedIsOverlay = false; + + var success = Entities.updateAction(this.grabbedThingID, this.actionID, { + targetPosition: newTargetPosition, + linearTimeScale: this.distanceGrabTimescale(this.mass, distanceToObject), + targetRotation: this.currentObjectRotation, + angularTimeScale: this.distanceGrabTimescale(this.mass, distanceToObject), + ttl: ACTION_TTL_ZERO + }); + + if (success) { + this.actionTimeout = now + (ACTION_TTL_ZERO * MSECS_PER_SEC); + } else { + print("continueDistanceHolding -- updateAction failed"); + } + this.setState(STATE_HOLD, "equipping '" + entityPropertiesCache.getProps(this.grabbedThingID).name + "'"); + return; + } + } + var rayPositionOnEntity = Vec3.subtract(grabbedProperties.position, this.offsetPosition); + //Far to Near Grab: If object is draw by user inside FAR_TO_NEAR_GRAB_MAX_DISTANCE, grab it + if (this.entityIsFarToNearGrabbable(rayPositionOnEntity, controllerLocation.position, FAR_TO_NEAR_GRAB_MAX_DISTANCE)) { + this.farToNearGrab = true; + + var success = Entities.updateAction(this.grabbedThingID, this.actionID, { + targetPosition: newTargetPosition, + linearTimeScale: this.distanceGrabTimescale(this.mass, distanceToObject), + targetRotation: this.currentObjectRotation, + angularTimeScale: this.distanceGrabTimescale(this.mass, distanceToObject), + ttl: ACTION_TTL_ZERO // Overriding ACTION_TTL,Assign ACTION_TTL_ZERO so that the object is dropped down immediately after the trigger is released. + }); + if (success) { + this.actionTimeout = now + (ACTION_TTL_ZERO * MSECS_PER_SEC); + } else { + print("continueDistanceHolding -- updateAction failed"); + } + this.setState(STATE_NEAR_GRABBING , "near grab entity '" + this.grabbedThingID + "'"); + return; + } + this.linearTimeScale = (this.linearTimeScale / 2); if (this.linearTimeScale <= DISTANCE_HOLDING_ACTION_TIMEFRAME) { this.linearTimeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME; @@ -2741,7 +2853,7 @@ function MyController(hand) { relativePosition: this.offsetPosition, relativeRotation: this.offsetRotation, ttl: ACTION_TTL, - kinematic: NEAR_GRABBING_KINEMATIC, + kinematic: this.kinematicGrab, kinematicSetVelocity: true, ignoreIK: this.ignoreIK }); @@ -2838,12 +2950,14 @@ function MyController(hand) { this.ignoreIK = true; } else { grabbedProperties = Entities.getEntityProperties(this.grabbedThingID, GRABBABLE_PROPERTIES); + var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedThingID, DEFAULT_GRABBABLE_DATA); if (FORCE_IGNORE_IK) { this.ignoreIK = true; } else { - var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedThingID, DEFAULT_GRABBABLE_DATA); this.ignoreIK = (grabbableData.ignoreIK !== undefined) ? grabbableData.ignoreIK : true; } + + this.kinematicGrab = (grabbableData.kinematic !== undefined) ? grabbableData.kinematic : NEAR_GRABBING_KINEMATIC; } var handRotation; @@ -3032,6 +3146,15 @@ function MyController(hand) { this.nearGrabbing = function(deltaTime, timestamp) { this.grabPointSphereOff(); + var ttl = ACTION_TTL; + + if (this.farToNearGrab) { + ttl = ACTION_TTL_ZERO; // farToNearGrab - Assign ACTION_TTL_ZERO so that, the object is dropped down immediately after the trigger is released. + if(!this.triggerClicked){ + this.farToNearGrab = false; + } + } + if (this.state == STATE_NEAR_GRABBING && (!this.triggerClicked && this.secondaryReleased())) { this.callEntityMethodOnGrabbed("releaseGrab"); this.setState(STATE_OFF, "trigger released"); @@ -3199,20 +3322,20 @@ function MyController(hand) { this.maybeScale(props); } - if (this.actionID && this.actionTimeout - now < ACTION_TTL_REFRESH * MSECS_PER_SEC) { + if (this.actionID && this.actionTimeout - now < ttl * MSECS_PER_SEC) { // if less than a 5 seconds left, refresh the actions ttl var success = Entities.updateAction(this.grabbedThingID, this.actionID, { hand: this.hand === RIGHT_HAND ? "right" : "left", timeScale: NEAR_GRABBING_ACTION_TIMEFRAME, relativePosition: this.offsetPosition, relativeRotation: this.offsetRotation, - ttl: ACTION_TTL, - kinematic: NEAR_GRABBING_KINEMATIC, + ttl: ttl, + kinematic: this.kinematicGrab, kinematicSetVelocity: true, ignoreIK: this.ignoreIK }); if (success) { - this.actionTimeout = now + (ACTION_TTL * MSECS_PER_SEC); + this.actionTimeout = now + (ttl * MSECS_PER_SEC); } else { print("continueNearGrabbing -- updateAction failed"); Entities.deleteAction(this.grabbedThingID, this.actionID); @@ -3339,7 +3462,14 @@ function MyController(hand) { }; this.offEnter = function() { + // Reuse the existing search distance if lasers were active since + // they will be shown in OFF state while in edit mode. + var existingSearchDistance = this.searchSphereDistance; this.release(); + + if (isInEditMode()) { + this.searchSphereDistance = existingSearchDistance; + } }; this.entityLaserTouchingEnter = function() { diff --git a/scripts/system/doppleganger.js b/scripts/system/doppleganger.js new file mode 100644 index 0000000000..271a9a67c5 --- /dev/null +++ b/scripts/system/doppleganger.js @@ -0,0 +1,494 @@ +"use strict"; + +// doppleganger.js +// +// Created by Timothy Dedischew on 04/21/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 +// + +/* global module */ +// @module doppleganger +// +// This module contains the `Doppleganger` class implementation for creating an inspectable replica of +// an Avatar (as a model directly in front of and facing them). Joint positions and rotations are copied +// over in an update thread, so that the model automatically mirrors the Avatar's joint movements. +// An Avatar can then for example walk around "themselves" and examine from the back, etc. +// +// This should be helpful for inspecting your own look and debugging avatars, etc. +// +// The doppleganger is created as an overlay so that others do not see it -- and this also allows for the +// highest possible update rate when keeping joint data in sync. + +module.exports = Doppleganger; + +// @property {bool} - when set true, Script.update will be used instead of setInterval for syncing joint data +Doppleganger.USE_SCRIPT_UPDATE = false; + +// @property {int} - the frame rate to target when using setInterval for joint updates +Doppleganger.TARGET_FPS = 60; + +// @property {int} - the maximum time in seconds to wait for the model overlay to finish loading +Doppleganger.MAX_WAIT_SECS = 10; + +// @function - derive mirrored joint names from a list of regular joint names +// @param {Array} - list of joint names to mirror +// @return {Array} - list of mirrored joint names (note: entries for non-mirrored joints will be `undefined`) +Doppleganger.getMirroredJointNames = function(jointNames) { + return jointNames.map(function(name, i) { + if (/Left/.test(name)) { + return name.replace('Left', 'Right'); + } + if (/Right/.test(name)) { + return name.replace('Right', 'Left'); + } + return undefined; + }); +}; + +// @class Doppleganger - Creates a new instance of a Doppleganger. +// @param {Avatar} [options.avatar=MyAvatar] - Avatar used to retrieve position and joint data. +// @param {bool} [options.mirrored=true] - Apply "symmetric mirroring" of Left/Right joints. +// @param {bool} [options.autoUpdate=true] - Automatically sync joint data. +function Doppleganger(options) { + options = options || {}; + this.avatar = options.avatar || MyAvatar; + this.mirrored = 'mirrored' in options ? options.mirrored : true; + this.autoUpdate = 'autoUpdate' in options ? options.autoUpdate : true; + + // @public + this.active = false; // whether doppleganger is currently being displayed/updated + this.overlayID = null; // current doppleganger's Overlay id + this.frame = 0; // current joint update frame + + // @signal - emitted when .active state changes + this.activeChanged = signal(function(active, reason) {}); + // @signal - emitted once model overlay is either loaded or errors out + this.modelOverlayLoaded = signal(function(error, result){}); + // @signal - emitted each time the model overlay's joint data has been synchronized + this.jointsUpdated = signal(function(overlayID){}); +} + +Doppleganger.prototype = { + // @public @method - toggles doppleganger on/off + toggle: function() { + if (this.active) { + log('toggling off'); + this.stop(); + } else { + log('toggling on'); + this.start(); + } + return this.active; + }, + + // @public @method - synchronize the joint data between Avatar / doppleganger + update: function() { + this.frame++; + try { + if (!this.overlayID) { + throw new Error('!this.overlayID'); + } + + if (this.avatar.skeletonModelURL !== this.skeletonModelURL) { + return this.stop('avatar_changed'); + } + + var rotations = this.avatar.getJointRotations(); + var translations = this.avatar.getJointTranslations(); + var size = rotations.length; + + // note: this mismatch can happen when the avatar's model is actively changing + if (size !== translations.length || + (this.jointStateCount && size !== this.jointStateCount)) { + log('mismatched joint counts (avatar model likely changed)', size, translations.length, this.jointStateCount); + this.stop('avatar_changed_joints'); + return; + } + this.jointStateCount = size; + + if (this.mirrored) { + var mirroredIndexes = this.mirroredIndexes; + var outRotations = new Array(size); + var outTranslations = new Array(size); + for (var i=0; i < size; i++) { + var index = mirroredIndexes[i]; + if (index < 0 || index === false) { + index = i; + } + var rot = rotations[index]; + var trans = translations[index]; + trans.x *= -1; + rot.y *= -1; + rot.z *= -1; + outRotations[i] = rot; + outTranslations[i] = trans; + } + rotations = outRotations; + translations = outTranslations; + } + Overlays.editOverlay(this.overlayID, { + jointRotations: rotations, + jointTranslations: translations + }); + + this.jointsUpdated(this.overlayID); + } catch (e) { + log('.update error: '+ e, index); + this.stop('update_error'); + } + }, + + // @public @method - show the doppleganger (and start the update thread, if options.autoUpdate was specified). + // @param {vec3} [options.position=(in front of avatar)] - starting position + // @param {quat} [options.orientation=avatar.orientation] - starting orientation + start: function(options) { + options = options || {}; + if (this.overlayID) { + log('start() called but overlay model already exists', this.overlayID); + return; + } + var avatar = this.avatar; + if (!avatar.jointNames.length) { + return this.stop('joints_unavailable'); + } + + this.frame = 0; + this.position = options.position || Vec3.sum(avatar.position, Quat.getForward(avatar.orientation)); + this.orientation = options.orientation || avatar.orientation; + this.skeletonModelURL = avatar.skeletonModelURL; + this.jointStateCount = 0; + this.jointNames = avatar.jointNames; + this.mirroredNames = Doppleganger.getMirroredJointNames(this.jointNames); + this.mirroredIndexes = this.mirroredNames.map(function(name) { + return name ? avatar.getJointIndex(name) : false; + }); + + this.overlayID = Overlays.addOverlay('model', { + visible: false, + url: this.skeletonModelURL, + position: this.position, + rotation: this.orientation + }); + + this.onModelOverlayLoaded = function(error, result) { + if (error) { + return this.stop(error); + } + log('ModelOverlay is ready; # joints == ' + result.jointNames.length); + Overlays.editOverlay(this.overlayID, { visible: true }); + if (!options.position) { + this.syncVerticalPosition(); + } + if (this.autoUpdate) { + this._createUpdateThread(); + } + }; + this.modelOverlayLoaded.connect(this, 'onModelOverlayLoaded'); + + log('doppleganger created; overlayID =', this.overlayID); + + // trigger clean up (and stop updates) if the overlay gets deleted + this.onDeletedOverlay = function(uuid) { + if (uuid === this.overlayID) { + log('onDeletedOverlay', uuid); + this.stop('overlay_deleted'); + } + }; + Overlays.overlayDeleted.connect(this, 'onDeletedOverlay'); + + if ('onLoadComplete' in avatar) { + // stop the current doppleganger if Avatar loads a different model URL + this.onLoadComplete = function() { + if (avatar.skeletonModelURL !== this.skeletonModelURL) { + this.stop('avatar_changed_load'); + } + }; + avatar.onLoadComplete.connect(this, 'onLoadComplete'); + } + + this.activeChanged(this.active = true, 'start'); + this._waitForModel(ModelCache.prefetch(this.skeletonModelURL)); + }, + + // @public @method - hide the doppleganger + // @param {String} [reason=stop] - the reason stop was called + stop: function(reason) { + reason = reason || 'stop'; + if (this.onUpdate) { + Script.update.disconnect(this, 'onUpdate'); + delete this.onUpdate; + } + if (this._interval) { + Script.clearInterval(this._interval); + this._interval = undefined; + } + if (this.onDeletedOverlay) { + Overlays.overlayDeleted.disconnect(this, 'onDeletedOverlay'); + delete this.onDeletedOverlay; + } + if (this.onLoadComplete) { + this.avatar.onLoadComplete.disconnect(this, 'onLoadComplete'); + delete this.onLoadComplete; + } + if (this.onModelOverlayLoaded) { + this.modelOverlayLoaded.disconnect(this, 'onModelOverlayLoaded'); + } + if (this.overlayID) { + Overlays.deleteOverlay(this.overlayID); + this.overlayID = undefined; + } + if (this.active) { + this.activeChanged(this.active = false, reason); + } else if (reason) { + log('already stopped so not triggering another activeChanged; latest reason was:', reason); + } + }, + + // @public @method - Reposition the doppleganger so it sees "eye to eye" with the Avatar. + // @param {String} [byJointName=Hips] - the reference joint used to align the Doppleganger and Avatar + syncVerticalPosition: function(byJointName) { + byJointName = byJointName || 'Hips'; + var names = Overlays.getProperty(this.overlayID, 'jointNames'), + positions = Overlays.getProperty(this.overlayID, 'jointPositions'), + dopplePosition = Overlays.getProperty(this.overlayID, 'position'), + doppleJointIndex = names.indexOf(byJointName), + doppleJointPosition = positions[doppleJointIndex]; + + var avatarPosition = this.avatar.position, + avatarJointIndex = this.avatar.getJointIndex(byJointName), + avatarJointPosition = this.avatar.getJointPosition(avatarJointIndex); + + var offset = avatarJointPosition.y - doppleJointPosition.y; + log('adjusting for offset', offset); + dopplePosition.y = avatarPosition.y + offset; + this.position = dopplePosition; + Overlays.editOverlay(this.overlayID, { position: this.position }); + }, + + // @private @method - creates the update thread to synchronize joint data + _createUpdateThread: function() { + if (Doppleganger.USE_SCRIPT_UPDATE) { + log('creating Script.update thread'); + this.onUpdate = this.update; + Script.update.connect(this, 'onUpdate'); + } else { + log('creating Script.setInterval thread @ ~', Doppleganger.TARGET_FPS +'fps'); + var timeout = 1000 / Doppleganger.TARGET_FPS; + this._interval = Script.setInterval(bind(this, 'update'), timeout); + } + }, + + // @private @method - waits for model to load and handles timeouts + // @param {ModelResource} resource - a prefetched resource to monitor loading state against + _waitForModel: function(resource) { + var RECHECK_MS = 50; + var id = this.overlayID, + watchdogTimer = null; + + function waitForJointNames() { + var error = null, result = null; + if (!watchdogTimer) { + error = 'joints_unavailable'; + } else if (resource.state === Resource.State.FAILED) { + error = 'prefetch_failed'; + } else if (resource.state === Resource.State.FINISHED) { + var names = Overlays.getProperty(id, 'jointNames'); + if (Array.isArray(names) && names.length) { + result = { overlayID: id, jointNames: names }; + } + } + if (error || result !== null) { + Script.clearInterval(this._interval); + this._interval = null; + if (watchdogTimer) { + Script.clearTimeout(watchdogTimer); + } + this.modelOverlayLoaded(error, result); + } + } + watchdogTimer = Script.setTimeout(function() { + watchdogTimer = null; + }, Doppleganger.MAX_WAIT_SECS * 1000); + this._interval = Script.setInterval(bind(this, waitForJointNames), RECHECK_MS); + } +}; + +// @function - bind a function to a `this` context +// @param {Object} - the `this` context +// @param {Function|String} - function or method name +function bind(thiz, method) { + method = thiz[method] || method; + 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 - debug logging +function log() { + print('doppleganger | ' + [].slice.call(arguments).join(' ')); +} + +// -- ADVANCED DEBUGGING -- +// @function - Add debug joint indicators / extra debugging info. +// @param {Doppleganger} - existing Doppleganger instance to add controls to +// +// @note: +// * rightclick toggles mirror mode on/off +// * shift-rightclick toggles the debug indicators on/off +// * clicking on an indicator displays the joint name and mirrored joint name in the debug log. +// +// Example use: +// var doppleganger = new Doppleganger(); +// Doppleganger.addDebugControls(doppleganger); +Doppleganger.addDebugControls = function(doppleganger) { + DebugControls.COLOR_DEFAULT = { red: 255, blue: 255, green: 255 }; + DebugControls.COLOR_SELECTED = { red: 0, blue: 255, green: 0 }; + + function DebugControls() { + this.enableIndicators = true; + this.selectedJointName = null; + this.debugOverlayIDs = undefined; + this.jointSelected = signal(function(result) {}); + } + DebugControls.prototype = { + start: function() { + if (!this.onMousePressEvent) { + this.onMousePressEvent = this._onMousePressEvent; + Controller.mousePressEvent.connect(this, 'onMousePressEvent'); + } + }, + + stop: function() { + this.removeIndicators(); + if (this.onMousePressEvent) { + Controller.mousePressEvent.disconnect(this, 'onMousePressEvent'); + delete this.onMousePressEvent; + } + }, + + createIndicators: function(jointNames) { + this.jointNames = jointNames; + return jointNames.map(function(name, i) { + return Overlays.addOverlay('shape', { + shape: 'Icosahedron', + scale: 0.1, + solid: false, + alpha: 0.5 + }); + }); + }, + + removeIndicators: function() { + if (this.debugOverlayIDs) { + this.debugOverlayIDs.forEach(Overlays.deleteOverlay); + this.debugOverlayIDs = undefined; + } + }, + + onJointsUpdated: function(overlayID) { + if (!this.enableIndicators) { + return; + } + var jointNames = Overlays.getProperty(overlayID, 'jointNames'), + jointOrientations = Overlays.getProperty(overlayID, 'jointOrientations'), + jointPositions = Overlays.getProperty(overlayID, 'jointPositions'), + selectedIndex = jointNames.indexOf(this.selectedJointName); + + if (!this.debugOverlayIDs) { + this.debugOverlayIDs = this.createIndicators(jointNames); + } + + // batch all updates into a single call (using the editOverlays({ id: {props...}, ... }) API) + var updatedOverlays = this.debugOverlayIDs.reduce(function(updates, id, i) { + updates[id] = { + position: jointPositions[i], + rotation: jointOrientations[i], + color: i === selectedIndex ? DebugControls.COLOR_SELECTED : DebugControls.COLOR_DEFAULT, + solid: i === selectedIndex + }; + return updates; + }, {}); + Overlays.editOverlays(updatedOverlays); + }, + + _onMousePressEvent: function(evt) { + if (!evt.isLeftButton || !this.enableIndicators || !this.debugOverlayIDs) { + return; + } + var ray = Camera.computePickRay(evt.x, evt.y), + hit = Overlays.findRayIntersection(ray, true, this.debugOverlayIDs); + + hit.jointIndex = this.debugOverlayIDs.indexOf(hit.overlayID); + hit.jointName = this.jointNames[hit.jointIndex]; + this.jointSelected(hit); + } + }; + + if ('$debugControls' in doppleganger) { + throw new Error('only one set of debug controls can be added per doppleganger'); + } + var debugControls = new DebugControls(); + doppleganger.$debugControls = debugControls; + + function onMousePressEvent(evt) { + if (evt.isRightButton) { + if (evt.isShifted) { + debugControls.enableIndicators = !debugControls.enableIndicators; + if (!debugControls.enableIndicators) { + debugControls.removeIndicators(); + } + } else { + doppleganger.mirrored = !doppleganger.mirrored; + } + } + } + + doppleganger.activeChanged.connect(function(active) { + if (active) { + debugControls.start(); + doppleganger.jointsUpdated.connect(debugControls, 'onJointsUpdated'); + Controller.mousePressEvent.connect(onMousePressEvent); + } else { + Controller.mousePressEvent.disconnect(onMousePressEvent); + doppleganger.jointsUpdated.disconnect(debugControls, 'onJointsUpdated'); + debugControls.stop(); + } + }); + + debugControls.jointSelected.connect(function(hit) { + debugControls.selectedJointName = hit.jointName; + if (hit.jointIndex < 0) { + return; + } + hit.mirroredJointName = Doppleganger.getMirroredJointNames([hit.jointName])[0]; + log('selected joint:', JSON.stringify(hit, 0, 2)); + }); + + Script.scriptEnding.connect(debugControls, 'removeIndicators'); + + return doppleganger; +}; diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 9988df425d..a6d2d165f7 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -12,7 +12,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global Script, SelectionDisplay, LightOverlayManager, CameraManager, Grid, GridTool, EntityListTool, Vec3, SelectionManager, Overlays, OverlayWebWindow, UserActivityLogger, Settings, Entities, Tablet, Toolbars, Messages, Menu, Camera, progressDialog, tooltip, MyAvatar, Quat, Controller, Clipboard, HMD, UndoStack, ParticleExplorerTool */ +/* global Script, SelectionDisplay, LightOverlayManager, CameraManager, Grid, GridTool, EntityListTool, Vec3, SelectionManager, Overlays, OverlayWebWindow, UserActivityLogger, + Settings, Entities, Tablet, Toolbars, Messages, Menu, Camera, progressDialog, tooltip, MyAvatar, Quat, Controller, Clipboard, HMD, UndoStack, ParticleExplorerTool */ (function() { // BEGIN LOCAL_SCOPE @@ -96,6 +97,10 @@ selectionManager.addEventListener(function () { particleExplorerTool.webView.emitScriptEvent(JSON.stringify(particleData)); } }); + + // Switch to particle explorer + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + tablet.sendToQml({method: 'selectTab', params: {id: 'particle'}}); } else { needToDestroyParticleExplorer = true; } @@ -213,6 +218,8 @@ function hideMarketplace() { // } var TOOLS_PATH = Script.resolvePath("assets/images/tools/"); +var GRABBABLE_ENTITIES_MENU_CATEGORY = "Edit"; +var GRABBABLE_ENTITIES_MENU_ITEM = "Create Entities As Grabbable"; var toolBar = (function () { var EDIT_SETTING = "io.highfidelity.isEditting"; // for communication with other scripts @@ -227,8 +234,11 @@ var toolBar = (function () { var position = getPositionToCreateEntity(); var entityID = null; if (position !== null && position !== undefined) { - position = grid.snapToSurface(grid.snapToGrid(position, false, dimensions), dimensions), - properties.position = position; + position = grid.snapToSurface(grid.snapToGrid(position, false, dimensions), dimensions); + properties.position = position; + if (Menu.isOptionChecked(GRABBABLE_ENTITIES_MENU_ITEM)) { + properties.userData = JSON.stringify({ grabbableKey: { grabbable: true } }); + } entityID = Entities.addEntity(properties); if (properties.type == "ParticleEffect") { selectParticleEntity(entityID); @@ -253,6 +263,7 @@ var toolBar = (function () { if (systemToolbar) { systemToolbar.removeButton(EDIT_TOGGLE_BUTTON); } + Menu.removeMenuItem(GRABBABLE_ENTITIES_MENU_CATEGORY, GRABBABLE_ENTITIES_MENU_ITEM); } var buttonHandlers = {}; // only used to tablet mode @@ -332,7 +343,7 @@ var toolBar = (function () { activeButton = tablet.addButton({ icon: "icons/tablet-icons/edit-i.svg", activeIcon: "icons/tablet-icons/edit-a.svg", - text: "EDIT", + text: "CREATE", sortOrder: 10 }); tablet.screenChanged.connect(function (type, url) { @@ -638,6 +649,27 @@ function findClickedEntity(event) { }; } +// Handles selections on overlays while in edit mode by querying entities from +// entityIconOverlayManager. +function handleOverlaySelectionToolUpdates(channel, message, sender) { + if (sender !== MyAvatar.sessionUUID || channel !== 'entityToolUpdates') + return; + + var data = JSON.parse(message); + + if (data.method === "selectOverlay") { + print("setting selection to overlay " + data.overlayID); + var entity = entityIconOverlayManager.findEntity(data.overlayID); + + if (entity !== null) { + selectionManager.setSelections([entity]); + } + } +} + +Messages.subscribe("entityToolUpdates"); +Messages.messageReceived.connect(handleOverlaySelectionToolUpdates); + var mouseHasMovedSincePress = false; var mousePressStartTime = 0; var mousePressStartPosition = { @@ -903,11 +935,21 @@ function setupModelMenus() { afterItem: "Parent Entity to Last", grouping: "Advanced" }); + + Menu.addMenuItem({ + menuName: GRABBABLE_ENTITIES_MENU_CATEGORY, + menuItemName: GRABBABLE_ENTITIES_MENU_ITEM, + afterItem: "Unparent Entity", + isCheckable: true, + isChecked: true, + grouping: "Advanced" + }); + Menu.addMenuItem({ menuName: "Edit", menuItemName: "Allow Selecting of Large Models", shortcutKey: "CTRL+META+L", - afterItem: "Unparent Entity", + afterItem: GRABBABLE_ENTITIES_MENU_ITEM, isCheckable: true, isChecked: true, grouping: "Advanced" @@ -1047,6 +1089,13 @@ Script.scriptEnding.connect(function () { Controller.keyReleaseEvent.disconnect(keyReleaseEvent); Controller.keyPressEvent.disconnect(keyPressEvent); + + Controller.mousePressEvent.disconnect(mousePressEvent); + Controller.mouseMoveEvent.disconnect(mouseMoveEventBuffered); + Controller.mouseReleaseEvent.disconnect(mouseReleaseEvent); + + Messages.messageReceived.disconnect(handleOverlaySelectionToolUpdates); + Messages.unsubscribe("entityToolUpdates"); }); var lastOrientation = null; @@ -2013,7 +2062,11 @@ function selectParticleEntity(entityID) { selectedParticleEntity = entityID; particleExplorerTool.setActiveParticleEntity(entityID); - particleExplorerTool.webView.emitScriptEvent(JSON.stringify(particleData)); + particleExplorerTool.webView.emitScriptEvent(JSON.stringify(particleData)); + + // Switch to particle explorer + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + tablet.sendToQml({method: 'selectTab', params: {id: 'particle'}}); } entityListTool.webView.webEventReceived.connect(function (data) { @@ -2041,3 +2094,4 @@ entityListTool.webView.webEventReceived.connect(function (data) { }); }()); // END LOCAL_SCOPE + diff --git a/scripts/system/html/SnapshotReview.html b/scripts/system/html/SnapshotReview.html index 145cfb16a9..fb40c04d05 100644 --- a/scripts/system/html/SnapshotReview.html +++ b/scripts/system/html/SnapshotReview.html @@ -1,15 +1,17 @@ Share - + -
    - +
    + + +

    @@ -17,30 +19,16 @@
    -
    -
    -
    - - - - -
    -
    -
    - -
    -
    -
    -
    - - - - - - - - +
    +
    +
    + +
    + +
    + +
    diff --git a/scripts/system/html/css/SnapshotReview.css b/scripts/system/html/css/SnapshotReview.css index 34b690a021..12b91d372b 100644 --- a/scripts/system/html/css/SnapshotReview.css +++ b/scripts/system/html/css/SnapshotReview.css @@ -8,142 +8,280 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html */ -body { - padding-top: 0; - padding-bottom: 14px; -} +/* +// START styling of top bar and its contents +*/ -.snapsection { - padding-top: 14px; - text-align: center; -} - -.snapsection.title { - padding-top: 0; +.title { + padding: 6px 10px; text-align: left; + height: 26px; + line-height: 26px; + clear: both; } .title label { - font-size: 18px; position: relative; - top: 12px; + font-size: 18px; + float: left; } +#snapshotSettings { + position: relative; + float: right; +} +#settingsLabel { + position: relative; + float: right; + font-family: Raleway-SemiBold; + font-size: 14px; +} +.hifi-glyph { + font-size: 30px; + top: -4px; +} +input[type=button].naked { + color: #afafaf; + background: none; +} +input[type=button].naked:hover { + color: #ffffff; +} +input[type=button].naked:active { + color: #afafaf; +} +/* +// END styling of top bar and its contents +*/ + +/* +// START styling of snapshot instructions panel +*/ +.snapshotInstructions { + font-family: Raleway-Regular; + margin: 0 20px; + width: 100%; + height: 50%; +} +/* +// END styling of snapshot instructions panel +*/ + +/* +// START styling of snapshot pane and its contents +*/ #snapshot-pane { width: 100%; - height: 100%; - position: absolute; - top: 0; - left: 0; - box-sizing: border-box; - padding-top: 56px; - padding-bottom: 175px; + height: 560px; + display: flex; + justify-content: center; + align-items: center; } #snapshot-images { - height: 100%; width: 100%; - position: relative; -} - -#snapshot-images > div { - position: relative; - text-align: center; } #snapshot-images img { max-width: 100%; max-height: 100%; +} + +.gifLabel { + position:absolute; + left: 15px; + top: 10px; + font-family: Raleway-SemiBold; + font-size: 18px; + color: white; + text-shadow: 2px 2px 3px #000000; +} +/* +// END styling of snapshot pane and its contents +*/ + +/* +// START styling of share bar +*/ +.shareControls { + display: flex; + justify-content: space-between; + flex-direction: row; + align-items: center; + height: 50px; + line-height: 60px; + width: calc(100% - 8px); position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - vertical-align: middle; + bottom: 4px; + left: 4px; + right: 4px; } - -#snapshot-images div.property { - margin-top: 0; +.shareButtons { + display: flex; + align-items: center; + margin-left: 30px; + height: 100%; + width: 80%; +} +.blastToConnections { + text-align: left; + margin-right: 25px; + height: 29px; +} +.shareWithEveryone { + background: #DDDDDD url(../img/shareToFeed.png) no-repeat scroll center; + border-width: 0px; + text-align: left; + margin-right: 8px; + height: 29px; + width: 30px; + border-radius: 3px; +} +.facebookButton { + background-image: url(../img/fb_logo.png); + width: 29px; + height: 29px; + display: inline-block; + margin-right: 8px; +} +.twitterButton { + background-image: url(../img/twitter_logo.png); + width: 29px; + height: 29px; + display: inline-block; + margin-right: 8px; + border-radius: 3px; +} +.showShareButtonsButtonDiv { + display: inline-flex; + align-items: center; + font-family: Raleway-SemiBold; + font-size: 14px; + color: white; + text-shadow: 2px 2px 3px #000000; + height: 100%; + margin-right: 10px; + width: 20%; +} +.showShareButton { + width: 40px; + height: 40px; + border-radius: 50%; + border-width: 0; + margin-left: 5px; + outline: none; +} +.showShareButton.active { + border-color: #00b4ef; + border-width: 3px; + background-color: white; +} +.showShareButton.active:hover { + background-color: #afafaf; +} +.showShareButton.active:active { + background-color: white; +} +.showShareButton.inactive { + border-width: 0; + background-color: white; +} +.showShareButton.inactive:hover { + background-color: #afafaf; +} +.showShareButton.inactive:active { + background-color: white; +} +.showShareButtonDots { + display: flex; + width: 32px; + height: 40px; position: absolute; - top: 50%; - left: 7px; - transform: translate(0%, -50%); + top: 5px; + right: 14px; + pointer-events: none; } - -#snapshot-images img { - box-sizing: border-box; - padding: 0 7px 0 7px; -} - -#snapshot-images img.multiple { - padding-left: 28px; +.showShareButtonDots > span { + width: 10px; + height: 10px; + margin: auto; + background-color: #0093C5; + border-radius: 50%; + border-width: 0; + display: inline; } +/* +// END styling of share overlay +*/ +/* +// START styling of snapshot controls (bottom panel) and its contents +*/ #snapshot-controls { width: 100%; position: absolute; left: 0; - bottom: 14px; + overflow: hidden; + display: flex; + justify-content: center; +} +#snap-settings { + display: inline; + width: 150px; + margin: 2px auto 0 auto; +} +#snap-settings form input { + margin-bottom: 5px; } -.prompt { - font-family: Raleway-SemiBold; - font-size: 14px; -} - -div.button { - padding-top: 21px; -} - -.compound-button { - position: relative; - height: auto; -} - -.compound-button input { - padding-left: 40px; -} - -.compound-button .glyph { - display: inline-block; - position: absolute; - left: 12px; - top: 16px; - width: 23px; - height: 23px; - background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgaGVpZ2h0PSI0MCIKICAgd2lkdGg9IjQwIgogICBpZD0ic3ZnMiIKICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgdmlld0JveD0iMCAwIDQwIDQwIgogICB5PSIwcHgiCiAgIHg9IjBweCIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGEzNCI+PHJkZjpSREY+PGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PjxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz48ZGM6dGl0bGU+PC9kYzp0aXRsZT48L2NjOldvcms+PC9yZGY6UkRGPjwvbWV0YWRhdGE+PGRlZnMKICAgICBpZD0iZGVmczMyIiAvPjxzdHlsZQogICAgIGlkPSJzdHlsZTQiCiAgICAgdHlwZT0idGV4dC9jc3MiPgoJLnN0MHtmaWxsOiM0MTQwNDI7fQoJLnN0MXtmaWxsOiNDQ0NDQ0M7fQoJLnN0MntmaWxsOiMxMzk4QkI7fQoJLnN0M3tmaWxsOiMzMUQ4RkY7fQo8L3N0eWxlPjxnCiAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwtMTEwKSIKICAgICBpZD0iTGF5ZXJfMSI+PGNpcmNsZQogICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MSIKICAgICAgIGlkPSJjaXJjbGUxMyIKICAgICAgIHI9IjQuNDQwMDAwMSIKICAgICAgIGN5PSIxMjYuMTciCiAgICAgICBjeD0iMjAuNTQwMDAxIgogICAgICAgY2xhc3M9InN0MSIgLz48cGF0aAogICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MSIKICAgICAgIGlkPSJwYXRoMTUiCiAgICAgICBkPSJtIDI4Ljg3LDEzOS4yNiBjIDAuMDEsLTAuMDEgMC4wMiwtMC4wMiAwLjAzLC0wLjAzIGwgMCwtMS44NiBjIDAsLTIuNjggLTIuMzMsLTQuNzcgLTUsLTQuNzcgbCAtNi40MiwwIGMgLTIuNjgsMCAtNC44NSwyLjA5IC00Ljg1LDQuNzcgbCAwLDEuODggMTYuMjQsMCB6IgogICAgICAgY2xhc3M9InN0MSIgLz48cGF0aAogICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MSIKICAgICAgIGlkPSJwYXRoMTciCiAgICAgICBkPSJtIDM4LjE3LDEyMy40MiBjIDAsLTMuOTcgLTMuMjIsLTcuMTkgLTcuMTksLTcuMTkgbCAtMjAuMzEsMCBjIC0zLjk3LDAgLTcuMTksMy4yMiAtNy4xOSw3LjE5IGwgMCwxNC4xOCBjIDAsMy45NyAzLjIyLDcuMTkgNy4xOSw3LjE5IGwgMjAuMzEsMCBjIDMuOTcsMCA3LjE5LC0zLjIyIDcuMTksLTcuMTkgbCAwLC0xNC4xOCB6IG0gLTEuNzgsMTQuMjcgYyAwLDMuMDMgLTIuNDYsNS40OSAtNS40OSw1LjQ5IGwgLTIwLjMyLDAgYyAtMy4wMywwIC01LjQ5LC0yLjQ2IC01LjQ5LC01LjQ5IGwgMCwtMTQuMTkgYyAwLC0zLjAzIDIuNDYsLTUuNDkgNS40OSwtNS40OSBsIDIwLjMzLDAgYyAzLjAzLDAgNS40OSwyLjQ2IDUuNDksNS40OSBsIDAsMTQuMTkgeiIKICAgICAgIGNsYXNzPSJzdDEiIC8+PC9nPjxnCiAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwtMTEwKSIKICAgICBpZD0iTGF5ZXJfMiIgLz48L3N2Zz4=); - background-repeat: no-repeat; - background-size: 23px 23px; -} - -.setting { - display: inline-table; - height: 28px; -} - -.setting label { - display: table-cell; - vertical-align: middle; - font-family: Raleway-SemiBold; - font-size: 14px; -} - -.setting + .setting { - margin-left: 18px; -} - -input[type=button].naked { - font-size: 40px; - line-height: 40px; - width: 30px; +#snap-button { + width: 72px; + height: 72px; padding: 0; - margin: 0 0 -6px 0; - position: relative; - top: -6px; - left: -8px; - background: none; + border-radius: 50%; + background: #EA4C5F; + border: 3px solid white; + margin: 2px auto 0 auto; + box-sizing: content-box; + display: inline; + outline:none; } +#snap-button:disabled { + background: gray; +} +#snap-button:hover:enabled { + background: #C62147; +} +#snap-button:active:enabled { + background: #EA4C5F; +} +#snap-settings-right { + display: inline; + width: 150px; + margin: auto; +} +/* +// END styling of snapshot controls (bottom panel) and its contents +*/ -input[type=button].naked:hover { - color: #00b4ef; - background: none; +/* +// START misc styling +*/ +body { + padding: 0; + margin: 0; + overflow: hidden; } +p { + margin: 2px 0; +} +h4 { + margin: 14px 0 0 0; +} +.centeredImage { + margin: 0 auto; + display: block; +} +/* +// END misc styling +*/ diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index 06a60b5405..fcb1815ca4 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -12,49 +12,56 @@ font-family: Raleway-Regular; src: url(../../../../resources/fonts/Raleway-Regular.ttf), /* Windows production */ url(../../../../fonts/Raleway-Regular.ttf), /* OSX production */ - url(../../../../interface/resources/fonts/Raleway-Regular.ttf); /* Development, running script in /HiFi/examples */ + url(../../../../interface/resources/fonts/Raleway-Regular.ttf), /* Development, running script in /HiFi/examples */ + url(../fonts/Raleway-Regular.ttf); /* Marketplace script */ } @font-face { font-family: Raleway-Light; src: url(../../../../resources/fonts/Raleway-Light.ttf), url(../../../../fonts/Raleway-Light.ttf), - url(../../../../interface/resources/fonts/Raleway-Light.ttf); + url(../../../../interface/resources/fonts/Raleway-Light.ttf), + url(../fonts/Raleway-Light.ttf); } @font-face { font-family: Raleway-Bold; src: url(../../../../resources/fonts/Raleway-Bold.ttf), url(../../../../fonts/Raleway-Bold.ttf), - url(../../../../interface/resources/fonts/Raleway-Bold.ttf); + url(../../../../interface/resources/fonts/Raleway-Bold.ttf), + url(../fonts/Raleway-Bold.ttf); } @font-face { font-family: Raleway-SemiBold; src: url(../../../../resources/fonts/Raleway-SemiBold.ttf), url(../../../../fonts/Raleway-SemiBold.ttf), - url(../../../../interface/resources/fonts/Raleway-SemiBold.ttf); + url(../../../../interface/resources/fonts/Raleway-SemiBold.ttf), + url(../fonts/Raleway-SemiBold.ttf); } @font-face { font-family: FiraSans-SemiBold; src: url(../../../../resources/fonts/FiraSans-SemiBold.ttf), url(../../../../fonts/FiraSans-SemiBold.ttf), - url(../../../../interface/resources/fonts/FiraSans-SemiBold.ttf); + url(../../../../interface/resources/fonts/FiraSans-SemiBold.ttf), + url(../fonts/FiraSans-SemiBold.ttf); } @font-face { font-family: AnonymousPro-Regular; src: url(../../../../resources/fonts/AnonymousPro-Regular.ttf), url(../../../../fonts/AnonymousPro-Regular.ttf), - url(../../../../interface/resources/fonts/AnonymousPro-Regular.ttf); + url(../../../../interface/resources/fonts/AnonymousPro-Regular.ttf), + url(../fonts/AnonymousPro-Regular.ttf); } @font-face { font-family: HiFi-Glyphs; src: url(../../../../resources/fonts/hifi-glyphs.ttf), url(../../../../fonts/hifi-glyphs.ttf), - url(../../../../interface/resources/fonts/hifi-glyphs.ttf); + url(../../../../interface/resources/fonts/hifi-glyphs.ttf), + url(../fonts/hifi-glyphs.ttf); } * { @@ -289,7 +296,7 @@ input[type=number]::-webkit-inner-spin-button:after { bottom: 4px; } -input[type=number].hover-up::-webkit-inner-spin-button:before, +input[type=number].hover-up::-webkit-inner-spin-button:before, input[type=number].hover-down::-webkit-inner-spin-button:after { color: #ffffff; } @@ -1037,6 +1044,12 @@ th#entity-hasTransparent .sort-order { top: -1px; } +#entity-table td.isBaked.glyph { + font-size: 22px; + position: relative; + top: -1px; +} + #entity-table tfoot { box-sizing: border-box; border: 2px solid #575757; @@ -1062,7 +1075,7 @@ th#entity-hasTransparent .sort-order { #col-locked, #col-visible { width: 9%; } -#col-verticesCount, #col-texturesCount, #col-texturesSize, #col-hasTransparent, #col-drawCalls, #col-hasScript { +#col-verticesCount, #col-texturesCount, #col-texturesSize, #col-hasTransparent, #col-isBaked, #col-drawCalls, #col-hasScript { width: 0; } @@ -1090,6 +1103,9 @@ th#entity-hasTransparent .sort-order { .showExtraInfo #col-hasTransparent { width: 4%; } +.showExtraInfo #col-isBaked { + width: 8%; +} .showExtraInfo #col-drawCalls { width: 8%; } @@ -1097,12 +1113,12 @@ th#entity-hasTransparent .sort-order { width: 6%; } -th#entity-verticesCount, th#entity-texturesCount, th#entity-texturesSize, th#entity-hasTransparent, th#entity-drawCalls, +th#entity-verticesCount, th#entity-texturesCount, th#entity-texturesSize, th#entity-hasTransparent, th#entity-isBaked, th#entity-drawCalls, th#entity-hasScript { display: none; } -.verticesCount, .texturesCount, .texturesSize, .hasTransparent, .drawCalls, .hasScript { +.verticesCount, .texturesCount, .texturesSize, .hasTransparent, .isBaked, .drawCalls, .hasScript { display: none; } @@ -1110,13 +1126,13 @@ th#entity-hasScript { border: none; } -.showExtraInfo #entity-verticesCount, .showExtraInfo #entity-texturesCount, .showExtraInfo #entity-texturesSize, -.showExtraInfo #entity-hasTransparent, .showExtraInfo #entity-drawCalls, .showExtraInfo #entity-hasScript { +.showExtraInfo #entity-verticesCount, .showExtraInfo #entity-texturesCount, .showExtraInfo #entity-texturesSize, +.showExtraInfo #entity-hasTransparent, .showExtraInfo #entity-isBaked, .showExtraInfo #entity-drawCalls, .showExtraInfo #entity-hasScript { display: inline-block; } -.showExtraInfo .verticesCount, .showExtraInfo .texturesCount, .showExtraInfo .texturesSize, .showExtraInfo .hasTransparent, -.showExtraInfo .drawCalls, .showExtraInfo .hasScript { +.showExtraInfo .verticesCount, .showExtraInfo .texturesCount, .showExtraInfo .texturesSize, .showExtraInfo .hasTransparent, +.showExtraInfo .isBaked, .showExtraInfo .drawCalls, .showExtraInfo .hasScript { display: table-cell; } diff --git a/scripts/system/html/css/hifi-style.css b/scripts/system/html/css/hifi-style.css new file mode 100644 index 0000000000..f1ace02eb0 --- /dev/null +++ b/scripts/system/html/css/hifi-style.css @@ -0,0 +1,170 @@ +/* +// hifi-style.css +// +// Created by Zach Fox on 2017-04-18 +// 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 +*/ + +@font-face { + font-family: Raleway-Regular; + src: url(../../../../resources/fonts/Raleway-Regular.ttf), /* Windows production */ + url(../../../../fonts/Raleway-Regular.ttf), /* OSX production */ + url(../../../../interface/resources/fonts/Raleway-Regular.ttf); /* Development, running script in /HiFi/examples */ +} + +@font-face { + font-family: Raleway-Light; + src: url(../../../../resources/fonts/Raleway-Light.ttf), + url(../../../../fonts/Raleway-Light.ttf), + url(../../../../interface/resources/fonts/Raleway-Light.ttf); +} + +@font-face { + font-family: Raleway-Bold; + src: url(../../../../resources/fonts/Raleway-Bold.ttf), + url(../../../../fonts/Raleway-Bold.ttf), + url(../../../../interface/resources/fonts/Raleway-Bold.ttf); +} + +@font-face { + font-family: Raleway-SemiBold; + src: url(../../../../resources/fonts/Raleway-SemiBold.ttf), + url(../../../../fonts/Raleway-SemiBold.ttf), + url(../../../../interface/resources/fonts/Raleway-SemiBold.ttf); +} + +@font-face { + font-family: FiraSans-SemiBold; + src: url(../../../../resources/fonts/FiraSans-SemiBold.ttf), + url(../../../../fonts/FiraSans-SemiBold.ttf), + url(../../../../interface/resources/fonts/FiraSans-SemiBold.ttf); +} + +@font-face { + font-family: AnonymousPro-Regular; + src: url(../../../../resources/fonts/AnonymousPro-Regular.ttf), + url(../../../../fonts/AnonymousPro-Regular.ttf), + url(../../../../interface/resources/fonts/AnonymousPro-Regular.ttf); +} + +@font-face { + font-family: HiFi-Glyphs; + src: url(../../../../resources/fonts/hifi-glyphs.ttf), + url(../../../../fonts/hifi-glyphs.ttf), + url(../../../../interface/resources/fonts/hifi-glyphs.ttf); +} + +body { + color: #afafaf; + background-color: #404040; + font-family: Raleway-Regular; + font-size: 15px; + + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + overflow-x: hidden; + overflow-y: auto; +} + +hr { + border: none; + background: #404040 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAjSURBVBhXY1RVVf3PgARYjIyMoEwIYHRwcEBRwQSloYCBAQCwjgPMiI7W2QAAAABJRU5ErkJggg==) repeat-x top left; + padding: 1px; + -webkit-margin-before: 0; + -webkit-margin-after: 0; + -webkit-margin-start: 0; + -webkit-margin-end: 0; + width: 100%; + position: absolute; +} + +.hifi-glyph { + font-family: HiFi-Glyphs; + border: none; + //margin: -10px; + padding: 0; +} + +input[type=radio] { + width: 2em; + margin: 0; + padding: 0; + font-size: 1em; + opacity: 0; +} +input[type=radio] + label{ + display: inline-block; + margin-left: -2em; + line-height: 2em; +} +input[type=radio] + label > span{ + display: inline-block; + width: 20px; + height: 20px; + margin: 5px; + border-radius: 50%; + background: #6B6A6B; + background-image: linear-gradient(#7D7D7D, #6B6A6B); + vertical-align: bottom; +} +input[type=radio]:checked + label > span{ + background-image: linear-gradient(#7D7D7D, #6B6A6B); +} +input[type=radio]:active + label > span, +input[type=radio]:hover + label > span{ + background-image: linear-gradient(#FFFFFF, #AFAFAF); +} +input[type=radio]:checked + label > span > span, +input[type=radio]:active + label > span > span{ + display: block; + width: 10px; + height: 10px; + margin: 3px; + border: 2px solid #36CDFF; + border-radius: 50%; + background: #00B4EF; +} + +.grayButton { + font-family: FiraSans-SemiBold; + color: white; + padding: 0px 10px; + border-width: 0px; + background-image: linear-gradient(#FFFFFF, #AFAFAF); +} +.grayButton:hover { + background-image: linear-gradient(#FFFFFF, #FFFFFF); +} +.grayButton:active { + background-image: linear-gradient(#AFAFAF, #AFAFAF); +} +.grayButton:disabled { + background-image: linear-gradient(#FFFFFF, ##AFAFAF); +} +.blueButton { + font-family: FiraSans-SemiBold; + color: white; + padding: 0px 10px; + border-radius: 3px; + border-width: 0px; + background-image: linear-gradient(#00B4EF, #1080B8); + min-height: 30px; + +} +.blueButton:hover { + background-image: linear-gradient(#00B4EF, #00B4EF); +} +.blueButton:active { + background-image: linear-gradient(#1080B8, #1080B8); +} +.blueButton:disabled { + background-image: linear-gradient(#FFFFFF, #AFAFAF); +} \ No newline at end of file diff --git a/scripts/system/html/css/record.css b/scripts/system/html/css/record.css new file mode 100644 index 0000000000..35751379b9 --- /dev/null +++ b/scripts/system/html/css/record.css @@ -0,0 +1,218 @@ +/* +// record.css +// +// Created by David Rowe on 5 Apr 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 +*/ + + +body { + padding: 0; + overflow: hidden; +} + +.title { + padding-left: 21px; +} + +.title label { + font-size: 18px; + position: relative; + top: 12px; +} + + +#recordings { + height: 100%; + position: relative; + box-sizing: border-box; + padding: 51px 0 185px 0; + margin: 0 21px 0 21px; +} + +#recordings #table-container { + height: 100%; + width: 100%; + overflow-x: hidden; + overflow-y: auto; + box-sizing: border-box; + border-left: 2px solid #575757; + border-right: 2px solid #575757; + background-color: #2e2e2e; +} + +#recordings table { + border: none; +} + +#recordings thead { + position: absolute; + top: 21px; + left: 0; + width: 100%; + box-sizing: border-box; + border: 2px solid #575757; + border-top-left-radius: 7px; + border-top-right-radius: 7px; + border-bottom: 1px solid #575757; + position: absolute; + word-wrap: nowrap; + white-space: nowrap; + overflow: hidden; +} + +#recordings table col#unload-column { + width: 100px; +} + +#recordings thead th:last-child { + width: 100px; +} + +#recordings table td { + text-overflow: ellipsis; +} + +#recordings table td:nth-child(2) { + text-align: center; +} + +#recordings tbody tr.filler td { + height: auto; + border-top: 1px solid #1c1c1c; +} + +#recordings-list input { + height: 22px; + width: 22px; + min-width: 22px; + font-size: 16px; + padding: 0 1px 0 0; +} + +#recordings tfoot { + position: absolute; + bottom: 159px; + left: 0; + width: 100%; + box-sizing: border-box; + border: 2px solid #575757; + border-bottom-left-radius: 7px; + border-bottom-right-radius: 7px; + border-top: 1px solid #575757; +} + +#recordings tfoot tr, #recordings tfoot td { + background: none; +} + + +#spinner { + text-align: center; + margin-top: 25%; + position: relative; +} + +#spinner span { + display: block; + position: relative; + top: -101px; + color: #e2334d; + font-size: 60px; + font-weight: bold; +} + + +#recordings tfoot tr { + height: 24px; +} + + +#instructions td { + white-space: normal; +} + +#instructions h1 { + font-size: 16px; + margin-top: 28px; +} + +#instructions h1 + p { + margin-top: 14px; +} + +#instructions p, #instructions ul { + margin-top: 21px; + font-size: 14px; +} + +#instructions p { + font-family: Raleway-Bold; +} + +#instructions ul { + font-family: Raleway-SemiBold; + margin-left: 21px; + font-weight: normal; +} + +#instructions li { + margin-top: 7px; +} + +#instructions ul input { + margin-left: 1px; + margin-top: 6px; + font-size: 14px; + padding: 0 7px; +} + + +#show-info-button { + font-family: HiFi-Glyphs; + font-size: 32px; + height: 16px; + line-height: 16px; + display: inline-block; + position: absolute; + top: 15px; + right: 5px; + margin-top: -11px; + margin-left: 7px; +} + +#show-info-button:hover { + color: #00b4ef; +} + + +#record-controls { + position: absolute; + bottom: 7px; + width: 100%; +} + +#record-controls #load-container { + position: absolute; + left: 21px; +} + +#record-controls #record-container { + text-align: center; +} + +#record-controls #checkbox-container { + margin-top: 31px; +} + +#record-controls div.property { + padding-left: 21px; +} + + +.hidden { + display: none; +} diff --git a/scripts/system/html/entityList.html b/scripts/system/html/entityList.html index 9d774f1861..d608ab63e5 100644 --- a/scripts/system/html/entityList.html +++ b/scripts/system/html/entityList.html @@ -49,6 +49,7 @@ + @@ -63,6 +64,7 @@ Texts Text MB + Baked Draws k @@ -78,6 +80,7 @@ + @@ -85,7 +88,7 @@ - + diff --git a/scripts/system/html/img/fb_logo.png b/scripts/system/html/img/fb_logo.png new file mode 100644 index 0000000000..1de20bacd8 Binary files /dev/null and b/scripts/system/html/img/fb_logo.png differ diff --git a/scripts/system/html/img/loader-red-countdown-ring.gif b/scripts/system/html/img/loader-red-countdown-ring.gif new file mode 100644 index 0000000000..eb15b9aedd Binary files /dev/null and b/scripts/system/html/img/loader-red-countdown-ring.gif differ diff --git a/scripts/system/html/img/shareIcon.png b/scripts/system/html/img/shareIcon.png new file mode 100644 index 0000000000..0486ac9202 Binary files /dev/null and b/scripts/system/html/img/shareIcon.png differ diff --git a/scripts/system/html/img/shareToFeed.png b/scripts/system/html/img/shareToFeed.png new file mode 100644 index 0000000000..f681c49d8f Binary files /dev/null and b/scripts/system/html/img/shareToFeed.png differ diff --git a/scripts/system/html/img/snapshotIcon.png b/scripts/system/html/img/snapshotIcon.png new file mode 100644 index 0000000000..5cb2742a32 Binary files /dev/null and b/scripts/system/html/img/snapshotIcon.png differ diff --git a/scripts/system/html/img/twitter_logo.png b/scripts/system/html/img/twitter_logo.png new file mode 100644 index 0000000000..59fd027c2a Binary files /dev/null and b/scripts/system/html/img/twitter_logo.png differ diff --git a/scripts/system/html/js/SnapshotReview.js b/scripts/system/html/js/SnapshotReview.js index f140c54e09..53f4d17930 100644 --- a/scripts/system/html/js/SnapshotReview.js +++ b/scripts/system/html/js/SnapshotReview.js @@ -10,117 +10,325 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var paths = [], idCounter = 0, imageCount; -function addImage(data) { - if (!data.localPath) { +var paths = []; +var idCounter = 0; +var imageCount = 0; +function showSetupInstructions() { + var snapshotImagesDiv = document.getElementById("snapshot-images"); + snapshotImagesDiv.className = "snapshotInstructions"; + snapshotImagesDiv.innerHTML = 'Snapshot Instructions' + + '
    ' + + '

    This app lets you take and share snaps and GIFs with your connections in High Fidelity.

    ' + + "

    Setup Instructions

    " + + "

    Before you can begin taking snaps, please choose where you'd like to save snaps on your computer:

    " + + '
    ' + + '
    ' + + '' + + '
    '; + document.getElementById("snap-button").disabled = true; +} +function showSetupComplete() { + var snapshotImagesDiv = document.getElementById("snapshot-images"); + snapshotImagesDiv.className = "snapshotInstructions"; + snapshotImagesDiv.innerHTML = 'Snapshot Instructions' + + '
    ' + + "

    You're all set!

    " + + '

    Try taking a snapshot by pressing the red button below.

    '; +} +function chooseSnapshotLocation() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "chooseSnapshotLocation" + })); +} +function clearImages() { + document.getElementById("snap-button").disabled = false; + var snapshotImagesDiv = document.getElementById("snapshot-images"); + snapshotImagesDiv.classList.remove("snapshotInstructions"); + while (snapshotImagesDiv.hasChildNodes()) { + snapshotImagesDiv.removeChild(snapshotImagesDiv.lastChild); + } + paths = []; + imageCount = 0; + idCounter = 0; +} +function addImage(image_data, isGifLoading, isShowingPreviousImages, canSharePreviousImages, hifiShareButtonsDisabled) { + if (!image_data.localPath) { return; } - var div = document.createElement("DIV"), - input = document.createElement("INPUT"), - label = document.createElement("LABEL"), - img = document.createElement("IMG"), - div2 = document.createElement("DIV"), - id = "p" + idCounter++; - img.id = id + "img"; - function toggle() { data.share = input.checked; } - div.style.height = "" + Math.floor(100 / imageCount) + "%"; + var id = "p" + idCounter++; + // imageContainer setup + var imageContainer = document.createElement("DIV"); + imageContainer.id = id; + imageContainer.style.width = "100%"; + imageContainer.style.height = "251px"; + imageContainer.style.display = "flex"; + imageContainer.style.justifyContent = "center"; + imageContainer.style.alignItems = "center"; + imageContainer.style.position = "relative"; + // img setup + var img = document.createElement("IMG"); + img.id = id + "img"; if (imageCount > 1) { img.setAttribute("class", "multiple"); } - img.src = data.localPath; - div.appendChild(img); - if (imageCount > 1) { // I'd rather use css, but the included stylesheet is quite particular. - // Our stylesheet(?) requires input.id to match label.for. Otherwise input doesn't display the check state. - label.setAttribute('for', id); // cannot do label.for = - input.id = id; - input.type = "checkbox"; - input.checked = false; - data.share = input.checked; - input.addEventListener('change', toggle); - div2.setAttribute("class", "property checkbox"); - div2.appendChild(input); - div2.appendChild(label); - div.appendChild(div2); - } else { - data.share = true; + img.src = image_data.localPath; + imageContainer.appendChild(img); + document.getElementById("snapshot-images").appendChild(imageContainer); + paths.push(image_data.localPath); + var isGif = img.src.split('.').pop().toLowerCase() === "gif"; + if (isGif) { + imageContainer.innerHTML += 'GIF'; + } + if (!isGifLoading && !isShowingPreviousImages) { + shareForUrl(id); + } else if (isShowingPreviousImages && canSharePreviousImages) { + appendShareBar(id, image_data.story_id, isGif, hifiShareButtonsDisabled) } - document.getElementById("snapshot-images").appendChild(div); - paths.push(data); } -function handleShareButtons(messageOptions) { - var openFeed = document.getElementById('openFeed'); - openFeed.checked = messageOptions.openFeedAfterShare; - openFeed.onchange = function () { +function appendShareBar(divID, story_id, isGif, hifiShareButtonsDisabled) { + var story_url = "https://highfidelity.com/user_stories/" + story_id; + var parentDiv = document.getElementById(divID); + parentDiv.setAttribute('data-story-id', story_id); + document.getElementById(divID).appendChild(createShareBar(divID, isGif, story_url, hifiShareButtonsDisabled)); + if (divID === "p0") { + selectImageToShare(divID, true); + } +} +function createShareBar(parentID, isGif, shareURL, hifiShareButtonsDisabled) { + var shareBar = document.createElement("div"); + shareBar.id = parentID + "shareBar"; + shareBar.className = "shareControls"; + var shareButtonsDivID = parentID + "shareButtonsDiv"; + var showShareButtonsButtonDivID = parentID + "showShareButtonsButtonDiv"; + var showShareButtonsButtonID = parentID + "showShareButtonsButton"; + var showShareButtonsLabelID = parentID + "showShareButtonsLabel"; + var blastToConnectionsButtonID = parentID + "blastToConnectionsButton"; + var shareWithEveryoneButtonID = parentID + "shareWithEveryoneButton"; + var facebookButtonID = parentID + "facebookButton"; + var twitterButtonID = parentID + "twitterButton"; + shareBar.innerHTML += '' + + '' + + '
    ' + + '' + + '' + + '
    ' + + '' + + '
    ' + + '
    '; + + // Add onclick handler to parent DIV's img to toggle share buttons + document.getElementById(parentID + 'img').onclick = function () { selectImageToShare(parentID, true) }; + + return shareBar; +} +function selectImageToShare(selectedID, isSelected) { + if (selectedID.id) { + selectedID = selectedID.id; // sometimes (?), `selectedID` is passed as an HTML object to these functions; we just want the ID + } + var imageContainer = document.getElementById(selectedID); + var image = document.getElementById(selectedID + 'img'); + var shareBar = document.getElementById(selectedID + "shareBar"); + var shareButtonsDiv = document.getElementById(selectedID + "shareButtonsDiv"); + var showShareButtonsButton = document.getElementById(selectedID + "showShareButtonsButton"); + + if (isSelected) { + showShareButtonsButton.onclick = function () { selectImageToShare(selectedID, false) }; + showShareButtonsButton.classList.remove("inactive"); + showShareButtonsButton.classList.add("active"); + + image.onclick = function () { selectImageToShare(selectedID, false) }; + imageContainer.style.outline = "4px solid #00b4ef"; + imageContainer.style.outlineOffset = "-4px"; + + shareBar.style.backgroundColor = "rgba(0, 0, 0, 0.5)"; + + shareButtonsDiv.style.visibility = "visible"; + } else { + showShareButtonsButton.onclick = function () { selectImageToShare(selectedID, true) }; + showShareButtonsButton.classList.remove("active"); + showShareButtonsButton.classList.add("inactive"); + + image.onclick = function () { selectImageToShare(selectedID, true) }; + imageContainer.style.outline = "none"; + + shareBar.style.backgroundColor = "rgba(0, 0, 0, 0.0)"; + + shareButtonsDiv.style.visibility = "hidden"; + } +} +function shareForUrl(selectedID) { + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "shareSnapshotForUrl", + data: paths[parseInt(selectedID.substring(1))] + })); +} +function blastToConnections(selectedID, isGif) { + selectedID = selectedID.id; // `selectedID` is passed as an HTML object to these functions; we just want the ID + + document.getElementById(selectedID + "blastToConnectionsButton").disabled = true; + document.getElementById(selectedID + "shareWithEveryoneButton").disabled = true; + + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "blastToConnections", + story_id: document.getElementById(selectedID).getAttribute("data-story-id"), + isGif: isGif + })); +} +function shareWithEveryone(selectedID, isGif) { + selectedID = selectedID.id; // `selectedID` is passed as an HTML object to these functions; we just want the ID + + document.getElementById(selectedID + "blastToConnectionsButton").disabled = true; + document.getElementById(selectedID + "shareWithEveryoneButton").disabled = true; + + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "shareSnapshotWithEveryone", + story_id: document.getElementById(selectedID).getAttribute("data-story-id"), + isGif: isGif + })); +} +function shareButtonClicked(selectedID) { + selectedID = selectedID.id; // `selectedID` is passed as an HTML object to these functions; we just want the ID + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "shareButtonClicked", + story_id: document.getElementById(selectedID).getAttribute("data-story-id") + })); +} +function cancelSharing(selectedID) { + selectedID = selectedID.id; // `selectedID` is passed as an HTML object to these functions; we just want the ID + var shareBar = document.getElementById(selectedID + "shareBar"); + + shareBar.style.display = "inline"; +} + +function handleCaptureSetting(setting) { + var stillAndGif = document.getElementById('stillAndGif'); + var stillOnly = document.getElementById('stillOnly'); + stillAndGif.checked = setting; + stillOnly.checked = !setting; + + stillAndGif.onclick = function () { EventBridge.emitWebEvent(JSON.stringify({ type: "snapshot", - action: (openFeed.checked ? "setOpenFeedTrue" : "setOpenFeedFalse") + action: "captureStillAndGif" })); - }; - - if (!messageOptions.canShare) { - // this means you may or may not be logged in, but can't share - // because you are not in a public place. - document.getElementById("sharing").innerHTML = "

    Snapshots can be shared when they're taken in shareable places."; } + stillOnly.onclick = function () { + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "captureStillOnly" + })); + } + } window.onload = function () { - // Something like the following will allow testing in a browser. - //addImage({localPath: 'c:/Users/howar/OneDrive/Pictures/hifi-snap-by--on-2016-07-27_12-58-43.jpg'}); - //addImage({ localPath: 'http://lorempixel.com/1512/1680' }); + // Uncomment the line below to test functionality in a browser. + // See definition of "testInBrowser()" to modify tests. + //testInBrowser(true); openEventBridge(function () { // Set up a handler for receiving the data, and tell the .js we are ready to receive it. EventBridge.scriptEventReceived.connect(function (message) { + message = JSON.parse(message); + if (message.type !== "snapshot") { return; } + + switch (message.action) { + case 'showSetupInstructions': + showSetupInstructions(); + break; + case 'snapshotLocationChosen': + clearImages(); + showSetupComplete(); + break; + case 'clearPreviousImages': + clearImages(); + break; + case 'showPreviousImages': + clearImages(); + var messageOptions = message.options; + imageCount = message.image_data.length; + message.image_data.forEach(function (element, idx, array) { + addImage(element, true, true, message.canShare, message.image_data[idx].buttonDisabled); + }); + break; + case 'addImages': + // The last element of the message contents list contains a bunch of options, + // including whether or not we can share stuff + // The other elements of the list contain image paths. + var messageOptions = message.options; - // The last element of the message contents list contains a bunch of options, - // including whether or not we can share stuff - // The other elements of the list contain image paths. - var messageOptions = message.action.pop(); - handleShareButtons(messageOptions); + if (messageOptions.containsGif) { + if (messageOptions.processingGif) { + imageCount = message.image_data.length + 1; // "+1" for the GIF that'll finish processing soon + message.image_data.unshift({ localPath: messageOptions.loadingGifPath }); + message.image_data.forEach(function (element, idx, array) { + addImage(element, idx === 0, false, false); + }); + } else { + var gifPath = message.image_data[0].localPath; + var p0img = document.getElementById('p0img'); + p0img.src = gifPath; - if (messageOptions.containsGif) { - if (messageOptions.processingGif) { - imageCount = message.action.length + 1; // "+1" for the GIF that'll finish processing soon - message.action.unshift({ localPath: messageOptions.loadingGifPath }); - message.action.forEach(addImage); - document.getElementById('p0').disabled = true; - } else { - var gifPath = message.action[0].localPath; - document.getElementById('p0').disabled = false; - document.getElementById('p0img').src = gifPath; - paths[0].localPath = gifPath; - } - } else { - imageCount = message.action.length; - message.action.forEach(addImage); + paths[0] = gifPath; + shareForUrl("p0"); + } + } else { + imageCount = message.image_data.length; + message.image_data.forEach(function (element, idx, array) { + addImage(element, false, false, false); + }); + } + break; + case 'captureSettings': + handleCaptureSetting(message.setting); + break; + case 'snapshotUploadComplete': + var isGif = message.image_url.split('.').pop().toLowerCase() === "gif"; + appendShareBar(isGif || imageCount === 1 ? "p0" : "p1", message.story_id, isGif); + break; + default: + console.log("Unknown message action received in SnapshotReview.js."); + break; } }); + EventBridge.emitWebEvent(JSON.stringify({ type: "snapshot", action: "ready" })); - }); - + });; }; -// beware of bug: Cannot send objects at top level. (Nested in arrays is fine.) -function shareSelected() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "snapshot", - action: paths - })); -} -function doNotShare() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "snapshot", - action: [] - })); -} function snapshotSettings() { EventBridge.emitWebEvent(JSON.stringify({ type: "snapshot", action: "openSettings" })); } +function takeSnapshot() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "takeSnapshot" + })); +} + +function testInBrowser(isTestingSetupInstructions) { + if (isTestingSetupInstructions) { + showSetupInstructions(); + } else { + imageCount = 1; + //addImage({ localPath: 'http://lorempixel.com/553/255' }); + addImage({ localPath: 'C:/Users/valef/Desktop/hifi-snap-by-zfox-on-2017-04-26_10-26-53.gif' }, false, true, true, false); + } +} diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index c6692fc26e..ea79750154 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -17,6 +17,7 @@ const DESCENDING_STRING = '▾'; const LOCKED_GLYPH = ""; const VISIBLE_GLYPH = ""; const TRANSPARENCY_GLYPH = ""; +const BAKED_GLYPH = "" const SCRIPT_GLYPH = "k"; const DELETE = 46; // Key code for the delete key. const KEY_P = 80; // Key code for letter p used for Parenting hotkey. @@ -77,6 +78,9 @@ function loaded() { document.getElementById("entity-hasTransparent").onclick = function () { setSortColumn('hasTransparent'); }; + document.getElementById("entity-isBaked").onclick = function () { + setSortColumn('isBaked'); + }; document.getElementById("entity-drawCalls").onclick = function () { setSortColumn('drawCalls'); }; @@ -147,7 +151,7 @@ function loaded() { } function addEntity(id, name, type, url, locked, visible, verticesCount, texturesCount, texturesSize, hasTransparent, - drawCalls, hasScript) { + isBaked, drawCalls, hasScript) { var urlParts = url.split('/'); var filename = urlParts[urlParts.length - 1]; @@ -157,7 +161,7 @@ function loaded() { id: id, name: name, type: type, url: filename, locked: locked, visible: visible, verticesCount: displayIfNonZero(verticesCount), texturesCount: displayIfNonZero(texturesCount), texturesSize: decimalMegabytes(texturesSize), hasTransparent: hasTransparent, - drawCalls: displayIfNonZero(drawCalls), hasScript: hasScript + isBaked: isBaked, drawCalls: displayIfNonZero(drawCalls), hasScript: hasScript }], function (items) { var currentElement = items[0].elm; @@ -201,6 +205,7 @@ function loaded() { texturesCount: document.querySelector('#entity-texturesCount .sort-order'), texturesSize: document.querySelector('#entity-texturesSize .sort-order'), hasTransparent: document.querySelector('#entity-hasTransparent .sort-order'), + isBaked: document.querySelector('#entity-isBaked .sort-order'), drawCalls: document.querySelector('#entity-drawCalls .sort-order'), hasScript: document.querySelector('#entity-hasScript .sort-order'), } @@ -350,6 +355,7 @@ function loaded() { newEntities[i].visible ? VISIBLE_GLYPH : null, newEntities[i].verticesCount, newEntities[i].texturesCount, newEntities[i].texturesSize, newEntities[i].hasTransparent ? TRANSPARENCY_GLYPH : null, + newEntities[i].isBaked ? BAKED_GLYPH : null, newEntities[i].drawCalls, newEntities[i].hasScript ? SCRIPT_GLYPH : null); } diff --git a/scripts/system/html/js/record.js b/scripts/system/html/js/record.js new file mode 100644 index 0000000000..c78500307d --- /dev/null +++ b/scripts/system/html/js/record.js @@ -0,0 +1,298 @@ +"use strict"; + +// +// record.js +// +// Created by David Rowe on 5 Apr 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 +// + +var isUsingToolbar = false, + isDisplayingInstructions = false, + isRecording = false, + numberOfPlayers = 0, + recordingsBeingPlayed = [], + elRecordings, + elRecordingsTable, + elRecordingsList, + elInstructions, + elPlayersUnused, + elHideInfoButton, + elShowInfoButton, + elLoadButton, + elSpinner, + elCountdownNumber, + elRecordButton, + elFinishOnOpen, + elFinishOnOpenLabel, + EVENT_BRIDGE_TYPE = "record", + BODY_LOADED_ACTION = "bodyLoaded", + USING_TOOLBAR_ACTION = "usingToolbar", + RECORDINGS_BEING_PLAYED_ACTION = "recordingsBeingPlayed", + NUMBER_OF_PLAYERS_ACTION = "numberOfPlayers", + STOP_PLAYING_RECORDING_ACTION = "stopPlayingRecording", + LOAD_RECORDING_ACTION = "loadRecording", + START_RECORDING_ACTION = "startRecording", + SET_COUNTDOWN_NUMBER_ACTION = "setCountdownNumber", + STOP_RECORDING_ACTION = "stopRecording", + FINISH_ON_OPEN_ACTION = "finishOnOpen"; + +function stopPlayingRecording(event) { + var playerID = event.target.getAttribute("playerID"); + EventBridge.emitWebEvent(JSON.stringify({ + type: EVENT_BRIDGE_TYPE, + action: STOP_PLAYING_RECORDING_ACTION, + value: playerID + })); +} + +function updatePlayersUnused() { + elPlayersUnused.innerHTML = numberOfPlayers - recordingsBeingPlayed.length; +} + +function orderRecording(a, b) { + return a.filename > b.filename ? 1 : -1; +} + +function updateRecordings() { + var tbody, + tr, + td, + input, + ths, + tds, + length, + i, + HIFI_GLYPH_CLOSE = "w"; + + recordingsBeingPlayed.sort(orderRecording); + + tbody = document.createElement("tbody"); + tbody.id = "recordings-list"; + + + // Filename + for (i = 0, length = recordingsBeingPlayed.length; i < length; i += 1) { + tr = document.createElement("tr"); + td = document.createElement("td"); + td.innerHTML = recordingsBeingPlayed[i].filename.slice(4); + tr.appendChild(td); + td = document.createElement("td"); + input = document.createElement("input"); + input.setAttribute("type", "button"); + input.setAttribute("class", "glyph red"); + input.setAttribute("value", HIFI_GLYPH_CLOSE); + input.setAttribute("playerID", recordingsBeingPlayed[i].playerID); + input.addEventListener("click", stopPlayingRecording); + td.appendChild(input); + tr.appendChild(td); + tbody.appendChild(tr); + } + + // Empty rows representing available players. + for (i = recordingsBeingPlayed.length, length = numberOfPlayers; i < length; i += 1) { + tr = document.createElement("tr"); + td = document.createElement("td"); + td.colSpan = 2; + tr.appendChild(td); + tbody.appendChild(tr); + } + + // Filler row for extra table space. + tr = document.createElement("tr"); + tr.classList.add("filler"); + td = document.createElement("td"); + td.colSpan = 2; + tr.appendChild(td); + tbody.appendChild(tr); + + // Update table content. + elRecordingsTable.replaceChild(tbody, elRecordingsList); + elRecordingsList = document.getElementById("recordings-list"); + + // Update header cell widths to match content widths. + ths = document.querySelectorAll("#recordings-table thead th"); + tds = document.querySelectorAll("#recordings-table tbody tr:first-child td"); + for (i = 0; i < ths.length; i += 1) { + ths[i].width = tds[i].offsetWidth; + } +} + +function updateInstructions() { + // Display show/hide instructions buttons if players are available. + if (numberOfPlayers === 0) { + elHideInfoButton.classList.add("hidden"); + elShowInfoButton.classList.add("hidden"); + } else { + elHideInfoButton.classList.remove("hidden"); + elShowInfoButton.classList.remove("hidden"); + } + + // Display instructions if user requested or no players available. + if (isDisplayingInstructions || numberOfPlayers === 0) { + elRecordingsList.classList.add("hidden"); + elInstructions.classList.remove("hidden"); + } else { + elInstructions.classList.add("hidden"); + elRecordingsList.classList.remove("hidden"); + } +} + +function showInstructions() { + isDisplayingInstructions = true; + updateInstructions(); +} + +function hideInstructions() { + isDisplayingInstructions = false; + updateInstructions(); +} + +function updateLoadButton() { + if (isRecording || numberOfPlayers <= recordingsBeingPlayed.length) { + elLoadButton.setAttribute("disabled", "disabled"); + } else { + elLoadButton.removeAttribute("disabled"); + } +} + +function updateSpinner() { + if (isRecording) { + elRecordings.classList.add("hidden"); + elSpinner.classList.remove("hidden"); + } else { + elSpinner.classList.add("hidden"); + elRecordings.classList.remove("hidden"); + } +} + +function updateFinishOnOpenLabel() { + var WINDOW_FINISH_ON_OPEN_LABEL = "Stop recording automatically when reopen this window", + TABLET_FINISH_ON_OPEN_LABEL = "Stop recording automatically when reopen tablet or window"; + + elFinishOnOpenLabel.innerHTML = isUsingToolbar ? WINDOW_FINISH_ON_OPEN_LABEL : TABLET_FINISH_ON_OPEN_LABEL; +} + +function onScriptEventReceived(data) { + var message = JSON.parse(data); + if (message.type === EVENT_BRIDGE_TYPE) { + switch (message.action) { + case USING_TOOLBAR_ACTION: + isUsingToolbar = message.value; + updateFinishOnOpenLabel(); + break; + case FINISH_ON_OPEN_ACTION: + elFinishOnOpen.checked = message.value; + break; + case START_RECORDING_ACTION: + isRecording = true; + elRecordButton.value = "Stop"; + updateSpinner(); + updateLoadButton(); + break; + case SET_COUNTDOWN_NUMBER_ACTION: + elCountdownNumber.innerHTML = message.value; + break; + case STOP_RECORDING_ACTION: + isRecording = false; + elRecordButton.value = "Record"; + updateSpinner(); + updateLoadButton(); + break; + case RECORDINGS_BEING_PLAYED_ACTION: + recordingsBeingPlayed = JSON.parse(message.value); + updateRecordings(); + updatePlayersUnused(); + updateInstructions(); + updateLoadButton(); + break; + case NUMBER_OF_PLAYERS_ACTION: + numberOfPlayers = message.value; + updateRecordings(); + updatePlayersUnused(); + updateInstructions(); + updateLoadButton(); + break; + } + } +} + +function onLoadButtonClicked() { + EventBridge.emitWebEvent(JSON.stringify({ + type: EVENT_BRIDGE_TYPE, + action: LOAD_RECORDING_ACTION + })); +} + +function onRecordButtonClicked() { + if (!isRecording) { + elRecordButton.value = "Stop"; + EventBridge.emitWebEvent(JSON.stringify({ + type: EVENT_BRIDGE_TYPE, + action: START_RECORDING_ACTION + })); + isRecording = true; + updateSpinner(); + updateLoadButton(); + } else { + elRecordButton.value = "Record"; + EventBridge.emitWebEvent(JSON.stringify({ + type: EVENT_BRIDGE_TYPE, + action: STOP_RECORDING_ACTION + })); + isRecording = false; + updateSpinner(); + updateLoadButton(); + } +} + +function onFinishOnOpenClicked() { + EventBridge.emitWebEvent(JSON.stringify({ + type: EVENT_BRIDGE_TYPE, + action: FINISH_ON_OPEN_ACTION, + value: elFinishOnOpen.checked + })); +} + +function signalBodyLoaded() { + EventBridge.emitWebEvent(JSON.stringify({ + type: EVENT_BRIDGE_TYPE, + action: BODY_LOADED_ACTION + })); +} + +function onBodyLoaded() { + + EventBridge.scriptEventReceived.connect(onScriptEventReceived); + + elRecordings = document.getElementById("recordings"); + + elRecordingsTable = document.getElementById("recordings-table"); + elRecordingsList = document.getElementById("recordings-list"); + elInstructions = document.getElementById("instructions"); + elPlayersUnused = document.getElementById("players-unused"); + + elHideInfoButton = document.getElementById("hide-info-button"); + elHideInfoButton.onclick = hideInstructions; + elShowInfoButton = document.getElementById("show-info-button"); + elShowInfoButton.onclick = showInstructions; + + elLoadButton = document.getElementById("load-button"); + elLoadButton.onclick = onLoadButtonClicked; + + elSpinner = document.getElementById("spinner"); + elCountdownNumber = document.getElementById("countdown-number"); + + elRecordButton = document.getElementById("record-button"); + elRecordButton.onclick = onRecordButtonClicked; + + elFinishOnOpen = document.getElementById("finish-on-open"); + elFinishOnOpen.onclick = onFinishOnOpenClicked; + + elFinishOnOpenLabel = document.getElementById("finish-on-open-label"); + + signalBodyLoaded(); +} diff --git a/scripts/system/html/record.html b/scripts/system/html/record.html new file mode 100644 index 0000000000..89392e6951 --- /dev/null +++ b/scripts/system/html/record.html @@ -0,0 +1,87 @@ + + + + + Record + + + + + +

    + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + +
    Recordings Being PlayedUnload
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + + +
    +
    + + + diff --git a/scripts/system/libraries/entityIconOverlayManager.js b/scripts/system/libraries/entityIconOverlayManager.js index 7f7a293bc3..f557a05f60 100644 --- a/scripts/system/libraries/entityIconOverlayManager.js +++ b/scripts/system/libraries/entityIconOverlayManager.js @@ -32,20 +32,25 @@ EntityIconOverlayManager = function(entityTypes, getOverlayPropertiesFunc) { } }; + // Finds the id for the corresponding entity that is associated with an overlay id. + // Returns null if the overlay id is not contained in this manager. + this.findEntity = function(overlayId) { + for (var id in entityOverlays) { + if (overlayId === entityOverlays[id]) { + return entityIDs[id]; + } + } + + return null; + }; + this.findRayIntersection = function(pickRay) { var result = Overlays.findRayIntersection(pickRay); - var found = false; if (result.intersects) { - for (var id in entityOverlays) { - if (result.overlayID === entityOverlays[id]) { - result.entityID = entityIDs[id]; - found = true; - break; - } - } + result.entityID = this.findEntity(result.overlayID); - if (!found) { + if (result.entityID === null) { result.intersects = false; } } diff --git a/scripts/system/libraries/entityList.js b/scripts/system/libraries/entityList.js index 3488733289..3b6d32ec1c 100644 --- a/scripts/system/libraries/entityList.js +++ b/scripts/system/libraries/entityList.js @@ -88,6 +88,7 @@ EntityListTool = function(opts) { texturesCount: valueIfDefined(properties.renderInfo.texturesCount), texturesSize: valueIfDefined(properties.renderInfo.texturesSize), hasTransparent: valueIfDefined(properties.renderInfo.hasTransparent), + isBaked: properties.type == "Model" ? properties.modelURL.toLowerCase().endsWith(".baked.fbx") : false, drawCalls: valueIfDefined(properties.renderInfo.drawCalls), hasScript: properties.script !== "" }); diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index 674da2d677..47082e882f 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -1,6 +1,8 @@ "use strict"; +/*jslint vars:true, plusplus:true, forin:true*/ +/*global Window, Script, Controller, MyAvatar, AvatarList, Entities, Messages, Audio, SoundCache, Account, UserActivityLogger, Vec3, Quat, XMLHttpRequest, location, print*/ // -// makeUserConnetion.js +// makeUserConnection.js // scripts/system // // Created by David Kelly on 3/7/2017. @@ -9,328 +11,349 @@ // 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 () { // BEGIN LOCAL_SCOPE -const label = "makeUserConnection"; -const MAX_AVATAR_DISTANCE = 0.2; // m -const GRIP_MIN = 0.05; // goes from 0-1, so 5% pressed is pressed -const MESSAGE_CHANNEL = "io.highfidelity.makeUserConnection"; -const STATES = { - inactive : 0, - waiting: 1, - connecting: 2, - makingConnection: 3 -}; -const STATE_STRINGS = ["inactive", "waiting", "connecting", "makingConnection"]; -const WAITING_INTERVAL = 100; // ms -const CONNECTING_INTERVAL = 100; // ms -const MAKING_CONNECTION_TIMEOUT = 800; // ms -const CONNECTING_TIME = 1600; // ms -const PARTICLE_RADIUS = 0.15; // m -const PARTICLE_ANGLE_INCREMENT = 360/45; // 1hz -const HANDSHAKE_SOUND_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/davidkelly/production/audio/4beat_sweep.wav"; -const SUCCESSFUL_HANDSHAKE_SOUND_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/davidkelly/production/audio/3rdbeat_success_bell.wav"; -const HAPTIC_DATA = { - initial: { duration: 20, strength: 0.6}, // duration is in ms - background: { duration: 100, strength: 0.3 }, // duration is in ms - success: { duration: 60, strength: 1.0} // duration is in ms -}; -const PARTICLE_EFFECT_PROPS = { - "alpha": 0.8, - "azimuthFinish": Math.PI, - "azimuthStart": -1*Math.PI, - "emitRate": 500, - "emitSpeed": 0.0, - "emitterShouldTrail": 1, - "isEmitting": 1, - "lifespan": 3, - "maxParticles": 1000, - "particleRadius": 0.003, - "polarStart": 1, - "polarFinish": 1, - "radiusFinish": 0.008, - "radiusStart": 0.0025, - "speedSpread": 0.025, - "textures": "http://hifi-content.s3.amazonaws.com/alan/dev/Particles/Bokeh-Particle.png", - "color": {"red": 255, "green": 255, "blue": 255}, - "colorFinish": {"red": 0, "green": 164, "blue": 255}, - "colorStart": {"red": 255, "green": 255, "blue": 255}, - "emitOrientation": {"w": -0.71, "x":0.0, "y":0.0, "z": 0.71}, - "emitAcceleration": {"x": 0.0, "y": 0.0, "z": 0.0}, - "accelerationSpread": {"x": 0.0, "y": 0.0, "z": 0.0}, - "dimensions": {"x":0.05, "y": 0.05, "z": 0.05}, - "type": "ParticleEffect" -}; -const MAKING_CONNECTION_PARTICLE_PROPS = { - "alpha": 0.07, - "alphaStart":0.011, - "alphaSpread": 0, - "alphaFinish": 0, - "azimuthFinish": Math.PI, - "azimuthStart": -1*Math.PI, - "emitRate": 2000, - "emitSpeed": 0.0, - "emitterShouldTrail": 1, - "isEmitting": 1, - "lifespan": 3.6, - "maxParticles": 4000, - "particleRadius": 0.048, - "polarStart": 0, - "polarFinish": 1, - "radiusFinish": 0.3, - "radiusStart": 0.04, - "speedSpread": 0.01, - "radiusSpread": 0.9, - "textures": "http://hifi-content.s3.amazonaws.com/alan/dev/Particles/Bokeh-Particle.png", - "color": {"red": 200, "green": 170, "blue": 255}, - "colorFinish": {"red": 0, "green": 134, "blue": 255}, - "colorStart": {"red": 185, "green": 222, "blue": 255}, - "emitOrientation": {"w": -0.71, "x":0.0, "y":0.0, "z": 0.71}, - "emitAcceleration": {"x": 0.0, "y": 0.0, "z": 0.0}, - "accelerationSpread": {"x": 0.0, "y": 0.0, "z": 0.0}, - "dimensions": {"x":0.05, "y": 0.05, "z": 0.05}, - "type": "ParticleEffect" -}; + var LABEL = "makeUserConnection"; + var MAX_AVATAR_DISTANCE = 0.2; // m + var GRIP_MIN = 0.75; // goes from 0-1, so 75% pressed is pressed + var MESSAGE_CHANNEL = "io.highfidelity.makeUserConnection"; + var STATES = { + INACTIVE: 0, + WAITING: 1, + CONNECTING: 2, + MAKING_CONNECTION: 3 + }; + var STATE_STRINGS = ["inactive", "waiting", "connecting", "makingConnection"]; + var HAND_STRING_PROPERTY = 'hand'; // Used in our message protocol. IWBNI we changed it to handString, but that would break compatability. + var WAITING_INTERVAL = 100; // ms + var CONNECTING_INTERVAL = 100; // ms + var MAKING_CONNECTION_TIMEOUT = 800; // ms + var CONNECTING_TIME = 1600; // ms + var PARTICLE_RADIUS = 0.15; // m + var PARTICLE_ANGLE_INCREMENT = 360 / 45; // 1hz + var HANDSHAKE_SOUND_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/davidkelly/production/audio/4beat_sweep.wav"; + var SUCCESSFUL_HANDSHAKE_SOUND_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/davidkelly/production/audio/3rdbeat_success_bell.wav"; + var PREFERRER_HAND_JOINT_POSTFIX_ORDER = ['Middle1', 'Index1', '']; + var HAPTIC_DATA = { + initial: { duration: 20, strength: 0.6 }, // duration is in ms + background: { duration: 100, strength: 0.3 }, // duration is in ms + success: { duration: 60, strength: 1.0 } // duration is in ms + }; + var PARTICLE_EFFECT_PROPS = { + "alpha": 0.8, + "azimuthFinish": Math.PI, + "azimuthStart": -1 * Math.PI, + "emitRate": 500, + "emitSpeed": 0.0, + "emitterShouldTrail": 1, + "isEmitting": 1, + "lifespan": 3, + "maxParticles": 1000, + "particleRadius": 0.003, + "polarStart": 1, + "polarFinish": 1, + "radiusFinish": 0.008, + "radiusStart": 0.0025, + "speedSpread": 0.025, + "textures": "http://hifi-content.s3.amazonaws.com/alan/dev/Particles/Bokeh-Particle.png", + "color": {"red": 255, "green": 255, "blue": 255}, + "colorFinish": {"red": 0, "green": 164, "blue": 255}, + "colorStart": {"red": 255, "green": 255, "blue": 255}, + "emitOrientation": {"w": -0.71, "x": 0.0, "y": 0.0, "z": 0.71}, + "emitAcceleration": {"x": 0.0, "y": 0.0, "z": 0.0}, + "accelerationSpread": {"x": 0.0, "y": 0.0, "z": 0.0}, + "dimensions": {"x": 0.05, "y": 0.05, "z": 0.05}, + "type": "ParticleEffect" + }; + var MAKING_CONNECTION_PARTICLE_PROPS = { + "alpha": 0.07, + "alphaStart": 0.011, + "alphaSpread": 0, + "alphaFinish": 0, + "azimuthFinish": Math.PI, + "azimuthStart": -1 * Math.PI, + "emitRate": 2000, + "emitSpeed": 0.0, + "emitterShouldTrail": 1, + "isEmitting": 1, + "lifespan": 3.6, + "maxParticles": 4000, + "particleRadius": 0.048, + "polarStart": 0, + "polarFinish": 1, + "radiusFinish": 0.3, + "radiusStart": 0.04, + "speedSpread": 0.01, + "radiusSpread": 0.9, + "textures": "http://hifi-content.s3.amazonaws.com/alan/dev/Particles/Bokeh-Particle.png", + "color": {"red": 200, "green": 170, "blue": 255}, + "colorFinish": {"red": 0, "green": 134, "blue": 255}, + "colorStart": {"red": 185, "green": 222, "blue": 255}, + "emitOrientation": {"w": -0.71, "x": 0.0, "y": 0.0, "z": 0.71}, + "emitAcceleration": {"x": 0.0, "y": 0.0, "z": 0.0}, + "accelerationSpread": {"x": 0.0, "y": 0.0, "z": 0.0}, + "dimensions": {"x": 0.05, "y": 0.05, "z": 0.05}, + "type": "ParticleEffect" + }; -var currentHand; -var state = STATES.inactive; -var connectingInterval; -var waitingInterval; -var makingConnectionTimeout; -var animHandlerId; -var connectingId; -var connectingHand; -var waitingList = {}; -var particleEffect; -var waitingBallScale; -var particleRotationAngle = 0.0; -var makingConnectionParticleEffect; -var makingConnectionEmitRate = 2000; -var particleEmitRate = 500; -var handshakeInjector; -var successfulHandshakeInjector; -var handshakeSound; -var successfulHandshakeSound; + var currentHand; + var currentHandJointIndex = -1; + var state = STATES.INACTIVE; + var connectingInterval; + var waitingInterval; + var makingConnectionTimeout; + var animHandlerId; + var connectingId; + var connectingHandJointIndex = -1; + var waitingList = {}; + var particleEffect; + var particleRotationAngle = 0.0; + var makingConnectionParticleEffect; + var makingConnectionEmitRate = 2000; + var particleEmitRate = 500; + var handshakeInjector; + var successfulHandshakeInjector; + var handshakeSound; + var successfulHandshakeSound; -function debug() { - var stateString = "<" + STATE_STRINGS[state] + ">"; - var connecting = "[" + connectingId + "/" + connectingHand + "]"; - print.apply(null, [].concat.apply([label, stateString, JSON.stringify(waitingList), connecting], [].map.call(arguments, JSON.stringify))); -} + function debug() { + var stateString = "<" + STATE_STRINGS[state] + ">"; + var connecting = "[" + connectingId + "/" + connectingHandJointIndex + "]"; + print.apply(null, [].concat.apply([LABEL, stateString, JSON.stringify(waitingList), connecting], + [].map.call(arguments, JSON.stringify))); + } -function cleanId(guidWithCurlyBraces) { - return guidWithCurlyBraces.slice(1, -1); -} -function request(options, callback) { // cb(error, responseOfCorrectContentType) of url. A subset of npm request. - var httpRequest = new XMLHttpRequest(), key; - // QT bug: apparently doesn't handle onload. Workaround using readyState. - httpRequest.onreadystatechange = function () { - var READY_STATE_DONE = 4; - var HTTP_OK = 200; - if (httpRequest.readyState >= READY_STATE_DONE) { - var error = (httpRequest.status !== HTTP_OK) && httpRequest.status.toString() + ':' + httpRequest.statusText, - response = !error && httpRequest.responseText, - contentType = !error && httpRequest.getResponseHeader('content-type'); - if (!error && contentType.indexOf('application/json') === 0) { // ignoring charset, etc. - try { - response = JSON.parse(response); - } catch (e) { - error = e; + function cleanId(guidWithCurlyBraces) { + return guidWithCurlyBraces.slice(1, -1); + } + function request(options, callback) { // cb(error, responseOfCorrectContentType) of url. A subset of npm request. + var httpRequest = new XMLHttpRequest(), key; + // QT bug: apparently doesn't handle onload. Workaround using readyState. + httpRequest.onreadystatechange = function () { + var READY_STATE_DONE = 4; + var HTTP_OK = 200; + if (httpRequest.readyState >= READY_STATE_DONE) { + var error = (httpRequest.status !== HTTP_OK) && httpRequest.status.toString() + ':' + httpRequest.statusText, + response = !error && httpRequest.responseText, + contentType = !error && httpRequest.getResponseHeader('content-type'); + if (!error && contentType.indexOf('application/json') === 0) { // ignoring charset, etc. + try { + response = JSON.parse(response); + } catch (e) { + error = e; + } + } + if (error) { + response = {statusCode: httpRequest.status}; + } + callback(error, response); + } + }; + if (typeof options === 'string') { + options = {uri: options}; + } + if (options.url) { + options.uri = options.url; + } + if (!options.method) { + options.method = 'GET'; + } + if (options.body && (options.method === 'GET')) { // add query parameters + var params = [], appender = (-1 === options.uri.search('?')) ? '?' : '&'; + for (key in options.body) { + if (options.body.hasOwnProperty(key)) { + params.push(key + '=' + options.body[key]); } } - if (error) { - response = {statusCode: httpRequest.status}; + options.uri += appender + params.join('&'); + delete options.body; + } + if (options.json) { + options.headers = options.headers || {}; + options.headers["Content-type"] = "application/json"; + options.body = JSON.stringify(options.body); + } + for (key in options.headers || {}) { + if (options.headers.hasOwnProperty(key)) { + httpRequest.setRequestHeader(key, options.headers[key]); } - callback(error, response); } - }; - if (typeof options === 'string') { - options = {uri: options}; + httpRequest.open(options.method, options.uri, true); + httpRequest.send(options.body); } - if (options.url) { - options.uri = options.url; - } - if (!options.method) { - options.method = 'GET'; - } - if (options.body && (options.method === 'GET')) { // add query parameters - var params = [], appender = (-1 === options.uri.search('?')) ? '?' : '&'; - for (key in options.body) { - params.push(key + '=' + options.body[key]); + + function handToString(hand) { + if (hand === Controller.Standard.RightHand) { + return "RightHand"; } - options.uri += appender + params.join('&'); - delete options.body; - } - if (options.json) { - options.headers = options.headers || {}; - options.headers["Content-type"] = "application/json"; - options.body = JSON.stringify(options.body); - } - for (key in options.headers || {}) { - httpRequest.setRequestHeader(key, options.headers[key]); - } - httpRequest.open(options.method, options.uri, true); - httpRequest.send(options.body); -} - -function handToString(hand) { - if (hand === Controller.Standard.RightHand) { - return "RightHand"; - } else if (hand === Controller.Standard.LeftHand) { - return "LeftHand"; - } - debug("handToString called without valid hand!"); - return ""; -} - -function stringToHand(hand) { - if (hand == "RightHand") { - return Controller.Standard.RightHand; - } else if (hand == "LeftHand") { - return Controller.Standard.LeftHand; - } - debug("stringToHand called with bad hand string:", hand); - return 0; -} - -function handToHaptic(hand) { - if (hand === Controller.Standard.RightHand) { - return 1; - } else if (hand === Controller.Standard.LeftHand) { - return 0; - } - debug("handToHaptic called without a valid hand!"); - return -1; -} - -function stopWaiting() { - if (waitingInterval) { - waitingInterval = Script.clearInterval(waitingInterval); - } -} - -function stopConnecting() { - if (connectingInterval) { - connectingInterval = Script.clearInterval(connectingInterval); - } -} - -function stopMakingConnection() { - if (makingConnectionTimeout) { - makingConnectionTimeout = Script.clearTimeout(makingConnectionTimeout); - } -} - -// This returns the position of the palm, really. Which relies on the avatar -// having the expected middle1 joint. TODO: fallback for when this isn't part -// of the avatar? -function getHandPosition(avatar, hand) { - if (!hand) { - debug("calling getHandPosition with no hand! (returning avatar position but this is a BUG)"); - debug(new Error().stack); - return avatar.position; - } - var jointName = handToString(hand) + "Middle1"; - return avatar.getJointPosition(avatar.getJointIndex(jointName)); -} - -function shakeHandsAnimation(animationProperties) { - // all we are doing here is moving the right hand to a spot - // that is in front of and a bit above the hips. Basing how - // far in front as scaling with the avatar's height (say hips - // to head distance) - var headIndex = MyAvatar.getJointIndex("Head"); - var offset = 0.5; // default distance of hand in front of you - var result = {}; - if (headIndex) { - offset = 0.8 * MyAvatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y; - } - var handPos = Vec3.multiply(offset, {x: -0.25, y: 0.8, z: 1.3}); - result.rightHandPosition = handPos; - result.rightHandRotation = Quat.fromPitchYawRollDegrees(90, 0, 90); - return result; -} - -function positionFractionallyTowards(posA, posB, frac) { - return Vec3.sum(posA, Vec3.multiply(frac, Vec3.subtract(posB, posA))); -} - -function deleteParticleEffect() { - if (particleEffect) { - particleEffect = Entities.deleteEntity(particleEffect); - } -} - -function deleteMakeConnectionParticleEffect() { - if (makingConnectionParticleEffect) { - makingConnectionParticleEffect = Entities.deleteEntity(makingConnectionParticleEffect); - } -} - -function stopHandshakeSound() { - if (handshakeInjector) { - handshakeInjector.stop(); - handshakeInjector = null; - } -} - -function calcParticlePos(myHand, otherHand, otherOrientation, reset) { - if (reset) { - particleRotationAngle = 0.0; - } - var position = positionFractionallyTowards(myHand, otherHand, 0.5); - particleRotationAngle += PARTICLE_ANGLE_INCREMENT; // about 0.5 hz - var radius = Math.min(PARTICLE_RADIUS, PARTICLE_RADIUS * particleRotationAngle / 360); - var axis = Vec3.mix(Quat.getFront(MyAvatar.orientation), Quat.inverse(Quat.getFront(otherOrientation)), 0.5); - return Vec3.sum(position, Vec3.multiplyQbyV(Quat.angleAxis(particleRotationAngle, axis), {x: 0, y: radius, z: 0})); -} - -// this is called frequently, but usually does nothing -function updateVisualization() { - if (state == STATES.inactive) { - deleteParticleEffect(); - deleteMakeConnectionParticleEffect(); - // this should always be true if inactive, but just in case: - currentHand = undefined; - return; + if (hand === Controller.Standard.LeftHand) { + return "LeftHand"; + } + debug("handToString called without valid hand! value: ", hand); + return ""; } - var myHandPosition = getHandPosition(MyAvatar, currentHand); - var otherHand; - var otherOrientation; - if (connectingId) { - var other = AvatarList.getAvatar(connectingId); - if (other) { - otherOrientation = other.orientation; - otherHand = getHandPosition(other, stringToHand(connectingHand)); + function handToHaptic(hand) { + if (hand === Controller.Standard.RightHand) { + return 1; + } + if (hand === Controller.Standard.LeftHand) { + return 0; + } + debug("handToHaptic called without a valid hand!"); + return -1; + } + + function stopWaiting() { + if (waitingInterval) { + waitingInterval = Script.clearInterval(waitingInterval); } } - var wrist = MyAvatar.getJointPosition(MyAvatar.getJointIndex(handToString(currentHand))); - var d = Math.min(MAX_AVATAR_DISTANCE, Vec3.distance(wrist, myHandPosition)); - switch (state) { - case STATES.waiting: + function stopConnecting() { + if (connectingInterval) { + connectingInterval = Script.clearInterval(connectingInterval); + } + } + + function stopMakingConnection() { + if (makingConnectionTimeout) { + makingConnectionTimeout = Script.clearTimeout(makingConnectionTimeout); + } + } + + // This returns the ideal hand joint index for the avatar. + // [handString]middle1 -> [handString]index1 -> [handString] + function getIdealHandJointIndex(avatar, handString) { + debug("get hand " + handString + " for avatar " + (avatar && avatar.sessionUUID)); + var suffixIndex, jointName, jointIndex; + for (suffixIndex = 0; suffixIndex < (avatar ? PREFERRER_HAND_JOINT_POSTFIX_ORDER.length : 0); suffixIndex++) { + jointName = handString + PREFERRER_HAND_JOINT_POSTFIX_ORDER[suffixIndex]; + jointIndex = avatar.getJointIndex(jointName); + if (jointIndex !== -1) { + debug('found joint ' + jointName + ' (' + jointIndex + ')'); + return jointIndex; + } + } + debug('no hand joint found.'); + return -1; + } + + // This returns the preferred hand position. + function getHandPosition(avatar, handJointIndex) { + if (handJointIndex === -1) { + debug("calling getHandPosition with no hand joint index! (returning avatar position but this is a BUG)"); + return avatar.position; + } + return avatar.getJointPosition(handJointIndex); + } + + var animationData = {}; + function updateAnimationData() { + // all we are doing here is moving the right hand to a spot + // that is in front of and a bit above the hips. Basing how + // far in front as scaling with the avatar's height (say hips + // to head distance) + var headIndex = MyAvatar.getJointIndex("Head"); + var offset = 0.5; // default distance of hand in front of you + if (headIndex) { + offset = 0.8 * MyAvatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y; + } + animationData.rightHandPosition = Vec3.multiply(offset, {x: -0.25, y: 0.8, z: 1.3}); + animationData.rightHandRotation = Quat.fromPitchYawRollDegrees(90, 0, 90); + } + function shakeHandsAnimation() { + return animationData; + } + function endHandshakeAnimation() { + if (animHandlerId) { + debug("removing animation"); + animHandlerId = MyAvatar.removeAnimationStateHandler(animHandlerId); + } + } + function startHandshakeAnimation() { + endHandshakeAnimation(); // just in case order of press/unpress is broken + debug("adding animation"); + updateAnimationData(); + animHandlerId = MyAvatar.addAnimationStateHandler(shakeHandsAnimation, []); + } + + function positionFractionallyTowards(posA, posB, frac) { + return Vec3.sum(posA, Vec3.multiply(frac, Vec3.subtract(posB, posA))); + } + + function deleteParticleEffect() { + if (particleEffect) { + particleEffect = Entities.deleteEntity(particleEffect); + } + } + + function deleteMakeConnectionParticleEffect() { + if (makingConnectionParticleEffect) { + makingConnectionParticleEffect = Entities.deleteEntity(makingConnectionParticleEffect); + } + } + + function stopHandshakeSound() { + if (handshakeInjector) { + handshakeInjector.stop(); + handshakeInjector = null; + } + } + + function calcParticlePos(myHandPosition, otherHandPosition, otherOrientation, reset) { + if (reset) { + particleRotationAngle = 0.0; + } + var position = positionFractionallyTowards(myHandPosition, otherHandPosition, 0.5); + particleRotationAngle += PARTICLE_ANGLE_INCREMENT; // about 0.5 hz + var radius = Math.min(PARTICLE_RADIUS, PARTICLE_RADIUS * particleRotationAngle / 360); + var axis = Vec3.mix(Quat.getFront(MyAvatar.orientation), Quat.inverse(Quat.getFront(otherOrientation)), 0.5); + return Vec3.sum(position, Vec3.multiplyQbyV(Quat.angleAxis(particleRotationAngle, axis), {x: 0, y: radius, z: 0})); + } + + // this is called frequently, but usually does nothing + function updateVisualization() { + if (state === STATES.INACTIVE) { + deleteParticleEffect(); + deleteMakeConnectionParticleEffect(); + return; + } + + var myHandPosition = getHandPosition(MyAvatar, currentHandJointIndex); + var otherHandPosition; + var otherOrientation; + if (connectingId) { + var other = AvatarList.getAvatar(connectingId); + if (other) { + otherOrientation = other.orientation; + otherHandPosition = getHandPosition(other, connectingHandJointIndex); + } + } + + switch (state) { + case STATES.WAITING: // no visualization while waiting deleteParticleEffect(); deleteMakeConnectionParticleEffect(); stopHandshakeSound(); break; - case STATES.connecting: + case STATES.CONNECTING: var particleProps = {}; // put the position between the 2 hands, if we have a connectingId. This // helps define the plane in which the particles move. - positionFractionallyTowards(myHandPosition, otherHand, 0.5); + positionFractionallyTowards(myHandPosition, otherHandPosition, 0.5); // now manage the rest of the entity if (!particleEffect) { particleRotationAngle = 0.0; particleEmitRate = 500; particleProps = PARTICLE_EFFECT_PROPS; particleProps.isEmitting = 0; - particleProps.position = calcParticlePos(myHandPosition, otherHand, otherOrientation); + particleProps.position = calcParticlePos(myHandPosition, otherHandPosition, otherOrientation); particleProps.parentID = MyAvatar.sessionUUID; particleEffect = Entities.addEntity(particleProps, true); } else { - particleProps.position = calcParticlePos(myHandPosition, otherHand, otherOrientation); + particleProps.position = calcParticlePos(myHandPosition, otherHandPosition, otherOrientation); particleProps.isEmitting = 1; Entities.editEntity(particleEffect, particleProps); } @@ -343,522 +366,535 @@ function updateVisualization() { makingConnectionParticleEffect = Entities.addEntity(props, true); } else { makingConnectionEmitRate *= 0.5; - Entities.editEntity(makingConnectionParticleEffect, {emitRate: makingConnectionEmitRate, position: myHandPosition, isEmitting: 1}); + Entities.editEntity(makingConnectionParticleEffect, { + emitRate: makingConnectionEmitRate, + position: myHandPosition, + isEmitting: true + }); } break; - case STATES.makingConnection: + case STATES.MAKING_CONNECTION: particleEmitRate = Math.max(50, particleEmitRate * 0.5); Entities.editEntity(makingConnectionParticleEffect, {emitRate: 0, isEmitting: 0, position: myHandPosition}); - Entities.editEntity(particleEffect, {position: calcParticlePos(myHandPosition, otherHand, otherOrientation), emitRate: particleEmitRate}); + Entities.editEntity(particleEffect, { + position: calcParticlePos(myHandPosition, otherHandPosition, otherOrientation), + emitRate: particleEmitRate + }); break; default: debug("unexpected state", state); break; - } -} - -function isNearby(id, hand) { - if (currentHand) { - var handPos = getHandPosition(MyAvatar, currentHand); - var avatar = AvatarList.getAvatar(id); - if (avatar) { - var otherHand = stringToHand(hand); - var distance = Vec3.distance(getHandPosition(avatar, otherHand), handPos); - return (distance < MAX_AVATAR_DISTANCE); } } - return false; -} -function findNearestWaitingAvatar() { - var handPos = getHandPosition(MyAvatar, currentHand); - var minDistance = MAX_AVATAR_DISTANCE; - var nearestAvatar = {}; - Object.keys(waitingList).forEach(function (identifier) { - var avatar = AvatarList.getAvatar(identifier); - if (avatar) { - var hand = stringToHand(waitingList[identifier]); - var distance = Vec3.distance(getHandPosition(avatar, hand), handPos); - if (distance < minDistance) { - minDistance = distance; - nearestAvatar = {avatar: identifier, hand: hand}; + function isNearby() { + if (currentHand) { + var handPosition = getHandPosition(MyAvatar, currentHandJointIndex); + var avatar = AvatarList.getAvatar(connectingId); + if (avatar) { + var distance = Vec3.distance(getHandPosition(avatar, connectingHandJointIndex), handPosition); + return (distance < MAX_AVATAR_DISTANCE); } } - }); - return nearestAvatar; -} + return false; + } - -// As currently implemented, we select the closest waiting avatar (if close enough) and send -// them a connectionRequest. If nobody is close enough we send a waiting message, and wait for a -// connectionRequest. If the 2 people who want to connect are both somewhat out of range when they -// initiate the shake, they will race to see who sends the connectionRequest after noticing the -// waiting message. Either way, they will start connecting eachother at that point. -function startHandshake(fromKeyboard) { - if (fromKeyboard) { - debug("adding animation"); - // just in case order of press/unpress is broken - if (animHandlerId) { - animHandlerId = MyAvatar.removeAnimationStateHandler(animHandlerId); + function findNearestWaitingAvatar() { + var handPosition = getHandPosition(MyAvatar, currentHandJointIndex); + var minDistance = MAX_AVATAR_DISTANCE; + var nearestAvatar = {}; + Object.keys(waitingList).forEach(function (identifier) { + var avatar = AvatarList.getAvatar(identifier); + if (avatar) { + var handJointIndex = waitingList[identifier]; + var distance = Vec3.distance(getHandPosition(avatar, handJointIndex), handPosition); + if (distance < minDistance) { + minDistance = distance; + nearestAvatar = {avatarId: identifier, jointIndex: handJointIndex}; + } + } + }); + return nearestAvatar; + } + function messageSend(message) { + Messages.sendMessage(MESSAGE_CHANNEL, JSON.stringify(message)); + } + function handStringMessageSend(message) { + message[HAND_STRING_PROPERTY] = handToString(currentHand); + messageSend(message); + } + function setupCandidate() { // find the closest in-range avatar, send connection request, and return true. (Otherwise falsey) + var nearestAvatar = findNearestWaitingAvatar(); + if (nearestAvatar.avatarId) { + connectingId = nearestAvatar.avatarId; + connectingHandJointIndex = nearestAvatar.jointIndex; + debug("sending connectionRequest to", connectingId); + handStringMessageSend({ + key: "connectionRequest", + id: connectingId + }); + return true; } - animHandlerId = MyAvatar.addAnimationStateHandler(shakeHandsAnimation, []); } - debug("starting handshake for", currentHand); - pollCount = 0; - state = STATES.waiting; - connectingId = undefined; - connectingHand = undefined; - // just in case - stopWaiting(); - stopConnecting(); - stopMakingConnection(); + function clearConnecting() { + connectingId = undefined; + connectingHandJointIndex = -1; + } - var nearestAvatar = findNearestWaitingAvatar(); - if (nearestAvatar.avatar) { - connectingId = nearestAvatar.avatar; - connectingHand = handToString(nearestAvatar.hand); - debug("sending connectionRequest to", connectingId); + function lookForWaitingAvatar() { + // we started with nobody close enough, but maybe I've moved + // or they did. Note that 2 people doing this race, so stop + // as soon as you have a connectingId (which means you got their + // message before noticing they were in range in this loop) + + // just in case we re-enter before stopping + stopWaiting(); + debug("started looking for waiting avatars"); + waitingInterval = Script.setInterval(function () { + if (state === STATES.WAITING && !connectingId) { + setupCandidate(); + } else { + // something happened, stop looking for avatars to connect + stopWaiting(); + debug("stopped looking for waiting avatars"); + } + }, WAITING_INTERVAL); + } + + var pollCount = 0, requestUrl = location.metaverseServerUrl + '/api/v1/user/connection_request'; + // As currently implemented, we select the closest waiting avatar (if close enough) and send + // them a connectionRequest. If nobody is close enough we send a waiting message, and wait for a + // connectionRequest. If the 2 people who want to connect are both somewhat out of range when they + // initiate the shake, they will race to see who sends the connectionRequest after noticing the + // waiting message. Either way, they will start connecting each other at that point. + function startHandshake(fromKeyboard) { + if (fromKeyboard) { + startHandshakeAnimation(); + } + debug("starting handshake for", currentHand); + pollCount = 0; + state = STATES.WAITING; + clearConnecting(); + // just in case + stopWaiting(); + stopConnecting(); + stopMakingConnection(); + if (!setupCandidate()) { + // send waiting message + debug("sending waiting message"); + handStringMessageSend({ + key: "waiting", + }); + lookForWaitingAvatar(); + } + } + + function endHandshake() { + debug("ending handshake for", currentHand); + + deleteParticleEffect(); + deleteMakeConnectionParticleEffect(); + currentHand = undefined; + currentHandJointIndex = -1; + // note that setting the state to inactive should really + // only be done here, unless we change how the triggering works, + // as we ignore the key release event when inactive. See updateTriggers + // below. + state = STATES.INACTIVE; + clearConnecting(); + stopWaiting(); + stopConnecting(); + stopMakingConnection(); + stopHandshakeSound(); + // send done to let connection know you are not making connections now messageSend({ - key: "connectionRequest", - id: connectingId, - hand: handToString(currentHand) + key: "done" }); - } else { - // send waiting message - debug("sending waiting message"); - messageSend({ - key: "waiting", - hand: handToString(currentHand) - }); - lookForWaitingAvatar(); + + endHandshakeAnimation(); + // No-op if we were successful, but this way we ensure that failures and abandoned handshakes don't leave us + // in a weird state. + request({uri: requestUrl, method: 'DELETE'}, debug); } -} -function endHandshake() { - debug("ending handshake for", currentHand); - - deleteParticleEffect(); - deleteMakeConnectionParticleEffect(); - currentHand = undefined; - // note that setting the state to inactive should really - // only be done here, unless we change how the triggering works, - // as we ignore the key release event when inactive. See updateTriggers - // below. - state = STATES.inactive; - connectingId = undefined; - connectingHand = undefined; - stopWaiting(); - stopConnecting(); - stopMakingConnection(); - stopHandshakeSound(); - // send done to let connection know you are not making connections now - messageSend({ - key: "done" - }); - - if (animHandlerId) { - debug("removing animation"); - MyAvatar.removeAnimationStateHandler(animHandlerId); - } - // No-op if we were successful, but this way we ensure that failures and abandoned handshakes don't leave us in a weird state. - request({uri: requestUrl, method: 'DELETE'}, debug); -} - -function updateTriggers(value, fromKeyboard, hand) { - if (currentHand && hand !== currentHand) { - debug("currentHand", currentHand, "ignoring messages from", hand); - return; - } - if (!currentHand) { + function updateTriggers(value, fromKeyboard, hand) { + if (currentHand && hand !== currentHand) { + debug("currentHand", currentHand, "ignoring messages from", hand); + return; + } currentHand = hand; - } - // ok now, we are either initiating or quitting... - var isGripping = value > GRIP_MIN; - if (isGripping) { - debug("updateTriggers called - gripping", handToString(hand)); - if (state != STATES.inactive) { - return; - } else { - startHandshake(fromKeyboard); - } - } else { - // TODO: should we end handshake even when inactive? Ponder - debug("updateTriggers called -- no longer gripping", handToString(hand)); - if (state != STATES.inactive) { - endHandshake(); - } else { - return; - } - } -} - -function messageSend(message) { - Messages.sendMessage(MESSAGE_CHANNEL, JSON.stringify(message)); -} - -function lookForWaitingAvatar() { - // we started with nobody close enough, but maybe I've moved - // or they did. Note that 2 people doing this race, so stop - // as soon as you have a connectingId (which means you got their - // message before noticing they were in range in this loop) - - // just in case we reenter before stopping - stopWaiting(); - debug("started looking for waiting avatars"); - waitingInterval = Script.setInterval(function () { - if (state == STATES.waiting && !connectingId) { - // find the closest in-range avatar, and send connection request - var nearestAvatar = findNearestWaitingAvatar(); - if (nearestAvatar.avatar) { - connectingId = nearestAvatar.avatar; - connectingHand = handToString(nearestAvatar.hand); - debug("sending connectionRequest to", connectingId); - messageSend({ - key: "connectionRequest", - id: connectingId, - hand: handToString(currentHand) - }); + currentHandJointIndex = getIdealHandJointIndex(MyAvatar, handToString(currentHand)); // Always, in case of changed skeleton. + // ok now, we are either initiating or quitting... + var isGripping = value > GRIP_MIN; + if (isGripping) { + debug("updateTriggers called - gripping", handToString(hand)); + if (state !== STATES.INACTIVE) { + return; } + startHandshake(fromKeyboard); } else { - // something happened, stop looking for avatars to connect - stopWaiting(); - debug("stopped looking for waiting avatars"); - } - }, WAITING_INTERVAL); -} - -/* There is a mini-state machine after entering STATES.makingConnection. - We make a request (which might immediately succeed, fail, or neither. - If we immediately fail, we tell the user. - Otherwise, we wait MAKING_CONNECTION_TIMEOUT. At that time, we poll until success or fail. - */ -var result, requestBody, pollCount = 0, requestUrl = location.metaverseServerUrl + '/api/v1/user/connection_request'; -function connectionRequestCompleted() { // Final result is in. Do effects. - if (result.status === 'success') { // set earlier - if (!successfulHandshakeInjector) { - successfulHandshakeInjector = Audio.playSound(successfulHandshakeSound, {position: getHandPosition(MyAvatar, currentHand), volume: 0.5, localOnly: true}); - } else { - successfulHandshakeInjector.restart(); - } - Controller.triggerHapticPulse(HAPTIC_DATA.success.strength, HAPTIC_DATA.success.duration, handToHaptic(currentHand)); - // don't change state (so animation continues while gripped) - // but do send a notification, by calling the slot that emits the signal for it - Window.makeConnection(true, result.connection.new_connection ? "You and " + result.connection.username + " are now connected!" : result.connection.username); - UserActivityLogger.makeUserConnection(connectingId, true, result.connection.new_connection ? "new connection" : "already connected"); - return; - } // failed - endHandshake(); - debug("failing with result data", result); - // IWBNI we also did some fail sound/visual effect. - Window.makeConnection(false, result.connection); - UserActivityLogger.makeUserConnection(connectingId, false, result.connection); -} -var POLL_INTERVAL_MS = 200, POLL_LIMIT = 5; -function handleConnectionResponseAndMaybeRepeat(error, response) { - // If response is 'pending', set a short timeout to try again. - // If we fail other than pending, set result and immediately call connectionRequestCompleted. - // If we succceed, set result and call connectionRequestCompleted immediately (if we've been polling), and otherwise on a timeout. - if (response && (response.connection === 'pending')) { - debug(response, 'pollCount', pollCount); - if (pollCount++ >= POLL_LIMIT) { // server will expire, but let's not wait that long. - debug('POLL LIMIT REACHED; TIMEOUT: expired message generated by CLIENT'); - result = {status: 'error', connection: 'expired'}; - connectionRequestCompleted(); - } else { // poll - Script.setTimeout(function () { - request({ - uri: requestUrl, - // N.B.: server gives bad request if we specify json content type, so don't do that. - body: requestBody - }, handleConnectionResponseAndMaybeRepeat); - }, POLL_INTERVAL_MS); - } - } else if (error || (response.status !== 'success')) { - debug('server fail', error, response.status); - if (response && (response.statusCode === 401)) { - error = "All participants must be logged in to connect."; - } - result = error ? {status: 'error', connection: error} : response; - UserActivityLogger.makeUserConnection(connectingId, false, error || response); - connectionRequestCompleted(); - } else { - debug('server success', result); - result = response; - if (pollCount++) { - connectionRequestCompleted(); - } else { // Wait for other guy, so that final succcess is at roughly the same time. - Script.setTimeout(connectionRequestCompleted, MAKING_CONNECTION_TIMEOUT); + // TODO: should we end handshake even when inactive? Ponder + debug("updateTriggers called -- no longer gripping", handToString(hand)); + if (state !== STATES.INACTIVE) { + endHandshake(); + } else { + return; + } } } -} -// this should be where we make the appropriate connection call. For now just make the -// visualization change. -function makeConnection(id) { - // send done to let the connection know you have made connection. - messageSend({ - key: "done", - connectionId: id - }); - - state = STATES.makingConnection; - - // continue the haptic background until the timeout fires. When we make calls, we will have an interval - // probably, in which we do this. - Controller.triggerHapticPulse(HAPTIC_DATA.background.strength, MAKING_CONNECTION_TIMEOUT, handToHaptic(currentHand)); - requestBody = {node_id: cleanId(MyAvatar.sessionUUID), proposed_node_id: cleanId(id)}; // for use when repeating - - // It would be "simpler" to skip this and just look at the response, but: - // 1. We don't want to bother the metaverse with request that we know will fail. - // 2. We don't want our code here to be dependent on precisely how the metaverse responds (400, 401, etc.) - if (!Account.isLoggedIn()) { - handleConnectionResponseAndMaybeRepeat("401:Unauthorized", {statusCode: 401}); - return; - } - - // This will immediately set response if successfull (e.g., the other guy got his request in first), or immediate failure, - // and will otherwise poll (using the requestBody we just set). - request({ // - uri: requestUrl, - method: 'POST', - json: true, - body: {user_connection_request: requestBody} - }, handleConnectionResponseAndMaybeRepeat); -} - -// we change states, start the connectionInterval where we check -// to be sure the hand is still close enough. If not, we terminate -// the interval, go back to the waiting state. If we make it -// the entire CONNECTING_TIME, we make the connection. -function startConnecting(id, hand) { - var count = 0; - debug("connecting", id, "hand", hand); - // do we need to do this? - connectingId = id; - connectingHand = hand; - state = STATES.connecting; - - // play sound - if (!handshakeInjector) { - handshakeInjector = Audio.playSound(handshakeSound, {position: getHandPosition(MyAvatar, currentHand), volume: 0.5, localOnly: true}); - } else { - handshakeInjector.restart(); - } - - // send message that we are connecting with them - messageSend({ - key: "connecting", - id: id, - hand: handToString(currentHand) - }); - Controller.triggerHapticPulse(HAPTIC_DATA.initial.strength, HAPTIC_DATA.initial.duration, handToHaptic(currentHand)); - - connectingInterval = Script.setInterval(function () { - count += 1; - Controller.triggerHapticPulse(HAPTIC_DATA.background.strength, HAPTIC_DATA.background.duration, handToHaptic(currentHand)); - if (state != STATES.connecting) { - debug("stopping connecting interval, state changed"); - stopConnecting(); - } else if (!isNearby(id, hand)) { - // gotta go back to waiting - debug(id, "moved, back to waiting"); - stopConnecting(); - messageSend({ - key: "done" + /* There is a mini-state machine after entering STATES.makingConnection. + We make a request (which might immediately succeed, fail, or neither. + If we immediately fail, we tell the user. + Otherwise, we wait MAKING_CONNECTION_TIMEOUT. At that time, we poll until success or fail. + */ + var result, requestBody; + function connectionRequestCompleted() { // Final result is in. Do effects. + if (result.status === 'success') { // set earlier + if (!successfulHandshakeInjector) { + successfulHandshakeInjector = Audio.playSound(successfulHandshakeSound, { + position: getHandPosition(MyAvatar, currentHandJointIndex), + volume: 0.5, + localOnly: true + }); + } else { + successfulHandshakeInjector.restart(); + } + Controller.triggerHapticPulse(HAPTIC_DATA.success.strength, HAPTIC_DATA.success.duration, + handToHaptic(currentHand)); + // don't change state (so animation continues while gripped) + // but do send a notification, by calling the slot that emits the signal for it + Window.makeConnection(true, + result.connection.new_connection ? + "You and " + result.connection.username + " are now connected!" : + result.connection.username); + UserActivityLogger.makeUserConnection(connectingId, + true, + result.connection.new_connection ? + "new connection" : + "already connected"); + return; + } // failed + endHandshake(); + debug("failing with result data", result); + // IWBNI we also did some fail sound/visual effect. + Window.makeConnection(false, result.connection); + if (Account.isLoggedIn()) { // Give extra failure info + request(location.metaverseServerUrl + '/api/v1/users/' + Account.username + '/location', function (error, response) { + var message = ''; + if (error || response.status !== 'success') { + message = 'Unable to get location.'; + } else if (!response.data || !response.data.location) { + message = "Unexpected location value: " + JSON.stringify(response); + } else if (response.data.location.node_id !== cleanId(MyAvatar.sessionUUID)) { + message = 'Session identification does not match database. Maybe you are logged in on another machine? That would prevent handshakes.' + JSON.stringify(response) + MyAvatar.sessionUUID; + } + if (message) { + Window.makeConnection(false, message); + } + debug("account location:", message || 'ok'); }); - startHandshake(); - } else if (count > CONNECTING_TIME/CONNECTING_INTERVAL) { - debug("made connection with " + id); - makeConnection(id); - stopConnecting(); } - }, CONNECTING_INTERVAL); -} -/* -A simple sequence diagram: NOTE that the ConnectionAck is somewhat -vestigial, and probably should be removed shortly. + UserActivityLogger.makeUserConnection(connectingId, false, result.connection); + } + // This is a bit fragile - but to account for skew in when people actually create the + // connection request, I've upped this to 2 seconds (plus the round-trip times) + // TODO: keep track of when the person we are connecting with is done, and don't stop + // until say 1 second after that. + var POLL_INTERVAL_MS = 200, POLL_LIMIT = 10; + function handleConnectionResponseAndMaybeRepeat(error, response) { + // If response is 'pending', set a short timeout to try again. + // If we fail other than pending, set result and immediately call connectionRequestCompleted. + // If we succeed, set result and call connectionRequestCompleted immediately (if we've been polling), + // and otherwise on a timeout. + if (response && (response.connection === 'pending')) { + debug(response, 'pollCount', pollCount); + if (pollCount++ >= POLL_LIMIT) { // server will expire, but let's not wait that long. + debug('POLL LIMIT REACHED; TIMEOUT: expired message generated by CLIENT'); + result = {status: 'error', connection: 'No logged-in partner found.'}; + connectionRequestCompleted(); + } else { // poll + Script.setTimeout(function () { + request({ + uri: requestUrl, + // N.B.: server gives bad request if we specify json content type, so don't do that. + body: requestBody + }, handleConnectionResponseAndMaybeRepeat); + }, POLL_INTERVAL_MS); + } + } else if (error || (response.status !== 'success')) { + debug('server fail', error, response.status); + if (response && (response.statusCode === 401)) { + error = "All participants must be logged in to connect."; + } + result = error ? {status: 'error', connection: error} : response; + UserActivityLogger.makeUserConnection(connectingId, false, error || response); + connectionRequestCompleted(); + } else { + result = response; + debug('server success', result); + if (pollCount++) { + connectionRequestCompleted(); + } else { // Wait for other guy, so that final success is at roughly the same time. + Script.setTimeout(connectionRequestCompleted, MAKING_CONNECTION_TIMEOUT); + } + } + } - Avatar A Avatar B - | | - | <-----(waiting) ----- startHandshake -startHandshake - (connectionRequest) -> | - | | - | <----(connectionAck) -------- | - | <-----(connecting) -- startConnecting - startConnecting ---(connecting) ----> | - | | - | connected - connected | - | <--------- (done) ---------- | - | ---------- (done) ---------> | -*/ -function messageHandler(channel, messageString, senderID) { - if (channel !== MESSAGE_CHANNEL) { - return; + function makeConnection(id) { + // send done to let the connection know you have made connection. + messageSend({ + key: "done", + connectionId: id + }); + + state = STATES.MAKING_CONNECTION; + + // continue the haptic background until the timeout fires. + Controller.triggerHapticPulse(HAPTIC_DATA.background.strength, MAKING_CONNECTION_TIMEOUT, handToHaptic(currentHand)); + requestBody = {'node_id': cleanId(MyAvatar.sessionUUID), 'proposed_node_id': cleanId(id)}; // for use when repeating + + // It would be "simpler" to skip this and just look at the response, but: + // 1. We don't want to bother the metaverse with request that we know will fail. + // 2. We don't want our code here to be dependent on precisely how the metaverse responds (400, 401, etc.) + if (!Account.isLoggedIn()) { + handleConnectionResponseAndMaybeRepeat("401:Unauthorized", {statusCode: 401}); + return; + } + + // This will immediately set response if successful (e.g., the other guy got his request in first), + // or immediate failure, and will otherwise poll (using the requestBody we just set). + request({ + uri: requestUrl, + method: 'POST', + json: true, + body: {'user_connection_request': requestBody} + }, handleConnectionResponseAndMaybeRepeat); } - if (MyAvatar.sessionUUID === senderID) { // ignore my own - return; + function setupConnecting(id, jointIndex) { + connectingId = id; + connectingHandJointIndex = jointIndex; } - var message = {}; - try { - message = JSON.parse(messageString); - } catch (e) { - debug(e); - } - switch (message.key) { - case "waiting": - // add this guy to waiting object. Any other message from this person will - // remove it from the list - waitingList[senderID] = message.hand; - break; - case "connectionRequest": - delete waitingList[senderID]; - if (state == STATES.waiting && message.id == MyAvatar.sessionUUID && (!connectingId || connectingId == senderID)) { - // you were waiting for a connection request, so send the ack. Or, you and the other - // guy raced and both send connectionRequests. Handle that too - connectingId = senderID; - connectingHand = message.hand; - messageSend({ - key: "connectionAck", - id: senderID, - hand: handToString(currentHand) + + // we change states, start the connectionInterval where we check + // to be sure the hand is still close enough. If not, we terminate + // the interval, go back to the waiting state. If we make it + // the entire CONNECTING_TIME, we make the connection. + function startConnecting(id, jointIndex) { + var count = 0; + debug("connecting", id, "hand", jointIndex); + // do we need to do this? + setupConnecting(id, jointIndex); + state = STATES.CONNECTING; + + // play sound + if (!handshakeInjector) { + handshakeInjector = Audio.playSound(handshakeSound, { + position: getHandPosition(MyAvatar, currentHandJointIndex), + volume: 0.5, + localOnly: true }); } else { - if (state == STATES.waiting && connectingId == senderID) { + handshakeInjector.restart(); + } + + // send message that we are connecting with them + handStringMessageSend({ + key: "connecting", + id: id + }); + Controller.triggerHapticPulse(HAPTIC_DATA.initial.strength, HAPTIC_DATA.initial.duration, handToHaptic(currentHand)); + + connectingInterval = Script.setInterval(function () { + count += 1; + Controller.triggerHapticPulse(HAPTIC_DATA.background.strength, HAPTIC_DATA.background.duration, + handToHaptic(currentHand)); + if (state !== STATES.CONNECTING) { + debug("stopping connecting interval, state changed"); + stopConnecting(); + } else if (!isNearby()) { + // gotta go back to waiting + debug(id, "moved, back to waiting"); + stopConnecting(); + messageSend({ + key: "done" + }); + startHandshake(); + } else if (count > CONNECTING_TIME / CONNECTING_INTERVAL) { + debug("made connection with " + id); + makeConnection(id); + stopConnecting(); + } + }, CONNECTING_INTERVAL); + } + /* + A simple sequence diagram: NOTE that the ConnectionAck is somewhat + vestigial, and probably should be removed shortly. + + Avatar A Avatar B + | | + | <-----(waiting) ----- startHandshake + startHandshake - (connectionRequest) -> | + | | + | <----(connectionAck) -------- | + | <-----(connecting) -- startConnecting + startConnecting ---(connecting) ----> | + | | + | connected + connected | + | <--------- (done) ---------- | + | ---------- (done) ---------> | + */ + function messageHandler(channel, messageString, senderID) { + var message = {}; + function exisitingOrSearchedJointIndex() { // If this is a new connectingId, we'll need to find the jointIndex + return connectingId ? connectingHandJointIndex : getIdealHandJointIndex(AvatarList.getAvatar(senderID), message[HAND_STRING_PROPERTY]); + } + if (channel !== MESSAGE_CHANNEL) { + return; + } + if (MyAvatar.sessionUUID === senderID) { // ignore my own + return; + } + try { + message = JSON.parse(messageString); + } catch (e) { + debug(e); + } + switch (message.key) { + case "waiting": + // add this guy to waiting object. Any other message from this person will remove it from the list + waitingList[senderID] = getIdealHandJointIndex(AvatarList.getAvatar(senderID), message[HAND_STRING_PROPERTY]); + break; + case "connectionRequest": + delete waitingList[senderID]; + if (state === STATES.WAITING && message.id === MyAvatar.sessionUUID && (!connectingId || connectingId === senderID)) { + // you were waiting for a connection request, so send the ack. Or, you and the other + // guy raced and both send connectionRequests. Handle that too + setupConnecting(senderID, exisitingOrSearchedJointIndex()); + handStringMessageSend({ + key: "connectionAck", + id: senderID, + }); + } else if (state === STATES.WAITING && connectingId === senderID) { // the person you are trying to connect sent a request to someone else. See the // if statement above. So, don't cry, just start the handshake over again startHandshake(); } - } - break; - case "connectionAck": - delete waitingList[senderID]; - if (state == STATES.waiting && (!connectingId || connectingId == senderID)) { - if (message.id == MyAvatar.sessionUUID) { - // start connecting... - connectingId = senderID; - connectingHand = message.hand; - stopWaiting(); - startConnecting(senderID, message.hand); - } else { - if (connectingId) { + break; + case "connectionAck": + delete waitingList[senderID]; + if (state === STATES.WAITING && (!connectingId || connectingId === senderID)) { + if (message.id === MyAvatar.sessionUUID) { + stopWaiting(); + startConnecting(senderID, exisitingOrSearchedJointIndex()); + } else if (connectingId) { // this is for someone else (we lost race in connectionRequest), // so lets start over startHandshake(); } } - } - // TODO: check to see if we are waiting for this but the person we are connecting sent it to - // someone else, and try again - break; - case "connecting": - delete waitingList[senderID]; - if (state == STATES.waiting && senderID == connectingId) { - // temporary logging - if (connectingHand != message.hand) { - debug("connecting hand", connectingHand, "not same as connecting hand in message", message.hand); + // TODO: check to see if we are waiting for this but the person we are connecting sent it to + // someone else, and try again + break; + case "connecting": + delete waitingList[senderID]; + if (state === STATES.WAITING && senderID === connectingId) { + if (message.id !== MyAvatar.sessionUUID) { + // the person we were trying to connect is connecting to someone else + // so try again + startHandshake(); + break; + } + startConnecting(senderID, connectingHandJointIndex); } - connectingHand = message.hand; - if (message.id != MyAvatar.sessionUUID) { - // the person we were trying to connect is connecting to someone else - // so try again - startHandshake(); - break; - } - startConnecting(senderID, message.hand); - } - break; - case "done": - delete waitingList[senderID]; - if (state == STATES.connecting && connectingId == senderID) { - // if they are done, and didn't connect us, terminate our - // connecting - if (message.connectionId !== MyAvatar.sessionUUID) { - stopConnecting(); - // now just call startHandshake. Should be ok to do so without a - // value for isKeyboard, as we should not change the animation - // state anyways (if any) - startHandshake(); - } - } else { - // if waiting or inactive, lets clear the connecting id. If in makingConnection, - // do nothing - if (state != STATES.makingConnection && connectingId == senderID) { - connectingId = undefined; - connectingHand = undefined; - if (state != STATES.inactive) { + break; + case "done": + delete waitingList[senderID]; + if (state === STATES.CONNECTING && connectingId === senderID) { + // if they are done, and didn't connect us, terminate our + // connecting + if (message.connectionId !== MyAvatar.sessionUUID) { + stopConnecting(); + // now just call startHandshake. Should be ok to do so without a + // value for isKeyboard, as we should not change the animation + // state anyways (if any) startHandshake(); } + } else { + // if waiting or inactive, lets clear the connecting id. If in makingConnection, + // do nothing + if (state !== STATES.MAKING_CONNECTION && connectingId === senderID) { + clearConnecting(); + if (state !== STATES.INACTIVE) { + startHandshake(); + } + } } + break; + default: + debug("unknown message", message); + break; } - break; - default: - debug("unknown message", message); - break; } -} -Messages.subscribe(MESSAGE_CHANNEL); -Messages.messageReceived.connect(messageHandler); + Messages.subscribe(MESSAGE_CHANNEL); + Messages.messageReceived.connect(messageHandler); - -function makeGripHandler(hand, animate) { - // determine if we are gripping or un-gripping - if (animate) { - return function(value) { - updateTriggers(value, true, hand); - }; - - } else { + function makeGripHandler(hand, animate) { + // determine if we are gripping or un-gripping + if (animate) { + return function (value) { + updateTriggers(value, true, hand); + }; + } return function (value) { updateTriggers(value, false, hand); }; } -} -function keyPressEvent(event) { - if ((event.text === "x") && !event.isAutoRepeat && !event.isShifted && !event.isMeta && !event.isControl && !event.isAlt) { - updateTriggers(1.0, true, Controller.Standard.RightHand); + function keyPressEvent(event) { + if ((event.text === "x") && !event.isAutoRepeat && !event.isShifted && !event.isMeta && !event.isControl && !event.isAlt) { + updateTriggers(1.0, true, Controller.Standard.RightHand); + } } -} -function keyReleaseEvent(event) { - if ((event.text === "x") && !event.isAutoRepeat && !event.isShifted && !event.isMeta && !event.isControl && !event.isAlt) { - updateTriggers(0.0, true, Controller.Standard.RightHand); + function keyReleaseEvent(event) { + if ((event.text === "x") && !event.isAutoRepeat && !event.isShifted && !event.isMeta && !event.isControl && !event.isAlt) { + updateTriggers(0.0, true, Controller.Standard.RightHand); + } } -} -// map controller actions -var connectionMapping = Controller.newMapping(Script.resolvePath('') + '-grip'); -connectionMapping.from(Controller.Standard.LeftGrip).peek().to(makeGripHandler(Controller.Standard.LeftHand)); -connectionMapping.from(Controller.Standard.RightGrip).peek().to(makeGripHandler(Controller.Standard.RightHand)); + // map controller actions + var connectionMapping = Controller.newMapping(Script.resolvePath('') + '-grip'); + connectionMapping.from(Controller.Standard.LeftGrip).peek().to(makeGripHandler(Controller.Standard.LeftHand)); + connectionMapping.from(Controller.Standard.RightGrip).peek().to(makeGripHandler(Controller.Standard.RightHand)); -// setup keyboard initiation -Controller.keyPressEvent.connect(keyPressEvent); -Controller.keyReleaseEvent.connect(keyReleaseEvent); + // setup keyboard initiation + Controller.keyPressEvent.connect(keyPressEvent); + Controller.keyReleaseEvent.connect(keyReleaseEvent); -// xbox controller cuz that's important -connectionMapping.from(Controller.Standard.RB).peek().to(makeGripHandler(Controller.Standard.RightHand, true)); + // Xbox controller because that is important + connectionMapping.from(Controller.Standard.RB).peek().to(makeGripHandler(Controller.Standard.RightHand, true)); -// it is easy to forget this and waste a lot of time for nothing -connectionMapping.enable(); + // it is easy to forget this and waste a lot of time for nothing + connectionMapping.enable(); -// connect updateVisualization to update frequently -Script.update.connect(updateVisualization); + // connect updateVisualization to update frequently + Script.update.connect(updateVisualization); -// load the sounds when the script loads -handshakeSound = SoundCache.getSound(HANDSHAKE_SOUND_URL); -successfulHandshakeSound = SoundCache.getSound(SUCCESSFUL_HANDSHAKE_SOUND_URL); + // load the sounds when the script loads + handshakeSound = SoundCache.getSound(HANDSHAKE_SOUND_URL); + successfulHandshakeSound = SoundCache.getSound(SUCCESSFUL_HANDSHAKE_SOUND_URL); -Script.scriptEnding.connect(function () { - debug("removing controller mappings"); - connectionMapping.disable(); - debug("removing key mappings"); - Controller.keyPressEvent.disconnect(keyPressEvent); - Controller.keyReleaseEvent.disconnect(keyReleaseEvent); - debug("disconnecting updateVisualization"); - Script.update.disconnect(updateVisualization); - deleteParticleEffect(); - deleteMakeConnectionParticleEffect(); -}); + Script.scriptEnding.connect(function () { + debug("removing controller mappings"); + connectionMapping.disable(); + debug("removing key mappings"); + Controller.keyPressEvent.disconnect(keyPressEvent); + Controller.keyReleaseEvent.disconnect(keyReleaseEvent); + debug("disconnecting updateVisualization"); + Script.update.disconnect(updateVisualization); + deleteParticleEffect(); + deleteMakeConnectionParticleEffect(); + }); }()); // END LOCAL_SCOPE - diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index 006ef3f90f..6429d6e0c6 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -1,5 +1,6 @@ "use strict"; - +/*jslint vars:true, plusplus:true, forin:true*/ +/*global Script, Settings, Window, Controller, Overlays, SoundArray, LODManager, MyAvatar, Tablet, Camera, HMD, Menu, Quat, Vec3*/ // // notifications.js // Version 0.801 @@ -56,613 +57,614 @@ // } // } -/* global Script, Controller, Overlays, SoundArray, Quat, Vec3, MyAvatar, Menu, HMD, AudioDevice, LODManager, Settings, Camera */ -(function() { // BEGIN LOCAL_SCOPE +(function () { // BEGIN LOCAL_SCOPE -Script.include("./libraries/soundArray.js"); + Script.include("./libraries/soundArray.js"); -var width = 340.0; //width of notification overlay -var windowDimensions = Controller.getViewportDimensions(); // get the size of the interface window -var overlayLocationX = (windowDimensions.x - (width + 20.0)); // positions window 20px from the right of the interface window -var buttonLocationX = overlayLocationX + (width - 28.0); -var locationY = 20.0; // position down from top of interface window -var topMargin = 13.0; -var leftMargin = 10.0; -var textColor = { red: 228, green: 228, blue: 228}; // text color -var backColor = { red: 2, green: 2, blue: 2}; // background color was 38,38,38 -var backgroundAlpha = 0; -var fontSize = 12.0; -var PERSIST_TIME_2D = 10.0; // Time in seconds before notification fades -var PERSIST_TIME_3D = 15.0; -var persistTime = PERSIST_TIME_2D; -var frame = 0; -var ourWidth = Window.innerWidth; -var ourHeight = Window.innerHeight; -var ctrlIsPressed = false; -var ready = true; -var MENU_NAME = 'Tools > Notifications'; -var PLAY_NOTIFICATION_SOUNDS_MENU_ITEM = "Play Notification Sounds"; -var NOTIFICATION_MENU_ITEM_POST = " Notifications"; -var PLAY_NOTIFICATION_SOUNDS_SETTING = "play_notification_sounds"; -var PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE = "play_notification_sounds_type_"; -var lodTextID = false; + var width = 340.0; //width of notification overlay + var windowDimensions = Controller.getViewportDimensions(); // get the size of the interface window + var overlayLocationX = (windowDimensions.x - (width + 20.0)); // positions window 20px from the right of the interface window + var buttonLocationX = overlayLocationX + (width - 28.0); + var locationY = 20.0; // position down from top of interface window + var topMargin = 13.0; + var leftMargin = 10.0; + var textColor = { red: 228, green: 228, blue: 228}; // text color + var backColor = { red: 2, green: 2, blue: 2}; // background color was 38,38,38 + var backgroundAlpha = 0; + var fontSize = 12.0; + var PERSIST_TIME_2D = 10.0; // Time in seconds before notification fades + var PERSIST_TIME_3D = 15.0; + var persistTime = PERSIST_TIME_2D; + var frame = 0; + var ctrlIsPressed = false; + var ready = true; + var MENU_NAME = 'Tools > Notifications'; + var PLAY_NOTIFICATION_SOUNDS_MENU_ITEM = "Play Notification Sounds"; + var NOTIFICATION_MENU_ITEM_POST = " Notifications"; + var PLAY_NOTIFICATION_SOUNDS_SETTING = "play_notification_sounds"; + var PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE = "play_notification_sounds_type_"; + var lodTextID = false; -var NotificationType = { - UNKNOWN: 0, - SNAPSHOT: 1, - LOD_WARNING: 2, - CONNECTION_REFUSED: 3, - EDIT_ERROR: 4, - TABLET: 5, - CONNECTION: 6, - properties: [ - { text: "Snapshot" }, - { text: "Level of Detail" }, - { text: "Connection Refused" }, - { text: "Edit error" }, - { text: "Tablet" }, - { text: "Connection" } - ], - getTypeFromMenuItem: function(menuItemName) { - if (menuItemName.substr(menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length) !== NOTIFICATION_MENU_ITEM_POST) { - return NotificationType.UNKNOWN; - } - var preMenuItemName = menuItemName.substr(0, menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length); - for (var type in this.properties) { - if (this.properties[type].text === preMenuItemName) { - return parseInt(type) + 1; + var NotificationType = { + UNKNOWN: 0, + SNAPSHOT: 1, + LOD_WARNING: 2, + CONNECTION_REFUSED: 3, + EDIT_ERROR: 4, + TABLET: 5, + CONNECTION: 6, + properties: [ + { text: "Snapshot" }, + { text: "Level of Detail" }, + { text: "Connection Refused" }, + { text: "Edit error" }, + { text: "Tablet" }, + { text: "Connection" } + ], + getTypeFromMenuItem: function (menuItemName) { + var type; + if (menuItemName.substr(menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length) !== NOTIFICATION_MENU_ITEM_POST) { + return NotificationType.UNKNOWN; } + var preMenuItemName = menuItemName.substr(0, menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length); + for (type in this.properties) { + if (this.properties[type].text === preMenuItemName) { + return parseInt(type, 10) + 1; + } + } + return NotificationType.UNKNOWN; + }, + getMenuString: function (type) { + return this.properties[type - 1].text + NOTIFICATION_MENU_ITEM_POST; } - return NotificationType.UNKNOWN; - }, - getMenuString: function(type) { - return this.properties[type - 1].text + NOTIFICATION_MENU_ITEM_POST; - } -}; - -var randomSounds = new SoundArray({ localOnly: true }, true); -var numberOfSounds = 2; -for (var i = 1; i <= numberOfSounds; i++) { - randomSounds.addSound(Script.resolvePath("assets/sounds/notification-general"+ i + ".raw")); -} - -var notifications = []; -var buttons = []; -var times = []; -var heights = []; -var myAlpha = []; -var arrays = []; -var isOnHMD = false, - NOTIFICATIONS_3D_DIRECTION = 0.0, // Degrees from avatar orientation. - NOTIFICATIONS_3D_DISTANCE = 0.6, // Horizontal distance from avatar position. - NOTIFICATIONS_3D_ELEVATION = -0.8, // Height of top middle of top notification relative to avatar eyes. - NOTIFICATIONS_3D_YAW = 0.0, // Degrees relative to notifications direction. - NOTIFICATIONS_3D_PITCH = -60.0, // Degrees from vertical. - NOTIFICATION_3D_SCALE = 0.002, // Multiplier that converts 2D overlay dimensions to 3D overlay dimensions. - NOTIFICATION_3D_BUTTON_WIDTH = 40 * NOTIFICATION_3D_SCALE, // Need a little more room for button in 3D. - overlay3DDetails = []; - -// push data from above to the 2 dimensional array -function createArrays(notice, button, createTime, height, myAlpha) { - arrays.push([notice, button, createTime, height, myAlpha]); -} - -// This handles the final dismissal of a notification after fading -function dismiss(firstNoteOut, firstButOut, firstOut) { - if (firstNoteOut == lodTextID) { - lodTextID = false; - } - - Overlays.deleteOverlay(firstNoteOut); - Overlays.deleteOverlay(firstButOut); - notifications.splice(firstOut, 1); - buttons.splice(firstOut, 1); - times.splice(firstOut, 1); - heights.splice(firstOut, 1); - myAlpha.splice(firstOut, 1); - overlay3DDetails.splice(firstOut, 1); -} - -function fadeIn(noticeIn, buttonIn) { - var q = 0, - qFade, - pauseTimer = null; - - pauseTimer = Script.setInterval(function () { - q += 1; - qFade = q / 10.0; - Overlays.editOverlay(noticeIn, { alpha: qFade }); - Overlays.editOverlay(buttonIn, { alpha: qFade }); - if (q >= 9.0) { - Script.clearInterval(pauseTimer); - } - }, 10); -} - -// this fades the notification ready for dismissal, and removes it from the arrays -function fadeOut(noticeOut, buttonOut, arraysOut) { - var r = 9.0, - rFade, - pauseTimer = null; - - pauseTimer = Script.setInterval(function () { - r -= 1; - rFade = r / 10.0; - Overlays.editOverlay(noticeOut, { alpha: rFade }); - Overlays.editOverlay(buttonOut, { alpha: rFade }); - if (r < 0) { - dismiss(noticeOut, buttonOut, arraysOut); - arrays.splice(arraysOut, 1); - ready = true; - Script.clearInterval(pauseTimer); - } - }, 20); -} - -function calculate3DOverlayPositions(noticeWidth, noticeHeight, y) { - // Calculates overlay positions and orientations in avatar coordinates. - var noticeY, - originOffset, - notificationOrientation, - notificationPosition, - buttonPosition; - - // Notification plane positions - noticeY = -y * NOTIFICATION_3D_SCALE - noticeHeight / 2; - notificationPosition = { x: 0, y: noticeY, z: 0 }; - buttonPosition = { x: (noticeWidth - NOTIFICATION_3D_BUTTON_WIDTH) / 2, y: noticeY, z: 0.001 }; - - // Rotate plane - notificationOrientation = Quat.fromPitchYawRollDegrees(NOTIFICATIONS_3D_PITCH, - NOTIFICATIONS_3D_DIRECTION + NOTIFICATIONS_3D_YAW, 0); - notificationPosition = Vec3.multiplyQbyV(notificationOrientation, notificationPosition); - buttonPosition = Vec3.multiplyQbyV(notificationOrientation, buttonPosition); - - // Translate plane - originOffset = Vec3.multiplyQbyV(Quat.fromPitchYawRollDegrees(0, NOTIFICATIONS_3D_DIRECTION, 0), - { x: 0, y: 0, z: -NOTIFICATIONS_3D_DISTANCE }); - originOffset.y += NOTIFICATIONS_3D_ELEVATION; - notificationPosition = Vec3.sum(originOffset, notificationPosition); - buttonPosition = Vec3.sum(originOffset, buttonPosition); - - return { - notificationOrientation: notificationOrientation, - notificationPosition: notificationPosition, - buttonPosition: buttonPosition }; -} -// Pushes data to each array and sets up data for 2nd dimension array -// to handle auxiliary data not carried by the overlay class -// specifically notification "heights", "times" of creation, and . -function notify(notice, button, height, imageProperties, image) { - var notificationText, - noticeWidth, - noticeHeight, - positions, - last; + var randomSounds = new SoundArray({ localOnly: true }, true); + var numberOfSounds = 2; + var soundIndex; + for (soundIndex = 1; soundIndex <= numberOfSounds; soundIndex++) { + randomSounds.addSound(Script.resolvePath("assets/sounds/notification-general" + soundIndex + ".raw")); + } - if (isOnHMD) { - // Calculate 3D values from 2D overlay properties. + var notifications = []; + var buttons = []; + var times = []; + var heights = []; + var myAlpha = []; + var arrays = []; + var isOnHMD = false, + NOTIFICATIONS_3D_DIRECTION = 0.0, // Degrees from avatar orientation. + NOTIFICATIONS_3D_DISTANCE = 0.6, // Horizontal distance from avatar position. + NOTIFICATIONS_3D_ELEVATION = -0.8, // Height of top middle of top notification relative to avatar eyes. + NOTIFICATIONS_3D_YAW = 0.0, // Degrees relative to notifications direction. + NOTIFICATIONS_3D_PITCH = -60.0, // Degrees from vertical. + NOTIFICATION_3D_SCALE = 0.002, // Multiplier that converts 2D overlay dimensions to 3D overlay dimensions. + NOTIFICATION_3D_BUTTON_WIDTH = 40 * NOTIFICATION_3D_SCALE, // Need a little more room for button in 3D. + overlay3DDetails = []; - noticeWidth = notice.width * NOTIFICATION_3D_SCALE + NOTIFICATION_3D_BUTTON_WIDTH; - noticeHeight = notice.height * NOTIFICATION_3D_SCALE; + // push data from above to the 2 dimensional array + function createArrays(notice, button, createTime, height, myAlpha) { + arrays.push([notice, button, createTime, height, myAlpha]); + } - notice.size = { x: noticeWidth, y: noticeHeight }; - - positions = calculate3DOverlayPositions(noticeWidth, noticeHeight, notice.y); - - notice.parentID = MyAvatar.sessionUUID; - notice.parentJointIndex = -2; - - if (!image) { - notice.topMargin = 0.75 * notice.topMargin * NOTIFICATION_3D_SCALE; - notice.leftMargin = 2 * notice.leftMargin * NOTIFICATION_3D_SCALE; - notice.bottomMargin = 0; - notice.rightMargin = 0; - notice.lineHeight = 10.0 * (fontSize / 12.0) * NOTIFICATION_3D_SCALE; - notice.isFacingAvatar = false; - - notificationText = Overlays.addOverlay("text3d", notice); - notifications.push(notificationText); - } else { - notifications.push(Overlays.addOverlay("image3d", notice)); + // This handles the final dismissal of a notification after fading + function dismiss(firstNoteOut, firstButOut, firstOut) { + if (firstNoteOut === lodTextID) { + lodTextID = false; } - button.url = button.imageURL; - button.scale = button.width * NOTIFICATION_3D_SCALE; - button.isFacingAvatar = false; - button.parentID = MyAvatar.sessionUUID; - button.parentJointIndex = -2; + Overlays.deleteOverlay(firstNoteOut); + Overlays.deleteOverlay(firstButOut); + notifications.splice(firstOut, 1); + buttons.splice(firstOut, 1); + times.splice(firstOut, 1); + heights.splice(firstOut, 1); + myAlpha.splice(firstOut, 1); + overlay3DDetails.splice(firstOut, 1); + } - buttons.push((Overlays.addOverlay("image3d", button))); - overlay3DDetails.push({ - notificationOrientation: positions.notificationOrientation, - notificationPosition: positions.notificationPosition, - buttonPosition: positions.buttonPosition, - width: noticeWidth, - height: noticeHeight - }); + function fadeIn(noticeIn, buttonIn) { + var q = 0, + qFade, + pauseTimer = null; + pauseTimer = Script.setInterval(function () { + q += 1; + qFade = q / 10.0; + Overlays.editOverlay(noticeIn, { alpha: qFade }); + Overlays.editOverlay(buttonIn, { alpha: qFade }); + if (q >= 9.0) { + Script.clearInterval(pauseTimer); + } + }, 10); + } - var defaultEyePosition, - avatarOrientation, - notificationPosition, + // this fades the notification ready for dismissal, and removes it from the arrays + function fadeOut(noticeOut, buttonOut, arraysOut) { + var r = 9.0, + rFade, + pauseTimer = null; + + pauseTimer = Script.setInterval(function () { + r -= 1; + rFade = Math.max(0.0, r / 10.0); + Overlays.editOverlay(noticeOut, { alpha: rFade }); + Overlays.editOverlay(buttonOut, { alpha: rFade }); + if (r <= 0) { + dismiss(noticeOut, buttonOut, arraysOut); + arrays.splice(arraysOut, 1); + ready = true; + Script.clearInterval(pauseTimer); + } + }, 20); + } + + function calculate3DOverlayPositions(noticeWidth, noticeHeight, y) { + // Calculates overlay positions and orientations in avatar coordinates. + var noticeY, + originOffset, notificationOrientation, + notificationPosition, buttonPosition; - if (isOnHMD && notifications.length > 0) { - // Update 3D overlays to maintain positions relative to avatar - defaultEyePosition = MyAvatar.getDefaultEyePosition(); - avatarOrientation = MyAvatar.orientation; + // Notification plane positions + noticeY = -y * NOTIFICATION_3D_SCALE - noticeHeight / 2; + notificationPosition = { x: 0, y: noticeY, z: 0 }; + buttonPosition = { x: (noticeWidth - NOTIFICATION_3D_BUTTON_WIDTH) / 2, y: noticeY, z: 0.001 }; - for (i = 0; i < notifications.length; i += 1) { - notificationPosition = Vec3.sum(defaultEyePosition, - Vec3.multiplyQbyV(avatarOrientation, - overlay3DDetails[i].notificationPosition)); - notificationOrientation = Quat.multiply(avatarOrientation, - overlay3DDetails[i].notificationOrientation); - buttonPosition = Vec3.sum(defaultEyePosition, - Vec3.multiplyQbyV(avatarOrientation, - overlay3DDetails[i].buttonPosition)); - Overlays.editOverlay(notifications[i], { position: notificationPosition, - rotation: notificationOrientation }); - Overlays.editOverlay(buttons[i], { position: buttonPosition, rotation: notificationOrientation }); - } - } + // Rotate plane + notificationOrientation = Quat.fromPitchYawRollDegrees(NOTIFICATIONS_3D_PITCH, + NOTIFICATIONS_3D_DIRECTION + NOTIFICATIONS_3D_YAW, 0); + notificationPosition = Vec3.multiplyQbyV(notificationOrientation, notificationPosition); + buttonPosition = Vec3.multiplyQbyV(notificationOrientation, buttonPosition); - } else { - if (!image) { - notificationText = Overlays.addOverlay("text", notice); - notifications.push((notificationText)); - } else { - notifications.push(Overlays.addOverlay("image", notice)); - } - buttons.push(Overlays.addOverlay("image", button)); + // Translate plane + originOffset = Vec3.multiplyQbyV(Quat.fromPitchYawRollDegrees(0, NOTIFICATIONS_3D_DIRECTION, 0), + { x: 0, y: 0, z: -NOTIFICATIONS_3D_DISTANCE }); + originOffset.y += NOTIFICATIONS_3D_ELEVATION; + notificationPosition = Vec3.sum(originOffset, notificationPosition); + buttonPosition = Vec3.sum(originOffset, buttonPosition); + + return { + notificationOrientation: notificationOrientation, + notificationPosition: notificationPosition, + buttonPosition: buttonPosition + }; } - height = height + 1.0; - heights.push(height); - times.push(new Date().getTime() / 1000); - last = notifications.length - 1; - myAlpha.push(notifications[last].alpha); - createArrays(notifications[last], buttons[last], times[last], heights[last], myAlpha[last]); - fadeIn(notifications[last], buttons[last]); + // Pushes data to each array and sets up data for 2nd dimension array + // to handle auxiliary data not carried by the overlay class + // specifically notification "heights", "times" of creation, and . + function notify(notice, button, height, imageProperties, image) { + var notificationText, + noticeWidth, + noticeHeight, + positions, + last; - if (imageProperties && !image) { - var imageHeight = notice.width / imageProperties.aspectRatio; - notice = { - x: notice.x, - y: notice.y + height, - width: notice.width, - height: imageHeight, - subImage: { x: 0, y: 0 }, + if (isOnHMD) { + // Calculate 3D values from 2D overlay properties. + + noticeWidth = notice.width * NOTIFICATION_3D_SCALE + NOTIFICATION_3D_BUTTON_WIDTH; + noticeHeight = notice.height * NOTIFICATION_3D_SCALE; + + notice.size = { x: noticeWidth, y: noticeHeight }; + + positions = calculate3DOverlayPositions(noticeWidth, noticeHeight, notice.y); + + notice.parentID = MyAvatar.sessionUUID; + notice.parentJointIndex = -2; + + if (!image) { + notice.topMargin = 0.75 * notice.topMargin * NOTIFICATION_3D_SCALE; + notice.leftMargin = 2 * notice.leftMargin * NOTIFICATION_3D_SCALE; + notice.bottomMargin = 0; + notice.rightMargin = 0; + notice.lineHeight = 10.0 * (fontSize / 12.0) * NOTIFICATION_3D_SCALE; + notice.isFacingAvatar = false; + + notificationText = Overlays.addOverlay("text3d", notice); + notifications.push(notificationText); + } else { + notifications.push(Overlays.addOverlay("image3d", notice)); + } + + button.url = button.imageURL; + button.scale = button.width * NOTIFICATION_3D_SCALE; + button.isFacingAvatar = false; + button.parentID = MyAvatar.sessionUUID; + button.parentJointIndex = -2; + + buttons.push((Overlays.addOverlay("image3d", button))); + overlay3DDetails.push({ + notificationOrientation: positions.notificationOrientation, + notificationPosition: positions.notificationPosition, + buttonPosition: positions.buttonPosition, + width: noticeWidth, + height: noticeHeight + }); + + + var defaultEyePosition, + avatarOrientation, + notificationPosition, + notificationOrientation, + buttonPosition, + notificationIndex; + + if (isOnHMD && notifications.length > 0) { + // Update 3D overlays to maintain positions relative to avatar + defaultEyePosition = MyAvatar.getDefaultEyePosition(); + avatarOrientation = MyAvatar.orientation; + + for (notificationIndex = 0; notificationIndex < notifications.length; notificationIndex += 1) { + notificationPosition = Vec3.sum(defaultEyePosition, + Vec3.multiplyQbyV(avatarOrientation, + overlay3DDetails[notificationIndex].notificationPosition)); + notificationOrientation = Quat.multiply(avatarOrientation, + overlay3DDetails[notificationIndex].notificationOrientation); + buttonPosition = Vec3.sum(defaultEyePosition, + Vec3.multiplyQbyV(avatarOrientation, + overlay3DDetails[notificationIndex].buttonPosition)); + Overlays.editOverlay(notifications[notificationIndex], { position: notificationPosition, + rotation: notificationOrientation }); + Overlays.editOverlay(buttons[notificationIndex], { position: buttonPosition, rotation: notificationOrientation }); + } + } + + } else { + if (!image) { + notificationText = Overlays.addOverlay("text", notice); + notifications.push(notificationText); + } else { + notifications.push(Overlays.addOverlay("image", notice)); + } + buttons.push(Overlays.addOverlay("image", button)); + } + + height = height + 1.0; + heights.push(height); + times.push(new Date().getTime() / 1000); + last = notifications.length - 1; + myAlpha.push(notifications[last].alpha); + createArrays(notifications[last], buttons[last], times[last], heights[last], myAlpha[last]); + fadeIn(notifications[last], buttons[last]); + + if (imageProperties && !image) { + var imageHeight = notice.width / imageProperties.aspectRatio; + notice = { + x: notice.x, + y: notice.y + height, + width: notice.width, + height: imageHeight, + subImage: { x: 0, y: 0 }, + color: { red: 255, green: 255, blue: 255}, + visible: true, + imageURL: imageProperties.path, + alpha: backgroundAlpha + }; + notify(notice, button, imageHeight, imageProperties, true); + } + + return notificationText; + } + + var CLOSE_NOTIFICATION_ICON = Script.resolvePath("assets/images/close-small-light.svg"); + + // This function creates and sizes the overlays + function createNotification(text, notificationType, imageProperties) { + var count = (text.match(/\n/g) || []).length, + breakPoint = 43.0, // length when new line is added + extraLine = 0, + breaks = 0, + height = 40.0, + stack = 0, + level, + noticeProperties, + bLevel, + buttonProperties, + i; + + if (text.length >= breakPoint) { + breaks = count; + } + extraLine = breaks * 16.0; + for (i = 0; i < heights.length; i += 1) { + stack = stack + heights[i]; + } + + level = (stack + 20.0); + height = height + extraLine; + + noticeProperties = { + x: overlayLocationX, + y: level, + width: width, + height: height, + color: textColor, + backgroundColor: backColor, + alpha: backgroundAlpha, + topMargin: topMargin, + leftMargin: leftMargin, + font: {size: fontSize}, + text: text + }; + + bLevel = level + 12.0; + buttonProperties = { + x: buttonLocationX, + y: bLevel, + width: 10.0, + height: 10.0, + subImage: { x: 0, y: 0, width: 10, height: 10 }, + imageURL: CLOSE_NOTIFICATION_ICON, color: { red: 255, green: 255, blue: 255}, visible: true, - imageURL: imageProperties.path, alpha: backgroundAlpha }; - notify(notice, button, imageHeight, imageProperties, true); + + if (Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) && + Menu.isOptionChecked(NotificationType.getMenuString(notificationType))) { + randomSounds.playRandom(); + } + + return notify(noticeProperties, buttonProperties, height, imageProperties); } - return notificationText; -} - -var CLOSE_NOTIFICATION_ICON = Script.resolvePath("assets/images/close-small-light.svg"); - -// This function creates and sizes the overlays -function createNotification(text, notificationType, imageProperties) { - var count = (text.match(/\n/g) || []).length, - breakPoint = 43.0, // length when new line is added - extraLine = 0, - breaks = 0, - height = 40.0, - stack = 0, - level, - noticeProperties, - bLevel, - buttonProperties, - i; - - if (text.length >= breakPoint) { - breaks = count; - } - extraLine = breaks * 16.0; - for (i = 0; i < heights.length; i += 1) { - stack = stack + heights[i]; + function deleteNotification(index) { + var notificationTextID = notifications[index]; + if (notificationTextID === lodTextID) { + lodTextID = false; + } + Overlays.deleteOverlay(notificationTextID); + Overlays.deleteOverlay(buttons[index]); + notifications.splice(index, 1); + buttons.splice(index, 1); + times.splice(index, 1); + heights.splice(index, 1); + myAlpha.splice(index, 1); + overlay3DDetails.splice(index, 1); + arrays.splice(index, 1); } - level = (stack + 20.0); - height = height + extraLine; - noticeProperties = { - x: overlayLocationX, - y: level, - width: width, - height: height, - color: textColor, - backgroundColor: backColor, - alpha: backgroundAlpha, - topMargin: topMargin, - leftMargin: leftMargin, - font: {size: fontSize}, - text: text - }; - - bLevel = level + 12.0; - buttonProperties = { - x: buttonLocationX, - y: bLevel, - width: 10.0, - height: 10.0, - subImage: { x: 0, y: 0, width: 10, height: 10 }, - imageURL: CLOSE_NOTIFICATION_ICON, - color: { red: 255, green: 255, blue: 255}, - visible: true, - alpha: backgroundAlpha - }; - - if (Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) && - Menu.isOptionChecked(NotificationType.getMenuString(notificationType))) { - randomSounds.playRandom(); - } - - return notify(noticeProperties, buttonProperties, height, imageProperties); -} - -function deleteNotification(index) { - var notificationTextID = notifications[index]; - if (notificationTextID == lodTextID) { - lodTextID = false; - } - Overlays.deleteOverlay(notificationTextID); - Overlays.deleteOverlay(buttons[index]); - notifications.splice(index, 1); - buttons.splice(index, 1); - times.splice(index, 1); - heights.splice(index, 1); - myAlpha.splice(index, 1); - overlay3DDetails.splice(index, 1); - arrays.splice(index, 1); -} - - -// Trims extra whitespace and breaks into lines of length no more than MAX_LENGTH, breaking at spaces. Trims extra whitespace. -var MAX_LENGTH = 42; -function wordWrap(string) { - var finishedLines = [], currentLine = ''; - string.split(/\s/).forEach(function (word) { - var tail = currentLine ? ' ' + word : word; - if ((currentLine.length + tail.length) <= MAX_LENGTH) { - currentLine += tail; - } else { + // Trims extra whitespace and breaks into lines of length no more than MAX_LENGTH, breaking at spaces. Trims extra whitespace. + var MAX_LENGTH = 42; + function wordWrap(string) { + var finishedLines = [], currentLine = ''; + string.split(/\s/).forEach(function (word) { + var tail = currentLine ? ' ' + word : word; + if ((currentLine.length + tail.length) <= MAX_LENGTH) { + currentLine += tail; + } else { + finishedLines.push(currentLine); + currentLine = word; + } + }); + if (currentLine) { finishedLines.push(currentLine); - currentLine = word; } - }); - if (currentLine) { - finishedLines.push(currentLine); - } - return finishedLines.join('\n'); -} - -function update() { - var nextOverlay, - noticeOut, - buttonOut, - arraysOut, - positions, - i, - j, - k; - - if (isOnHMD !== HMD.active) { - while (arrays.length > 0) { - deleteNotification(0); - } - isOnHMD = !isOnHMD; - persistTime = isOnHMD ? PERSIST_TIME_3D : PERSIST_TIME_2D; - return; + return finishedLines.join('\n'); } - frame += 1; - if ((frame % 60.0) === 0) { // only update once a second - locationY = 20.0; - for (i = 0; i < arrays.length; i += 1) { //repositions overlays as others fade - nextOverlay = Overlays.getOverlayAtPoint({ x: overlayLocationX, y: locationY }); - Overlays.editOverlay(notifications[i], { x: overlayLocationX, y: locationY }); - Overlays.editOverlay(buttons[i], { x: buttonLocationX, y: locationY + 12.0 }); - if (isOnHMD) { - positions = calculate3DOverlayPositions(overlay3DDetails[i].width, - overlay3DDetails[i].height, locationY); - overlay3DDetails[i].notificationOrientation = positions.notificationOrientation; - overlay3DDetails[i].notificationPosition = positions.notificationPosition; - overlay3DDetails[i].buttonPosition = positions.buttonPosition; + function update() { + var noticeOut, + buttonOut, + arraysOut, + positions, + i, + j, + k; + + if (isOnHMD !== HMD.active) { + while (arrays.length > 0) { + deleteNotification(0); } - locationY = locationY + arrays[i][3]; + isOnHMD = !isOnHMD; + persistTime = isOnHMD ? PERSIST_TIME_3D : PERSIST_TIME_2D; + return; } - } - // This checks the age of the notification and prepares to fade it after 9.0 seconds (var persistTime - 1) - for (i = 0; i < arrays.length; i += 1) { - if (ready) { - j = arrays[i][2]; - k = j + persistTime; - if (k < (new Date().getTime() / 1000)) { - ready = false; - noticeOut = arrays[i][0]; - buttonOut = arrays[i][1]; - arraysOut = i; - fadeOut(noticeOut, buttonOut, arraysOut); + frame += 1; + if ((frame % 60.0) === 0) { // only update once a second + locationY = 20.0; + for (i = 0; i < arrays.length; i += 1) { //repositions overlays as others fade + Overlays.editOverlay(notifications[i], { x: overlayLocationX, y: locationY }); + Overlays.editOverlay(buttons[i], { x: buttonLocationX, y: locationY + 12.0 }); + if (isOnHMD) { + positions = calculate3DOverlayPositions(overlay3DDetails[i].width, + overlay3DDetails[i].height, locationY); + overlay3DDetails[i].notificationOrientation = positions.notificationOrientation; + overlay3DDetails[i].notificationPosition = positions.notificationPosition; + overlay3DDetails[i].buttonPosition = positions.buttonPosition; + } + locationY = locationY + arrays[i][3]; + } + } + + // This checks the age of the notification and prepares to fade it after 9.0 seconds (var persistTime - 1) + for (i = 0; i < arrays.length; i += 1) { + if (ready) { + j = arrays[i][2]; + k = j + persistTime; + if (k < (new Date().getTime() / 1000)) { + ready = false; + noticeOut = arrays[i][0]; + buttonOut = arrays[i][1]; + arraysOut = i; + fadeOut(noticeOut, buttonOut, arraysOut); + } } } } -} -var STARTUP_TIMEOUT = 500, // ms - startingUp = true, - startupTimer = null; + var STARTUP_TIMEOUT = 500, // ms + startingUp = true, + startupTimer = null; -function finishStartup() { - startingUp = false; - Script.clearTimeout(startupTimer); -} + function finishStartup() { + startingUp = false; + Script.clearTimeout(startupTimer); + } -function isStartingUp() { - // Is starting up until get no checks that it is starting up for STARTUP_TIMEOUT - if (startingUp) { - if (startupTimer) { - Script.clearTimeout(startupTimer); + function isStartingUp() { + // Is starting up until get no checks that it is starting up for STARTUP_TIMEOUT + if (startingUp) { + if (startupTimer) { + Script.clearTimeout(startupTimer); + } + startupTimer = Script.setTimeout(finishStartup, STARTUP_TIMEOUT); } - startupTimer = Script.setTimeout(finishStartup, STARTUP_TIMEOUT); - } - return startingUp; -} - -function onDomainConnectionRefused(reason) { - createNotification("Connection refused: " + reason, NotificationType.CONNECTION_REFUSED); -} - -function onEditError(msg) { - createNotification(wordWrap(msg), NotificationType.EDIT_ERROR); -} - -function onNotify(msg) { - createNotification(wordWrap(msg), NotificationType.UNKNOWN); // Needs a generic notification system for user feedback, thus using this -} - -function onSnapshotTaken(pathStillSnapshot, notify) { - if (notify) { - var imageProperties = { - path: "file:///" + pathStillSnapshot, - aspectRatio: Window.innerWidth / Window.innerHeight - }; - createNotification(wordWrap("Snapshot saved to " + pathStillSnapshot), NotificationType.SNAPSHOT, imageProperties); - } -} - -function tabletNotification() { - createNotification("Tablet needs your attention", NotificationType.TABLET); -} - -function processingGif() { - createNotification("Processing GIF snapshot...", NotificationType.SNAPSHOT); -} - -function connectionAdded(connectionName) { - createNotification(connectionName, NotificationType.CONNECTION); -} - -function connectionError(error) { - createNotification(wordWrap("Error trying to make connection: " + error), NotificationType.CONNECTION); -} - -// handles mouse clicks on buttons -function mousePressEvent(event) { - var pickRay, - clickedOverlay, - i; - - if (isOnHMD) { - pickRay = Camera.computePickRay(event.x, event.y); - clickedOverlay = Overlays.findRayIntersection(pickRay).overlayID; - } else { - clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); + return startingUp; } - for (i = 0; i < buttons.length; i += 1) { - if (clickedOverlay === buttons[i]) { - deleteNotification(i); + function onDomainConnectionRefused(reason) { + createNotification("Connection refused: " + reason, NotificationType.CONNECTION_REFUSED); + } + + function onEditError(msg) { + createNotification(wordWrap(msg), NotificationType.EDIT_ERROR); + } + + function onNotify(msg) { + createNotification(wordWrap(msg), NotificationType.UNKNOWN); // Needs a generic notification system for user feedback, thus using this + } + + function onSnapshotTaken(pathStillSnapshot, notify) { + if (notify) { + var imageProperties = { + path: "file:///" + pathStillSnapshot, + aspectRatio: Window.innerWidth / Window.innerHeight + }; + createNotification(wordWrap("Snapshot saved to " + pathStillSnapshot), NotificationType.SNAPSHOT, imageProperties); } } -} -// Control key remains active only while key is held down -function keyReleaseEvent(key) { - if (key.key === 16777249) { - ctrlIsPressed = false; + function tabletNotification() { + createNotification("Tablet needs your attention", NotificationType.TABLET); } -} -// Triggers notification on specific key driven events -function keyPressEvent(key) { - if (key.key === 16777249) { - ctrlIsPressed = true; + function processingGif() { + createNotification("Processing GIF snapshot...", NotificationType.SNAPSHOT); } -} -function setup() { - Menu.addMenu(MENU_NAME); - var checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING); - checked = checked === '' ? true : checked; - Menu.addMenuItem({ - menuName: MENU_NAME, - menuItemName: PLAY_NOTIFICATION_SOUNDS_MENU_ITEM, - isCheckable: true, - isChecked: Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING) - }); - Menu.addSeparator(MENU_NAME, "Play sounds for:"); - for (var type in NotificationType.properties) { - checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + (parseInt(type) + 1)); + function connectionAdded(connectionName) { + createNotification(connectionName, NotificationType.CONNECTION); + } + + function connectionError(error) { + createNotification(wordWrap("Error trying to make connection: " + error), NotificationType.CONNECTION); + } + + // handles mouse clicks on buttons + function mousePressEvent(event) { + var pickRay, + clickedOverlay, + i; + + if (isOnHMD) { + pickRay = Camera.computePickRay(event.x, event.y); + clickedOverlay = Overlays.findRayIntersection(pickRay).overlayID; + } else { + clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); + } + + for (i = 0; i < buttons.length; i += 1) { + if (clickedOverlay === buttons[i]) { + deleteNotification(i); + } + } + } + + // Control key remains active only while key is held down + function keyReleaseEvent(key) { + if (key.key === 16777249) { + ctrlIsPressed = false; + } + } + + // Triggers notification on specific key driven events + function keyPressEvent(key) { + if (key.key === 16777249) { + ctrlIsPressed = true; + } + } + + function setup() { + var type; + Menu.addMenu(MENU_NAME); + var checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING); checked = checked === '' ? true : checked; Menu.addMenuItem({ menuName: MENU_NAME, - menuItemName: NotificationType.properties[type].text + NOTIFICATION_MENU_ITEM_POST, + menuItemName: PLAY_NOTIFICATION_SOUNDS_MENU_ITEM, isCheckable: true, - isChecked: checked + isChecked: Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING) }); + Menu.addSeparator(MENU_NAME, "Play sounds for:"); + for (type in NotificationType.properties) { + checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + (parseInt(type, 10) + 1)); + checked = checked === '' ? true : checked; + Menu.addMenuItem({ + menuName: MENU_NAME, + menuItemName: NotificationType.properties[type].text + NOTIFICATION_MENU_ITEM_POST, + isCheckable: true, + isChecked: checked + }); + } } -} -// When our script shuts down, we should clean up all of our overlays -function scriptEnding() { - for (var i = 0; i < notifications.length; i++) { - Overlays.deleteOverlay(notifications[i]); - Overlays.deleteOverlay(buttons[i]); + // When our script shuts down, we should clean up all of our overlays + function scriptEnding() { + var notificationIndex; + for (notificationIndex = 0; notificationIndex < notifications.length; notificationIndex++) { + Overlays.deleteOverlay(notifications[notificationIndex]); + Overlays.deleteOverlay(buttons[notificationIndex]); + } + Menu.removeMenu(MENU_NAME); } - Menu.removeMenu(MENU_NAME); -} -function menuItemEvent(menuItem) { - if (menuItem === PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) { - Settings.setValue(PLAY_NOTIFICATION_SOUNDS_SETTING, Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM)); - return; + function menuItemEvent(menuItem) { + if (menuItem === PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) { + Settings.setValue(PLAY_NOTIFICATION_SOUNDS_SETTING, Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM)); + return; + } + var notificationType = NotificationType.getTypeFromMenuItem(menuItem); + if (notificationType !== notificationType.UNKNOWN) { + Settings.setValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + notificationType, Menu.isOptionChecked(menuItem)); + } } - var notificationType = NotificationType.getTypeFromMenuItem(menuItem); - if (notificationType !== notificationType.UNKNOWN) { - Settings.setValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + notificationType, Menu.isOptionChecked(menuItem)); - } -} -LODManager.LODDecreased.connect(function() { - var warningText = "\n" + - "Due to the complexity of the content, the \n" + - "level of detail has been decreased. " + - "You can now see: \n" + - LODManager.getLODFeedbackText(); + LODManager.LODDecreased.connect(function () { + var warningText = "\n" + + "Due to the complexity of the content, the \n" + + "level of detail has been decreased. " + + "You can now see: \n" + + LODManager.getLODFeedbackText(); - if (lodTextID === false) { - lodTextID = createNotification(warningText, NotificationType.LOD_WARNING); - } else { - Overlays.editOverlay(lodTextID, { text: warningText }); - } -}); + if (lodTextID === false) { + lodTextID = createNotification(warningText, NotificationType.LOD_WARNING); + } else { + Overlays.editOverlay(lodTextID, { text: warningText }); + } + }); -Controller.keyPressEvent.connect(keyPressEvent); -Controller.mousePressEvent.connect(mousePressEvent); -Controller.keyReleaseEvent.connect(keyReleaseEvent); -Script.update.connect(update); -Script.scriptEnding.connect(scriptEnding); -Menu.menuItemEvent.connect(menuItemEvent); -Window.domainConnectionRefused.connect(onDomainConnectionRefused); -Window.stillSnapshotTaken.connect(onSnapshotTaken); -Window.processingGifStarted.connect(processingGif); -Window.connectionAdded.connect(connectionAdded); -Window.connectionError.connect(connectionError); -Window.notifyEditError = onEditError; -Window.notify = onNotify; -Tablet.tabletNotification.connect(tabletNotification); -setup(); + Controller.keyPressEvent.connect(keyPressEvent); + Controller.mousePressEvent.connect(mousePressEvent); + Controller.keyReleaseEvent.connect(keyReleaseEvent); + Script.update.connect(update); + Script.scriptEnding.connect(scriptEnding); + Menu.menuItemEvent.connect(menuItemEvent); + Window.domainConnectionRefused.connect(onDomainConnectionRefused); + Window.stillSnapshotTaken.connect(onSnapshotTaken); + Window.processingGifStarted.connect(processingGif); + Window.connectionAdded.connect(connectionAdded); + Window.connectionError.connect(connectionError); + Window.announcement.connect(onNotify); + Window.notifyEditError = onEditError; + Window.notify = onNotify; + Tablet.tabletNotification.connect(tabletNotification); + setup(); }()); // END LOCAL_SCOPE diff --git a/scripts/system/pal.js b/scripts/system/pal.js index ae64065216..4a6b8d4142 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -723,7 +723,6 @@ function startup() { activeIcon: "icons/tablet-icons/people-a.svg", sortOrder: 7 }); - tablet.fromQml.connect(fromQml); button.clicked.connect(onTabletButtonClicked); tablet.screenChanged.connect(onTabletScreenChanged); Users.usernameFromIDReply.connect(usernameFromIDReply); @@ -789,8 +788,23 @@ function onTabletButtonClicked() { audioTimer = createAudioInterval(conserveResources ? AUDIO_LEVEL_CONSERVED_UPDATE_INTERVAL_MS : AUDIO_LEVEL_UPDATE_INTERVAL_MS); } } +var hasEventBridge = false; +function wireEventBridge(on) { + if (on) { + if (!hasEventBridge) { + tablet.fromQml.connect(fromQml); + hasEventBridge = true; + } + } else { + if (hasEventBridge) { + tablet.fromQml.disconnect(fromQml); + hasEventBridge = false; + } + } +} function onTabletScreenChanged(type, url) { + wireEventBridge(shouldActivateButton); // for toolbar mode: change button to active when window is first openend, false otherwise. button.editProperties({isActive: shouldActivateButton}); shouldActivateButton = false; diff --git a/scripts/system/particle_explorer/particleExplorer.html b/scripts/system/particle_explorer/particleExplorer.html index d12ceac14b..d0d86d79da 100644 --- a/scripts/system/particle_explorer/particleExplorer.html +++ b/scripts/system/particle_explorer/particleExplorer.html @@ -14,10 +14,13 @@ --> + + +