diff --git a/BUILD.md b/BUILD.md index fc2359b057..c45b7cb636 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,4 +1,4 @@ -###Dependencies +### Dependencies * [cmake](https://cmake.org/download/) ~> 3.3.2 * [Qt](https://www.qt.io/download-open-source) ~> 5.6.2 @@ -6,7 +6,7 @@ * IMPORTANT: Use the latest available version of OpenSSL to avoid security vulnerabilities. * [VHACD](https://github.com/virneo/v-hacd)(clone this repository)(Optional) -####CMake External Project Dependencies +#### CMake External Project Dependencies * [boostconfig](https://github.com/boostorg/config) ~> 1.58 * [Bullet Physics Engine](https://github.com/bulletphysics/bullet3/releases) ~> 2.83 @@ -30,16 +30,19 @@ These are not placed in your normal build tree when doing an out of source build If you would like to use a specific install of a dependency instead of the version that would be grabbed as a CMake ExternalProject, you can pass -DUSE_LOCAL_$NAME=0 (where $NAME is the name of the subfolder in [cmake/externals](cmake/externals)) when you run CMake to tell it not to get that dependency as an external project. -###OS Specific Build Guides +### OS Specific Build Guides + * [BUILD_OSX.md](BUILD_OSX.md) - additional instructions for OS X. * [BUILD_LINUX.md](BUILD_LINUX.md) - additional instructions for Linux. * [BUILD_WIN.md](BUILD_WIN.md) - additional instructions for Windows. * [BUILD_ANDROID.md](BUILD_ANDROID.md) - additional instructions for Android -###CMake +### CMake + Hifi uses CMake to generate build files and project files for your platform. -####Qt +#### Qt + In order for CMake to find the Qt5 find modules, you will need to set a QT_CMAKE_PREFIX_PATH environment variable pointing to your Qt installation. This can either be entered directly into your shell session before you build or in your shell profile (e.g.: ~/.bash_profile, ~/.bashrc, ~/.zshrc - this depends on your shell and environment). @@ -50,7 +53,8 @@ The path it needs to be set to will depend on where and how Qt5 was installed. e 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 +#### Generating build files + Create a build directory in the root of your checkout and then run the CMake build from there. This will keep the rest of the directory clean. mkdir build @@ -59,14 +63,15 @@ Create a build directory in the root of your checkout and then run the CMake bui If cmake gives you the same error message repeatedly after the build fails (e.g. you had a typo in the QT_CMAKE_PREFIX_PATH that you fixed but the `.cmake` files still cannot be found), try removing `CMakeCache.txt`. -####Variables +#### Variables + Any variables that need to be set for CMake to find dependencies can be set as ENV variables in your shell profile, or passed directly to CMake with a `-D` flag appended to the `cmake ..` command. For example, to pass the QT_CMAKE_PREFIX_PATH variable during build file generation: cmake .. -DQT_CMAKE_PREFIX_PATH=/usr/local/qt/5.6.2/lib/cmake -####Finding Dependencies +#### Finding Dependencies The following applies for dependencies we do not grab via CMake ExternalProject (OpenSSL is an example), or for dependencies you have opted not to grab as a CMake ExternalProject (via -DUSE_LOCAL_$NAME=0). The list of dependencies we grab by default as external projects can be found in [the CMake External Project Dependencies section](#cmake-external-project-dependencies). @@ -78,8 +83,8 @@ In the examples below the variable $NAME would be replaced by the name of the de * $NAME_ROOT_DIR - set this variable in your ENV * HIFI_LIB_DIR - set this variable in your ENV to your High Fidelity lib folder, should contain a folder '$name' -###Optional Components +### Optional Components -####Devices +#### Devices You can support external input/output devices such as Leap Motion, MIDI, and more by adding each individual SDK in the visible building path. Refer to the readme file available in each device folder in [interface/external/](interface/external) for the detailed explanation of the requirements to use the device. diff --git a/BUILD_ANDROID.md b/BUILD_ANDROID.md index 1f144bf3ba..d69d20ee8a 100644 --- a/BUILD_ANDROID.md +++ b/BUILD_ANDROID.md @@ -1,6 +1,6 @@ Please read the [general build guide](BUILD.md) for information on dependencies required for all platforms. Only Android specific instructions are found in this file. -###Android Dependencies +### Android Dependencies You will need the following tools to build our Android targets. @@ -17,23 +17,23 @@ You will need the following tools to build our Android targets. You will also need to cross-compile the dependencies required for all platforms for Android, and help CMake find these compiled libraries on your machine. -####Scribe +#### Scribe High Fidelity has a shader pre-processing tool called `scribe` that various libraries will call on during the build process. You must compile scribe using your native toolchain (following the build instructions for your platform) and then pass a CMake variable or set an ENV variable `SCRIBE_PATH` that is a path to the scribe executable. CMake will fatally error if it does not find the scribe executable while using the android toolchain. -####Optional Components +#### Optional Components * [Oculus Mobile SDK](https://developer.oculus.com/downloads/#sdk=mobile) ~> 0.4.2 -####ANDROID_LIB_DIR +#### ANDROID_LIB_DIR Since you won't be installing Android dependencies to system paths on your development machine, CMake will need a little help tracking down your Android dependencies. This is most easily accomplished by installing all Android dependencies in the same folder. You can place this folder wherever you like on your machine. In this build guide and across our CMakeLists files this folder is referred to as `ANDROID_LIB_DIR`. You can set `ANDROID_LIB_DIR` in your environment or by passing when you run CMake. -####Qt +#### Qt Install Qt 5.5.1 for Android for your host environment from the [Qt downloads page](http://www.qt.io/download/). Install Qt to ``$ANDROID_LIB_DIR/Qt``. This is required so that our root CMakeLists file can help CMake find your Android Qt installation. @@ -41,7 +41,7 @@ The component required for the Android build is the `Android armv7` component. If you would like to install Qt to a different location, or attempt to build with a different Qt version, you can pass `ANDROID_QT_CMAKE_PREFIX_PATH` to CMake. Point to the `cmake` folder inside `$VERSION_NUMBER/android_armv7/lib`. Otherwise, our root CMakeLists will set it to `$ANDROID_LIB_DIR/Qt/5.5/android_armv7/lib/cmake`. -####OpenSSL +#### OpenSSL Cross-compilation of OpenSSL has been tested from an OS X machine running 10.10 compiling OpenSSL 1.0.2. It is likely that the steps below will work for other OpenSSL versions than 1.0.2. @@ -76,7 +76,7 @@ This should generate libcrypto and libssl in the root of the OpenSSL directory. If you have been building other components it is possible that the OpenSSL compile will fail based on the values other cross-compilations (tbb, bullet) have set. Ensure that you are in a new terminal window to avoid compilation errors from previously set environment variables. -####Oculus Mobile SDK +#### Oculus Mobile SDK The Oculus Mobile SDK is optional, for Gear VR support. It is not required to compile gvr-interface. @@ -91,7 +91,7 @@ ndk-build This will create the liboculus.a archive that our FindLibOVR module will look for when cmake is run. -#####Hybrid testing +##### Hybrid testing Currently the 'vr_dual' mode that would allow us to run a hybrid app has limited support in the Oculus Mobile SDK. The best way to have an application we can launch without having to connect to the GearVR is to put the Gear VR Service into developer mode. This stops Oculus Home from taking over the device when it is plugged into the Gear VR headset, and allows the application to be launched from the Applications page. @@ -99,7 +99,7 @@ To put the Gear VR Service into developer mode you need an application with an O Once the application is on your device, go to `Settings->Application Manager->Gear VR Service->Manage Storage`. Tap on `VR Service Version` six times. It will scan your device to verify that you have an osig file in an application on your device, and then it will let you enable Developer mode. -###CMake +### CMake We use CMake to generate the makefiles that compile and deploy the Android APKs to your device. In order to create Makefiles for the Android targets, CMake requires that some environment variables are set, and that other variables are passed to it when it is run. diff --git a/BUILD_LINUX.md b/BUILD_LINUX.md index 34ba41894c..d40576a75d 100644 --- a/BUILD_LINUX.md +++ b/BUILD_LINUX.md @@ -1,6 +1,7 @@ Please read the [general build guide](BUILD.md) for information on dependencies required for all platforms. Only Linux specific instructions are found in this file. -###Qt5 Dependencies +### Qt5 Dependencies + Should you choose not to install Qt5 via a package manager that handles dependencies for you, you may be missing some Qt5 dependencies. On Ubuntu, for example, the following additional packages are required: libasound2 libxmu-dev libxi-dev freeglut3-dev libasound2-dev libjack0 libjack-dev libxrandr-dev libudev-dev libssl-dev diff --git a/BUILD_OSX.md b/BUILD_OSX.md index afd3fa040c..3365627b8c 100644 --- a/BUILD_OSX.md +++ b/BUILD_OSX.md @@ -1,12 +1,13 @@ Please read the [general build guide](BUILD.md) for information on dependencies required for all platforms. Only OS X specific instructions are found in this file. -###Homebrew +### Homebrew + [Homebrew](https://brew.sh/) is an excellent package manager for OS X. It makes install of some High Fidelity dependencies very simple. brew tap homebrew/versions brew install cmake openssl -###OpenSSL +### OpenSSL Assuming you've installed OpenSSL using the homebrew instructions above, you'll need to set OPENSSL_ROOT_DIR so CMake can find your installations. For OpenSSL installed via homebrew, set OPENSSL_ROOT_DIR: @@ -15,7 +16,8 @@ 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 +### Qt + 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). Keep the default components checked when going through the installer. @@ -23,7 +25,8 @@ 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.2/5.6/clang_64/lib/cmake/` directory. -###Xcode +### Xcode + If Xcode is your editor of choice, you can ask CMake to generate Xcode project files instead of Unix Makefiles. cmake .. -GXcode diff --git a/BUILD_WIN.md b/BUILD_WIN.md index 841cfba3b3..818a176f75 100644 --- a/BUILD_WIN.md +++ b/BUILD_WIN.md @@ -1,62 +1,66 @@ This is a stand-alone guide for creating your first High Fidelity build for Windows 64-bit. -###Step 1. Installing Visual Studio 2013 +## Building High Fidelity + +### Step 1. Installing Visual Studio 2013 If you don't already have the Community or Professional edition of Visual Studio 2013, download and install [Visual Studio Community 2013](https://www.visualstudio.com/en-us/news/releasenotes/vs2013-community-vs). You do not need to install any of the optional components when going through the installer. Note: Newer versions of Visual Studio are not yet compatible. -###Step 2. Installing CMake +### Step 2. Installing CMake 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 +### Step 3. Installing Qt 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). Keep the default components checked when going through the installer. -###Step 4. Setting Qt Environment Variable +### 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 +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": `%QT_DIR%\5.6\msvc2013_64\lib\cmake` -###Step 5. Installing OpenSSL +### Step 5. Installing OpenSSL -Download and install the [Win64 OpenSSL v1.0.2k Installer](https://slproweb.com/download/Win64OpenSSL-1_0_2k.exe). +Download and install the [Win64 OpenSSL v1.0.2L Installer](https://slproweb.com/download/Win64OpenSSL-1_0_2L.exe). -###Step 6. Running CMake to Generate Build Files +### Step 6. Running CMake to Generate Build Files Run Command Prompt from Start and run the following commands: - cd "%HIFI_DIR%" - mkdir build - cd build - cmake .. -G "Visual Studio 12 Win64" +```` +cd "%HIFI_DIR%" +mkdir build +cd build +cmake .. -G "Visual Studio 12 Win64" +```` -Where %HIFI_DIR% is the directory for the highfidelity repository. +Where `%HIFI_DIR%` is the directory for the highfidelity repository. -###Step 7. Making a Build +### Step 7. Making a Build -Open '%HIFI_DIR%\build\hifi.sln' using Visual Studio. +Open `%HIFI_DIR%\build\hifi.sln` using Visual Studio. Change the Solution Configuration (next to the green play button) from "Debug" to "Release" for best performance. -Run Build > Build Solution. +Run `Build > Build Solution`. -###Step 8. Testing Interface +### Step 8. Testing Interface Create another environment variable (see Step #4) -* Set "Variable name": _NO_DEBUG_HEAP -* Set "Variable value": 1 +* Set "Variable name": `_NO_DEBUG_HEAP` +* Set "Variable value": `1` -In Visual Studio, right+click "interface" under the Apps folder in Solution Explorer and select "Set as Startup Project". Run Debug > Start Debugging. +In Visual Studio, right+click "interface" under the Apps folder in Solution Explorer and select "Set as Startup Project". Run `Debug > Start Debugging`. Now, you should have a full build of High Fidelity and be able to run the Interface using Visual Studio. Please check our [Docs](https://wiki.highfidelity.com/wiki/Main_Page) for more information regarding the programming workflow. -Note: You can also run Interface by launching it from command line or File Explorer from %HIFI_DIR%\build\interface\Release\interface.exe +Note: You can also run Interface by launching it from command line or File Explorer from `%HIFI_DIR%\build\interface\Release\interface.exe` -###Troubleshooting +## Troubleshooting For any problems after Step #6, first try this: * Delete your locally cloned copy of the highfidelity repository @@ -64,18 +68,18 @@ For any problems after Step #6, first try this: * Redownload the [repository](https://github.com/highfidelity/hifi) * Restart directions from Step #6 -####CMake gives you the same error message repeatedly after the build fails +#### CMake gives you the same error message repeatedly after the build fails -Remove `CMakeCache.txt` found in the '%HIFI_DIR%\build' directory +Remove `CMakeCache.txt` found in the `%HIFI_DIR%\build` directory. -####nmake cannot be found +#### nmake cannot be found Make sure nmake.exe is located at the following path: + C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin If not, add the directory where nmake is located to the PATH environment variable. -####Qt is throwing an error - -Make sure you have the correct version (5.6.2) installed and 'QT_CMAKE_PREFIX_PATH' environment variable is set correctly. +#### Qt is throwing an error +Make sure you have the correct version (5.6.2) installed and `QT_CMAKE_PREFIX_PATH` environment variable is set correctly. diff --git a/INSTALL.md b/INSTALL.md index 701752f6af..79d7e96977 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -2,15 +2,15 @@ Follow the [build guide](BUILD.md) to figure out how to build High Fidelity for During generation, CMake should produce an `install` target and a `package` target. -###Install +### Install The `install` target will copy the High Fidelity targets and their dependencies to your `CMAKE_INSTALL_PREFIX`. -###Packaging +### Packaging To produce an installer, run the `package` target. -####Windows +#### Windows To produce an executable installer on Windows, the following are required: @@ -20,6 +20,6 @@ To produce an executable installer on Windows, the following are required: Run the `package` target to create an executable installer using the Nullsoft Scriptable Install System. -####OS X +#### OS X Run the `package` target to create an Apple Disk Image (.dmg). diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 47836727fe..1dc3aefb61 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -166,11 +166,7 @@ void Agent::run() { // Setup MessagesClient auto messagesClient = DependencyManager::set(); - QThread* messagesThread = new QThread; - messagesThread->setObjectName("Messages Client Thread"); - messagesClient->moveToThread(messagesThread); - connect(messagesThread, &QThread::started, messagesClient.data(), &MessagesClient::init); - messagesThread->start(); + messagesClient->startThread(); // make sure we hear about connected nodes so we can grab an ATP script if a request is pending connect(nodeList.data(), &LimitedNodeList::nodeActivated, this, &Agent::nodeActivated); @@ -410,6 +406,7 @@ void Agent::executeScript() { bool openedInLastBlock = !_audioGateOpen && audioGateOpen; // the gate just opened bool closedInLastBlock = _audioGateOpen && !audioGateOpen; // the gate just closed _audioGateOpen = audioGateOpen; + Q_UNUSED(openedInLastBlock); // the codec must be flushed to silence before sending silent packets, // so delay the transition to silent packets by one packet after becoming silent. diff --git a/assignment-client/src/AssignmentClient.cpp b/assignment-client/src/AssignmentClient.cpp index 0869329d68..abfc66ac55 100644 --- a/assignment-client/src/AssignmentClient.cpp +++ b/assignment-client/src/AssignmentClient.cpp @@ -69,17 +69,7 @@ AssignmentClient::AssignmentClient(Assignment::Type requestAssignmentType, QStri DependencyManager::set(); DependencyManager::set(); - // setup a thread for the NodeList and its PacketReceiver - QThread* nodeThread = new QThread(this); - nodeThread->setObjectName("NodeList Thread"); - nodeThread->start(); - - // make sure the node thread is given highest priority - nodeThread->setPriority(QThread::TimeCriticalPriority); - - // put the NodeList on the node thread - nodeList->moveToThread(nodeThread); - + nodeList->startThread(); // set the logging target to the the CHILD_TARGET_NAME LogHandler::getInstance().setTargetName(ASSIGNMENT_CLIENT_TARGET_NAME); @@ -166,14 +156,8 @@ void AssignmentClient::stopAssignmentClient() { } AssignmentClient::~AssignmentClient() { - QThread* nodeThread = DependencyManager::get()->thread(); - // remove the NodeList from the DependencyManager DependencyManager::destroy(); - - // ask the node thread to quit and wait until it is done - nodeThread->quit(); - nodeThread->wait(); } void AssignmentClient::aboutToQuit() { diff --git a/assignment-client/src/audio/AudioMixerSlavePool.h b/assignment-client/src/audio/AudioMixerSlavePool.h index 4082ea18d9..25047faa89 100644 --- a/assignment-client/src/audio/AudioMixerSlavePool.h +++ b/assignment-client/src/audio/AudioMixerSlavePool.h @@ -16,10 +16,10 @@ #include #include -#include - #include +#include + #include "AudioMixerSlave.h" class AudioMixerSlavePool; diff --git a/assignment-client/src/avatars/AvatarMixerSlavePool.h b/assignment-client/src/avatars/AvatarMixerSlavePool.h index e6ac2a1f4e..15bd681b2c 100644 --- a/assignment-client/src/avatars/AvatarMixerSlavePool.h +++ b/assignment-client/src/avatars/AvatarMixerSlavePool.h @@ -16,10 +16,9 @@ #include #include -#include - #include +#include #include #include "AvatarMixerSlave.h" diff --git a/assignment-client/src/scripts/EntityScriptServer.cpp b/assignment-client/src/scripts/EntityScriptServer.cpp index 270a22e17b..1b226ab642 100644 --- a/assignment-client/src/scripts/EntityScriptServer.cpp +++ b/assignment-client/src/scripts/EntityScriptServer.cpp @@ -236,11 +236,7 @@ void EntityScriptServer::run() { // Setup MessagesClient auto messagesClient = DependencyManager::set(); - QThread* messagesThread = new QThread; - messagesThread->setObjectName("Messages Client Thread"); - messagesClient->moveToThread(messagesThread); - connect(messagesThread, &QThread::started, messagesClient.data(), &MessagesClient::init); - messagesThread->start(); + messagesClient->startThread(); DomainHandler& domainHandler = DependencyManager::get()->getDomainHandler(); connect(&domainHandler, &DomainHandler::settingsReceived, this, &EntityScriptServer::handleSettings); diff --git a/cmake/externals/nvtt/CMakeLists.txt b/cmake/externals/nvtt/CMakeLists.txt index 0e1c240c77..fa9d7b5ea1 100644 --- a/cmake/externals/nvtt/CMakeLists.txt +++ b/cmake/externals/nvtt/CMakeLists.txt @@ -8,8 +8,8 @@ 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 + URL http://s3.amazonaws.com/hifi-public/dependencies/nvtt-win-2.1.0.hifi.zip + URL_MD5 10da01cf601f88f6dc12a6bc13c89136 CONFIGURE_COMMAND "" BUILD_COMMAND "" INSTALL_COMMAND "" @@ -29,8 +29,8 @@ else () ExternalProject_Add( ${EXTERNAL_NAME} - URL http://hifi-public.s3.amazonaws.com/dependencies/nvidia-texture-tools-2.1.0.zip - URL_MD5 81b8fa6a9ee3f986088eb6e2215d6a57 + URL http://hifi-public.s3.amazonaws.com/dependencies/nvidia-texture-tools-2.1.0.hifi.zip + URL_MD5 5794b950f8b265a9a41b2839b3bf7ebb 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 diff --git a/domain-server/resources/web/content/index.shtml b/domain-server/resources/web/content/index.shtml index c7eb765878..e1ba5499b6 100644 --- a/domain-server/resources/web/content/index.shtml +++ b/domain-server/resources/web/content/index.shtml @@ -21,7 +21,7 @@

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
+
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/gvr-interface/src/RenderingClient.cpp b/gvr-interface/src/RenderingClient.cpp index e6d2f6b585..b7c6f30a73 100644 --- a/gvr-interface/src/RenderingClient.cpp +++ b/gvr-interface/src/RenderingClient.cpp @@ -37,20 +37,12 @@ RenderingClient::RenderingClient(QObject *parent, const QString& launchURLString DependencyManager::set(); // get our audio client setup on its own thread - QThread* audioThread = new QThread(); auto audioClient = DependencyManager::set(); - audioClient->setPositionGetter(getPositionForAudio); audioClient->setOrientationGetter(getOrientationForAudio); + audioClient->startThread(); - audioClient->moveToThread(audioThread); - connect(audioThread, &QThread::started, audioClient.data(), &AudioClient::start); - connect(audioClient.data(), &AudioClient::destroyed, audioThread, &QThread::quit); - connect(audioThread, &QThread::finished, audioThread, &QThread::deleteLater); - - audioThread->start(); - - + connect(&_avatarTimer, &QTimer::timeout, this, &RenderingClient::sendAvatarPacket); _avatarTimer.setInterval(16); // 60 FPS _avatarTimer.start(); diff --git a/interface/resources/avatar/avatar-animation.json b/interface/resources/avatar/avatar-animation.json index 35f2d4b9af..1412b45968 100644 --- a/interface/resources/avatar/avatar-animation.json +++ b/interface/resources/avatar/avatar-animation.json @@ -103,8 +103,8 @@ "rotationVar": "spine2Rotation", "typeVar": "spine2Type", "weightVar": "spine2Weight", - "weight": 1.0, - "flexCoefficients": [1.0, 0.5, 0.5] + "weight": 2.0, + "flexCoefficients": [1.0, 0.5, 0.25] }, { "jointName": "Head", @@ -113,7 +113,7 @@ "typeVar": "headType", "weightVar": "headWeight", "weight": 4.0, - "flexCoefficients": [1, 0.05, 0.25, 0.25, 0.25] + "flexCoefficients": [1, 0.5, 0.25, 0.2, 0.1] }, { "jointName": "LeftArm", diff --git a/interface/resources/config/render.json b/interface/resources/config/render.json index 414d94e11e..b5b72d7d07 100644 --- a/interface/resources/config/render.json +++ b/interface/resources/config/render.json @@ -1,14 +1,16 @@ { - "RenderShadowTask": { - "Enabled": { - "enabled": true - } - }, - "RenderDeferredTask": { - "AmbientOcclusion": { + "RenderMainView": { + "RenderShadowTask": { "Enabled": { "enabled": true } + }, + "RenderDeferredTask": { + "AmbientOcclusion": { + "Enabled": { + "enabled": true + } + } } } } diff --git a/interface/resources/controllers/oculus_touch.json b/interface/resources/controllers/oculus_touch.json index aa574e36dd..03fc1cbefb 100644 --- a/interface/resources/controllers/oculus_touch.json +++ b/interface/resources/controllers/oculus_touch.json @@ -57,6 +57,8 @@ { "from": "OculusTouch.LeftThumbUp", "to": "Standard.LeftThumbUp" }, { "from": "OculusTouch.RightThumbUp", "to": "Standard.RightThumbUp" }, { "from": "OculusTouch.LeftIndexPoint", "to": "Standard.LeftIndexPoint" }, - { "from": "OculusTouch.RightIndexPoint", "to": "Standard.RightIndexPoint" } + { "from": "OculusTouch.RightIndexPoint", "to": "Standard.RightIndexPoint" }, + + { "from": "OculusTouch.Head", "to" : "Standard.Head", "when" : [ "Application.InHMD"] } ] } diff --git a/interface/resources/controllers/vive.json b/interface/resources/controllers/vive.json index 591b6f515e..a0e9bd30d4 100644 --- a/interface/resources/controllers/vive.json +++ b/interface/resources/controllers/vive.json @@ -35,35 +35,35 @@ { "from": "Vive.RightApplicationMenu", "to": "Standard.RightSecondaryThumb" }, { "from": "Vive.LeftHand", "to": "Standard.LeftHand", "when": [ "Application.InHMD" ] }, - { "from": "Vive.RightHand", "to": "Standard.RightHand", "when": [ "Application.InHMD" ] }, + { "from": "Vive.RightHand", "to": "Standard.RightHand", "when": [ "Application.InHMD" ] }, { "from": "Vive.LeftFoot", "to" : "Standard.LeftFoot", "filters" : [{"type" : "lowVelocity", "rotation" : 1.0, "translation": 1.0}], - "when": [ "Application.InHMD"] + "when": [ "Application.InHMD" ] }, { "from": "Vive.RightFoot", "to" : "Standard.RightFoot", "filters" : [{"type" : "lowVelocity", "rotation" : 1.0, "translation": 1.0}], - "when": [ "Application.InHMD"] + "when": [ "Application.InHMD" ] }, { "from": "Vive.Hips", "to" : "Standard.Hips", "filters" : [{"type" : "lowVelocity", "rotation" : 0.01, "translation": 0.01}], - "when": [ "Application.InHMD"] + "when": [ "Application.InHMD" ] }, { "from": "Vive.Spine2", "to" : "Standard.Spine2", "filters" : [{"type" : "lowVelocity", "rotation" : 0.01, "translation": 0.01}], - "when": [ "Application.InHMD"] + "when": [ "Application.InHMD" ] }, - { "from": "Vive.Head", "to" : "Standard.Head", "when" : [ "Application.InHMD"] }, + { "from": "Vive.Head", "to" : "Standard.Head", "when": [ "Application.InHMD" ] }, - { "from": "Vive.RightArm", "to" : "Standard.RightArm", "when" : [ "Application.InHMD"] }, - { "from": "Vive.LeftArm", "to" : "Standard.LeftArm", "when" : [ "Application.InHMD"] } + { "from": "Vive.RightArm", "to" : "Standard.RightArm", "when": [ "Application.InHMD" ] }, + { "from": "Vive.LeftArm", "to" : "Standard.LeftArm", "when": [ "Application.InHMD" ] } ] } diff --git a/interface/resources/fonts/hifi-glyphs.ttf b/interface/resources/fonts/hifi-glyphs.ttf index 93f6fe6d13..548f61e1de 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/createGlobalEventBridge.js b/interface/resources/html/createGlobalEventBridge.js index 027d6fe8db..4a0de464c3 100644 --- a/interface/resources/html/createGlobalEventBridge.js +++ b/interface/resources/html/createGlobalEventBridge.js @@ -32,7 +32,7 @@ var EventBridge; var webChannel = new QWebChannel(qt.webChannelTransport, function (channel) { // replace the TempEventBridge with the real one. var tempEventBridge = EventBridge; - EventBridge = channel.objects.eventBridgeWrapper.eventBridge; + EventBridge = channel.objects.eventBridge; tempEventBridge._callbacks.forEach(function (callback) { EventBridge.scriptEventReceived.connect(callback); }); diff --git a/interface/resources/meshes/Jointy3/Jointy3.fbx b/interface/resources/meshes/Jointy3/Jointy3.fbx deleted file mode 100644 index 9df7b17eac..0000000000 Binary files a/interface/resources/meshes/Jointy3/Jointy3.fbx and /dev/null differ diff --git a/interface/resources/meshes/defaultAvatar_full.fst b/interface/resources/meshes/defaultAvatar_full.fst index 3abbaf9ff7..eb8e356196 100644 --- a/interface/resources/meshes/defaultAvatar_full.fst +++ b/interface/resources/meshes/defaultAvatar_full.fst @@ -1,85 +1,89 @@ -name = Jointy3 +name = mannequin type = body+head scale = 1 -filename = Jointy3/Jointy3.fbx -texdir = Jointy3/textures +filename = mannequin/mannequin.baked.fbx +joint = jointEyeLeft = LeftEye +joint = jointRightHand = RightHand +joint = jointHead = Head +joint = jointEyeRight = RightEye +joint = jointLean = Spine joint = jointNeck = Neck joint = jointLeftHand = LeftHand -joint = jointEyeRight = RightEye -joint = jointHead = Head -joint = jointRightHand = RightHand joint = jointRoot = Hips -joint = jointLean = Spine -joint = jointEyeLeft = LeftEye freeJoint = LeftArm freeJoint = LeftForeArm freeJoint = RightArm freeJoint = RightForeArm -jointIndex = RightHand = 17 -jointIndex = LeftHandIndex3 = 56 -jointIndex = Hips = 0 -jointIndex = LeftHandRing2 = 47 -jointIndex = LeftHandThumb3 = 60 -jointIndex = RightShoulder = 14 -jointIndex = RightHandRing1 = 30 -jointIndex = RightHandRing3 = 32 -jointIndex = LeftHandPinky4 = 45 -jointIndex = LeftHandRing1 = 46 -jointIndex = LeftFoot = 8 -jointIndex = RightHandIndex2 = 23 -jointIndex = RightToeBase = 4 -jointIndex = RightHandMiddle4 = 29 -jointIndex = RightHandPinky4 = 37 -jointIndex = LeftToe_End = 10 -jointIndex = RightEye = 66 -jointIndex = RightHandPinky2 = 35 -jointIndex = RightHandRing2 = 31 -jointIndex = LeftHand = 41 -jointIndex = RightToe_End = 5 -jointIndex = LeftEye = 65 -jointIndex = LeftHandThumb2 = 59 -jointIndex = pCylinder73Shape1 = 67 -jointIndex = LeftShoulder = 38 -jointIndex = LeftHandIndex2 = 55 -jointIndex = RightForeArm = 16 -jointIndex = LeftHandMiddle2 = 51 -jointIndex = RightHandRing4 = 33 -jointIndex = LeftLeg = 7 -jointIndex = LeftHandThumb4 = 61 -jointIndex = LeftForeArm = 40 -jointIndex = HeadTop_End = 64 -jointIndex = RightHandPinky1 = 34 -jointIndex = RightHandIndex1 = 22 -jointIndex = LeftHandIndex1 = 54 -jointIndex = RightLeg = 2 -jointIndex = RightHandIndex4 = 25 -jointIndex = Neck = 62 -jointIndex = LeftHandMiddle1 = 50 -jointIndex = RightHandPinky3 = 36 -jointIndex = LeftHandPinky2 = 43 -jointIndex = RightHandMiddle3 = 28 -jointIndex = RightHandThumb4 = 21 -jointIndex = LeftUpLeg = 6 -jointIndex = RightFoot = 3 -jointIndex = LeftHandThumb1 = 58 -jointIndex = LeftArm = 39 -jointIndex = RightHandMiddle1 = 26 -jointIndex = LeftHandRing3 = 48 -jointIndex = LeftHandMiddle4 = 53 -jointIndex = RightUpLeg = 1 -jointIndex = RightHandMiddle2 = 27 -jointIndex = LeftToeBase = 9 -jointIndex = RightHandThumb2 = 19 -jointIndex = Spine2 = 13 -jointIndex = Spine = 11 -jointIndex = LeftHandRing4 = 49 -jointIndex = Head = 63 -jointIndex = LeftHandPinky3 = 44 +bs = EyeBlink_L = blink = 1 +bs = JawOpen = mouth_Open = 1 +bs = LipsFunnel = Oo = 1 +bs = BrowsU_L = brow_Up = 1 +jointIndex = RightHandIndex2 = 27 +jointIndex = LeftHandIndex2 = 51 +jointIndex = RightUpLeg = 6 +jointIndex = RightToe_End = 10 +jointIndex = RightEye = 65 jointIndex = LeftHandPinky1 = 42 -jointIndex = RightHandThumb1 = 18 -jointIndex = LeftHandIndex4 = 57 -jointIndex = LeftHandMiddle3 = 52 -jointIndex = RightHandIndex3 = 24 -jointIndex = Spine1 = 12 +jointIndex = RightHandRing1 = 22 +jointIndex = face = 67 +jointIndex = LeftUpLeg = 1 +jointIndex = LeftHand = 41 +jointIndex = LeftHandMiddle1 = 58 +jointIndex = LeftHandIndex1 = 50 +jointIndex = LeftEye = 64 +jointIndex = RightHandIndex1 = 26 +jointIndex = LeftHandPinky4 = 45 jointIndex = RightArm = 15 -jointIndex = RightHandThumb3 = 20 +jointIndex = LeftShoulder = 38 +jointIndex = RightHandPinky2 = 19 +jointIndex = RightHandThumb1 = 30 +jointIndex = RightForeArm = 16 +jointIndex = LeftHandMiddle3 = 60 +jointIndex = Neck = 62 +jointIndex = LeftHandThumb1 = 54 +jointIndex = RightHandMiddle2 = 35 +jointIndex = LeftHandMiddle4 = 61 +jointIndex = mannequin = 68 +jointIndex = Spine1 = 12 +jointIndex = RightFoot = 8 +jointIndex = RightHand = 17 +jointIndex = LeftHandIndex3 = 52 +jointIndex = RightHandIndex3 = 28 +jointIndex = RightHandMiddle4 = 37 +jointIndex = LeftLeg = 2 +jointIndex = RightHandMiddle1 = 34 +jointIndex = Spine2 = 13 +jointIndex = LeftHandMiddle2 = 59 +jointIndex = LeftHandPinky3 = 44 +jointIndex = LeftHandThumb3 = 56 +jointIndex = LeftHandRing4 = 49 +jointIndex = RightHandThumb2 = 31 +jointIndex = LeftHandRing3 = 48 +jointIndex = HeadTop_End = 66 +jointIndex = LeftHandThumb4 = 57 +jointIndex = RightHandThumb3 = 32 +jointIndex = RightHandPinky1 = 18 +jointIndex = RightLeg = 7 +jointIndex = RightHandMiddle3 = 36 +jointIndex = RightHandPinky3 = 20 +jointIndex = LeftToeBase = 4 +jointIndex = LeftForeArm = 40 +jointIndex = RightShoulder = 14 +jointIndex = LeftHandRing2 = 47 +jointIndex = LeftHandThumb2 = 55 +jointIndex = Head = 63 +jointIndex = RightHandRing4 = 25 +jointIndex = LeftHandRing1 = 46 +jointIndex = LeftFoot = 3 +jointIndex = RightHandRing3 = 24 +jointIndex = RightHandThumb4 = 33 +jointIndex = LeftArm = 39 +jointIndex = LeftToe_End = 5 +jointIndex = RightToeBase = 9 +jointIndex = RightHandPinky4 = 21 +jointIndex = Spine = 11 +jointIndex = LeftHandIndex4 = 53 +jointIndex = LeftHandPinky2 = 43 +jointIndex = RightHandIndex4 = 29 +jointIndex = Hips = 0 +jointIndex = RightHandRing2 = 23 diff --git a/interface/resources/meshes/mannequin/Eyes.ktx b/interface/resources/meshes/mannequin/Eyes.ktx new file mode 100755 index 0000000000..4da922936b Binary files /dev/null and b/interface/resources/meshes/mannequin/Eyes.ktx differ diff --git a/interface/resources/meshes/mannequin/lambert1_Base_Color.ktx b/interface/resources/meshes/mannequin/lambert1_Base_Color.ktx new file mode 100755 index 0000000000..fcca382445 Binary files /dev/null and b/interface/resources/meshes/mannequin/lambert1_Base_Color.ktx differ diff --git a/interface/resources/meshes/mannequin/lambert1_Normal_OpenGL.ktx b/interface/resources/meshes/mannequin/lambert1_Normal_OpenGL.ktx new file mode 100755 index 0000000000..3ae717c5d7 Binary files /dev/null and b/interface/resources/meshes/mannequin/lambert1_Normal_OpenGL.ktx differ diff --git a/interface/resources/meshes/mannequin/lambert1_Roughness.ktx b/interface/resources/meshes/mannequin/lambert1_Roughness.ktx new file mode 100755 index 0000000000..fe9b42a54b Binary files /dev/null and b/interface/resources/meshes/mannequin/lambert1_Roughness.ktx differ diff --git a/interface/resources/meshes/mannequin/mannequin.baked.fbx b/interface/resources/meshes/mannequin/mannequin.baked.fbx new file mode 100755 index 0000000000..b405b4cfbb Binary files /dev/null and b/interface/resources/meshes/mannequin/mannequin.baked.fbx differ diff --git a/interface/resources/qml/AssetServer.qml b/interface/resources/qml/AssetServer.qml index bf46ba121c..eb47afc951 100644 --- a/interface/resources/qml/AssetServer.qml +++ b/interface/resources/qml/AssetServer.qml @@ -177,7 +177,7 @@ ScrollingWindow { SHAPE_TYPE_STATIC_MESH ], checkStateOnDisable: false, - warningOnDisable: "Models with 'Exact' automatic collisions cannot be dynamic" + warningOnDisable: "Models with 'Exact' automatic collisions cannot be dynamic, and should not be used as floors" } }); diff --git a/interface/resources/qml/Browser.qml b/interface/resources/qml/Browser.qml index 47fb610469..55927fda24 100644 --- a/interface/resources/qml/Browser.qml +++ b/interface/resources/qml/Browser.qml @@ -2,7 +2,6 @@ import QtQuick 2.5 import QtQuick.Controls 1.2 import QtWebChannel 1.0 import QtWebEngine 1.2 -import FileTypeProfile 1.0 import "controls-uit" import "styles" as HifiStyles @@ -22,8 +21,6 @@ ScrollingWindow { property alias url: webview.url property alias webView: webview - property alias eventBridge: eventBridgeWrapper.eventBridge - signal loadingChanged(int status) x: 100 @@ -209,21 +206,7 @@ ScrollingWindow { WebView { id: webview url: "https://highfidelity.com/" - - property alias eventBridgeWrapper: eventBridgeWrapper - - QtObject { - id: eventBridgeWrapper - WebChannel.id: "eventBridgeWrapper" - property var eventBridge; - } - - profile: FileTypeProfile { - id: webviewProfile - storageName: "qmlWebEngine" - } - - webChannel.registeredObjects: [eventBridgeWrapper] + profile: FileTypeProfile; // Create a global EventBridge object for raiseAndLowerKeyboard. WebEngineScript { @@ -271,6 +254,8 @@ ScrollingWindow { } Component.onCompleted: { + webChannel.registerObject("eventBridge", eventBridge); + webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper); desktop.initWebviewProfileHandlers(webview.profile); } } diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml index d40b1595ba..d2daf0fa1d 100644 --- a/interface/resources/qml/QmlWebWindow.qml +++ b/interface/resources/qml/QmlWebWindow.qml @@ -26,15 +26,8 @@ Windows.ScrollingWindow { // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer destroyOnCloseButton: false property alias source: webview.url - property alias eventBridge: eventBridgeWrapper.eventBridge; property alias scriptUrl: webview.userScriptUrl - QtObject { - id: eventBridgeWrapper - WebChannel.id: "eventBridgeWrapper" - property var eventBridge; - } - // This is for JS/QML communication, which is unused in a WebWindow, // but not having this here results in spurious warnings about a // missing signal @@ -70,7 +63,6 @@ Windows.ScrollingWindow { url: "about:blank" anchors.fill: parent focus: true - webChannel.registeredObjects: [eventBridgeWrapper] property string userScriptUrl: "" @@ -107,6 +99,8 @@ Windows.ScrollingWindow { } Component.onCompleted: { + webChannel.registerObject("eventBridge", eventBridge); + webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper); eventBridge.webEventReceived.connect(onWebEventReceived); } } diff --git a/interface/resources/qml/QmlWindow.qml b/interface/resources/qml/QmlWindow.qml index ac18d36ce6..9ef151b32e 100644 --- a/interface/resources/qml/QmlWindow.qml +++ b/interface/resources/qml/QmlWindow.qml @@ -30,15 +30,6 @@ Windows.Window { property bool keyboardRaised: false property bool punctuationMode: false - // JavaScript event bridge object in case QML content includes Web content. - property alias eventBridge: eventBridgeWrapper.eventBridge; - - QtObject { - id: eventBridgeWrapper - WebChannel.id: "eventBridgeWrapper" - property var eventBridge; - } - onSourceChanged: { if (dynamicContent) { dynamicContent.destroy(); diff --git a/interface/resources/qml/TabletBrowser.qml b/interface/resources/qml/TabletBrowser.qml index ee4d05a701..c3d879c513 100644 --- a/interface/resources/qml/TabletBrowser.qml +++ b/interface/resources/qml/TabletBrowser.qml @@ -8,7 +8,6 @@ import "controls-uit" as HifiControls import "styles" as HifiStyles import "styles-uit" import "windows" -import HFTabletWebEngineProfile 1.0 Item { id: root @@ -19,7 +18,6 @@ Item { property variant permissionsBar: {'securityOrigin':'none','feature':'none'} property alias url: webview.url property WebEngineView webView: webview - property alias eventBridge: eventBridgeWrapper.eventBridge property bool canGoBack: webview.canGoBack property bool canGoForward: webview.canGoForward @@ -33,12 +31,6 @@ Item { webview.profile = profile; } - QtObject { - id: eventBridgeWrapper - WebChannel.id: "eventBridgeWrapper" - property var eventBridge; - } - WebEngineView { id: webview objectName: "webEngineView" @@ -47,10 +39,7 @@ Item { width: parent.width height: keyboardEnabled && keyboardRaised ? parent.height - keyboard.height : parent.height - profile: HFTabletWebEngineProfile { - id: webviewTabletProfile - storageName: "qmlTabletWebEngine" - } + profile: HFTabletWebEngineProfile; property string userScriptUrl: "" @@ -82,9 +71,10 @@ Item { property string newUrl: "" - webChannel.registeredObjects: [eventBridgeWrapper] - Component.onCompleted: { + webChannel.registerObject("eventBridge", eventBridge); + webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper); + // Ensure the JS from the web-engine makes it to our logging webview.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { console.log("Web Entity JS message: " + sourceID + " " + lineNumber + " " + message); diff --git a/interface/resources/qml/ToolWindow.qml b/interface/resources/qml/ToolWindow.qml index 9c0b0a8c1a..b1120058f9 100644 --- a/interface/resources/qml/ToolWindow.qml +++ b/interface/resources/qml/ToolWindow.qml @@ -79,15 +79,11 @@ ScrollingWindow { id: webView anchors.fill: parent enabled: false - property alias eventBridgeWrapper: eventBridgeWrapper - - QtObject { - id: eventBridgeWrapper - WebChannel.id: "eventBridgeWrapper" - property var eventBridge + Component.onCompleted: { + webChannel.registerObject("eventBridge", eventBridge); + webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper); } - webChannel.registeredObjects: [eventBridgeWrapper] onEnabledChanged: toolWindow.updateVisiblity() } } @@ -251,12 +247,9 @@ ScrollingWindow { tab.enabled = true; tab.originalUrl = properties.source; - var eventBridge = properties.eventBridge; - var result = tab.item; result.enabled = true; tabView.tabCount++; - result.eventBridgeWrapper.eventBridge = eventBridge; result.url = properties.source; return result; } diff --git a/interface/resources/qml/controls-uit/BaseWebView.qml b/interface/resources/qml/controls-uit/BaseWebView.qml index 670aea71aa..3ca57f03bf 100644 --- a/interface/resources/qml/controls-uit/BaseWebView.qml +++ b/interface/resources/qml/controls-uit/BaseWebView.qml @@ -10,13 +10,10 @@ import QtQuick 2.5 import QtWebEngine 1.2 -import HFWebEngineProfile 1.0 WebEngineView { id: root - // profile: desktop.browserProfile - Component.onCompleted: { console.log("Connecting JS messaging to Hifi Logging") // Ensure the JS from the web-engine makes it to our logging diff --git a/interface/resources/qml/controls/TabletWebScreen.qml b/interface/resources/qml/controls/TabletWebScreen.qml index 93ded724a1..68f8226e21 100644 --- a/interface/resources/qml/controls/TabletWebScreen.qml +++ b/interface/resources/qml/controls/TabletWebScreen.qml @@ -2,12 +2,10 @@ 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 @@ -23,12 +21,6 @@ Item { } */ - QtObject { - id: eventBridgeWrapper - WebChannel.id: "eventBridgeWrapper" - property var eventBridge; - } - property alias viewProfile: root.profile WebEngineView { @@ -39,10 +31,7 @@ Item { width: parent.width height: keyboardEnabled && keyboardRaised ? parent.height - keyboard.height : parent.height - profile: HFTabletWebEngineProfile { - id: webviewProfile - storageName: "qmlTabletWebEngine" - } + profile: HFTabletWebEngineProfile; property string userScriptUrl: "" @@ -75,10 +64,11 @@ Item { userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ] property string newUrl: "" - - webChannel.registeredObjects: [eventBridgeWrapper] + Component.onCompleted: { + webChannel.registerObject("eventBridge", eventBridge); + webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper); // 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); diff --git a/interface/resources/qml/controls/TabletWebView.qml b/interface/resources/qml/controls/TabletWebView.qml index 215ac68ac0..0a5a68717e 100644 --- a/interface/resources/qml/controls/TabletWebView.qml +++ b/interface/resources/qml/controls/TabletWebView.qml @@ -2,7 +2,6 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 import QtWebEngine 1.2 import QtWebChannel 1.0 -import HFTabletWebEngineProfile 1.0 import "../controls-uit" as HiFiControls import "../styles" as HifiStyles import "../styles-uit" @@ -18,7 +17,6 @@ Item { property int headerHeight: 70 property string url property string scriptURL - property alias eventBridge: eventBridgeWrapper.eventBridge property bool keyboardEnabled: false property bool keyboardRaised: false property bool punctuationMode: false @@ -136,12 +134,6 @@ Item { loadUrl(url); } - QtObject { - id: eventBridgeWrapper - WebChannel.id: "eventBridgeWrapper" - property var eventBridge; - } - WebEngineView { id: webview objectName: "webEngineView" @@ -150,10 +142,7 @@ Item { width: parent.width height: keyboardEnabled && keyboardRaised ? parent.height - keyboard.height - web.headerHeight : parent.height - web.headerHeight anchors.top: buttons.bottom - profile: HFTabletWebEngineProfile { - id: webviewTabletProfile - storageName: "qmlTabletWebEngine" - } + profile: HFTabletWebEngineProfile; property string userScriptUrl: "" @@ -186,9 +175,9 @@ Item { property string newUrl: "" - webChannel.registeredObjects: [eventBridgeWrapper] - Component.onCompleted: { + webChannel.registerObject("eventBridge", eventBridge); + webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper); // Ensure the JS from the web-engine makes it to our logging webview.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { console.log("Web Entity JS message: " + sourceID + " " + lineNumber + " " + message); diff --git a/interface/resources/qml/controls/WebEntityView.qml b/interface/resources/qml/controls/WebEntityView.qml index a3d5fe903b..3bd6aad053 100644 --- a/interface/resources/qml/controls/WebEntityView.qml +++ b/interface/resources/qml/controls/WebEntityView.qml @@ -10,13 +10,9 @@ import QtQuick 2.5 import "." -import FileTypeProfile 1.0 WebView { - viewProfile: FileTypeProfile { - id: webviewProfile - storageName: "qmlWebEngine" - } + viewProfile: FileTypeProfile; urlTag: "noDownload=true"; } diff --git a/interface/resources/qml/controls/WebView.qml b/interface/resources/qml/controls/WebView.qml index 06766fa6ef..38136c7eec 100644 --- a/interface/resources/qml/controls/WebView.qml +++ b/interface/resources/qml/controls/WebView.qml @@ -2,12 +2,10 @@ import QtQuick 2.5 import QtWebEngine 1.1 import QtWebChannel 1.0 import "../controls-uit" as HiFiControls -import HFWebEngineProfile 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 @@ -23,12 +21,6 @@ Item { } */ - QtObject { - id: eventBridgeWrapper - WebChannel.id: "eventBridgeWrapper" - property var eventBridge; - } - property alias viewProfile: root.profile WebEngineView { @@ -39,10 +31,7 @@ Item { width: parent.width height: keyboardEnabled && keyboardRaised ? parent.height - keyboard.height : parent.height - profile: HFWebEngineProfile { - id: webviewProfile - storageName: "qmlWebEngine" - } + profile: HFWebEngineProfile; property string userScriptUrl: "" @@ -76,9 +65,9 @@ Item { property string newUrl: "" - webChannel.registeredObjects: [eventBridgeWrapper] - Component.onCompleted: { + webChannel.registerObject("eventBridge", eventBridge); + webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper); // 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); diff --git a/interface/resources/qml/dialogs/TabletLoginDialog.qml b/interface/resources/qml/dialogs/TabletLoginDialog.qml index 30c3e678b4..36ca480b24 100644 --- a/interface/resources/qml/dialogs/TabletLoginDialog.qml +++ b/interface/resources/qml/dialogs/TabletLoginDialog.qml @@ -20,7 +20,6 @@ TabletModalWindow { id: loginDialogRoot objectName: "LoginDialog" - property var eventBridge; signal sendToScript(var message); property bool isHMD: false property bool gotoPreviousApp: false; diff --git a/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml b/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml index 652e02b6b9..e2a012ad46 100644 --- a/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml +++ b/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml @@ -24,8 +24,6 @@ Window { resizable: true modality: Qt.ApplicationModal - property alias eventBridge: eventBridgeWrapper.eventBridge - Item { anchors.fill: parent @@ -45,16 +43,6 @@ Window { bottom: keyboard.top } - property alias eventBridgeWrapper: eventBridgeWrapper - - QtObject { - id: eventBridgeWrapper - WebChannel.id: "eventBridgeWrapper" - property var eventBridge; - } - - webChannel.registeredObjects: [eventBridgeWrapper] - // Create a global EventBridge object for raiseAndLowerKeyboard. WebEngineScript { id: createGlobalEventBridge @@ -73,6 +61,10 @@ Window { userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard ] + Component.onCompleted: { + webChannel.registerObject("eventBridge", eventBridge); + webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper); + } } Keyboard { diff --git a/interface/resources/qml/dialogs/preferences/AvatarPreference.qml b/interface/resources/qml/dialogs/preferences/AvatarPreference.qml index bcc5a1d9e6..b27827d9d7 100644 --- a/interface/resources/qml/dialogs/preferences/AvatarPreference.qml +++ b/interface/resources/qml/dialogs/preferences/AvatarPreference.qml @@ -116,9 +116,7 @@ Preference { Component { id: tabletAvatarBrowserBuilder; - TabletAvatarBrowser { - eventBridge: tabletRoot.eventBridge - } + TabletAvatarBrowser { } } } diff --git a/interface/resources/qml/hifi/Desktop.qml b/interface/resources/qml/hifi/Desktop.qml index 7857eda3c2..ea9ec2f6c9 100644 --- a/interface/resources/qml/hifi/Desktop.qml +++ b/interface/resources/qml/hifi/Desktop.qml @@ -2,7 +2,6 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 import QtWebEngine 1.1; import Qt.labs.settings 1.0 -import HFWebEngineProfile 1.0 import "../desktop" as OriginalDesktop import ".." @@ -27,11 +26,6 @@ OriginalDesktop.Desktop { property alias toolWindow: toolWindow ToolWindow { id: toolWindow } - property var browserProfile: HFWebEngineProfile { - id: webviewProfile - storageName: "qmlWebEngine" - } - Action { text: "Open Browser" shortcut: "Ctrl+B" diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 8f6b00f459..250015bab9 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -44,7 +44,6 @@ Rectangle { property var activeTab: "nearbyTab"; property bool currentlyEditingDisplayName: false property bool punctuationMode: false; - property var eventBridge; HifiConstants { id: hifi; } @@ -388,8 +387,13 @@ Rectangle { sortIndicatorColumn: settings.nearbySortIndicatorColumn; sortIndicatorOrder: settings.nearbySortIndicatorOrder; onSortIndicatorColumnChanged: { - settings.nearbySortIndicatorColumn = sortIndicatorColumn; - sortModel(); + if (sortIndicatorColumn > 2) { + // these are not sortable, switch back to last column + sortIndicatorColumn = settings.nearbySortIndicatorColumn; + } else { + settings.nearbySortIndicatorColumn = sortIndicatorColumn; + sortModel(); + } } onSortIndicatorOrderChanged: { settings.nearbySortIndicatorOrder = sortIndicatorOrder; @@ -1038,7 +1042,6 @@ Rectangle { } // Keyboard HifiControls.TabletWebView { - eventBridge: pal.eventBridge; id: userInfoViewer; anchors { top: parent.top; diff --git a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml index 85f8a2f59e..47c9af1f57 100644 --- a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml +++ b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml @@ -24,7 +24,6 @@ Rectangle { property string title: "Asset Browser" property bool keyboardRaised: false - property var eventBridge; signal sendToScript(var message); property bool isHMD: false @@ -179,7 +178,7 @@ Rectangle { SHAPE_TYPE_STATIC_MESH ], checkStateOnDisable: false, - warningOnDisable: "Models with 'Exact' automatic collisions cannot be dynamic" + warningOnDisable: "Models with 'Exact' automatic collisions cannot be dynamic, and should not be used as floors" } }); diff --git a/interface/resources/qml/hifi/dialogs/TabletDCDialog.qml b/interface/resources/qml/hifi/dialogs/TabletDCDialog.qml index b33b957e15..0f363d1be9 100644 --- a/interface/resources/qml/hifi/dialogs/TabletDCDialog.qml +++ b/interface/resources/qml/hifi/dialogs/TabletDCDialog.qml @@ -20,7 +20,6 @@ Rectangle { id: root objectName: "DCConectionTiming" - property var eventBridge; signal sendToScript(var message); property bool isHMD: false diff --git a/interface/resources/qml/hifi/dialogs/TabletDebugWindow.qml b/interface/resources/qml/hifi/dialogs/TabletDebugWindow.qml index d4bbe0af04..22e9dc07a2 100644 --- a/interface/resources/qml/hifi/dialogs/TabletDebugWindow.qml +++ b/interface/resources/qml/hifi/dialogs/TabletDebugWindow.qml @@ -19,7 +19,6 @@ Rectangle { id: root objectName: "DebugWindow" - property var eventBridge; property var title: "Debug Window" property bool isHMD: false diff --git a/interface/resources/qml/hifi/dialogs/TabletEntityStatistics.qml b/interface/resources/qml/hifi/dialogs/TabletEntityStatistics.qml index 35ee58be0c..da295917a0 100644 --- a/interface/resources/qml/hifi/dialogs/TabletEntityStatistics.qml +++ b/interface/resources/qml/hifi/dialogs/TabletEntityStatistics.qml @@ -20,7 +20,6 @@ Rectangle { id: root objectName: "EntityStatistics" - property var eventBridge; signal sendToScript(var message); property bool isHMD: false diff --git a/interface/resources/qml/hifi/dialogs/TabletLODTools.qml b/interface/resources/qml/hifi/dialogs/TabletLODTools.qml index 26e9759e0d..2291a42bf6 100644 --- a/interface/resources/qml/hifi/dialogs/TabletLODTools.qml +++ b/interface/resources/qml/hifi/dialogs/TabletLODTools.qml @@ -20,7 +20,6 @@ Rectangle { id: root objectName: "LODTools" - property var eventBridge; signal sendToScript(var message); property bool isHMD: false diff --git a/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml b/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml index d826b40ad1..11643ae1f1 100644 --- a/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml +++ b/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml @@ -23,7 +23,6 @@ Rectangle { property string title: "Running Scripts" HifiConstants { id: hifi } signal sendToScript(var message); - property var eventBridge; property var scripts: ScriptDiscoveryService; property var scriptsModel: scripts.scriptsModelFilter property var runningScriptsModel: ListModel { } diff --git a/interface/resources/qml/hifi/tablet/Edit.qml b/interface/resources/qml/hifi/tablet/Edit.qml index ea31eb26d8..e2e8c4362e 100644 --- a/interface/resources/qml/hifi/tablet/Edit.qml +++ b/interface/resources/qml/hifi/tablet/Edit.qml @@ -7,14 +7,12 @@ StackView { objectName: "stack" initialItem: Qt.resolvedUrl('EditTabView.qml') - property var eventBridge; signal sendToScript(var message); HifiConstants { id: hifi } function pushSource(path) { editRoot.push(Qt.resolvedUrl(path)); - editRoot.currentItem.eventBridge = editRoot.eventBridge; editRoot.currentItem.sendToScript.connect(editRoot.sendToScript); } diff --git a/interface/resources/qml/hifi/tablet/EditTabView.qml b/interface/resources/qml/hifi/tablet/EditTabView.qml index 35f2b82f0f..e4a20a0316 100644 --- a/interface/resources/qml/hifi/tablet/EditTabView.qml +++ b/interface/resources/qml/hifi/tablet/EditTabView.qml @@ -5,7 +5,6 @@ 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" @@ -182,7 +181,6 @@ TabView { WebView { id: entityListToolWebView url: "../../../../../scripts/system/html/entityList.html" - eventBridge: editRoot.eventBridge anchors.fill: parent enabled: true } @@ -197,7 +195,6 @@ TabView { WebView { id: entityPropertiesWebView url: "../../../../../scripts/system/html/entityProperties.html" - eventBridge: editRoot.eventBridge anchors.fill: parent enabled: true } @@ -212,7 +209,6 @@ TabView { WebView { id: gridControlsWebView url: "../../../../../scripts/system/html/gridControls.html" - eventBridge: editRoot.eventBridge anchors.fill: parent enabled: true } @@ -227,7 +223,6 @@ TabView { WebView { id: particleExplorerWebView url: "../../../../../scripts/system/particle_explorer/particleExplorer.html" - eventBridge: editRoot.eventBridge anchors.fill: parent enabled: true } @@ -290,7 +285,7 @@ TabView { editTabView.currentIndex = id; } else { console.warn('Attempt to switch to invalid tab:', id); - } + } } else if (typeof id === 'string'){ switch (id.toLowerCase()) { case 'create': diff --git a/interface/resources/qml/hifi/tablet/InputRecorder.qml b/interface/resources/qml/hifi/tablet/InputRecorder.qml index 76b122d07d..292deb751e 100644 --- a/interface/resources/qml/hifi/tablet/InputRecorder.qml +++ b/interface/resources/qml/hifi/tablet/InputRecorder.qml @@ -18,7 +18,6 @@ import "../../dialogs" Rectangle { id: inputRecorder - property var eventBridge; HifiConstants { id: hifi } signal sendToScript(var message); color: hifi.colors.baseGray; diff --git a/interface/resources/qml/hifi/tablet/NewModelDialog.qml b/interface/resources/qml/hifi/tablet/NewModelDialog.qml index 5dbb733872..5040c043f4 100644 --- a/interface/resources/qml/hifi/tablet/NewModelDialog.qml +++ b/interface/resources/qml/hifi/tablet/NewModelDialog.qml @@ -20,7 +20,6 @@ Rectangle { // height: parent.height HifiConstants { id: hifi } color: hifi.colors.baseGray; - property var eventBridge; signal sendToScript(var message); property bool keyboardEnabled: false property bool punctuationMode: false @@ -118,7 +117,7 @@ Rectangle { id: text2 width: 160 color: "#ffffff" - text: qsTr("Models with automatic collisions set to 'Exact' cannot be dynamic") + text: qsTr("Models with automatic collisions set to 'Exact' cannot be dynamic, and should not be used as floors") wrapMode: Text.WordWrap font.pixelSize: 12 } diff --git a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml index 7159b078ee..073f143dbe 100644 --- a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml +++ b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml @@ -29,7 +29,6 @@ StackView { initialItem: addressBarDialog width: parent !== null ? parent.width : undefined height: parent !== null ? parent.height : undefined - property var eventBridge; property int cardWidth: 212; property int cardHeight: 152; property string metaverseBase: addressBarDialog.metaverseServerUrl + "/api/v1/"; @@ -80,7 +79,6 @@ StackView { var card = tabletWebView.createObject(); card.url = addressBarDialog.metaverseServerUrl + targetString; card.parentStackItem = root; - card.eventBridge = root.eventBridge; root.push(card); return; } diff --git a/interface/resources/qml/hifi/tablet/TabletAttachmentsDialog.qml b/interface/resources/qml/hifi/tablet/TabletAttachmentsDialog.qml index 634c9d41ec..19548365aa 100644 --- a/interface/resources/qml/hifi/tablet/TabletAttachmentsDialog.qml +++ b/interface/resources/qml/hifi/tablet/TabletAttachmentsDialog.qml @@ -25,7 +25,6 @@ Item { property bool keyboardRaised: false property bool punctuationMode: false - property var eventBridge; signal sendToScript(var message); anchors.fill: parent diff --git a/interface/resources/qml/hifi/tablet/TabletAudioBuffers.qml b/interface/resources/qml/hifi/tablet/TabletAudioBuffers.qml index b7f9089e6f..1b4d0feaca 100644 --- a/interface/resources/qml/hifi/tablet/TabletAudioBuffers.qml +++ b/interface/resources/qml/hifi/tablet/TabletAudioBuffers.qml @@ -20,6 +20,7 @@ StackView { property string title: "Audio Buffers" property alias gotoPreviousApp: root.gotoPreviousApp; property var eventBridge; + signal sendToScript(var message); function pushSource(path) { diff --git a/interface/resources/qml/hifi/tablet/TabletAvatarPreferences.qml b/interface/resources/qml/hifi/tablet/TabletAvatarPreferences.qml index 75973f32ae..94fb29c6a1 100644 --- a/interface/resources/qml/hifi/tablet/TabletAvatarPreferences.qml +++ b/interface/resources/qml/hifi/tablet/TabletAvatarPreferences.qml @@ -19,7 +19,6 @@ StackView { objectName: "stack" property string title: "Avatar Settings" - property var eventBridge; signal sendToScript(var message); function pushSource(path) { diff --git a/interface/resources/qml/hifi/tablet/TabletGeneralPreferences.qml b/interface/resources/qml/hifi/tablet/TabletGeneralPreferences.qml index 17d3f1b959..fe043f6ac7 100644 --- a/interface/resources/qml/hifi/tablet/TabletGeneralPreferences.qml +++ b/interface/resources/qml/hifi/tablet/TabletGeneralPreferences.qml @@ -19,7 +19,6 @@ StackView { objectName: "stack" property string title: "General Settings" property alias gotoPreviousApp: root.gotoPreviousApp; - property var eventBridge; signal sendToScript(var message); function pushSource(path) { diff --git a/interface/resources/qml/hifi/tablet/TabletGraphicsPreferences.qml b/interface/resources/qml/hifi/tablet/TabletGraphicsPreferences.qml index 95ee2c3a72..25b5be05f2 100644 --- a/interface/resources/qml/hifi/tablet/TabletGraphicsPreferences.qml +++ b/interface/resources/qml/hifi/tablet/TabletGraphicsPreferences.qml @@ -19,7 +19,6 @@ StackView { objectName: "stack" property string title: "Graphics Settings" - property var eventBridge; signal sendToScript(var message); function pushSource(path) { diff --git a/interface/resources/qml/hifi/tablet/TabletLodPreferences.qml b/interface/resources/qml/hifi/tablet/TabletLodPreferences.qml index 6f38fee8b9..b502c26245 100644 --- a/interface/resources/qml/hifi/tablet/TabletLodPreferences.qml +++ b/interface/resources/qml/hifi/tablet/TabletLodPreferences.qml @@ -19,7 +19,6 @@ StackView { objectName: "stack" property string title: "LOD Settings" - property var eventBridge; signal sendToScript(var message); function pushSource(path) { diff --git a/interface/resources/qml/hifi/tablet/TabletMenu.qml b/interface/resources/qml/hifi/tablet/TabletMenu.qml index af36f72c82..457fe84c3a 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenu.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenu.qml @@ -4,7 +4,6 @@ import QtQuick.Controls 1.4 import QtQml 2.2 import QtWebChannel 1.0 import QtWebEngine 1.1 -import HFWebEngineProfile 1.0 import "." @@ -22,7 +21,6 @@ FocusScope { property var point: Qt.point(50, 50); TabletMenuStack { id: menuPopperUpper } property string subMenu: "" - property var eventBridge; signal sendToScript(var message); Rectangle { diff --git a/interface/resources/qml/hifi/tablet/TabletMenuStack.qml b/interface/resources/qml/hifi/tablet/TabletMenuStack.qml index bacc11228e..2fd33e9cbc 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenuStack.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenuStack.qml @@ -49,7 +49,6 @@ Item { function pushSource(path) { d.push(Qt.resolvedUrl(path)); - d.currentItem.eventBridge = tabletMenu.eventBridge d.currentItem.sendToScript.connect(tabletMenu.sendToScript); d.currentItem.focus = true; d.currentItem.forceActiveFocus(); diff --git a/interface/resources/qml/hifi/tablet/TabletNetworkingPreferences.qml b/interface/resources/qml/hifi/tablet/TabletNetworkingPreferences.qml index 7184d91044..91d6140fc3 100644 --- a/interface/resources/qml/hifi/tablet/TabletNetworkingPreferences.qml +++ b/interface/resources/qml/hifi/tablet/TabletNetworkingPreferences.qml @@ -19,7 +19,6 @@ StackView { objectName: "stack" property var title: "Networking Settings" - property var eventBridge; signal sendToScript(var message); function pushSource(path) { diff --git a/interface/resources/qml/hifi/tablet/TabletRoot.qml b/interface/resources/qml/hifi/tablet/TabletRoot.qml index 0cd52a8d80..faa4013bce 100644 --- a/interface/resources/qml/hifi/tablet/TabletRoot.qml +++ b/interface/resources/qml/hifi/tablet/TabletRoot.qml @@ -1,7 +1,6 @@ import QtQuick 2.0 import Hifi 1.0 import QtQuick.Controls 1.4 -import HFTabletWebEngineProfile 1.0 import "../../dialogs" import "../../controls" @@ -9,7 +8,6 @@ Item { id: tabletRoot objectName: "tabletRoot" property string username: "Unknown user" - property var eventBridge; property var rootMenu; property var openModal: null; property var openMessage: null; @@ -112,7 +110,6 @@ 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); @@ -166,21 +163,21 @@ Item { objectName: "loader" asynchronous: false - width: parent.width height: parent.height - onLoaded: { - if (loader.item.hasOwnProperty("eventBridge")) { - loader.item.eventBridge = eventBridge; - - // Hook up callback for clara.io download from the marketplace. - eventBridge.webEventReceived.connect(function (event) { - if (event.slice(0, 17) === "CLARA.IO DOWNLOAD") { - ApplicationInterface.addAssetToWorldFromURL(event.slice(18)); - } - }); + // Hook up callback for clara.io download from the marketplace. + Connections { + id: eventBridgeConnection + target: eventBridge + onWebEventReceived: { + if (message.slice(0, 17) === "CLARA.IO DOWNLOAD") { + ApplicationInterface.addAssetToWorldFromURL(message.slice(18)); + } } + } + + onLoaded: { if (loader.item.hasOwnProperty("sendToScript")) { loader.item.sendToScript.connect(tabletRoot.sendToScript); } diff --git a/interface/resources/qml/hifi/tablet/WindowRoot.qml b/interface/resources/qml/hifi/tablet/WindowRoot.qml index 849b857453..ea0c2844a1 100644 --- a/interface/resources/qml/hifi/tablet/WindowRoot.qml +++ b/interface/resources/qml/hifi/tablet/WindowRoot.qml @@ -18,7 +18,6 @@ Windows.ScrollingWindow { id: tabletRoot objectName: "tabletRoot" property string username: "Unknown user" - property var eventBridge; property var rootMenu; property string subMenu: "" @@ -86,17 +85,18 @@ Windows.ScrollingWindow { anchors.left: parent.left anchors.top: parent.top - onLoaded: { - if (loader.item.hasOwnProperty("eventBridge")) { - loader.item.eventBridge = eventBridge; - - // Hook up callback for clara.io download from the marketplace. - eventBridge.webEventReceived.connect(function (event) { - if (event.slice(0, 17) === "CLARA.IO DOWNLOAD") { - ApplicationInterface.addAssetToWorldFromURL(event.slice(18)); - } - }); + // Hook up callback for clara.io download from the marketplace. + Connections { + id: eventBridgeConnection + target: eventBridge + onWebEventReceived: { + if (message.slice(0, 17) === "CLARA.IO DOWNLOAD") { + ApplicationInterface.addAssetToWorldFromURL(message.slice(18)); + } } + } + + onLoaded: { if (loader.item.hasOwnProperty("sendToScript")) { loader.item.sendToScript.connect(tabletRoot.sendToScript); } diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml index 3e497b053e..fcb3e9ff92 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml @@ -136,7 +136,7 @@ Item { for (var i = 0; i < sections.length; i++) { totalHeight += sections[i].height + sections[i].getPreferencesHeight(); } - var bottomPadding = 100; + var bottomPadding = 170; return (totalHeight + bottomPadding); } } diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletAvatarBrowser.qml b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletAvatarBrowser.qml index 029cf7d46b..cab76bf818 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletAvatarBrowser.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletAvatarBrowser.qml @@ -27,8 +27,6 @@ Item { property bool keyboardRaised: false property bool punctuationMode: false - property alias eventBridge: eventBridgeWrapper.eventBridge - anchors.fill: parent BaseWebView { @@ -43,14 +41,6 @@ Item { bottom: footer.top } - QtObject { - id: eventBridgeWrapper - WebChannel.id: "eventBridgeWrapper" - property var eventBridge; - } - - webChannel.registeredObjects: [eventBridgeWrapper] - // Create a global EventBridge object for raiseAndLowerKeyboard. WebEngineScript { id: createGlobalEventBridge @@ -68,6 +58,11 @@ Item { } userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard ] + + Component.onCompleted: { + webChannel.registerObject("eventBridge", eventBridge); + webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper); + } } Rectangle { diff --git a/interface/resources/snapshot/img/no-image.jpg b/interface/resources/snapshot/img/no-image.jpg new file mode 100644 index 0000000000..fff41c4e54 Binary files /dev/null and b/interface/resources/snapshot/img/no-image.jpg differ diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 5ca5634ceb..41d7bee000 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -11,6 +11,9 @@ #include "Application.h" +#include +#include + #include #include #include @@ -69,6 +72,7 @@ #include #include #include +#include #include #include #include @@ -110,7 +114,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -144,10 +150,8 @@ #include "InterfaceLogging.h" #include "LODManager.h" #include "ModelPackager.h" -#include "networking/HFWebEngineProfile.h" -#include "networking/HFTabletWebEngineProfile.h" -#include "networking/FileTypeProfile.h" #include "scripting/Audio.h" +#include "networking/CloseEventSender.h" #include "scripting/TestScriptingInterface.h" #include "scripting/AccountScriptingInterface.h" #include "scripting/AssetMappingsScriptingInterface.h" @@ -244,6 +248,8 @@ static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStanda Setting::Handle maxOctreePacketsPerSecond("maxOctreePPS", DEFAULT_MAX_OCTREE_PPS); static const QString MARKETPLACE_CDN_HOSTNAME = "mpassets.highfidelity.com"; +static const int INTERVAL_TO_CHECK_HMD_WORN_STATUS = 500; // milliseconds +static const QString DESKTOP_DISPLAY_PLUGIN_NAME = "Desktop"; static const QString SYSTEM_TABLET = "com.highfidelity.interface.tablet.system"; @@ -438,6 +444,10 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { const char* portStr = getCmdOption(argc, constArgv, "--listenPort"); const int listenPort = portStr ? atoi(portStr) : INVALID_PORT; + static const auto SUPPRESS_SETTINGS_RESET = "--suppress-settings-reset"; + bool suppressPrompt = cmdOptionExists(argc, const_cast(argv), SUPPRESS_SETTINGS_RESET); + bool previousSessionCrashed = CrashHandler::checkForResetSettings(runningMarkerExisted, suppressPrompt); + Setting::init(); if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { @@ -458,10 +468,6 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { QCoreApplication::addLibraryPath(audioDLLPath); #endif - static const auto SUPPRESS_SETTINGS_RESET = "--suppress-settings-reset"; - bool suppressPrompt = cmdOptionExists(argc, const_cast(argv), SUPPRESS_SETTINGS_RESET); - bool previousSessionCrashed = CrashHandler::checkForResetSettings(runningMarkerExisted, suppressPrompt); - DependencyManager::registerInheritance(); DependencyManager::registerInheritance(); DependencyManager::registerInheritance(); @@ -536,6 +542,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); return previousSessionCrashed; } @@ -649,6 +656,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo Model::setAbstractViewStateInterface(this); // The model class will sometimes need to know view state details from us auto nodeList = DependencyManager::get(); + nodeList->startThread(); // Set up a watchdog thread to intentionally crash the application on deadlocks _deadlockWatchdogThread = new DeadlockWatchdogThread(); @@ -674,25 +682,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo updateHeartbeat(); - // start the nodeThread so its event loop is running - QThread* nodeThread = new QThread(this); - nodeThread->setObjectName("NodeList Thread"); - nodeThread->start(); - - // make sure the node thread is given highest priority - nodeThread->setPriority(QThread::TimeCriticalPriority); - // setup a timer for domain-server check ins QTimer* domainCheckInTimer = new QTimer(nodeList.data()); connect(domainCheckInTimer, &QTimer::timeout, nodeList.data(), &NodeList::sendDomainServerCheckIn); domainCheckInTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS); - // put the NodeList and datagram processing on the node thread - nodeList->moveToThread(nodeThread); - - // put the audio processing on a separate thread - QThread* audioThread = new QThread(); - audioThread->setObjectName("Audio Thread"); auto audioIO = DependencyManager::get(); audioIO->setPositionGetter([]{ @@ -708,7 +702,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo return myAvatar ? myAvatar->getOrientationForAudio() : Quaternions::IDENTITY; }); - audioIO->moveToThread(audioThread); recording::Frame::registerFrameHandler(AudioConstants::getAudioFrameName(), [=](recording::Frame::ConstPointer frame) { audioIO->handleRecordedAudioInput(frame->data); }); @@ -720,11 +713,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo recorder->recordFrame(AUDIO_FRAME_TYPE, audio); } }); - - connect(audioThread, &QThread::started, audioIO.data(), &AudioClient::start); - connect(audioIO.data(), &AudioClient::destroyed, audioThread, &QThread::quit); - connect(audioThread, &QThread::finished, audioThread, &QThread::deleteLater); - audioThread->start(); + audioIO->startThread(); auto audioScriptingInterface = DependencyManager::set(); connect(audioIO.data(), &AudioClient::mutedByMixer, audioScriptingInterface.data(), &AudioScriptingInterface::mutedByMixer); @@ -750,12 +739,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo updateHeartbeat(); // Setup MessagesClient - auto messagesClient = DependencyManager::get(); - QThread* messagesThread = new QThread; - messagesThread->setObjectName("Messages Client Thread"); - messagesClient->moveToThread(messagesThread); - connect(messagesThread, &QThread::started, messagesClient.data(), &MessagesClient::init); - messagesThread->start(); + DependencyManager::get()->startThread(); const DomainHandler& domainHandler = nodeList->getDomainHandler(); @@ -950,6 +934,13 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo properties["processor_l3_cache_count"] = procInfo.numProcessorCachesL3; } + // add firstRun flag from settings to launch event + Setting::Handle firstRun { Settings::firstRun, true }; + properties["first_run"] = firstRun.get(); + + // add the user's machine ID to the launch event + properties["machine_fingerprint"] = uuidStringWithoutCurlyBraces(FingerprintUtils::getMachineFingerprint()); + UserActivityLogger::getInstance().logAction("launch", properties); // Tell our entity edit sender about our known jurisdictions @@ -1309,6 +1300,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo properties["kbps_in"] = bandwidthRecorder->getCachedTotalAverageInputKilobitsPerSecond(); properties["kbps_out"] = bandwidthRecorder->getCachedTotalAverageOutputKilobitsPerSecond(); + properties["atp_in_kbps"] = bandwidthRecorder->getAverageInputKilobitsPerSecond(NodeType::AssetServer); + auto nodeList = DependencyManager::get(); SharedNodePointer entityServerNode = nodeList->soloNodeOfType(NodeType::EntityServer); SharedNodePointer audioMixerNode = nodeList->soloNodeOfType(NodeType::AudioMixer); @@ -1322,8 +1315,61 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo properties["messages_ping"] = messagesMixerNode ? messagesMixerNode->getPingMs() : -1; auto loadingRequests = ResourceCache::getLoadingRequests(); + + QJsonArray loadingRequestsStats; + for (const auto& request : loadingRequests) { + QJsonObject requestStats; + requestStats["filename"] = request->getURL().fileName(); + requestStats["received"] = request->getBytesReceived(); + requestStats["total"] = request->getBytesTotal(); + requestStats["attempts"] = (int)request->getDownloadAttempts(); + loadingRequestsStats.append(requestStats); + } + properties["active_downloads"] = loadingRequests.size(); properties["pending_downloads"] = ResourceCache::getPendingRequestCount(); + properties["active_downloads_details"] = loadingRequestsStats; + + auto statTracker = DependencyManager::get(); + + properties["processing_resources"] = statTracker->getStat("Processing").toInt(); + properties["pending_processing_resources"] = statTracker->getStat("PendingProcessing").toInt(); + + QJsonObject startedRequests; + startedRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_STARTED).toInt(); + startedRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_STARTED).toInt(); + startedRequests["file"] = statTracker->getStat(STAT_FILE_REQUEST_STARTED).toInt(); + startedRequests["total"] = startedRequests["atp"].toInt() + startedRequests["http"].toInt() + + startedRequests["file"].toInt(); + properties["started_requests"] = startedRequests; + + QJsonObject successfulRequests; + successfulRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_SUCCESS).toInt(); + successfulRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_SUCCESS).toInt(); + successfulRequests["file"] = statTracker->getStat(STAT_FILE_REQUEST_SUCCESS).toInt(); + successfulRequests["total"] = successfulRequests["atp"].toInt() + successfulRequests["http"].toInt() + + successfulRequests["file"].toInt(); + properties["successful_requests"] = successfulRequests; + + QJsonObject failedRequests; + failedRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_FAILED).toInt(); + failedRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_FAILED).toInt(); + failedRequests["file"] = statTracker->getStat(STAT_FILE_REQUEST_FAILED).toInt(); + failedRequests["total"] = failedRequests["atp"].toInt() + failedRequests["http"].toInt() + + failedRequests["file"].toInt(); + properties["failed_requests"] = failedRequests; + + QJsonObject cacheRequests; + cacheRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_CACHE).toInt(); + cacheRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_CACHE).toInt(); + cacheRequests["total"] = cacheRequests["atp"].toInt() + cacheRequests["http"].toInt(); + properties["cache_requests"] = cacheRequests; + + QJsonObject atpMappingRequests; + atpMappingRequests["started"] = statTracker->getStat(STAT_ATP_MAPPING_REQUEST_STARTED).toInt(); + atpMappingRequests["failed"] = statTracker->getStat(STAT_ATP_MAPPING_REQUEST_FAILED).toInt(); + atpMappingRequests["successful"] = statTracker->getStat(STAT_ATP_MAPPING_REQUEST_SUCCESS).toInt(); + properties["atp_mapping_requests"] = atpMappingRequests; properties["throttled"] = _displayPlugin ? _displayPlugin->isThrottled() : false; @@ -1339,9 +1385,43 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo properties["deleted_entity_cnt"] = entityActivityTracking.deletedEntityCount; properties["edited_entity_cnt"] = entityActivityTracking.editedEntityCount; + NodeToOctreeSceneStats* octreeServerSceneStats = getOcteeSceneStats(); + unsigned long totalServerOctreeElements = 0; + for (NodeToOctreeSceneStatsIterator i = octreeServerSceneStats->begin(); i != octreeServerSceneStats->end(); i++) { + totalServerOctreeElements += i->second.getTotalElements(); + } + + properties["local_octree_elements"] = (qint64) OctreeElement::getInternalNodeCount(); + properties["server_octree_elements"] = (qint64) totalServerOctreeElements; + properties["active_display_plugin"] = getActiveDisplayPlugin()->getName(); properties["using_hmd"] = isHMDMode(); + _autoSwitchDisplayModeSupportedHMDPlugin = nullptr; + foreach(DisplayPluginPointer displayPlugin, PluginManager::getInstance()->getDisplayPlugins()) { + if (displayPlugin->isHmd() && + displayPlugin->getSupportsAutoSwitch()) { + _autoSwitchDisplayModeSupportedHMDPlugin = displayPlugin; + _autoSwitchDisplayModeSupportedHMDPluginName = + _autoSwitchDisplayModeSupportedHMDPlugin->getName(); + _previousHMDWornStatus = + _autoSwitchDisplayModeSupportedHMDPlugin->isDisplayVisible(); + break; + } + } + + if (_autoSwitchDisplayModeSupportedHMDPlugin) { + if (getActiveDisplayPlugin() != _autoSwitchDisplayModeSupportedHMDPlugin && + !_autoSwitchDisplayModeSupportedHMDPlugin->isSessionActive()) { + startHMDStandBySession(); + } + // Poll periodically to check whether the user has worn HMD or not. Switch Display mode accordingly. + // If the user wears HMD then switch to VR mode. If the user removes HMD then switch to Desktop mode. + QTimer* autoSwitchDisplayModeTimer = new QTimer(this); + connect(autoSwitchDisplayModeTimer, SIGNAL(timeout()), this, SLOT(switchDisplayMode())); + autoSwitchDisplayModeTimer->start(INTERVAL_TO_CHECK_HMD_WORN_STATUS); + } + auto glInfo = getGLContextData(); properties["gl_info"] = glInfo; properties["gpu_used_memory"] = (int)BYTES_TO_MB(gpu::Context::getUsedGPUMemSize()); @@ -1364,6 +1444,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo lastLeftHandPose = leftHandPose; lastRightHandPose = rightHandPose; + properties["local_socket_changes"] = DependencyManager::get()->getStat(LOCAL_SOCKET_CHANGE_STAT).toInt(); + UserActivityLogger::getInstance().logAction("stats", properties); }); sendStatsTimer->start(); @@ -1463,6 +1545,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo updateSystemTabletMode(); connect(&_myCamera, &Camera::modeUpdated, this, &Application::cameraModeChanged); + + qCDebug(interfaceapp) << "Metaverse session ID is" << uuidStringWithoutCurlyBraces(accountManager->getSessionID()); } void Application::domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo) { @@ -1571,6 +1655,12 @@ void Application::aboutToQuit() { } getActiveDisplayPlugin()->deactivate(); + if (_autoSwitchDisplayModeSupportedHMDPlugin + && _autoSwitchDisplayModeSupportedHMDPlugin->isSessionActive()) { + _autoSwitchDisplayModeSupportedHMDPlugin->endSession(); + } + // use the CloseEventSender via a QThread to send an event that says the user asked for the app to close + DependencyManager::get()->startThread(); // Hide Running Scripts dialog so that it gets destroyed in an orderly manner; prevents warnings at shutdown. DependencyManager::get()->hide("RunningScripts"); @@ -1658,6 +1748,8 @@ void Application::cleanupBeforeQuit() { // stop QML DependencyManager::destroy(); + DependencyManager::destroy(); + if (_snapshotSoundInjector != nullptr) { _snapshotSoundInjector->stop(); } @@ -1685,6 +1777,10 @@ Application::~Application() { _physicsEngine->setCharacterController(nullptr); + // the _shapeManager should have zero references + _shapeManager.collectGarbage(); + assert(_shapeManager.getNumShapes() == 0); + // shutdown render engine _main3DScene = nullptr; _renderEngine = nullptr; @@ -1713,15 +1809,9 @@ Application::~Application() { ResourceManager::cleanup(); - QThread* nodeThread = DependencyManager::get()->thread(); - // remove the NodeList from the DependencyManager DependencyManager::destroy(); - // ask the node thread to quit and wait until it is done - nodeThread->quit(); - nodeThread->wait(); - Leapmotion::destroy(); if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { @@ -1736,6 +1826,15 @@ Application::~Application() { _window->deleteLater(); + // make sure that the quit event has finished sending before we take the application down + auto closeEventSender = DependencyManager::get(); + while (!closeEventSender->hasFinishedQuitEvent() && !closeEventSender->hasTimedOutQuitEvent()) { + // sleep a little so we're not spinning at 100% + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + // quit the thread used by the closure event sender + closeEventSender->thread()->quit(); + // Can't log to file passed this point, FileLogger about to be deleted qInstallMessageHandler(LogHandler::verboseMessageHandler); } @@ -1771,15 +1870,9 @@ void Application::initializeGL() { // Set up the render engine render::CullFunctor cullFunctor = LODManager::shouldRender; - _renderEngine->addJob("RenderShadowTask", cullFunctor); - const auto items = _renderEngine->addJob("FetchCullSort", cullFunctor); - assert(items.canCast()); static const QString RENDER_FORWARD = "HIFI_RENDER_FORWARD"; - if (QProcessEnvironment::systemEnvironment().contains(RENDER_FORWARD)) { - _renderEngine->addJob("Forward", items); - } else { - _renderEngine->addJob("RenderDeferredTask", items); - } + bool isDeferred = !QProcessEnvironment::systemEnvironment().contains(RENDER_FORWARD); + _renderEngine->addJob("RenderMainView", cullFunctor, isDeferred); _renderEngine->load(); _renderEngine->registerScene(_main3DScene); @@ -1828,14 +1921,10 @@ void Application::initializeUi() { UpdateDialog::registerType(); qmlRegisterType("Hifi", 1, 0, "Preference"); - qmlRegisterType("HFWebEngineProfile", 1, 0, "HFWebEngineProfile"); - qmlRegisterType("HFTabletWebEngineProfile", 1, 0, "HFTabletWebEngineProfile"); - qmlRegisterType("FileTypeProfile", 1, 0, "FileTypeProfile"); - auto offscreenUi = DependencyManager::get(); offscreenUi->create(_glWidget->qglContext()); - auto rootContext = offscreenUi->getRootContext(); + auto surfaceContext = offscreenUi->getSurfaceContext(); offscreenUi->setProxyWindow(_window->windowHandle()); offscreenUi->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/")); @@ -1847,7 +1936,7 @@ void Application::initializeUi() { // do better detection in the offscreen UI of what has focus offscreenUi->setNavigationFocused(false); - auto engine = rootContext->engine(); + auto engine = surfaceContext->engine(); connect(engine, &QQmlEngine::quit, [] { qApp->quit(); }); @@ -1856,78 +1945,77 @@ void Application::initializeUi() { // For some reason there is already an "Application" object in the QML context, // though I can't find it. Hence, "ApplicationInterface" - rootContext->setContextProperty("ApplicationInterface", this); - rootContext->setContextProperty("Audio", DependencyManager::get().data()); - rootContext->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); - rootContext->setContextProperty("AudioScope", DependencyManager::get().data()); + surfaceContext->setContextProperty("Audio", DependencyManager::get().data()); + surfaceContext->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); + surfaceContext->setContextProperty("AudioScope", DependencyManager::get().data()); - rootContext->setContextProperty("Controller", DependencyManager::get().data()); - rootContext->setContextProperty("Entities", DependencyManager::get().data()); + surfaceContext->setContextProperty("Controller", DependencyManager::get().data()); + surfaceContext->setContextProperty("Entities", DependencyManager::get().data()); _fileDownload = new FileScriptingInterface(engine); - rootContext->setContextProperty("File", _fileDownload); + surfaceContext->setContextProperty("File", _fileDownload); connect(_fileDownload, &FileScriptingInterface::unzipResult, this, &Application::handleUnzip); - rootContext->setContextProperty("MyAvatar", getMyAvatar().get()); - rootContext->setContextProperty("Messages", DependencyManager::get().data()); - rootContext->setContextProperty("Recording", DependencyManager::get().data()); - rootContext->setContextProperty("Preferences", DependencyManager::get().data()); - rootContext->setContextProperty("AddressManager", DependencyManager::get().data()); - rootContext->setContextProperty("FrameTimings", &_frameTimingsScriptingInterface); - rootContext->setContextProperty("Rates", new RatesScriptingInterface(this)); - rootContext->setContextProperty("pathToFonts", "../../"); + surfaceContext->setContextProperty("MyAvatar", getMyAvatar().get()); + surfaceContext->setContextProperty("Messages", DependencyManager::get().data()); + surfaceContext->setContextProperty("Recording", DependencyManager::get().data()); + surfaceContext->setContextProperty("Preferences", DependencyManager::get().data()); + surfaceContext->setContextProperty("AddressManager", DependencyManager::get().data()); + surfaceContext->setContextProperty("FrameTimings", &_frameTimingsScriptingInterface); + surfaceContext->setContextProperty("Rates", new RatesScriptingInterface(this)); - rootContext->setContextProperty("TREE_SCALE", TREE_SCALE); - rootContext->setContextProperty("Quat", new Quat()); - rootContext->setContextProperty("Vec3", new Vec3()); - rootContext->setContextProperty("Uuid", new ScriptUUID()); - rootContext->setContextProperty("Assets", DependencyManager::get().data()); + surfaceContext->setContextProperty("TREE_SCALE", TREE_SCALE); + // FIXME Quat and Vec3 won't work with QJSEngine used by QML + surfaceContext->setContextProperty("Quat", new Quat()); + surfaceContext->setContextProperty("Vec3", new Vec3()); + surfaceContext->setContextProperty("Uuid", new ScriptUUID()); + surfaceContext->setContextProperty("Assets", DependencyManager::get().data()); - rootContext->setContextProperty("AvatarList", DependencyManager::get().data()); - rootContext->setContextProperty("Users", DependencyManager::get().data()); + surfaceContext->setContextProperty("AvatarList", DependencyManager::get().data()); + surfaceContext->setContextProperty("Users", DependencyManager::get().data()); - rootContext->setContextProperty("UserActivityLogger", DependencyManager::get().data()); + surfaceContext->setContextProperty("UserActivityLogger", DependencyManager::get().data()); - rootContext->setContextProperty("Camera", &_myCamera); + surfaceContext->setContextProperty("Camera", &_myCamera); #if defined(Q_OS_MAC) || defined(Q_OS_WIN) - rootContext->setContextProperty("SpeechRecognizer", DependencyManager::get().data()); + surfaceContext->setContextProperty("SpeechRecognizer", DependencyManager::get().data()); #endif - rootContext->setContextProperty("Overlays", &_overlays); - rootContext->setContextProperty("Window", DependencyManager::get().data()); - rootContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); - rootContext->setContextProperty("Stats", Stats::getInstance()); - rootContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); - rootContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); - rootContext->setContextProperty("AvatarBookmarks", DependencyManager::get().data()); - rootContext->setContextProperty("LocationBookmarks", DependencyManager::get().data()); + surfaceContext->setContextProperty("Overlays", &_overlays); + surfaceContext->setContextProperty("Window", DependencyManager::get().data()); + surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); + surfaceContext->setContextProperty("Stats", Stats::getInstance()); + surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); + surfaceContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); + surfaceContext->setContextProperty("AvatarBookmarks", DependencyManager::get().data()); + surfaceContext->setContextProperty("LocationBookmarks", DependencyManager::get().data()); // Caches - rootContext->setContextProperty("AnimationCache", DependencyManager::get().data()); - rootContext->setContextProperty("TextureCache", DependencyManager::get().data()); - rootContext->setContextProperty("ModelCache", DependencyManager::get().data()); - rootContext->setContextProperty("SoundCache", DependencyManager::get().data()); + surfaceContext->setContextProperty("AnimationCache", DependencyManager::get().data()); + surfaceContext->setContextProperty("TextureCache", DependencyManager::get().data()); + surfaceContext->setContextProperty("ModelCache", DependencyManager::get().data()); + surfaceContext->setContextProperty("SoundCache", DependencyManager::get().data()); - rootContext->setContextProperty("Account", AccountScriptingInterface::getInstance()); - rootContext->setContextProperty("Tablet", DependencyManager::get().data()); - rootContext->setContextProperty("DialogsManager", _dialogsManagerScriptingInterface); - rootContext->setContextProperty("GlobalServices", GlobalServicesScriptingInterface::getInstance()); - rootContext->setContextProperty("FaceTracker", DependencyManager::get().data()); - rootContext->setContextProperty("AvatarManager", DependencyManager::get().data()); - rootContext->setContextProperty("UndoStack", &_undoStackScriptingInterface); - rootContext->setContextProperty("LODManager", DependencyManager::get().data()); - rootContext->setContextProperty("Paths", DependencyManager::get().data()); - rootContext->setContextProperty("HMD", DependencyManager::get().data()); - rootContext->setContextProperty("Scene", DependencyManager::get().data()); - rootContext->setContextProperty("Render", _renderEngine->getConfiguration().get()); - rootContext->setContextProperty("Reticle", getApplicationCompositor().getReticleInterface()); - rootContext->setContextProperty("Snapshot", DependencyManager::get().data()); + surfaceContext->setContextProperty("Account", AccountScriptingInterface::getInstance()); + surfaceContext->setContextProperty("Tablet", DependencyManager::get().data()); + surfaceContext->setContextProperty("DialogsManager", _dialogsManagerScriptingInterface); + surfaceContext->setContextProperty("GlobalServices", GlobalServicesScriptingInterface::getInstance()); + surfaceContext->setContextProperty("FaceTracker", DependencyManager::get().data()); + surfaceContext->setContextProperty("AvatarManager", DependencyManager::get().data()); + surfaceContext->setContextProperty("UndoStack", &_undoStackScriptingInterface); + surfaceContext->setContextProperty("LODManager", DependencyManager::get().data()); + surfaceContext->setContextProperty("Paths", DependencyManager::get().data()); + surfaceContext->setContextProperty("HMD", DependencyManager::get().data()); + surfaceContext->setContextProperty("Scene", DependencyManager::get().data()); + surfaceContext->setContextProperty("Render", _renderEngine->getConfiguration().get()); + surfaceContext->setContextProperty("Reticle", getApplicationCompositor().getReticleInterface()); + surfaceContext->setContextProperty("Snapshot", DependencyManager::get().data()); - rootContext->setContextProperty("ApplicationCompositor", &getApplicationCompositor()); + surfaceContext->setContextProperty("ApplicationCompositor", &getApplicationCompositor()); - rootContext->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); + surfaceContext->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { - rootContext->setContextProperty("Steam", new SteamScriptingInterface(engine, steamClient.get())); + surfaceContext->setContextProperty("Steam", new SteamScriptingInterface(engine, steamClient.get())); } @@ -2066,7 +2154,7 @@ void Application::paintGL() { _myCamera.setOrientation(glm::quat_cast(camMat)); } else { _myCamera.setPosition(myAvatar->getDefaultEyePosition()); - _myCamera.setOrientation(myAvatar->getMyHead()->getCameraOrientation()); + _myCamera.setOrientation(myAvatar->getMyHead()->getHeadOrientation()); } } else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { if (isHMDMode()) { @@ -2186,6 +2274,9 @@ void Application::paintGL() { }); renderArgs._context->setStereoProjections(eyeProjections); renderArgs._context->setStereoViews(eyeOffsets); + + // Configure the type of display / stereo + renderArgs._displayMode = (isHMDMode() ? RenderArgs::STEREO_HMD : RenderArgs::STEREO_MONITOR); } renderArgs._blitFramebuffer = finalFramebuffer; displaySide(&renderArgs, _myCamera); @@ -2379,15 +2470,16 @@ void Application::handleSandboxStatus(QNetworkReply* reply) { // Check HMD use (may be technically available without being in use) bool hasHMD = PluginUtils::isHMDAvailable(); - bool isUsingHMD = hasHMD && hasHandControllers && _displayPlugin->isHmd(); + bool isUsingHMD = _displayPlugin->isHmd(); + bool isUsingHMDAndHandControllers = hasHMD && hasHandControllers && isUsingHMD; Setting::Handle tutorialComplete{ "tutorialComplete", false }; Setting::Handle firstRun{ Settings::firstRun, true }; bool isTutorialComplete = tutorialComplete.get(); - bool shouldGoToTutorial = isUsingHMD && hasTutorialContent && !isTutorialComplete; + bool shouldGoToTutorial = isUsingHMDAndHandControllers && hasTutorialContent && !isTutorialComplete; - qCDebug(interfaceapp) << "HMD:" << hasHMD << ", Hand Controllers: " << hasHandControllers << ", Using HMD: " << isUsingHMD; + qCDebug(interfaceapp) << "HMD:" << hasHMD << ", Hand Controllers: " << hasHandControllers << ", Using HMD: " << isUsingHMDAndHandControllers; qCDebug(interfaceapp) << "Tutorial version:" << contentVersion << ", sufficient:" << hasTutorialContent << ", complete:" << isTutorialComplete << ", should go:" << shouldGoToTutorial; @@ -2401,10 +2493,18 @@ void Application::handleSandboxStatus(QNetworkReply* reply) { const QString TUTORIAL_PATH = "/tutorial_begin"; + static const QString SENT_TO_TUTORIAL = "tutorial"; + static const QString SENT_TO_PREVIOUS_LOCATION = "previous_location"; + static const QString SENT_TO_ENTRY = "entry"; + static const QString SENT_TO_SANDBOX = "sandbox"; + + QString sentTo; + if (shouldGoToTutorial) { if (sandboxIsRunning) { qCDebug(interfaceapp) << "Home sandbox appears to be running, going to Home."; DependencyManager::get()->goToLocalSandbox(TUTORIAL_PATH); + sentTo = SENT_TO_TUTORIAL; } else { qCDebug(interfaceapp) << "Home sandbox does not appear to be running, going to Entry."; if (firstRun.get()) { @@ -2412,8 +2512,10 @@ void Application::handleSandboxStatus(QNetworkReply* reply) { } if (addressLookupString.isEmpty()) { DependencyManager::get()->goToEntry(); + sentTo = SENT_TO_ENTRY; } else { DependencyManager::get()->loadSettings(addressLookupString); + sentTo = SENT_TO_PREVIOUS_LOCATION; } } } else { @@ -2426,23 +2528,40 @@ void Application::handleSandboxStatus(QNetworkReply* reply) { // If this is a first run we short-circuit the address passed in if (isFirstRun) { - if (isUsingHMD) { + if (isUsingHMDAndHandControllers) { if (sandboxIsRunning) { qCDebug(interfaceapp) << "Home sandbox appears to be running, going to Home."; DependencyManager::get()->goToLocalSandbox(); + sentTo = SENT_TO_SANDBOX; } else { qCDebug(interfaceapp) << "Home sandbox does not appear to be running, going to Entry."; DependencyManager::get()->goToEntry(); + sentTo = SENT_TO_ENTRY; } } else { DependencyManager::get()->goToEntry(); + sentTo = SENT_TO_ENTRY; } } else { qCDebug(interfaceapp) << "Not first run... going to" << qPrintable(addressLookupString.isEmpty() ? QString("previous location") : addressLookupString); DependencyManager::get()->loadSettings(addressLookupString); + sentTo = SENT_TO_PREVIOUS_LOCATION; } } + UserActivityLogger::getInstance().logAction("startup_sent_to", { + { "sent_to", sentTo }, + { "sandbox_is_running", sandboxIsRunning }, + { "has_hmd", hasHMD }, + { "has_hand_controllers", hasHandControllers }, + { "is_using_hmd", isUsingHMD }, + { "is_using_hmd_and_hand_controllers", isUsingHMDAndHandControllers }, + { "content_version", contentVersion }, + { "is_tutorial_complete", isTutorialComplete }, + { "has_tutorial_content", hasTutorialContent }, + { "should_go_to_tutorial", shouldGoToTutorial } + }); + _connectionMonitor.init(); // After all of the constructor is completed, then set firstRun to false. @@ -2777,6 +2896,17 @@ void Application::keyPressEvent(QKeyEvent* event) { if (isShifted && isMeta && !isOption) { Menu::getInstance()->triggerOption(MenuOption::SuppressShortTimings); } else if (!isOption && !isShifted && isMeta) { + AudioInjectorOptions options; + options.localOnly = true; + options.stereo = true; + + if (_snapshotSoundInjector) { + _snapshotSoundInjector->setOptions(options); + _snapshotSoundInjector->restart(); + } else { + QByteArray samples = _snapshotSound->getByteArray(); + _snapshotSoundInjector = AudioInjector::playSound(samples, options); + } takeSnapshot(true); } break; @@ -3951,6 +4081,7 @@ void Application::updateMyAvatarLookAtPosition() { lookAtPosition.x = -lookAtPosition.x; } if (isHMD) { + // TODO -- this code is probably wrong, getHeadPose() returns something in sensor frame, not avatar glm::mat4 headPose = getActiveDisplayPlugin()->getHeadPose(); glm::quat hmdRotation = glm::quat_cast(headPose); lookAtSpot = _myCamera.getPosition() + myAvatar->getOrientation() * (hmdRotation * lookAtPosition); @@ -3992,9 +4123,10 @@ void Application::updateMyAvatarLookAtPosition() { } } else { // I am not looking at anyone else, so just look forward - if (isHMD) { - glm::mat4 worldHMDMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); - lookAtSpot = transformPoint(worldHMDMat, glm::vec3(0.0f, 0.0f, -TREE_SCALE)); + auto headPose = myAvatar->getHeadControllerPoseInSensorFrame(); + if (headPose.isValid()) { + glm::mat4 worldHeadMat = myAvatar->getSensorToWorldMatrix() * headPose.getMatrix(); + lookAtSpot = transformPoint(worldHeadMat, glm::vec3(0.0f, 0.0f, TREE_SCALE)); } else { lookAtSpot = myAvatar->getHead()->getEyePosition() + (myAvatar->getHead()->getFinalOrientationInWorldFrame() * glm::vec3(0.0f, 0.0f, -TREE_SCALE)); @@ -4492,12 +4624,13 @@ void Application::update(float deltaTime) { getEntities()->getTree()->withWriteLock([&] { PerformanceTimer perfTimer("handleOutgoingChanges"); - const VectorOfMotionStates& deactivations = _physicsEngine->getDeactivatedMotionStates(); - _entitySimulation->handleDeactivatedMotionStates(deactivations); const VectorOfMotionStates& outgoingChanges = _physicsEngine->getChangedMotionStates(); _entitySimulation->handleChangedMotionStates(outgoingChanges); avatarManager->handleChangedMotionStates(outgoingChanges); + + const VectorOfMotionStates& deactivations = _physicsEngine->getDeactivatedMotionStates(); + _entitySimulation->handleDeactivatedMotionStates(deactivations); }); if (!_aboutToQuit) { @@ -4885,12 +5018,6 @@ QRect Application::getDesirableApplicationGeometry() const { return applicationGeometry; } -glm::vec3 Application::getSunDirection() const { - // Sun direction is in fact just the location of the sun relative to the origin - auto skyStage = DependencyManager::get()->getSkyStage(); - return skyStage->getSunLight()->getDirection(); -} - // FIXME, preprocessor guard this check to occur only in DEBUG builds static QThread * activeRenderingThread = nullptr; @@ -4947,7 +5074,7 @@ namespace render { template <> const ItemKey payloadGetKey(const WorldBoxRenderData::Pointer& stuff) { return ItemKey::Builder::opaqueShape(); } template <> const Item::Bound payloadGetBound(const WorldBoxRenderData::Pointer& stuff) { return Item::Bound(); } template <> void payloadRender(const WorldBoxRenderData::Pointer& stuff, RenderArgs* args) { - if (args->_renderMode != RenderArgs::MIRROR_RENDER_MODE && Menu::getInstance()->isOptionChecked(MenuOption::WorldAxes)) { + if (Menu::getInstance()->isOptionChecked(MenuOption::WorldAxes)) { PerformanceTimer perfTimer("worldBox"); auto& batch = *args->_batch; @@ -4957,90 +5084,6 @@ namespace render { } } -// Background Render Data & rendering functions -class BackgroundRenderData { -public: - typedef render::Payload Payload; - typedef Payload::DataPointer Pointer; - - static render::ItemID _item; // unique WorldBoxRenderData -}; - -render::ItemID BackgroundRenderData::_item = 0; - -namespace render { - template <> const ItemKey payloadGetKey(const BackgroundRenderData::Pointer& stuff) { - return ItemKey::Builder::background(); - } - - template <> const Item::Bound payloadGetBound(const BackgroundRenderData::Pointer& stuff) { - return Item::Bound(); - } - - template <> void payloadRender(const BackgroundRenderData::Pointer& background, RenderArgs* args) { - Q_ASSERT(args->_batch); - gpu::Batch& batch = *args->_batch; - - // Background rendering decision - auto skyStage = DependencyManager::get()->getSkyStage(); - auto backgroundMode = skyStage->getBackgroundMode(); - - switch (backgroundMode) { - case model::SunSkyStage::SKY_DEFAULT: { - auto scene = DependencyManager::get()->getStage(); - auto sceneKeyLight = scene->getKeyLight(); - - scene->setSunModelEnable(false); - sceneKeyLight->setColor(ColorUtils::toVec3(KeyLightPropertyGroup::DEFAULT_KEYLIGHT_COLOR)); - sceneKeyLight->setIntensity(KeyLightPropertyGroup::DEFAULT_KEYLIGHT_INTENSITY); - sceneKeyLight->setAmbientIntensity(KeyLightPropertyGroup::DEFAULT_KEYLIGHT_AMBIENT_INTENSITY); - sceneKeyLight->setDirection(KeyLightPropertyGroup::DEFAULT_KEYLIGHT_DIRECTION); - // fall through: render a skybox (if available), or the defaults (if requested) - } - - case model::SunSkyStage::SKY_BOX: { - auto skybox = skyStage->getSkybox(); - if (!skybox->empty()) { - PerformanceTimer perfTimer("skybox"); - skybox->render(batch, args->getViewFrustum()); - break; - } - // fall through: render defaults (if requested) - } - - case model::SunSkyStage::SKY_DEFAULT_AMBIENT_TEXTURE: { - if (Menu::getInstance()->isOptionChecked(MenuOption::DefaultSkybox)) { - auto scene = DependencyManager::get()->getStage(); - auto sceneKeyLight = scene->getKeyLight(); - auto defaultSkyboxAmbientTexture = qApp->getDefaultSkyboxAmbientTexture(); - if (defaultSkyboxAmbientTexture) { - sceneKeyLight->setAmbientSphere(defaultSkyboxAmbientTexture->getIrradiance()); - sceneKeyLight->setAmbientMap(defaultSkyboxAmbientTexture); - } else { - static QString repeatedMessage = LogHandler::getInstance().addRepeatedMessageRegex( - "Failed to get a valid Default Skybox Ambient Texture ? probably because it couldn't be find during initialization step"); - } - // fall through: render defaults skybox - } else { - break; - } - } - - case model::SunSkyStage::SKY_DEFAULT_TEXTURE: - if (Menu::getInstance()->isOptionChecked(MenuOption::DefaultSkybox)) { - qApp->getDefaultSkybox()->render(batch, args->getViewFrustum()); - } - break; - - // Any other cases require no extra rendering - case model::SunSkyStage::NO_BACKGROUND: - default: - break; - } - } -} - - void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool selfAvatarOnly) { // FIXME: This preDisplayRender call is temporary until we create a separate render::scene for the mirror rendering. @@ -5064,15 +5107,6 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se // The pending changes collecting the changes here render::Transaction transaction; - // FIXME: Move this out of here!, Background / skybox should be driven by the enityt content just like the other entities - // Background rendering decision - if (!render::Item::isValidID(BackgroundRenderData::_item)) { - auto backgroundRenderData = make_shared(); - auto backgroundRenderPayload = make_shared(backgroundRenderData); - BackgroundRenderData::_item = _main3DScene->allocateID(); - transaction.resetItem(BackgroundRenderData::_item, backgroundRenderPayload); - } - // Assuming nothing gets rendered through that if (!selfAvatarOnly) { if (DependencyManager::get()->shouldRenderEntities()) { @@ -5108,12 +5142,6 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se }); } - // Setup the current Zone Entity lighting - { - auto stage = DependencyManager::get()->getSkyStage(); - DependencyManager::get()->setGlobalLight(stage->getSunLight()); - } - { PerformanceTimer perfTimer("SceneProcessTransaction"); _main3DScene->enqueueTransaction(transaction); @@ -6384,21 +6412,6 @@ void Application::loadAddAvatarBookmarkDialog() const { } void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRatio) { - - //keep sound thread out of event loop scope - - AudioInjectorOptions options; - options.localOnly = true; - options.stereo = true; - - if (_snapshotSoundInjector) { - _snapshotSoundInjector->setOptions(options); - _snapshotSoundInjector->restart(); - } else { - QByteArray samples = _snapshotSound->getByteArray(); - _snapshotSoundInjector = AudioInjector::playSound(samples, options); - } - postLambdaEvent([notify, includeAnimated, aspectRatio, this] { // Get a screenshot and save it QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); @@ -6811,6 +6824,35 @@ void Application::updateDisplayMode() { Q_ASSERT_X(_displayPlugin, "Application::updateDisplayMode", "could not find an activated display plugin"); } +void Application::switchDisplayMode() { + if (!_autoSwitchDisplayModeSupportedHMDPlugin) { + return; + } + bool currentHMDWornStatus = _autoSwitchDisplayModeSupportedHMDPlugin->isDisplayVisible(); + if (currentHMDWornStatus != _previousHMDWornStatus) { + // Switch to respective mode as soon as currentHMDWornStatus changes + if (currentHMDWornStatus) { + qCDebug(interfaceapp) << "Switching from Desktop to HMD mode"; + endHMDSession(); + setActiveDisplayPlugin(_autoSwitchDisplayModeSupportedHMDPluginName); + } else { + qCDebug(interfaceapp) << "Switching from HMD to desktop mode"; + setActiveDisplayPlugin(DESKTOP_DISPLAY_PLUGIN_NAME); + startHMDStandBySession(); + } + emit activeDisplayPluginChanged(); + } + _previousHMDWornStatus = currentHMDWornStatus; +} + +void Application::startHMDStandBySession() { + _autoSwitchDisplayModeSupportedHMDPlugin->startStandBySession(); +} + +void Application::endHMDSession() { + _autoSwitchDisplayModeSupportedHMDPlugin->endSession(); +} + mat4 Application::getEyeProjection(int eye) const { QMutexLocker viewLocker(&_viewMutex); if (isHMDMode()) { diff --git a/interface/src/Application.h b/interface/src/Application.h index b5f96ca4af..9db492b177 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -439,7 +439,7 @@ private slots: void addAssetToWorldErrorTimeout(); void handleSandboxStatus(QNetworkReply* reply); - + void switchDisplayMode(); private: static void initDisplay(); void init(); @@ -457,8 +457,6 @@ private: void queryOctree(NodeType_t serverType, PacketType packetType, NodeToJurisdictionMap& jurisdictions, bool forceResend = false); - glm::vec3 getSunDirection() const; - void renderRearViewMirror(RenderArgs* renderArgs, const QRect& region, bool isZoomed); int sendNackPackets(); @@ -679,7 +677,11 @@ private: FileScriptingInterface* _fileDownload; AudioInjector* _snapshotSoundInjector { nullptr }; SharedSoundPointer _snapshotSound; + + DisplayPluginPointer _autoSwitchDisplayModeSupportedHMDPlugin; + QString _autoSwitchDisplayModeSupportedHMDPluginName; + bool _previousHMDWornStatus; + void startHMDStandBySession(); + void endHMDSession(); }; - - #endif // hifi_Application_h diff --git a/interface/src/AvatarBookmarks.cpp b/interface/src/AvatarBookmarks.cpp index 5cdfc8213f..db2a240b92 100644 --- a/interface/src/AvatarBookmarks.cpp +++ b/interface/src/AvatarBookmarks.cpp @@ -21,15 +21,35 @@ #include "MainWindow.h" #include "Menu.h" - #include "AvatarBookmarks.h" +#include "InterfaceLogging.h" + #include AvatarBookmarks::AvatarBookmarks() { - _bookmarksFilename = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/" + AVATARBOOKMARKS_FILENAME; + _bookmarksFilename = PathUtils::getAppDataPath() + "/" + AVATARBOOKMARKS_FILENAME; readFromFile(); } +void AvatarBookmarks::readFromFile() { + // migrate old avatarbookmarks.json, used to be in 'local' folder on windows + QString oldConfigPath = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/" + AVATARBOOKMARKS_FILENAME; + QFile oldConfig(oldConfigPath); + + // I imagine that in a year from now, this code for migrating (as well as the two lines above) + // may be removed since all bookmarks should have been migrated by then + // - Robbie Uvanni (6.8.2017) + if (oldConfig.exists()) { + if (QDir().rename(oldConfigPath, _bookmarksFilename)) { + qCDebug(interfaceapp) << "Successfully migrated" << AVATARBOOKMARKS_FILENAME; + } else { + qCDebug(interfaceapp) << "Failed to migrate" << AVATARBOOKMARKS_FILENAME; + } + } + + Bookmarks::readFromFile(); +} + void AvatarBookmarks::setupMenus(Menu* menubar, MenuWrapper* menu) { // Add menus/actions auto bookmarkAction = menubar->addActionToQMenuAndActionHash(menu, MenuOption::BookmarkAvatar); diff --git a/interface/src/AvatarBookmarks.h b/interface/src/AvatarBookmarks.h index 725af88b0d..dc5a0aee6e 100644 --- a/interface/src/AvatarBookmarks.h +++ b/interface/src/AvatarBookmarks.h @@ -29,6 +29,7 @@ public slots: protected: void addBookmarkToMenu(Menu* menubar, const QString& name, const QString& address) override; + void readFromFile(); private: const QString AVATARBOOKMARKS_FILENAME = "avatarbookmarks.json"; diff --git a/interface/src/CrashHandler.cpp b/interface/src/CrashHandler.cpp index 8081dd3ffc..f7d71de3fe 100644 --- a/interface/src/CrashHandler.cpp +++ b/interface/src/CrashHandler.cpp @@ -24,12 +24,15 @@ #include "Application.h" #include "Menu.h" -#include #include +#include +#include + bool CrashHandler::checkForResetSettings(bool wasLikelyCrash, bool suppressPrompt) { - Settings settings; + QSettings::setDefaultFormat(JSON_FORMAT); + QSettings settings; settings.beginGroup("Developer"); QVariant displayCrashOptions = settings.value(MenuOption::DisplayCrashOptions); QVariant askToResetSettingsOption = settings.value(MenuOption::AskToResetSettings); @@ -70,7 +73,7 @@ CrashHandler::Action CrashHandler::promptUserForAction(bool showCrashMessage) { layout->addWidget(label); QRadioButton* option1 = new QRadioButton("Reset all my settings"); - QRadioButton* option2 = new QRadioButton("Reset my settings but retain avatar info."); + QRadioButton* option2 = new QRadioButton("Reset my settings but keep essential info"); QRadioButton* option3 = new QRadioButton("Continue with my current settings"); option3->setChecked(true); layout->addWidget(option1); @@ -92,7 +95,7 @@ CrashHandler::Action CrashHandler::promptUserForAction(bool showCrashMessage) { return CrashHandler::DELETE_INTERFACE_INI; } if (option2->isChecked()) { - return CrashHandler::RETAIN_AVATAR_INFO; + return CrashHandler::RETAIN_IMPORTANT_INFO; } } @@ -101,24 +104,27 @@ CrashHandler::Action CrashHandler::promptUserForAction(bool showCrashMessage) { } void CrashHandler::handleCrash(CrashHandler::Action action) { - if (action != CrashHandler::DELETE_INTERFACE_INI && action != CrashHandler::RETAIN_AVATAR_INFO) { + if (action != CrashHandler::DELETE_INTERFACE_INI && action != CrashHandler::RETAIN_IMPORTANT_INFO) { // CrashHandler::DO_NOTHING or unexpected value return; } - Settings settings; + QSettings settings; const QString ADDRESS_MANAGER_GROUP = "AddressManager"; const QString ADDRESS_KEY = "address"; const QString AVATAR_GROUP = "Avatar"; const QString DISPLAY_NAME_KEY = "displayName"; const QString FULL_AVATAR_URL_KEY = "fullAvatarURL"; const QString FULL_AVATAR_MODEL_NAME_KEY = "fullAvatarModelName"; + const QString TUTORIAL_COMPLETE_FLAG_KEY = "tutorialComplete"; + QString displayName; QUrl fullAvatarURL; QString fullAvatarModelName; QUrl address; + bool tutorialComplete = false; - if (action == CrashHandler::RETAIN_AVATAR_INFO) { + if (action == CrashHandler::RETAIN_IMPORTANT_INFO) { // Read avatar info // Location and orientation @@ -132,6 +138,9 @@ void CrashHandler::handleCrash(CrashHandler::Action action) { fullAvatarURL = settings.value(FULL_AVATAR_URL_KEY).toUrl(); fullAvatarModelName = settings.value(FULL_AVATAR_MODEL_NAME_KEY).toString(); settings.endGroup(); + + // Tutorial complete + tutorialComplete = settings.value(TUTORIAL_COMPLETE_FLAG_KEY).toBool(); } // Delete Interface.ini @@ -140,7 +149,7 @@ void CrashHandler::handleCrash(CrashHandler::Action action) { settingsFile.remove(); } - if (action == CrashHandler::RETAIN_AVATAR_INFO) { + if (action == CrashHandler::RETAIN_IMPORTANT_INFO) { // Write avatar info // Location and orientation @@ -154,6 +163,9 @@ void CrashHandler::handleCrash(CrashHandler::Action action) { settings.setValue(FULL_AVATAR_URL_KEY, fullAvatarURL); settings.setValue(FULL_AVATAR_MODEL_NAME_KEY, fullAvatarModelName); settings.endGroup(); + + // Tutorial complete + settings.setValue(TUTORIAL_COMPLETE_FLAG_KEY, tutorialComplete); } } diff --git a/interface/src/CrashHandler.h b/interface/src/CrashHandler.h index da2e1575db..bff8bba6dd 100644 --- a/interface/src/CrashHandler.h +++ b/interface/src/CrashHandler.h @@ -22,7 +22,7 @@ public: private: enum Action { DELETE_INTERFACE_INI, - RETAIN_AVATAR_INFO, + RETAIN_IMPORTANT_INFO, DO_NOTHING }; diff --git a/interface/src/DiscoverabilityManager.cpp b/interface/src/DiscoverabilityManager.cpp index 36f6d8633e..94fdacb5c0 100644 --- a/interface/src/DiscoverabilityManager.cpp +++ b/interface/src/DiscoverabilityManager.cpp @@ -25,7 +25,7 @@ #include -const Discoverability::Mode DEFAULT_DISCOVERABILITY_MODE = Discoverability::Friends; +const Discoverability::Mode DEFAULT_DISCOVERABILITY_MODE = Discoverability::Connections; DiscoverabilityManager::DiscoverabilityManager() : _mode("discoverabilityMode", DEFAULT_DISCOVERABILITY_MODE) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index e19457d484..f8b1d495b3 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -405,6 +405,12 @@ Menu::Menu() { #endif + { + auto action = addActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::RenderClearKtxCache); + connect(action, &QAction::triggered, []{ + Setting::Handle(KTXCache::SETTING_VERSION_NAME, KTXCache::INVALID_VERSION).set(KTXCache::INVALID_VERSION); + }); + } // Developer > Render > LOD Tools addActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::LodTools, 0, diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 5e597f1a0f..f69860099d 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -142,6 +142,7 @@ namespace MenuOption { const QString Quit = "Quit"; const QString ReloadAllScripts = "Reload All Scripts"; const QString ReloadContent = "Reload Content (Clears all caches)"; + const QString RenderClearKtxCache = "Clear KTX Cache (requires restart)"; const QString RenderMaxTextureMemory = "Maximum Texture Memory"; const QString RenderMaxTextureAutomatic = "Automatic Texture Memory"; const QString RenderMaxTexture4MB = "4 MB"; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 72a9281564..24a25f314d 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -294,6 +294,7 @@ void MyAvatar::simulateAttachments(float deltaTime) { QByteArray MyAvatar::toByteArrayStateful(AvatarDataDetail dataDetail) { CameraMode mode = qApp->getCamera().getMode(); _globalPosition = getPosition(); + // This might not be right! Isn't the capsule local offset in avatar space, and don't we need to add the radius to the y as well? -HRS 5/26/17 _globalBoundingBoxDimensions.x = _characterController.getCapsuleRadius(); _globalBoundingBoxDimensions.y = _characterController.getCapsuleHalfHeight(); _globalBoundingBoxDimensions.z = _characterController.getCapsuleRadius(); @@ -409,7 +410,7 @@ void MyAvatar::update(float deltaTime) { // update moving average of HMD facing in xz plane. const float HMD_FACING_TIMESCALE = 4.0f; // very slow average float tau = deltaTime / HMD_FACING_TIMESCALE; - _hmdSensorFacingMovingAverage = lerp(_hmdSensorFacingMovingAverage, _hmdSensorFacing, tau); + _headControllerFacingMovingAverage = lerp(_headControllerFacingMovingAverage, _headControllerFacing, tau); if (_smoothOrientationTimer < SMOOTH_TIME_ORIENTATION) { _rotationChanged = usecTimestampNow(); @@ -417,16 +418,18 @@ void MyAvatar::update(float deltaTime) { } #ifdef DEBUG_DRAW_HMD_MOVING_AVERAGE - glm::vec3 p = transformPoint(getSensorToWorldMatrix(), _hmdSensorPosition + glm::vec3(_hmdSensorFacingMovingAverage.x, 0.0f, _hmdSensorFacingMovingAverage.y)); + glm::vec3 p = transformPoint(getSensorToWorldMatrix(), getHeadControllerPoseInAvatarFrame() * + glm::vec3(_headControllerFacingMovingAverage.x, 0.0f, _headControllerFacingMovingAverage.y)); DebugDraw::getInstance().addMarker("facing-avg", getOrientation(), p, glm::vec4(1.0f)); - p = transformPoint(getSensorToWorldMatrix(), _hmdSensorPosition + glm::vec3(_hmdSensorFacing.x, 0.0f, _hmdSensorFacing.y)); + p = transformPoint(getSensorToWorldMatrix(), getHMDSensorPosition() + + glm::vec3(_headControllerFacing.x, 0.0f, _headControllerFacing.y)); DebugDraw::getInstance().addMarker("facing", getOrientation(), p, glm::vec4(1.0f)); #endif if (_goToPending) { setPosition(_goToPosition); setOrientation(_goToOrientation); - _hmdSensorFacingMovingAverage = _hmdSensorFacing; // reset moving average + _headControllerFacingMovingAverage = _headControllerFacing; // reset moving average _goToPending = false; // updateFromHMDSensorMatrix (called from paintGL) expects that the sensorToWorldMatrix is updated for any position changes // that happen between render and Application::update (which calls updateSensorToWorldMatrix to do so). @@ -434,6 +437,13 @@ void MyAvatar::update(float deltaTime) { // so we update now. It's ok if it updates again in the normal way. updateSensorToWorldMatrix(); emit positionGoneTo(); + // Run safety tests as soon as we can after goToLocation, or clear if we're not colliding. + _physicsSafetyPending = getCollisionsEnabled(); + } + if (_physicsSafetyPending && qApp->isPhysicsEnabled() && _characterController.isEnabledAndReady()) { + // When needed and ready, arrange to check and fix. + _physicsSafetyPending = false; + safeLanding(_goToPosition); // no-op if already safe } Head* head = getHead(); @@ -448,6 +458,7 @@ void MyAvatar::update(float deltaTime) { setAudioAverageLoudness(audio->getAudioAverageInputLoudness()); glm::vec3 halfBoundingBoxDimensions(_characterController.getCapsuleRadius(), _characterController.getCapsuleHalfHeight(), _characterController.getCapsuleRadius()); + // This might not be right! Isn't the capsule local offset in avatar space? -HRS 5/26/17 halfBoundingBoxDimensions += _characterController.getCapsuleLocalOffset(); QMetaObject::invokeMethod(audio.data(), "setAvatarBoundingBoxParameters", Q_ARG(glm::vec3, (getPosition() - halfBoundingBoxDimensions)), @@ -633,15 +644,21 @@ void MyAvatar::updateFromHMDSensorMatrix(const glm::mat4& hmdSensorMatrix) { _hmdSensorMatrix = hmdSensorMatrix; auto newHmdSensorPosition = extractTranslation(hmdSensorMatrix); - if (newHmdSensorPosition != _hmdSensorPosition && + if (newHmdSensorPosition != getHMDSensorPosition() && glm::length(newHmdSensorPosition) > MAX_HMD_ORIGIN_DISTANCE) { qWarning() << "Invalid HMD sensor position " << newHmdSensorPosition; // Ignore unreasonable HMD sensor data return; } + _hmdSensorPosition = newHmdSensorPosition; _hmdSensorOrientation = glm::quat_cast(hmdSensorMatrix); - _hmdSensorFacing = getFacingDir2D(_hmdSensorOrientation); + auto headPose = _headControllerPoseInSensorFrameCache.get(); + if (headPose.isValid()) { + _headControllerFacing = getFacingDir2D(headPose.rotation); + } else { + _headControllerFacing = glm::vec2(1.0f, 0.0f); + } } void MyAvatar::updateJointFromController(controller::Action poseKey, ThreadSafeValueCache& matrixCache) { @@ -679,7 +696,7 @@ void MyAvatar::updateSensorToWorldMatrix() { // Update avatar head rotation with sensor data void MyAvatar::updateFromTrackers(float deltaTime) { - glm::vec3 estimatedPosition, estimatedRotation; + glm::vec3 estimatedRotation; bool inHmd = qApp->isHMDMode(); bool playing = DependencyManager::get()->isPlaying(); @@ -690,11 +707,7 @@ void MyAvatar::updateFromTrackers(float deltaTime) { FaceTracker* tracker = qApp->getActiveFaceTracker(); bool inFacetracker = tracker && !FaceTracker::isMuted(); - if (inHmd) { - estimatedPosition = extractTranslation(getHMDSensorMatrix()); - estimatedPosition.x *= -1.0f; - } else if (inFacetracker) { - estimatedPosition = tracker->getHeadTranslation(); + if (inFacetracker) { estimatedRotation = glm::degrees(safeEulerAngles(tracker->getHeadRotation())); } @@ -781,6 +794,77 @@ controller::Pose MyAvatar::getRightHandTipPose() const { return pose; } +glm::vec3 MyAvatar::worldToJointPoint(const glm::vec3& position, const int jointIndex) const { + glm::vec3 jointPos = getPosition();//default value if no or invalid joint specified + glm::quat jointRot = getRotation();//default value if no or invalid joint specified + if (jointIndex != -1) { + if (_skeletonModel->getJointPositionInWorldFrame(jointIndex, jointPos)) { + _skeletonModel->getJointRotationInWorldFrame(jointIndex, jointRot); + } else { + qWarning() << "Invalid joint index specified: " << jointIndex; + } + } + glm::vec3 modelOffset = position - jointPos; + glm::vec3 jointSpacePosition = glm::inverse(jointRot) * modelOffset; + + return jointSpacePosition; +} + +glm::vec3 MyAvatar::worldToJointDirection(const glm::vec3& worldDir, const int jointIndex) const { + glm::quat jointRot = getRotation();//default value if no or invalid joint specified + if ((jointIndex != -1) && (!_skeletonModel->getJointRotationInWorldFrame(jointIndex, jointRot))) { + qWarning() << "Invalid joint index specified: " << jointIndex; + } + + glm::vec3 jointSpaceDir = glm::inverse(jointRot) * worldDir; + return jointSpaceDir; +} + +glm::quat MyAvatar::worldToJointRotation(const glm::quat& worldRot, const int jointIndex) const { + glm::quat jointRot = getRotation();//default value if no or invalid joint specified + if ((jointIndex != -1) && (!_skeletonModel->getJointRotationInWorldFrame(jointIndex, jointRot))) { + qWarning() << "Invalid joint index specified: " << jointIndex; + } + glm::quat jointSpaceRot = glm::inverse(jointRot) * worldRot; + return jointSpaceRot; +} + +glm::vec3 MyAvatar::jointToWorldPoint(const glm::vec3& jointSpacePos, const int jointIndex) const { + glm::vec3 jointPos = getPosition();//default value if no or invalid joint specified + glm::quat jointRot = getRotation();//default value if no or invalid joint specified + + if (jointIndex != -1) { + if (_skeletonModel->getJointPositionInWorldFrame(jointIndex, jointPos)) { + _skeletonModel->getJointRotationInWorldFrame(jointIndex, jointRot); + } else { + qWarning() << "Invalid joint index specified: " << jointIndex; + } + } + + glm::vec3 worldOffset = jointRot * jointSpacePos; + glm::vec3 worldPos = jointPos + worldOffset; + + return worldPos; +} + +glm::vec3 MyAvatar::jointToWorldDirection(const glm::vec3& jointSpaceDir, const int jointIndex) const { + glm::quat jointRot = getRotation();//default value if no or invalid joint specified + if ((jointIndex != -1) && (!_skeletonModel->getJointRotationInWorldFrame(jointIndex, jointRot))) { + qWarning() << "Invalid joint index specified: " << jointIndex; + } + glm::vec3 worldDir = jointRot * jointSpaceDir; + return worldDir; +} + +glm::quat MyAvatar::jointToWorldRotation(const glm::quat& jointSpaceRot, const int jointIndex) const { + glm::quat jointRot = getRotation();//default value if no or invalid joint specified + if ((jointIndex != -1) && (!_skeletonModel->getJointRotationInWorldFrame(jointIndex, jointRot))) { + qWarning() << "Invalid joint index specified: " << jointIndex; + } + glm::quat worldRot = jointRot * jointSpaceRot; + return worldRot; +} + // virtual void MyAvatar::render(RenderArgs* renderArgs) { // don't render if we've been asked to disable local rendering @@ -1479,12 +1563,12 @@ void MyAvatar::updateMotors() { if (_motionBehaviors & AVATAR_MOTION_ACTION_MOTOR_ENABLED) { if (_characterController.getState() == CharacterController::State::Hover || _characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) { - motorRotation = getMyHead()->getCameraOrientation(); + motorRotation = getMyHead()->getHeadOrientation(); } else { // non-hovering = walking: follow camera twist about vertical but not lift // so we decompose camera's rotation and store the twist part in motorRotation glm::quat liftRotation; - swingTwistDecomposition(getMyHead()->getCameraOrientation(), _worldUpDirection, liftRotation, motorRotation); + swingTwistDecomposition(getMyHead()->getHeadOrientation(), _worldUpDirection, liftRotation, motorRotation); } const float DEFAULT_MOTOR_TIMESCALE = 0.2f; const float INVALID_MOTOR_TIMESCALE = 1.0e6f; @@ -1498,7 +1582,7 @@ void MyAvatar::updateMotors() { } if (_motionBehaviors & AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED) { if (_scriptedMotorFrame == SCRIPTED_MOTOR_CAMERA_FRAME) { - motorRotation = getMyHead()->getCameraOrientation() * glm::angleAxis(PI, Vectors::UNIT_Y); + motorRotation = getMyHead()->getHeadOrientation() * glm::angleAxis(PI, Vectors::UNIT_Y); } else if (_scriptedMotorFrame == SCRIPTED_MOTOR_AVATAR_FRAME) { motorRotation = getOrientation() * glm::angleAxis(PI, Vectors::UNIT_Y); } else { @@ -1548,6 +1632,10 @@ void MyAvatar::harvestResultsFromPhysicsSimulation(float deltaTime) { if (_characterController.isEnabledAndReady()) { setVelocity(_characterController.getLinearVelocity() + _characterController.getFollowVelocity()); + if (_characterController.isStuck()) { + _physicsSafetyPending = true; + _goToPosition = getPosition(); + } } else { setVelocity(getVelocity() + _characterController.getFollowVelocity()); } @@ -1847,7 +1935,7 @@ void MyAvatar::updateOrientation(float deltaTime) { if (getCharacterController()->getState() == CharacterController::State::Hover) { // This is the direction the user desires to fly in. - glm::vec3 desiredFacing = getMyHead()->getCameraOrientation() * Vectors::UNIT_Z; + glm::vec3 desiredFacing = getMyHead()->getHeadOrientation() * Vectors::UNIT_Z; desiredFacing.y = 0.0f; // This is our reference frame, it is captured when the user begins to move. @@ -1886,11 +1974,9 @@ void MyAvatar::updateOrientation(float deltaTime) { getHead()->setBasePitch(getHead()->getBasePitch() + getDriveKey(PITCH) * _pitchSpeed * deltaTime); - if (qApp->isHMDMode()) { - glm::quat orientation = glm::quat_cast(getSensorToWorldMatrix()) * getHMDSensorOrientation(); - glm::quat bodyOrientation = getWorldBodyOrientation(); - glm::quat localOrientation = glm::inverse(bodyOrientation) * orientation; - + auto headPose = getHeadControllerPoseInAvatarFrame(); + if (headPose.isValid()) { + glm::quat localOrientation = headPose.rotation * Quaternions::Y_180; // these angles will be in radians // ... so they need to be converted to degrees before we do math... glm::vec3 euler = glm::eulerAngles(localOrientation) * DEGREES_PER_RADIAN; @@ -2004,11 +2090,14 @@ void MyAvatar::updatePosition(float deltaTime) { } // capture the head rotation, in sensor space, when the user first indicates they would like to move/fly. - if (!_hoverReferenceCameraFacingIsCaptured && (fabs(getDriveKey(TRANSLATE_Z)) > 0.1f || fabs(getDriveKey(TRANSLATE_X)) > 0.1f)) { + if (!_hoverReferenceCameraFacingIsCaptured && + (fabs(getDriveKey(TRANSLATE_Z)) > 0.1f || fabs(getDriveKey(TRANSLATE_X)) > 0.1f)) { _hoverReferenceCameraFacingIsCaptured = true; // transform the camera facing vector into sensor space. - _hoverReferenceCameraFacing = transformVectorFast(glm::inverse(_sensorToWorldMatrix), getMyHead()->getCameraOrientation() * Vectors::UNIT_Z); - } else if (_hoverReferenceCameraFacingIsCaptured && (fabs(getDriveKey(TRANSLATE_Z)) <= 0.1f && fabs(getDriveKey(TRANSLATE_X)) <= 0.1f)) { + _hoverReferenceCameraFacing = transformVectorFast(glm::inverse(_sensorToWorldMatrix), + getMyHead()->getHeadOrientation() * Vectors::UNIT_Z); + } else if (_hoverReferenceCameraFacingIsCaptured && + (fabs(getDriveKey(TRANSLATE_Z)) <= 0.1f && fabs(getDriveKey(TRANSLATE_X)) <= 0.1f)) { _hoverReferenceCameraFacingIsCaptured = false; } } @@ -2219,6 +2308,144 @@ void MyAvatar::goToLocation(const glm::vec3& newPosition, emit transformChanged(); } +void MyAvatar::goToLocationAndEnableCollisions(const glm::vec3& position) { // See use case in safeLanding. + goToLocation(position); + QMetaObject::invokeMethod(this, "setCollisionsEnabled", Qt::QueuedConnection, Q_ARG(bool, true)); +} +bool MyAvatar::safeLanding(const glm::vec3& position) { + // Considers all collision hull or non-collisionless primitive intersections on a vertical line through the point. + // There needs to be a "landing" if: + // a) the closest above and the closest below are less than the avatar capsule height apart, or + // b) the above point is the top surface of an entity, indicating that we are inside it. + // If no landing is required, we go to that point directly and return false; + // When a landing is required by a, we find the highest intersection on that closest-agbove entity + // (which may be that same "nearest above intersection"). That highest intersection is the candidate landing point. + // For b, use that top surface point. + // We then place our feet there, recurse with new capsule center point, and return true. + + if (QThread::currentThread() != thread()) { + bool result; + QMetaObject::invokeMethod(this, "safeLanding", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, result), Q_ARG(const glm::vec3&, position)); + return result; + } + glm::vec3 better; + if (!requiresSafeLanding(position, better)) { + return false; + } + if (!getCollisionsEnabled()) { + goToLocation(better); // recurses on next update + } else { // If you try to go while stuck, physics will keep you stuck. + setCollisionsEnabled(false); + // Don't goToLocation just yet. Yield so that physics can act on the above. + QMetaObject::invokeMethod(this, "goToLocationAndEnableCollisions", Qt::QueuedConnection, // The equivalent of javascript nextTick + Q_ARG(glm::vec3, better)); + } + return true; +} + +// If position is not reliably safe from being stuck by physics, answer true and place a candidate better position in betterPositionOut. +bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& betterPositionOut) { + // We begin with utilities and tests. The Algorithm in four parts is below. + auto halfHeight = _characterController.getCapsuleHalfHeight() + _characterController.getCapsuleRadius(); + if (halfHeight == 0) { + return false; // zero height avatar + } + auto entityTree = DependencyManager::get()->getTree(); + if (!entityTree) { + return false; // no entity tree + } + // More utilities. + const auto offset = getOrientation() *_characterController.getCapsuleLocalOffset(); + const auto capsuleCenter = positionIn + offset; + const auto up = _worldUpDirection, down = -up; + glm::vec3 upperIntersection, upperNormal, lowerIntersection, lowerNormal; + EntityItemID upperId, lowerId; + QVector include{}, ignore{}; + auto mustMove = [&] { // Place bottom of capsule at the upperIntersection, and check again based on the capsule center. + betterPositionOut = upperIntersection + (up * halfHeight) - offset; + return true; + }; + auto findIntersection = [&](const glm::vec3& startPointIn, const glm::vec3& directionIn, glm::vec3& intersectionOut, EntityItemID& entityIdOut, glm::vec3& normalOut) { + OctreeElementPointer element; + EntityItemPointer intersectedEntity = NULL; + float distance; + BoxFace face; + const bool visibleOnly = false; + // This isn't quite what we really want here. findRayIntersection always works on mesh, skipping entirely based on collidable. + // What we really want is to use the collision hull! + // See https://highfidelity.fogbugz.com/f/cases/5003/findRayIntersection-has-option-to-use-collidableOnly-but-doesn-t-actually-use-colliders + const bool collidableOnly = true; + const bool precisionPicking = true; + const auto lockType = Octree::Lock; // Should we refactor to take a lock just once? + bool* accurateResult = NULL; + + bool intersects = entityTree->findRayIntersection(startPointIn, directionIn, include, ignore, visibleOnly, collidableOnly, precisionPicking, + element, distance, face, normalOut, (void**)&intersectedEntity, lockType, accurateResult); + if (!intersects || !intersectedEntity) { + return false; + } + intersectionOut = startPointIn + (directionIn * distance); + entityIdOut = intersectedEntity->getEntityItemID(); + return true; + }; + + // The Algorithm, in four parts: + + if (!findIntersection(capsuleCenter, up, upperIntersection, upperId, upperNormal)) { + // We currently believe that physics will reliably push us out if our feet are embedded, + // as long as our capsule center is out and there's room above us. Here we have those + // conditions, so no need to check our feet below. + return false; // nothing above + } + + if (!findIntersection(capsuleCenter, down, lowerIntersection, lowerId, lowerNormal)) { + // Our head may be embedded, but our center is out and there's room below. See corresponding comment above. + return false; // nothing below + } + + // See if we have room between entities above and below, but that we are not contained. + // First check if the surface above us is the bottom of something, and the surface below us it the top of something. + // I.e., we are in a clearing between two objects. + if (isDown(upperNormal) && isUp(lowerNormal)) { + auto spaceBetween = glm::distance(upperIntersection, lowerIntersection); + const float halfHeightFactor = 2.5f; // Until case 5003 is fixed (and maybe after?), we need a fudge factor. Also account for content modelers not being precise. + if (spaceBetween > (halfHeightFactor * halfHeight)) { + // There is room for us to fit in that clearing. If there wasn't, physics would oscilate us between the objects above and below. + // We're now going to iterate upwards through successive upperIntersections, testing to see if we're contained within the top surface of some entity. + // There will be one of two outcomes: + // a) We're not contained, so we have enough room and our position is good. + // b) We are contained, so we'll bail out of this but try again at a position above the containing entity. + const int iterationLimit = 1000; + for (int counter = 0; counter < iterationLimit; counter++) { + ignore.push_back(upperId); + if (!findIntersection(upperIntersection, up, upperIntersection, upperId, upperNormal)) { + // We're not inside an entity, and from the nested tests, we have room between what is above and below. So position is good! + return false; // enough room + } + if (isUp(upperNormal)) { + // This new intersection is the top surface of an entity that we have not yet seen, which means we're contained within it. + // We could break here and recurse from the top of the original ceiling, but since we've already done the work to find the top + // of the enclosing entity, let's put our feet at upperIntersection and start over. + return mustMove(); + } + // We found a new bottom surface, which we're not interested in. + // But there could still be a top surface above us for an entity we haven't seen, so keep looking upward. + } + qCDebug(interfaceapp) << "Loop in requiresSafeLanding. Floor/ceiling do not make sense."; + } + } + + include.push_back(upperId); // We're now looking for the intersection from above onto this entity. + const float big = (float)TREE_SCALE; + const auto skyHigh = up * big; + auto fromAbove = capsuleCenter + skyHigh; + if (!findIntersection(fromAbove, down, upperIntersection, upperId, upperNormal)) { + return false; // Unable to find a landing + } + // Our arbitrary rule is to always go up. There's no need to look down or sideways for a "closer" safe candidate. + return mustMove(); +} + void MyAvatar::updateMotionBehaviorFromMenu() { if (QThread::currentThread() != thread()) { @@ -2322,36 +2549,27 @@ bool MyAvatar::isDriveKeyDisabled(DriveKeys key) const { } } -glm::vec3 MyAvatar::getWorldBodyPosition() const { - return transformPoint(_sensorToWorldMatrix, extractTranslation(_bodySensorMatrix)); -} - -glm::quat MyAvatar::getWorldBodyOrientation() const { - return glm::quat_cast(_sensorToWorldMatrix * _bodySensorMatrix); -} - // old school meat hook style glm::mat4 MyAvatar::deriveBodyFromHMDSensor() const { - - // HMD is in sensor space. - const glm::vec3 hmdPosition = getHMDSensorPosition(); - const glm::quat hmdOrientation = getHMDSensorOrientation(); - const glm::quat hmdOrientationYawOnly = cancelOutRollAndPitch(hmdOrientation); + glm::vec3 headPosition; + glm::quat headOrientation; + auto headPose = getHeadControllerPoseInSensorFrame(); + if (headPose.isValid()) { + headPosition = getHeadControllerPoseInSensorFrame().translation; + headOrientation = getHeadControllerPoseInSensorFrame().rotation * Quaternions::Y_180; + } + const glm::quat headOrientationYawOnly = cancelOutRollAndPitch(headOrientation); const Rig& rig = _skeletonModel->getRig(); - int rightEyeIndex = rig.indexOfJoint("RightEye"); - int leftEyeIndex = rig.indexOfJoint("LeftEye"); + int headIndex = rig.indexOfJoint("Head"); int neckIndex = rig.indexOfJoint("Neck"); int hipsIndex = rig.indexOfJoint("Hips"); - 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 rigHeadPos = headIndex != -1 ? rig.getAbsoluteDefaultPose(headIndex).trans() : DEFAULT_AVATAR_HEAD_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 localHead = (rigHeadPos - rigHipsPos); glm::vec3 localNeck = (rigNeckPos - rigHipsPos); // apply simplistic head/neck model @@ -2360,11 +2578,11 @@ glm::mat4 MyAvatar::deriveBodyFromHMDSensor() const { // eyeToNeck offset is relative full HMD orientation. // while neckToRoot offset is only relative to HMDs yaw. // Y_180 is necessary because rig is z forward and hmdOrientation is -z forward - glm::vec3 eyeToNeck = hmdOrientation * Quaternions::Y_180 * (localNeck - localEyes); - glm::vec3 neckToRoot = hmdOrientationYawOnly * Quaternions::Y_180 * -localNeck; - glm::vec3 bodyPos = hmdPosition + eyeToNeck + neckToRoot; + glm::vec3 headToNeck = headOrientation * Quaternions::Y_180 * (localNeck - localHead); + glm::vec3 neckToRoot = headOrientationYawOnly * Quaternions::Y_180 * -localNeck; + glm::vec3 bodyPos = headPosition + headToNeck + neckToRoot; - return createMatFromQuatAndPos(hmdOrientationYawOnly, bodyPos); + return createMatFromQuatAndPos(headOrientationYawOnly, bodyPos); } glm::vec3 MyAvatar::getPositionForAudio() { @@ -2480,7 +2698,7 @@ bool MyAvatar::FollowHelper::shouldActivateRotation(const MyAvatar& myAvatar, co } else { const float FOLLOW_ROTATION_THRESHOLD = cosf(PI / 6.0f); // 30 degrees glm::vec2 bodyFacing = getFacingDir2D(currentBodyMatrix); - return glm::dot(myAvatar.getHMDSensorFacingMovingAverage(), bodyFacing) < FOLLOW_ROTATION_THRESHOLD; + return glm::dot(-myAvatar.getHeadControllerFacingMovingAverage(), bodyFacing) < FOLLOW_ROTATION_THRESHOLD; } } @@ -2517,8 +2735,8 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(const MyAvatar& myAvatar, co return (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); } -void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix, bool hasDriveInput) { - _desiredBodyMatrix = desiredBodyMatrix; +void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, + const glm::mat4& currentBodyMatrix, bool hasDriveInput) { if (myAvatar.getHMDLeanRecenterEnabled()) { if (!isActive(Rotation) && shouldActivateRotation(myAvatar, desiredBodyMatrix, currentBodyMatrix)) { @@ -2532,7 +2750,7 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat } } - glm::mat4 desiredWorldMatrix = myAvatar.getSensorToWorldMatrix() * _desiredBodyMatrix; + glm::mat4 desiredWorldMatrix = myAvatar.getSensorToWorldMatrix() * desiredBodyMatrix; glm::mat4 currentWorldMatrix = myAvatar.getSensorToWorldMatrix() * currentBodyMatrix; AnimPose followWorldPose(currentWorldMatrix); @@ -2627,9 +2845,10 @@ glm::mat4 MyAvatar::computeCameraRelativeHandControllerMatrix(const glm::mat4& c cameraWorldMatrix *= createMatFromScaleQuatAndPos(vec3(-1.0f, 1.0f, 1.0f), glm::quat(), glm::vec3()); } - // compute a NEW sensorToWorldMatrix for the camera. The equation is cameraWorldMatrix = cameraSensorToWorldMatrix * _hmdSensorMatrix. + // compute a NEW sensorToWorldMatrix for the camera. + // The equation is cameraWorldMatrix = cameraSensorToWorldMatrix * _hmdSensorMatrix. // here we solve for the unknown cameraSensorToWorldMatrix. - glm::mat4 cameraSensorToWorldMatrix = cameraWorldMatrix * glm::inverse(_hmdSensorMatrix); + glm::mat4 cameraSensorToWorldMatrix = cameraWorldMatrix * glm::inverse(getHMDSensorMatrix()); // Using the new cameraSensorToWorldMatrix, compute where the controller is in world space. glm::mat4 controllerWorldMatrix = cameraSensorToWorldMatrix * controllerSensorMatrix; @@ -2770,7 +2989,7 @@ glm::mat4 MyAvatar::getLeftFootCalibrationMat() const { auto leftFootRot = getAbsoluteDefaultJointRotationInObjectFrame(leftFootIndex); return createMatFromQuatAndPos(leftFootRot, leftFootPos); } else { - return createMatFromQuatAndPos(DEFAULT_AVATAR_LEFTFOOT_POS, DEFAULT_AVATAR_LEFTFOOT_POS); + return createMatFromQuatAndPos(DEFAULT_AVATAR_LEFTFOOT_ROT, DEFAULT_AVATAR_LEFTFOOT_POS); } } @@ -2782,7 +3001,7 @@ glm::mat4 MyAvatar::getRightFootCalibrationMat() const { auto rightFootRot = getAbsoluteDefaultJointRotationInObjectFrame(rightFootIndex); return createMatFromQuatAndPos(rightFootRot, rightFootPos); } else { - return createMatFromQuatAndPos(DEFAULT_AVATAR_RIGHTFOOT_POS, DEFAULT_AVATAR_RIGHTFOOT_POS); + return createMatFromQuatAndPos(DEFAULT_AVATAR_RIGHTFOOT_ROT, DEFAULT_AVATAR_RIGHTFOOT_POS); } } @@ -2805,7 +3024,7 @@ glm::mat4 MyAvatar::getLeftArmCalibrationMat() const { auto leftArmRot = getAbsoluteDefaultJointRotationInObjectFrame(leftArmIndex); return createMatFromQuatAndPos(leftArmRot, leftArmPos); } else { - return createMatFromQuatAndPos(DEFAULT_AVATAR_LEFTARM_ROT, DEFAULT_AVATAR_RIGHTARM_POS); + return createMatFromQuatAndPos(DEFAULT_AVATAR_LEFTARM_ROT, DEFAULT_AVATAR_LEFTARM_POS); } } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index e0a3a1559e..cfe66eb10e 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -185,7 +185,6 @@ public: const glm::mat4& getHMDSensorMatrix() const { return _hmdSensorMatrix; } const glm::vec3& getHMDSensorPosition() const { return _hmdSensorPosition; } const glm::quat& getHMDSensorOrientation() const { return _hmdSensorOrientation; } - const glm::vec2& getHMDSensorFacingMovingAverage() const { return _hmdSensorFacingMovingAverage; } Q_INVOKABLE void setOrientationVar(const QVariant& newOrientationVar); Q_INVOKABLE QVariant getOrientationVar() const; @@ -379,6 +378,15 @@ public: Q_INVOKABLE controller::Pose getLeftHandTipPose() const; Q_INVOKABLE controller::Pose getRightHandTipPose() const; + // world-space to avatar-space rigconversion functions + Q_INVOKABLE glm::vec3 worldToJointPoint(const glm::vec3& position, const int jointIndex = -1) const; + Q_INVOKABLE glm::vec3 worldToJointDirection(const glm::vec3& direction, const int jointIndex = -1) const; + Q_INVOKABLE glm::quat worldToJointRotation(const glm::quat& rotation, const int jointIndex = -1) const; + + Q_INVOKABLE glm::vec3 jointToWorldPoint(const glm::vec3& position, const int jointIndex = -1) const; + Q_INVOKABLE glm::vec3 jointToWorldDirection(const glm::vec3& direction, const int jointIndex = -1) const; + Q_INVOKABLE glm::quat jointToWorldRotation(const glm::quat& rotation, const int jointIndex = -1) const; + AvatarWeakPointer getLookAtTargetAvatar() const { return _lookAtTargetAvatar; } void updateLookAtTargetAvatar(); void clearLookAtTargetAvatar(); @@ -470,6 +478,8 @@ public: controller::Pose getHeadControllerPoseInSensorFrame() const; controller::Pose getHeadControllerPoseInWorldFrame() const; controller::Pose getHeadControllerPoseInAvatarFrame() const; + const glm::vec2& getHeadControllerFacingMovingAverage() const { return _headControllerFacingMovingAverage; } + void setArmControllerPosesInSensorFrame(const controller::Pose& left, const controller::Pose& right); controller::Pose getLeftArmControllerPoseInSensorFrame() const; @@ -509,6 +519,10 @@ public: // results are in HMD frame glm::mat4 deriveBodyFromHMDSensor() const; + Q_INVOKABLE bool isUp(const glm::vec3& direction) { return glm::dot(direction, _worldUpDirection) > 0.0f; }; // true iff direction points up wrt avatar's definition of up. + Q_INVOKABLE bool isDown(const glm::vec3& direction) { return glm::dot(direction, _worldUpDirection) < 0.0f; }; + + public slots: void increaseSize(); void decreaseSize(); @@ -518,6 +532,8 @@ public slots: bool hasOrientation = false, const glm::quat& newOrientation = glm::quat(), bool shouldFaceLocation = false); void goToLocation(const QVariant& properties); + void goToLocationAndEnableCollisions(const glm::vec3& newPosition); + bool safeLanding(const glm::vec3& position); void restrictScaleFromDomainSettings(const QJsonObject& domainSettingsObject); void clearScaleRestriction(); @@ -563,9 +579,7 @@ signals: private: - glm::vec3 getWorldBodyPosition() const; - glm::quat getWorldBodyOrientation() const; - + bool requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& positionOut); virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail) override; @@ -676,13 +690,13 @@ private: // working copies -- see AvatarData for thread-safe _sensorToWorldMatrixCache, used for outward facing access glm::mat4 _sensorToWorldMatrix { glm::mat4() }; - // cache of the current HMD sensor position and orientation - // in sensor space. + // cache of the current HMD sensor position and orientation in sensor space. glm::mat4 _hmdSensorMatrix; glm::quat _hmdSensorOrientation; glm::vec3 _hmdSensorPosition; - glm::vec2 _hmdSensorFacing; // facing vector in xz plane - glm::vec2 _hmdSensorFacingMovingAverage { 0, 0 }; // facing vector in xz plane + // cache head controller pose in sensor space + glm::vec2 _headControllerFacing; // facing vector in xz plane + glm::vec2 _headControllerFacingMovingAverage { 0, 0 }; // facing vector in xz plane // cache of the current body position and orientation of the avatar's body, // in sensor space. @@ -697,7 +711,6 @@ private: Vertical, NumFollowTypes }; - glm::mat4 _desiredBodyMatrix; float _timeRemaining[NumFollowTypes]; void deactivate(); @@ -716,7 +729,8 @@ private: }; FollowHelper _follow; - bool _goToPending; + bool _goToPending { false }; + bool _physicsSafetyPending { false }; glm::vec3 _goToPosition; glm::quat _goToOrientation; diff --git a/interface/src/avatar/MyHead.cpp b/interface/src/avatar/MyHead.cpp index f02aefec5b..9f2d080cd6 100644 --- a/interface/src/avatar/MyHead.cpp +++ b/interface/src/avatar/MyHead.cpp @@ -26,19 +26,20 @@ using namespace std; MyHead::MyHead(MyAvatar* owningAvatar) : Head(owningAvatar) { } -glm::quat MyHead::getCameraOrientation() const { - // NOTE: Head::getCameraOrientation() is not used for orienting the camera "view" while in Oculus mode, so +glm::quat MyHead::getHeadOrientation() const { + // NOTE: Head::getHeadOrientation() is not used for orienting the camera "view" while in Oculus mode, so // you may wonder why this code is here. This method will be called while in Oculus mode to determine how // to change the driving direction while in Oculus mode. It is used to support driving toward where you're // head is looking. Note that in oculus mode, your actual camera view and where your head is looking is not // always the same. - if (qApp->isHMDMode()) { - MyAvatar* myAvatar = static_cast(_owningAvatar); - return glm::quat_cast(myAvatar->getSensorToWorldMatrix()) * myAvatar->getHMDSensorOrientation(); - } else { - Avatar* owningAvatar = static_cast(_owningAvatar); - return owningAvatar->getWorldAlignedOrientation() * glm::quat(glm::radians(glm::vec3(_basePitch, 0.0f, 0.0f))); + + MyAvatar* myAvatar = static_cast(_owningAvatar); + auto headPose = myAvatar->getHeadControllerPoseInWorldFrame(); + if (headPose.isValid()) { + return headPose.rotation * Quaternions::Y_180; } + + return myAvatar->getWorldAlignedOrientation() * glm::quat(glm::radians(glm::vec3(_basePitch, 0.0f, 0.0f))); } void MyHead::simulate(float deltaTime) { diff --git a/interface/src/avatar/MyHead.h b/interface/src/avatar/MyHead.h index 097415153c..c999311b80 100644 --- a/interface/src/avatar/MyHead.h +++ b/interface/src/avatar/MyHead.h @@ -18,7 +18,7 @@ public: explicit MyHead(MyAvatar* owningAvatar); /// \return orientationBody * orientationBasePitch - glm::quat getCameraOrientation() const; + glm::quat getHeadOrientation() const; void simulate(float deltaTime) override; private: diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp index a88631f47f..e74df4cf0f 100644 --- a/interface/src/avatar/MySkeletonModel.cpp +++ b/interface/src/avatar/MySkeletonModel.cpp @@ -52,26 +52,18 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { // 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()); + glm::mat4 rigHeadMat = Matrices::Y_180 * + createMatFromQuatAndPos(avatarHeadPose.getRotation(), avatarHeadPose.getTranslation()); headParams.rigHeadPosition = extractTranslation(rigHeadMat); headParams.rigHeadOrientation = glmExtractRotation(rigHeadMat); headParams.headEnabled = true; } else { - 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; - } + // 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(); diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 49517eb38e..83cac6d9bb 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -24,7 +24,6 @@ #include #include - #include "AddressManager.h" #include "Application.h" #include "InterfaceLogging.h" @@ -191,7 +190,7 @@ int main(int argc, const char* argv[]) { int exitCode; { - RunningMarker runningMarker(nullptr, RUNNING_MARKER_FILENAME); + RunningMarker runningMarker(RUNNING_MARKER_FILENAME); bool runningMarkerExisted = runningMarker.fileExists(); runningMarker.writeRunningMarkerFile(); @@ -200,14 +199,11 @@ int main(int argc, const char* argv[]) { bool serverContentPathOptionIsSet = parser.isSet(serverContentPathOption); QString serverContentPath = serverContentPathOptionIsSet ? parser.value(serverContentPathOption) : QString(); if (runServer) { - SandboxUtils::runLocalSandbox(serverContentPath, true, RUNNING_MARKER_FILENAME, noUpdater); + SandboxUtils::runLocalSandbox(serverContentPath, true, noUpdater); } Application app(argc, const_cast(argv), startupTime, runningMarkerExisted); - // Now that the main event loop is setup, launch running marker thread - runningMarker.startRunningMarker(); - // If we failed the OpenGLVersion check, log it. if (override) { auto accountManager = DependencyManager::get(); diff --git a/interface/src/networking/CloseEventSender.cpp b/interface/src/networking/CloseEventSender.cpp new file mode 100644 index 0000000000..de8bd897b2 --- /dev/null +++ b/interface/src/networking/CloseEventSender.cpp @@ -0,0 +1,95 @@ +// +// CloseEventSender.cpp +// interface/src/networking +// +// Created by Stephen Birarda on 5/31/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "CloseEventSender.h" + +QNetworkRequest createNetworkRequest() { + + QNetworkRequest request; + + QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL; + requestURL.setPath(USER_ACTIVITY_URL); + + request.setUrl(requestURL); + + auto accountManager = DependencyManager::get(); + + if (accountManager->hasValidAccessToken()) { + request.setRawHeader(ACCESS_TOKEN_AUTHORIZATION_HEADER, + accountManager->getAccountInfo().getAccessToken().authorizationHeaderValue()); + } + + request.setRawHeader(METAVERSE_SESSION_ID_HEADER, + uuidStringWithoutCurlyBraces(accountManager->getSessionID()).toLocal8Bit()); + + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + request.setPriority(QNetworkRequest::HighPriority); + + return request; +} + +QByteArray postDataForAction(QString action) { + return QString("{\"action_name\": \"" + action + "\"}").toUtf8(); +} + +QNetworkReply* replyForAction(QString action) { + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + return networkAccessManager.post(createNetworkRequest(), postDataForAction(action)); +} + +void CloseEventSender::sendQuitEventAsync() { + if (UserActivityLogger::getInstance().isEnabled()) { + QNetworkReply* reply = replyForAction("quit"); + connect(reply, &QNetworkReply::finished, this, &CloseEventSender::handleQuitEventFinished); + _quitEventStartTimestamp = QDateTime::currentMSecsSinceEpoch(); + } else { + _hasFinishedQuitEvent = true; + } +} + +void CloseEventSender::handleQuitEventFinished() { + _hasFinishedQuitEvent = true; + + auto reply = qobject_cast(sender()); + if (reply->error() == QNetworkReply::NoError) { + qCDebug(networking) << "Quit event sent successfully"; + } else { + qCDebug(networking) << "Failed to send quit event -" << reply->errorString(); + } + + reply->deleteLater(); +} + +bool CloseEventSender::hasTimedOutQuitEvent() { + const int CLOSURE_EVENT_TIMEOUT_MS = 5000; + return _quitEventStartTimestamp != 0 + && QDateTime::currentMSecsSinceEpoch() - _quitEventStartTimestamp > CLOSURE_EVENT_TIMEOUT_MS; +} + +void CloseEventSender::startThread() { + moveToNewNamedThread(this, "CloseEvent Logger Thread", [this] { + sendQuitEventAsync(); + }); +} diff --git a/interface/src/networking/CloseEventSender.h b/interface/src/networking/CloseEventSender.h new file mode 100644 index 0000000000..b74412c41c --- /dev/null +++ b/interface/src/networking/CloseEventSender.h @@ -0,0 +1,42 @@ +// +// CloseEventSender.h +// interface/src/networking +// +// Created by Stephen Birarda on 5/31/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_CloseEventSender_h +#define hifi_CloseEventSender_h + +#include + +#include +#include + +#include + +class CloseEventSender : public QObject, public Dependency { + Q_OBJECT + SINGLETON_DEPENDENCY + +public: + void startThread(); + bool hasTimedOutQuitEvent(); + bool hasFinishedQuitEvent() { return _hasFinishedQuitEvent; } + +public slots: + void sendQuitEventAsync(); + +private slots: + void handleQuitEventFinished(); + +private: + std::atomic _hasFinishedQuitEvent { false }; + std::atomic _quitEventStartTimestamp; +}; + +#endif // hifi_CloseEventSender_h diff --git a/interface/src/scripting/HMDScriptingInterface.cpp b/interface/src/scripting/HMDScriptingInterface.cpp index 95bf5eb028..883a6e758e 100644 --- a/interface/src/scripting/HMDScriptingInterface.cpp +++ b/interface/src/scripting/HMDScriptingInterface.cpp @@ -54,6 +54,10 @@ bool HMDScriptingInterface::isHMDAvailable(const QString& name) { return PluginUtils::isHMDAvailable(name); } +bool HMDScriptingInterface::isHeadControllerAvailable(const QString& name) { + return PluginUtils::isHeadControllerAvailable(name); +} + bool HMDScriptingInterface::isHandControllerAvailable(const QString& name) { return PluginUtils::isHandControllerAvailable(name); } diff --git a/interface/src/scripting/HMDScriptingInterface.h b/interface/src/scripting/HMDScriptingInterface.h index 7ecafdcbcb..4657e61d05 100644 --- a/interface/src/scripting/HMDScriptingInterface.h +++ b/interface/src/scripting/HMDScriptingInterface.h @@ -43,6 +43,7 @@ public: Q_INVOKABLE QString preferredAudioOutput() const; Q_INVOKABLE bool isHMDAvailable(const QString& name = ""); + Q_INVOKABLE bool isHeadControllerAvailable(const QString& name = ""); Q_INVOKABLE bool isHandControllerAvailable(const QString& name = ""); Q_INVOKABLE bool isSubdeviceContainingNameAvailable(const QString& name); diff --git a/interface/src/scripting/LimitlessVoiceRecognitionScriptingInterface.cpp b/interface/src/scripting/LimitlessVoiceRecognitionScriptingInterface.cpp index 1352630f84..ebb5ca9280 100644 --- a/interface/src/scripting/LimitlessVoiceRecognitionScriptingInterface.cpp +++ b/interface/src/scripting/LimitlessVoiceRecognitionScriptingInterface.cpp @@ -9,9 +9,12 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include + +#include #include #include -#include + #include "LimitlessVoiceRecognitionScriptingInterface.h" const float LimitlessVoiceRecognitionScriptingInterface::_audioLevelThreshold = 0.33f; @@ -24,9 +27,7 @@ LimitlessVoiceRecognitionScriptingInterface::LimitlessVoiceRecognitionScriptingI connect(&_voiceTimer, &QTimer::timeout, this, &LimitlessVoiceRecognitionScriptingInterface::voiceTimeout); connect(&_connection, &LimitlessConnection::onReceivedTranscription, this, [this](QString transcription){emit onReceivedTranscription(transcription);}); connect(&_connection, &LimitlessConnection::onFinishedSpeaking, this, [this](QString transcription){emit onFinishedSpeaking(transcription);}); - _connection.moveToThread(&_connectionThread); - _connectionThread.setObjectName("Limitless Connection"); - _connectionThread.start(); + moveToNewNamedThread(&_connection, "Limitless Connection"); } void LimitlessVoiceRecognitionScriptingInterface::update() { diff --git a/interface/src/scripting/LimitlessVoiceRecognitionScriptingInterface.h b/interface/src/scripting/LimitlessVoiceRecognitionScriptingInterface.h index d1b1139695..2a35c37ab0 100644 --- a/interface/src/scripting/LimitlessVoiceRecognitionScriptingInterface.h +++ b/interface/src/scripting/LimitlessVoiceRecognitionScriptingInterface.h @@ -41,7 +41,6 @@ private: static const int _voiceTimeoutDuration; QTimer _voiceTimer; - QThread _connectionThread; LimitlessConnection _connection; void voiceTimeout(); diff --git a/interface/src/ui/ModelsBrowser.cpp b/interface/src/ui/ModelsBrowser.cpp index 430cc805ed..44089119c6 100644 --- a/interface/src/ui/ModelsBrowser.cpp +++ b/interface/src/ui/ModelsBrowser.cpp @@ -23,6 +23,7 @@ #include #include +#include #include #include @@ -84,12 +85,7 @@ ModelsBrowser::ModelsBrowser(FSTReader::ModelType modelsType, QWidget* parent) : _handler->connect(this, SIGNAL(destroyed()), SLOT(exit())); // Setup and launch update thread - QThread* thread = new QThread(); - thread->setObjectName("Models Browser"); - thread->connect(_handler, SIGNAL(destroyed()), SLOT(quit())); - thread->connect(thread, SIGNAL(finished()), SLOT(deleteLater())); - _handler->moveToThread(thread); - thread->start(); + moveToNewNamedThread(_handler, "Models Browser"); emit startDownloading(); // Initialize the view diff --git a/interface/src/ui/overlays/Text3DOverlay.cpp b/interface/src/ui/overlays/Text3DOverlay.cpp index 2e2d586abc..ebc28ca86a 100644 --- a/interface/src/ui/overlays/Text3DOverlay.cpp +++ b/interface/src/ui/overlays/Text3DOverlay.cpp @@ -51,6 +51,16 @@ Text3DOverlay::~Text3DOverlay() { } } +const QString Text3DOverlay::getText() const { + QMutexLocker lock(&_mutex); + return _text; +} + +void Text3DOverlay::setText(const QString& text) { + QMutexLocker lock(&_mutex); + _text = text; +} + xColor Text3DOverlay::getBackgroundColor() { if (_colorPulse == 0.0f) { return _backgroundColor; @@ -125,7 +135,7 @@ void Text3DOverlay::render(RenderArgs* args) { // FIXME: Factor out textRenderer so that Text3DOverlay overlay parts can be grouped by pipeline // for a gpu performance increase. Currently, // Text renderer sets its own pipeline, - _textRenderer->draw(batch, 0, 0, _text, textColor, glm::vec2(-1.0f), getDrawInFront()); + _textRenderer->draw(batch, 0, 0, getText(), textColor, glm::vec2(-1.0f), getDrawInFront()); // so before we continue, we must reset the pipeline batch.setPipeline(args->_pipeline->pipeline); args->_pipeline->prepare(batch); @@ -188,7 +198,7 @@ void Text3DOverlay::setProperties(const QVariantMap& properties) { QVariant Text3DOverlay::getProperty(const QString& property) { if (property == "text") { - return _text; + return getText(); } if (property == "textAlpha") { return _textAlpha; @@ -231,7 +241,7 @@ QSizeF Text3DOverlay::textSize(const QString& text) const { return QSizeF(extents.x, extents.y) * pointToWorldScale; } -bool Text3DOverlay::findRayIntersection(const glm::vec3 &origin, const glm::vec3 &direction, float &distance, +bool Text3DOverlay::findRayIntersection(const glm::vec3 &origin, const glm::vec3 &direction, float &distance, BoxFace &face, glm::vec3& surfaceNormal) { Transform transform = getTransform(); applyTransformTo(transform, true); diff --git a/interface/src/ui/overlays/Text3DOverlay.h b/interface/src/ui/overlays/Text3DOverlay.h index 5ba4fe5939..e7b09c9040 100644 --- a/interface/src/ui/overlays/Text3DOverlay.h +++ b/interface/src/ui/overlays/Text3DOverlay.h @@ -12,14 +12,14 @@ #define hifi_Text3DOverlay_h #include - +#include #include "Billboard3DOverlay.h" class TextRenderer3D; class Text3DOverlay : public Billboard3DOverlay { Q_OBJECT - + public: static QString const TYPE; virtual QString getType() const override { return TYPE; } @@ -34,7 +34,7 @@ public: virtual const render::ShapeKey getShapeKey() override; // getters - const QString& getText() const { return _text; } + const QString getText() const; float getLineHeight() const { return _lineHeight; } float getLeftMargin() const { return _leftMargin; } float getTopMargin() const { return _topMargin; } @@ -45,7 +45,7 @@ public: float getBackgroundAlpha() { return getAlpha(); } // setters - void setText(const QString& text) { _text = text; } + void setText(const QString& text); void setTextAlpha(float alpha) { _textAlpha = alpha; } void setLineHeight(float value) { _lineHeight = value; } void setLeftMargin(float margin) { _leftMargin = margin; } @@ -58,15 +58,16 @@ public: QSizeF textSize(const QString& test) const; // Meters - virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, + virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal) override; virtual Text3DOverlay* createClone() const override; private: TextRenderer3D* _textRenderer = nullptr; - + QString _text; + mutable QMutex _mutex; // used to make get/setText threadsafe, mutable so can be used in const functions xColor _backgroundColor = xColor { 0, 0, 0 }; float _textAlpha { 1.0f }; float _lineHeight { 1.0f }; diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 1102e20047..4f939b6046 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -134,7 +134,7 @@ Web3DOverlay::~Web3DOverlay() { void Web3DOverlay::update(float deltatime) { if (_webSurface) { // update globalPosition - _webSurface->getRootContext()->setContextProperty("globalPosition", vec3toVariant(getPosition())); + _webSurface->getSurfaceContext()->setContextProperty("globalPosition", vec3toVariant(getPosition())); } } @@ -162,57 +162,56 @@ void Web3DOverlay::loadSourceURL() { _webSurface->resume(); _webSurface->getRootItem()->setProperty("url", _url); _webSurface->getRootItem()->setProperty("scriptURL", _scriptURL); - _webSurface->getRootContext()->setContextProperty("ApplicationInterface", qApp); } else { _webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath())); _webSurface->load(_url, [&](QQmlContext* context, QObject* obj) {}); _webSurface->resume(); - _webSurface->getRootContext()->setContextProperty("Users", DependencyManager::get().data()); - _webSurface->getRootContext()->setContextProperty("HMD", DependencyManager::get().data()); - _webSurface->getRootContext()->setContextProperty("UserActivityLogger", DependencyManager::get().data()); - _webSurface->getRootContext()->setContextProperty("Preferences", DependencyManager::get().data()); - _webSurface->getRootContext()->setContextProperty("Vec3", new Vec3()); - _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()); + _webSurface->getSurfaceContext()->setContextProperty("Users", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("HMD", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("UserActivityLogger", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("Preferences", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("Vec3", new Vec3()); + _webSurface->getSurfaceContext()->setContextProperty("Quat", new Quat()); + _webSurface->getSurfaceContext()->setContextProperty("MyAvatar", DependencyManager::get()->getMyAvatar().get()); + _webSurface->getSurfaceContext()->setContextProperty("Entities", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("Snapshot", DependencyManager::get().data()); if (_webSurface->getRootItem() && _webSurface->getRootItem()->objectName() == "tabletRoot") { auto tabletScriptingInterface = DependencyManager::get(); auto flags = tabletScriptingInterface->getFlags(); - _webSurface->getRootContext()->setContextProperty("offscreenFlags", flags); - _webSurface->getRootContext()->setContextProperty("AddressManager", DependencyManager::get().data()); - _webSurface->getRootContext()->setContextProperty("Account", AccountScriptingInterface::getInstance()); - _webSurface->getRootContext()->setContextProperty("Audio", DependencyManager::get().data()); - _webSurface->getRootContext()->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); - _webSurface->getRootContext()->setContextProperty("HMD", DependencyManager::get().data()); - _webSurface->getRootContext()->setContextProperty("fileDialogHelper", new FileDialogHelper()); - _webSurface->getRootContext()->setContextProperty("MyAvatar", DependencyManager::get()->getMyAvatar().get()); - _webSurface->getRootContext()->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); - _webSurface->getRootContext()->setContextProperty("Tablet", DependencyManager::get().data()); - _webSurface->getRootContext()->setContextProperty("Assets", DependencyManager::get().data()); - _webSurface->getRootContext()->setContextProperty("LODManager", DependencyManager::get().data()); - _webSurface->getRootContext()->setContextProperty("OctreeStats", DependencyManager::get().data()); - _webSurface->getRootContext()->setContextProperty("DCModel", DependencyManager::get().data()); - _webSurface->getRootContext()->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); - _webSurface->getRootContext()->setContextProperty("GlobalServices", GlobalServicesScriptingInterface::getInstance()); - _webSurface->getRootContext()->setContextProperty("AvatarList", DependencyManager::get().data()); - _webSurface->getRootContext()->setContextProperty("DialogsManager", DialogsManagerScriptingInterface::getInstance()); + _webSurface->getSurfaceContext()->setContextProperty("offscreenFlags", flags); + _webSurface->getSurfaceContext()->setContextProperty("AddressManager", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("Account", AccountScriptingInterface::getInstance()); + _webSurface->getSurfaceContext()->setContextProperty("Audio", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); + _webSurface->getSurfaceContext()->setContextProperty("HMD", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("fileDialogHelper", new FileDialogHelper()); + _webSurface->getSurfaceContext()->setContextProperty("MyAvatar", DependencyManager::get()->getMyAvatar().get()); + _webSurface->getSurfaceContext()->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("Tablet", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("Assets", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("LODManager", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("OctreeStats", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("DCModel", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); + _webSurface->getSurfaceContext()->setContextProperty("GlobalServices", GlobalServicesScriptingInterface::getInstance()); + _webSurface->getSurfaceContext()->setContextProperty("AvatarList", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("DialogsManager", DialogsManagerScriptingInterface::getInstance()); - _webSurface->getRootContext()->setContextProperty("pathToFonts", "../../"); + _webSurface->getSurfaceContext()->setContextProperty("pathToFonts", "../../"); tabletScriptingInterface->setQmlTabletRoot("com.highfidelity.interface.tablet.system", _webSurface->getRootItem(), _webSurface.data()); // mark the TabletProxy object as cpp ownership. QObject* tablet = tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system"); - _webSurface->getRootContext()->engine()->setObjectOwnership(tablet, QQmlEngine::CppOwnership); + _webSurface->getSurfaceContext()->engine()->setObjectOwnership(tablet, QQmlEngine::CppOwnership); // Override min fps for tablet UI, for silky smooth scrolling setMaxFPS(90); } } - _webSurface->getRootContext()->setContextProperty("globalPosition", vec3toVariant(getPosition())); + _webSurface->getSurfaceContext()->setContextProperty("globalPosition", vec3toVariant(getPosition())); } void Web3DOverlay::setMaxFPS(uint8_t maxFPS) { @@ -443,6 +442,9 @@ void Web3DOverlay::handlePointerEventAsTouch(const PointerEvent& event) { QCoreApplication::postEvent(_webSurface->getWindow(), touchEvent); + if (this->_pressed && event.getType() == PointerEvent::Move) { + return; + } // Send mouse events to the Web surface so that HTML dialog elements work with mouse press and hover. // FIXME: Scroll bar dragging is a bit unstable in the tablet (content can jump up and down at times). // This may be improved in Qt 5.8. Release notes: "Cleaned up touch and mouse event delivery". diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index d613e42866..77437e79b9 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -20,6 +20,8 @@ #include "ElbowConstraint.h" #include "SwingTwistConstraint.h" #include "AnimationLogging.h" +#include "CubicHermiteSpline.h" +#include "AnimUtil.h" AnimInverseKinematics::IKTargetVar::IKTargetVar(const QString& jointNameIn, const QString& positionVarIn, const QString& rotationVarIn, const QString& typeVarIn, const QString& weightVarIn, float weightIn, const std::vector& flexCoefficientsIn) : @@ -59,7 +61,8 @@ AnimInverseKinematics::AnimInverseKinematics(const QString& id) : AnimNode(AnimN AnimInverseKinematics::~AnimInverseKinematics() { clearConstraints(); - _accumulators.clear(); + _rotationAccumulators.clear(); + _translationAccumulators.clear(); _targetVarVec.clear(); } @@ -72,10 +75,12 @@ void AnimInverseKinematics::loadPoses(const AnimPoseVec& poses) { assert(_skeleton && ((poses.size() == 0) || (_skeleton->getNumJoints() == (int)poses.size()))); if (_skeleton->getNumJoints() == (int)poses.size()) { _relativePoses = poses; - _accumulators.resize(_relativePoses.size()); + _rotationAccumulators.resize(_relativePoses.size()); + _translationAccumulators.resize(_relativePoses.size()); } else { _relativePoses.clear(); - _accumulators.clear(); + _rotationAccumulators.clear(); + _translationAccumulators.clear(); } } @@ -175,14 +180,17 @@ void AnimInverseKinematics::computeTargets(const AnimVariantMap& animVars, std:: } } -void AnimInverseKinematics::solveWithCyclicCoordinateDescent(const AnimContext& context, const std::vector& targets) { +void AnimInverseKinematics::solve(const AnimContext& context, const std::vector& targets) { // compute absolute poses that correspond to relative target poses AnimPoseVec absolutePoses; absolutePoses.resize(_relativePoses.size()); computeAbsolutePoses(absolutePoses); // clear the accumulators before we start the IK solver - for (auto& accumulator: _accumulators) { + for (auto& accumulator : _rotationAccumulators) { + accumulator.clearAndClean(); + } + for (auto& accumulator : _translationAccumulators) { accumulator.clearAndClean(); } @@ -197,14 +205,22 @@ void AnimInverseKinematics::solveWithCyclicCoordinateDescent(const AnimContext& // solve all targets for (auto& target: targets) { - solveTargetWithCCD(context, target, absolutePoses, debug); + if (target.getType() == IKTarget::Type::Spline) { + solveTargetWithSpline(context, target, absolutePoses, debug); + } else { + solveTargetWithCCD(context, target, absolutePoses, debug); + } } // harvest accumulated rotations and apply the average for (int i = 0; i < (int)_relativePoses.size(); ++i) { - if (_accumulators[i].size() > 0) { - _relativePoses[i].rot() = _accumulators[i].getAverage(); - _accumulators[i].clear(); + if (_rotationAccumulators[i].size() > 0) { + _relativePoses[i].rot() = _rotationAccumulators[i].getAverage(); + _rotationAccumulators[i].clear(); + } + if (_translationAccumulators[i].size() > 0) { + _relativePoses[i].trans() = _translationAccumulators[i].getAverage(); + _translationAccumulators[i].clear(); } } @@ -236,7 +252,7 @@ void AnimInverseKinematics::solveWithCyclicCoordinateDescent(const AnimContext& int parentIndex = _skeleton->getParentIndex(tipIndex); // update rotationOnly targets that don't lie on the ik chain of other ik targets. - if (parentIndex != -1 && !_accumulators[tipIndex].isDirty() && target.getType() == IKTarget::Type::RotationOnly) { + if (parentIndex != -1 && !_rotationAccumulators[tipIndex].isDirty() && target.getType() == IKTarget::Type::RotationOnly) { const glm::quat& targetRotation = target.getRotation(); // compute tip's new parent-relative rotation // Q = Qp * q --> q' = Qp^ * Q @@ -311,10 +327,13 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const } // store the relative rotation change in the accumulator - _accumulators[tipIndex].add(tipRelativeRotation, target.getWeight()); + _rotationAccumulators[tipIndex].add(tipRelativeRotation, target.getWeight()); + + glm::vec3 tipRelativeTranslation = _relativePoses[target.getIndex()].trans(); + _translationAccumulators[tipIndex].add(tipRelativeTranslation); if (debug) { - debugJointMap[tipIndex] = DebugJoint(tipRelativeRotation, constrained); + debugJointMap[tipIndex] = DebugJoint(tipRelativeRotation, tipRelativeTranslation, constrained); } } @@ -422,10 +441,13 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const } // store the relative rotation change in the accumulator - _accumulators[pivotIndex].add(newRot, target.getWeight()); + _rotationAccumulators[pivotIndex].add(newRot, target.getWeight()); + + glm::vec3 newTrans = _relativePoses[pivotIndex].trans(); + _translationAccumulators[pivotIndex].add(newTrans); if (debug) { - debugJointMap[pivotIndex] = DebugJoint(newRot, constrained); + debugJointMap[pivotIndex] = DebugJoint(newRot, newTrans, constrained); } // keep track of tip's new transform as we descend towards root @@ -444,6 +466,187 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const } } +static CubicHermiteSplineFunctorWithArcLength computeSplineFromTipAndBase(const AnimPose& tipPose, const AnimPose& basePose, float baseGain = 1.0f, float tipGain = 1.0f) { + float linearDistance = glm::length(basePose.trans() - tipPose.trans()); + glm::vec3 p0 = basePose.trans(); + glm::vec3 m0 = baseGain * linearDistance * (basePose.rot() * Vectors::UNIT_Y); + glm::vec3 p1 = tipPose.trans(); + glm::vec3 m1 = tipGain * linearDistance * (tipPose.rot() * Vectors::UNIT_Y); + + return CubicHermiteSplineFunctorWithArcLength(p0, m0, p1, m1); +} + +// pre-compute information about each joint influeced by this spline IK target. +void AnimInverseKinematics::computeSplineJointInfosForIKTarget(const AnimContext& context, const IKTarget& target) { + std::vector splineJointInfoVec; + + // build spline between the default poses. + AnimPose tipPose = _skeleton->getAbsoluteDefaultPose(target.getIndex()); + AnimPose basePose = _skeleton->getAbsoluteDefaultPose(_hipsIndex); + + CubicHermiteSplineFunctorWithArcLength spline; + if (target.getIndex() == _headIndex) { + // set gain factors so that more curvature occurs near the tip of the spline. + const float HIPS_GAIN = 0.5f; + const float HEAD_GAIN = 1.0f; + spline = computeSplineFromTipAndBase(tipPose, basePose, HIPS_GAIN, HEAD_GAIN); + } else { + spline = computeSplineFromTipAndBase(tipPose, basePose); + } + + // measure the total arc length along the spline + float totalArcLength = spline.arcLength(1.0f); + + glm::vec3 baseToTip = tipPose.trans() - basePose.trans(); + float baseToTipLength = glm::length(baseToTip); + glm::vec3 baseToTipNormal = baseToTip / baseToTipLength; + + int index = target.getIndex(); + int endIndex = _skeleton->getParentIndex(_hipsIndex); + while (index != endIndex) { + AnimPose defaultPose = _skeleton->getAbsoluteDefaultPose(index); + + float ratio = glm::dot(defaultPose.trans() - basePose.trans(), baseToTipNormal) / baseToTipLength; + + // compute offset from spline to the default pose. + float t = spline.arcLengthInverse(ratio * totalArcLength); + + // compute the rotation by using the derivative of the spline as the y-axis, and the defaultPose x-axis + glm::vec3 y = glm::normalize(spline.d(t)); + glm::vec3 x = defaultPose.rot() * Vectors::UNIT_X; + glm::vec3 u, v, w; + generateBasisVectors(y, x, v, u, w); + glm::mat3 m(u, v, glm::cross(u, v)); + glm::quat rot = glm::normalize(glm::quat_cast(m)); + + AnimPose pose(glm::vec3(1.0f), rot, spline(t)); + AnimPose offsetPose = pose.inverse() * defaultPose; + + SplineJointInfo splineJointInfo = { index, ratio, offsetPose }; + splineJointInfoVec.push_back(splineJointInfo); + index = _skeleton->getParentIndex(index); + } + + _splineJointInfoMap[target.getIndex()] = splineJointInfoVec; +} + +const std::vector* AnimInverseKinematics::findOrCreateSplineJointInfo(const AnimContext& context, const IKTarget& target) { + // find or create splineJointInfo for this target + auto iter = _splineJointInfoMap.find(target.getIndex()); + if (iter != _splineJointInfoMap.end()) { + return &(iter->second); + } else { + computeSplineJointInfosForIKTarget(context, target); + auto iter = _splineJointInfoMap.find(target.getIndex()); + if (iter != _splineJointInfoMap.end()) { + return &(iter->second); + } + } + + return nullptr; +} + +void AnimInverseKinematics::solveTargetWithSpline(const AnimContext& context, const IKTarget& target, const AnimPoseVec& absolutePoses, bool debug) { + + std::map debugJointMap; + + const int baseIndex = _hipsIndex; + + // build spline from tip to base + AnimPose tipPose = AnimPose(glm::vec3(1.0f), target.getRotation(), target.getTranslation()); + AnimPose basePose = absolutePoses[baseIndex]; + CubicHermiteSplineFunctorWithArcLength spline; + if (target.getIndex() == _headIndex) { + // set gain factors so that more curvature occurs near the tip of the spline. + const float HIPS_GAIN = 0.5f; + const float HEAD_GAIN = 1.0f; + spline = computeSplineFromTipAndBase(tipPose, basePose, HIPS_GAIN, HEAD_GAIN); + } else { + spline = computeSplineFromTipAndBase(tipPose, basePose); + } + float totalArcLength = spline.arcLength(1.0f); + + // This prevents the rotation interpolation from rotating the wrong physical way (but correct mathematical way) + // when the head is arched backwards very far. + glm::quat halfRot = glm::normalize(glm::lerp(basePose.rot(), tipPose.rot(), 0.5f)); + if (glm::dot(halfRot * Vectors::UNIT_Z, basePose.rot() * Vectors::UNIT_Z) < 0.0f) { + tipPose.rot() = -tipPose.rot(); + } + + // find or create splineJointInfo for this target + const std::vector* splineJointInfoVec = findOrCreateSplineJointInfo(context, target); + + if (splineJointInfoVec && splineJointInfoVec->size() > 0) { + const int baseParentIndex = _skeleton->getParentIndex(baseIndex); + AnimPose parentAbsPose = (baseParentIndex >= 0) ? absolutePoses[baseParentIndex] : AnimPose(); + + // go thru splineJointInfoVec backwards (base to tip) + for (int i = (int)splineJointInfoVec->size() - 1; i >= 0; i--) { + const SplineJointInfo& splineJointInfo = (*splineJointInfoVec)[i]; + float t = spline.arcLengthInverse(splineJointInfo.ratio * totalArcLength); + glm::vec3 trans = spline(t); + + // for head splines, preform most twist toward the tip by using ease in function. t^2 + float rotT = t; + if (target.getIndex() == _headIndex) { + rotT = t * t; + } + glm::quat twistRot = glm::normalize(glm::lerp(basePose.rot(), tipPose.rot(), rotT)); + + // compute the rotation by using the derivative of the spline as the y-axis, and the twistRot x-axis + glm::vec3 y = glm::normalize(spline.d(t)); + glm::vec3 x = twistRot * Vectors::UNIT_X; + glm::vec3 u, v, w; + generateBasisVectors(y, x, v, u, w); + glm::mat3 m(u, v, glm::cross(u, v)); + glm::quat rot = glm::normalize(glm::quat_cast(m)); + + AnimPose desiredAbsPose = AnimPose(glm::vec3(1.0f), rot, trans) * splineJointInfo.offsetPose; + + // apply flex coefficent + AnimPose flexedAbsPose; + ::blend(1, &absolutePoses[splineJointInfo.jointIndex], &desiredAbsPose, target.getFlexCoefficient(i), &flexedAbsPose); + + AnimPose relPose = parentAbsPose.inverse() * flexedAbsPose; + _rotationAccumulators[splineJointInfo.jointIndex].add(relPose.rot(), target.getWeight()); + + bool constrained = false; + if (splineJointInfo.jointIndex != _hipsIndex) { + // constrain the amount the spine can stretch or compress + float length = glm::length(relPose.trans()); + const float EPSILON = 0.0001f; + if (length > EPSILON) { + float defaultLength = glm::length(_skeleton->getRelativeDefaultPose(splineJointInfo.jointIndex).trans()); + const float STRETCH_COMPRESS_PERCENTAGE = 0.15f; + const float MAX_LENGTH = defaultLength * (1.0f + STRETCH_COMPRESS_PERCENTAGE); + const float MIN_LENGTH = defaultLength * (1.0f - STRETCH_COMPRESS_PERCENTAGE); + if (length > MAX_LENGTH) { + relPose.trans() = (relPose.trans() / length) * MAX_LENGTH; + constrained = true; + } else if (length < MIN_LENGTH) { + relPose.trans() = (relPose.trans() / length) * MIN_LENGTH; + constrained = true; + } + } else { + relPose.trans() = glm::vec3(0.0f); + } + } + + _translationAccumulators[splineJointInfo.jointIndex].add(relPose.trans(), target.getWeight()); + + if (debug) { + debugJointMap[splineJointInfo.jointIndex] = DebugJoint(relPose.rot(), relPose.trans(), constrained); + } + + parentAbsPose = flexedAbsPose; + } + } + + if (debug) { + debugDrawIKChain(debugJointMap, context); + } +} + //virtual const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, AnimNode::Triggers& triggersOut) { // don't call this function, call overlay() instead @@ -453,14 +656,9 @@ const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVar //virtual const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) { - // allows solutionSource to be overridden by an animVar auto solutionSource = animVars.lookup(_solutionSourceVar, (int)_solutionSource); - if (context.getEnableDebugDrawIKConstraints()) { - debugDrawConstraints(context); - } - const float MAX_OVERLAY_DT = 1.0f / 30.0f; // what to clamp delta-time to in AnimInverseKinematics::overlay if (dt > MAX_OVERLAY_DT) { dt = MAX_OVERLAY_DT; @@ -569,7 +767,7 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars { PROFILE_RANGE_EX(simulation_animation, "ik/ccd", 0xffff00ff, 0); - solveWithCyclicCoordinateDescent(context, targets); + solve(context, targets); } if (_hipsTargetIndex < 0) { @@ -579,6 +777,20 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars _hipsOffset = Vectors::ZERO; } } + + if (context.getEnableDebugDrawIKConstraints()) { + debugDrawConstraints(context); + } + } + + if (_leftHandIndex > -1) { + _uncontrolledLeftHandPose = _skeleton->getAbsolutePose(_leftHandIndex, underPoses); + } + if (_rightHandIndex > -1) { + _uncontrolledRightHandPose = _skeleton->getAbsolutePose(_rightHandIndex, underPoses); + } + if (_hipsIndex > -1) { + _uncontrolledHipsPose = _skeleton->getAbsolutePose(_hipsIndex, underPoses); } return _relativePoses; @@ -722,8 +934,10 @@ void AnimInverseKinematics::initConstraints() { loadDefaultPoses(_skeleton->getRelativeBindPoses()); - // compute corresponding absolute poses int numJoints = (int)_defaultRelativePoses.size(); + + /* KEEP THIS CODE for future experimentation + // compute corresponding absolute poses AnimPoseVec absolutePoses; absolutePoses.resize(numJoints); for (int i = 0; i < numJoints; ++i) { @@ -734,6 +948,7 @@ void AnimInverseKinematics::initConstraints() { absolutePoses[i] = absolutePoses[parentIndex] * _defaultRelativePoses[i]; } } + */ clearConstraints(); for (int i = 0; i < numJoints; ++i) { @@ -1045,7 +1260,10 @@ void AnimInverseKinematics::setSkeletonInternal(AnimSkeleton::ConstPointer skele _maxTargetIndex = -1; - for (auto& accumulator: _accumulators) { + for (auto& accumulator: _rotationAccumulators) { + accumulator.clearAndClean(); + } + for (auto& accumulator: _translationAccumulators) { accumulator.clearAndClean(); } @@ -1061,12 +1279,21 @@ void AnimInverseKinematics::setSkeletonInternal(AnimSkeleton::ConstPointer skele } else { _hipsParentIndex = -1; } + + _leftHandIndex = _skeleton->nameToJointIndex("LeftHand"); + _rightHandIndex = _skeleton->nameToJointIndex("RightHand"); } else { clearConstraints(); _headIndex = -1; _hipsIndex = -1; _hipsParentIndex = -1; + _leftHandIndex = -1; + _rightHandIndex = -1; } + + _uncontrolledLeftHandPose = AnimPose(); + _uncontrolledRightHandPose = AnimPose(); + _uncontrolledHipsPose = AnimPose(); } static glm::vec3 sphericalToCartesian(float phi, float theta) { @@ -1117,6 +1344,7 @@ void AnimInverseKinematics::debugDrawIKChain(std::map& debugJoi // copy debug joint rotations into the relative poses for (auto& debugJoint : debugJointMap) { poses[debugJoint.first].rot() = debugJoint.second.relRot; + poses[debugJoint.first].trans() = debugJoint.second.relTrans; } // convert relative poses to absolute @@ -1282,7 +1510,7 @@ void AnimInverseKinematics::blendToPoses(const AnimPoseVec& targetPoses, const A int numJoints = (int)_relativePoses.size(); for (int i = 0; i < numJoints; ++i) { float dotSign = copysignf(1.0f, glm::dot(_relativePoses[i].rot(), targetPoses[i].rot())); - if (_accumulators[i].isDirty()) { + if (_rotationAccumulators[i].isDirty()) { // this joint is affected by IK --> blend toward the targetPoses rotation _relativePoses[i].rot() = glm::normalize(glm::lerp(_relativePoses[i].rot(), dotSign * targetPoses[i].rot(), blendFactor)); } else { @@ -1316,3 +1544,46 @@ void AnimInverseKinematics::initRelativePosesFromSolutionSource(SolutionSource s break; } } + +void AnimInverseKinematics::debugDrawSpineSplines(const AnimContext& context, const std::vector& targets) const { + + for (auto& target : targets) { + + if (target.getType() != IKTarget::Type::Spline) { + continue; + } + + const int baseIndex = _hipsIndex; + + // build spline + AnimPose tipPose = AnimPose(glm::vec3(1.0f), target.getRotation(), target.getTranslation()); + AnimPose basePose = _skeleton->getAbsolutePose(baseIndex, _relativePoses); + + CubicHermiteSplineFunctorWithArcLength spline; + if (target.getIndex() == _headIndex) { + // set gain factors so that more curvature occurs near the tip of the spline. + const float HIPS_GAIN = 0.5f; + const float HEAD_GAIN = 1.0f; + spline = computeSplineFromTipAndBase(tipPose, basePose, HIPS_GAIN, HEAD_GAIN); + } else { + spline = computeSplineFromTipAndBase(tipPose, basePose); + } + float totalArcLength = spline.arcLength(1.0f); + + const glm::vec4 RED(1.0f, 0.0f, 0.0f, 1.0f); + const glm::vec4 WHITE(1.0f, 1.0f, 1.0f, 1.0f); + + // draw red and white stripped spline, parameterized by arc length. + // i.e. each stripe should be the same length. + AnimPose geomToWorldPose = AnimPose(context.getRigToWorldMatrix() * context.getGeometryToRigMatrix()); + const int NUM_SEGMENTS = 20; + const float dArcLength = totalArcLength / NUM_SEGMENTS; + float arcLength = 0.0f; + for (int i = 0; i < NUM_SEGMENTS; i++) { + float prevT = spline.arcLengthInverse(arcLength); + float nextT = spline.arcLengthInverse(arcLength + dArcLength); + DebugDraw::getInstance().drawRay(geomToWorldPose.xformPoint(spline(prevT)), geomToWorldPose.xformPoint(spline(nextT)), (i % 2) == 0 ? RED : WHITE); + arcLength += dArcLength; + } + } +} diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h index 0267f14650..cc919c1684 100644 --- a/libraries/animation/src/AnimInverseKinematics.h +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -19,6 +19,7 @@ #include "IKTarget.h" #include "RotationAccumulator.h" +#include "TranslationAccumulator.h" class RotationConstraint; @@ -56,23 +57,39 @@ public: void setSolutionSource(SolutionSource solutionSource) { _solutionSource = solutionSource; } void setSolutionSourceVar(const QString& solutionSourceVar) { _solutionSourceVar = solutionSourceVar; } + const AnimPose& getUncontrolledLeftHandPose() { return _uncontrolledLeftHandPose; } + const AnimPose& getUncontrolledRightHandPose() { return _uncontrolledRightHandPose; } + const AnimPose& getUncontrolledHipPose() { return _uncontrolledHipsPose; } + protected: void computeTargets(const AnimVariantMap& animVars, std::vector& targets, const AnimPoseVec& underPoses); - void solveWithCyclicCoordinateDescent(const AnimContext& context, const std::vector& targets); + void solve(const AnimContext& context, const std::vector& targets); void solveTargetWithCCD(const AnimContext& context, const IKTarget& target, const AnimPoseVec& absolutePoses, bool debug); + void solveTargetWithSpline(const AnimContext& context, const IKTarget& target, const AnimPoseVec& absolutePoses, bool debug); virtual void setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) override; struct DebugJoint { DebugJoint() : relRot(), constrained(false) {} - DebugJoint(const glm::quat& relRotIn, bool constrainedIn) : relRot(relRotIn), constrained(constrainedIn) {} + DebugJoint(const glm::quat& relRotIn, const glm::vec3& relTransIn, bool constrainedIn) : relRot(relRotIn), relTrans(relTransIn), constrained(constrainedIn) {} glm::quat relRot; + glm::vec3 relTrans; bool constrained; }; void debugDrawIKChain(std::map& debugJointMap, const AnimContext& context) const; void debugDrawRelativePoses(const AnimContext& context) const; void debugDrawConstraints(const AnimContext& context) const; + void debugDrawSpineSplines(const AnimContext& context, const std::vector& targets) const; void initRelativePosesFromSolutionSource(SolutionSource solutionSource, const AnimPoseVec& underPose); void blendToPoses(const AnimPoseVec& targetPoses, const AnimPoseVec& underPose, float blendFactor); + // used to pre-compute information about each joint influeced by a spline IK target. + struct SplineJointInfo { + int jointIndex; // joint in the skeleton that this information pertains to. + float ratio; // percentage (0..1) along the spline for this joint. + AnimPose offsetPose; // local offset from the spline to the joint. + }; + + void computeSplineJointInfosForIKTarget(const AnimContext& context, const IKTarget& target); + const std::vector* findOrCreateSplineJointInfo(const AnimContext& context, const IKTarget& target); // for AnimDebugDraw rendering virtual const AnimPoseVec& getPosesInternal() const override { return _relativePoses; } @@ -105,12 +122,15 @@ protected: }; std::map _constraints; - std::vector _accumulators; + std::vector _rotationAccumulators; + std::vector _translationAccumulators; std::vector _targetVarVec; AnimPoseVec _defaultRelativePoses; // poses of the relaxed state AnimPoseVec _relativePoses; // current relative poses AnimPoseVec _limitCenterPoses; // relative + std::map> _splineJointInfoMap; + // experimental data for moving hips during IK glm::vec3 _hipsOffset { Vectors::ZERO }; float _maxHipsOffsetLength{ FLT_MAX }; @@ -118,6 +138,8 @@ protected: int _hipsIndex { -1 }; int _hipsParentIndex { -1 }; int _hipsTargetIndex { -1 }; + int _leftHandIndex { -1 }; + int _rightHandIndex { -1 }; // _maxTargetIndex is tracked to help optimize the recalculation of absolute poses // during the the cyclic coordinate descent algorithm @@ -127,6 +149,10 @@ protected: bool _previousEnableDebugIKTargets { false }; SolutionSource _solutionSource { SolutionSource::RelaxToUnderPoses }; QString _solutionSourceVar; + + AnimPose _uncontrolledLeftHandPose { AnimPose() }; + AnimPose _uncontrolledRightHandPose { AnimPose() }; + AnimPose _uncontrolledHipsPose { AnimPose() }; }; #endif // hifi_AnimInverseKinematics_h diff --git a/libraries/animation/src/IKTarget.cpp b/libraries/animation/src/IKTarget.cpp index 2fe767b08d..41cac62fa3 100644 --- a/libraries/animation/src/IKTarget.cpp +++ b/libraries/animation/src/IKTarget.cpp @@ -44,6 +44,9 @@ void IKTarget::setType(int type) { case (int)Type::HipsRelativeRotationAndPosition: _type = Type::HipsRelativeRotationAndPosition; break; + case (int)Type::Spline: + _type = Type::Spline; + break; default: _type = Type::Unknown; } diff --git a/libraries/animation/src/IKTarget.h b/libraries/animation/src/IKTarget.h index 4f464c103c..011175aedf 100644 --- a/libraries/animation/src/IKTarget.h +++ b/libraries/animation/src/IKTarget.h @@ -21,6 +21,7 @@ public: RotationOnly, HmdHead, HipsRelativeRotationAndPosition, + Spline, Unknown }; diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index add3a461af..20a2aab2b6 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -28,6 +28,7 @@ #include "AnimClip.h" #include "AnimInverseKinematics.h" #include "AnimSkeleton.h" +#include "AnimUtil.h" #include "IKTarget.h" static bool isEqual(const glm::vec3& u, const glm::vec3& v) { @@ -401,16 +402,6 @@ void Rig::setJointRotation(int index, bool valid, const glm::quat& rotation, flo } } -void Rig::restoreJointRotation(int index, float fraction, float priority) { - // AJT: DEAD CODE? - ASSERT(false); -} - -void Rig::restoreJointTranslation(int index, float fraction, float priority) { - // AJT: DEAD CODE? - ASSERT(false); -} - bool Rig::getJointPositionInWorldFrame(int jointIndex, glm::vec3& position, glm::vec3 translation, glm::quat rotation) const { if (isIndexValid(jointIndex)) { position = (rotation * _internalPoseSet._absolutePoses[jointIndex].trans()) + translation; @@ -1040,8 +1031,8 @@ void Rig::updateFromHeadParameters(const HeadParameters& params, float dt) { _animVars.set("hipsType", (int)IKTarget::Type::Unknown); } - if (params.spine2Enabled) { - _animVars.set("spine2Type", (int)IKTarget::Type::RotationAndPosition); + if (params.hipsEnabled && params.spine2Enabled) { + _animVars.set("spine2Type", (int)IKTarget::Type::Spline); _animVars.set("spine2Position", extractTranslation(params.spine2Matrix)); _animVars.set("spine2Rotation", glmExtractRotation(params.spine2Matrix)); } else { @@ -1051,7 +1042,7 @@ void Rig::updateFromHeadParameters(const HeadParameters& params, float dt) { if (params.leftArmEnabled) { _animVars.set("leftArmType", (int)IKTarget::Type::RotationAndPosition); _animVars.set("leftArmPosition", params.leftArmPosition); - _animVars.set("leftArmRotation", params.leftArmRotation); + _animVars.set("leftArmRotation", params.leftArmRotation); } else { _animVars.set("leftArmType", (int)IKTarget::Type::Unknown); } @@ -1101,9 +1092,9 @@ void Rig::updateHeadAnimVars(const HeadParameters& params) { _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); + // Since there is an explicit hips ik target, switch the head to use the more flexible Spline IK chain type. + // this will allow the spine to compress/expand and bend more natrually, ensuring that it can reach the head target position. + _animVars.set("headType", (int)IKTarget::Type::Spline); _animVars.unset("headWeight"); // use the default weight for this target. } else { // When there is no hips IK target, use the HmdHead IK chain type. This will make the spine very stiff, @@ -1135,9 +1126,9 @@ void Rig::updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm } glm::vec3 headUp = headQuat * Vectors::UNIT_Y; - glm::vec3 z, y, x; - generateBasisVectors(lookAtVector, headUp, z, y, x); - glm::mat3 m(glm::cross(y, z), y, z); + glm::vec3 z, y, zCrossY; + generateBasisVectors(lookAtVector, headUp, z, y, zCrossY); + glm::mat3 m(-zCrossY, y, z); glm::quat desiredQuat = glm::normalize(glm::quat_cast(m)); glm::quat deltaQuat = desiredQuat * glm::inverse(headQuat); @@ -1172,10 +1163,10 @@ void Rig::updateFromHandAndFeetParameters(const HandAndFeetParameters& params, f // TODO: add isHipsEnabled bool bodySensorTrackingEnabled = params.isLeftFootEnabled || params.isRightFootEnabled; + const float RELAX_DURATION = 0.6f; + if (params.isLeftEnabled) { - glm::vec3 handPosition = params.leftPosition; - if (!bodySensorTrackingEnabled) { // prevent the hand IK targets from intersecting the body capsule glm::vec3 displacement; @@ -1187,16 +1178,38 @@ void Rig::updateFromHandAndFeetParameters(const HandAndFeetParameters& params, f _animVars.set("leftHandPosition", handPosition); _animVars.set("leftHandRotation", params.leftOrientation); _animVars.set("leftHandType", (int)IKTarget::Type::RotationAndPosition); + + _isLeftHandControlled = true; + _lastLeftHandControlledPose = AnimPose(glm::vec3(1.0f), params.leftOrientation, handPosition); } else { - _animVars.unset("leftHandPosition"); - _animVars.unset("leftHandRotation"); - _animVars.set("leftHandType", (int)IKTarget::Type::HipsRelativeRotationAndPosition); + if (_isLeftHandControlled) { + _leftHandRelaxDuration = RELAX_DURATION; + _isLeftHandControlled = false; + } + + if (_leftHandRelaxDuration > 0) { + // Move hand from controlled position to non-controlled position. + _leftHandRelaxDuration = std::max(_leftHandRelaxDuration - dt, 0.0f); + auto ikNode = getAnimInverseKinematicsNode(); + if (ikNode) { + float alpha = 1.0f - _leftHandRelaxDuration / RELAX_DURATION; + const AnimPose geometryToRigTransform(_geometryToRigTransform); + AnimPose uncontrolledHandPose = geometryToRigTransform * ikNode->getUncontrolledLeftHandPose(); + AnimPose handPose; + ::blend(1, &_lastLeftHandControlledPose, &uncontrolledHandPose, alpha, &handPose); + _animVars.set("leftHandPosition", handPose.trans()); + _animVars.set("leftHandRotation", handPose.rot()); + _animVars.set("leftHandType", (int)IKTarget::Type::RotationAndPosition); + } + } else { + _animVars.unset("leftHandPosition"); + _animVars.unset("leftHandRotation"); + _animVars.set("leftHandType", (int)IKTarget::Type::HipsRelativeRotationAndPosition); + } } if (params.isRightEnabled) { - glm::vec3 handPosition = params.rightPosition; - if (!bodySensorTrackingEnabled) { // prevent the hand IK targets from intersecting the body capsule glm::vec3 displacement; @@ -1208,10 +1221,34 @@ void Rig::updateFromHandAndFeetParameters(const HandAndFeetParameters& params, f _animVars.set("rightHandPosition", handPosition); _animVars.set("rightHandRotation", params.rightOrientation); _animVars.set("rightHandType", (int)IKTarget::Type::RotationAndPosition); + + _isRightHandControlled = true; + _lastRightHandControlledPose = AnimPose(glm::vec3(1.0f), params.rightOrientation, handPosition); } else { - _animVars.unset("rightHandPosition"); - _animVars.unset("rightHandRotation"); - _animVars.set("rightHandType", (int)IKTarget::Type::HipsRelativeRotationAndPosition); + if (_isRightHandControlled) { + _rightHandRelaxDuration = RELAX_DURATION; + _isRightHandControlled = false; + } + + if (_rightHandRelaxDuration > 0) { + // Move hand from controlled position to non-controlled position. + _rightHandRelaxDuration = std::max(_rightHandRelaxDuration - dt, 0.0f); + auto ikNode = getAnimInverseKinematicsNode(); + if (ikNode) { + float alpha = 1.0f - _rightHandRelaxDuration / RELAX_DURATION; + const AnimPose geometryToRigTransform(_geometryToRigTransform); + AnimPose uncontrolledHandPose = geometryToRigTransform * ikNode->getUncontrolledRightHandPose(); + AnimPose handPose; + ::blend(1, &_lastRightHandControlledPose, &uncontrolledHandPose, alpha, &handPose); + _animVars.set("rightHandPosition", handPose.trans()); + _animVars.set("rightHandRotation", handPose.rot()); + _animVars.set("rightHandType", (int)IKTarget::Type::RotationAndPosition); + } + } else { + _animVars.unset("rightHandPosition"); + _animVars.unset("rightHandRotation"); + _animVars.set("rightHandType", (int)IKTarget::Type::HipsRelativeRotationAndPosition); + } } if (params.isLeftFootEnabled) { @@ -1233,7 +1270,6 @@ void Rig::updateFromHandAndFeetParameters(const HandAndFeetParameters& params, f _animVars.unset("rightFootRotation"); _animVars.set("rightFootType", (int)IKTarget::Type::RotationAndPosition); } - } } @@ -1509,5 +1545,3 @@ void Rig::computeAvatarBoundingCapsule( glm::vec3 rigCenter = (geometryToRig * (0.5f * (totalExtents.maximum + totalExtents.minimum))); localOffsetOut = rigCenter - (geometryToRig * rootPosition); } - - diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index c9d52d8c72..994bd4b074 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -134,10 +134,6 @@ public: void setJointTranslation(int index, bool valid, const glm::vec3& translation, float priority); void setJointRotation(int index, bool valid, const glm::quat& rotation, float priority); - // legacy - void restoreJointRotation(int index, float fraction, float priority); - void restoreJointTranslation(int index, float fraction, float priority); - // if translation and rotation is identity, position will be in rig space bool getJointPositionInWorldFrame(int jointIndex, glm::vec3& position, glm::vec3 translation, glm::quat rotation) const; @@ -355,6 +351,13 @@ private: QMap _stateHandlers; int _nextStateHandlerId { 0 }; QMutex _stateMutex; + + bool _isLeftHandControlled { false }; + bool _isRightHandControlled { false }; + float _leftHandRelaxDuration { 0.0f }; + float _rightHandRelaxDuration { 0.0f }; + AnimPose _lastLeftHandControlledPose; + AnimPose _lastRightHandControlledPose; }; #endif /* defined(__hifi__Rig__) */ diff --git a/libraries/animation/src/RotationAccumulator.cpp b/libraries/animation/src/RotationAccumulator.cpp index a4940e1989..2b8fc7d66a 100644 --- a/libraries/animation/src/RotationAccumulator.cpp +++ b/libraries/animation/src/RotationAccumulator.cpp @@ -1,5 +1,5 @@ // -// RotationAccumulator.h +// RotationAccumulator.cpp // // Copyright 2015 High Fidelity, Inc. // @@ -27,7 +27,7 @@ void RotationAccumulator::clear() { _numRotations = 0; } -void RotationAccumulator::clearAndClean() { +void RotationAccumulator::clearAndClean() { clear(); _isDirty = false; } diff --git a/libraries/animation/src/TranslationAccumulator.cpp b/libraries/animation/src/TranslationAccumulator.cpp new file mode 100644 index 0000000000..4b110b1564 --- /dev/null +++ b/libraries/animation/src/TranslationAccumulator.cpp @@ -0,0 +1,34 @@ +// +// TranslationAccumulator.cpp +// +// 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 "TranslationAccumulator.h" + +void TranslationAccumulator::add(const glm::vec3& translation, float weight) { + _accum += weight * translation; + _totalWeight += weight; + _isDirty = true; +} + +glm::vec3 TranslationAccumulator::getAverage() { + if (_totalWeight > 0.0f) { + return _accum / _totalWeight; + } else { + return glm::vec3(); + } +} + +void TranslationAccumulator::clear() { + _accum *= 0.0f; + _totalWeight = 0.0f; +} + +void TranslationAccumulator::clearAndClean() { + clear(); + _isDirty = false; +} diff --git a/libraries/animation/src/TranslationAccumulator.h b/libraries/animation/src/TranslationAccumulator.h new file mode 100644 index 0000000000..18cac5ec7a --- /dev/null +++ b/libraries/animation/src/TranslationAccumulator.h @@ -0,0 +1,42 @@ +// +// TranslationAccumulator.h +// +// 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_TranslationAccumulator_h +#define hifi_TranslationAccumulator_h + +#include + +class TranslationAccumulator { +public: + TranslationAccumulator() : _accum(0.0f, 0.0f, 0.0f), _totalWeight(0), _isDirty(false) { } + + int size() const { return _totalWeight > 0.0f; } + + /// \param translation translation to add + /// \param weight contribution factor of this translation to total accumulation + void add(const glm::vec3& translation, float weight = 1.0f); + + glm::vec3 getAverage(); + + /// \return true if any translation were accumulated + bool isDirty() const { return _isDirty; } + + /// \brief clear accumulated translation but don't change _isDirty + void clear(); + + /// \brief clear accumulated translation and set _isDirty to false + void clearAndClean(); + +private: + glm::vec3 _accum; + float _totalWeight; + bool _isDirty; +}; + +#endif // hifi_TranslationAccumulator_h diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index cdaed013b8..b03da75181 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -31,10 +31,13 @@ #include #endif +#include +#include #include #include #include +#include #include #include #include @@ -83,52 +86,11 @@ QList getAvailableDevices(QAudio::Mode mode) { return QAudioDeviceInfo::availableDevices(mode); } -class BackgroundThread : public QThread { -public: - BackgroundThread(AudioClient* client) : QThread((QObject*)client), _client(client) {} - virtual void join() = 0; -protected: - AudioClient* _client; -}; - -// background thread continuously polling device changes -class CheckDevicesThread : public BackgroundThread { -public: - CheckDevicesThread(AudioClient* client) : BackgroundThread(client) {} - - void join() override { - _shouldQuit = true; - std::unique_lock lock(_joinMutex); - _joinCondition.wait(lock, [&]{ return !_isRunning; }); - } - -protected: - void run() override { - while (!_shouldQuit) { - _client->checkDevices(); - - const unsigned long DEVICE_CHECK_INTERVAL_MSECS = 2 * 1000; - QThread::msleep(DEVICE_CHECK_INTERVAL_MSECS); - } - std::lock_guard lock(_joinMutex); - _isRunning = false; - _joinCondition.notify_one(); - } - -private: - std::atomic _shouldQuit { false }; - bool _isRunning { true }; - std::mutex _joinMutex; - std::condition_variable _joinCondition; -}; - // now called from a background thread, to keep blocking operations off the audio thread void AudioClient::checkDevices() { auto inputDevices = getAvailableDevices(QAudio::AudioInput); auto outputDevices = getAvailableDevices(QAudio::AudioOutput); - Lock lock(_deviceMutex); - if (inputDevices != _inputDevices) { _inputDevices.swap(inputDevices); emit devicesChanged(QAudio::AudioInput, _inputDevices); @@ -160,21 +122,6 @@ QList AudioClient::getAudioDevices(QAudio::Mode mode) const { } } - -// background thread buffering local injectors -class LocalInjectorsThread : public BackgroundThread { - Q_OBJECT -public: - LocalInjectorsThread(AudioClient* client) : BackgroundThread(client) {} - - void join() override { return; } - -private slots: - void prepare() { _client->prepareLocalAudioInjectors(); } -}; - -#include "AudioClient.moc" - static void channelUpmix(int16_t* source, int16_t* dest, int numSamples, int numExtraChannels) { for (int i = 0; i < numSamples/2; i++) { @@ -262,7 +209,10 @@ AudioClient::AudioClient() : connect(&_receivedAudioStream, &MixedProcessedAudioStream::processSamples, this, &AudioClient::processReceivedSamples, Qt::DirectConnection); - connect(this, &AudioClient::changeDevice, this, [=](const QAudioDeviceInfo& outputDeviceInfo) { switchOutputToAudioDevice(outputDeviceInfo); }); + connect(this, &AudioClient::changeDevice, this, [=](const QAudioDeviceInfo& outputDeviceInfo) { + qCDebug(audioclient) << "got AudioClient::changeDevice signal, about to call switchOutputToAudioDevice() outputDeviceInfo: [" << outputDeviceInfo.deviceName() << "]"; + switchOutputToAudioDevice(outputDeviceInfo); + }); connect(&_receivedAudioStream, &InboundAudioStream::mismatchedAudioCodec, this, &AudioClient::handleMismatchAudioFormat); @@ -270,16 +220,15 @@ AudioClient::AudioClient() : getAvailableDevices(QAudio::AudioInput); getAvailableDevices(QAudio::AudioOutput); - // start a thread to detect any device changes - _checkDevicesThread = new CheckDevicesThread(this); - _checkDevicesThread->setObjectName("AudioClient CheckDevices Thread"); - _checkDevicesThread->setPriority(QThread::LowPriority); - _checkDevicesThread->start(); - // start a thread to process local injectors - _localInjectorsThread = new LocalInjectorsThread(this); - _localInjectorsThread->setObjectName("AudioClient LocalInjectors Thread"); - _localInjectorsThread->start(); + // start a thread to detect any device changes + _checkDevicesTimer = new QTimer(this); + connect(_checkDevicesTimer, &QTimer::timeout, [this] { + QtConcurrent::run(QThreadPool::globalInstance(), [this] { + checkDevices(); + }); + }); + configureReverb(); @@ -310,15 +259,7 @@ void AudioClient::cleanupBeforeQuit() { stop(); - if (_checkDevicesThread) { - static_cast(_checkDevicesThread)->join(); - delete _checkDevicesThread; - } - - if (_localInjectorsThread) { - static_cast(_localInjectorsThread)->join(); - delete _localInjectorsThread; - } + _checkDevicesTimer->stop(); } void AudioClient::handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec) { @@ -478,7 +419,8 @@ QAudioDeviceInfo defaultAudioDeviceForMode(QAudio::Mode mode) { CoUninitialize(); } - qCDebug(audioclient) << "[" << deviceName << "] [" << getNamedAudioDeviceForMode(mode, deviceName).deviceName() << "]"; + qCDebug(audioclient) << "defaultAudioDeviceForMode mode: " << (mode == QAudio::AudioOutput ? "Output" : "Input") + << " [" << deviceName << "] [" << getNamedAudioDeviceForMode(mode, deviceName).deviceName() << "]"; return getNamedAudioDeviceForMode(mode, deviceName); #endif @@ -654,8 +596,12 @@ void AudioClient::start() { } void AudioClient::stop() { + // "switch" to invalid devices in order to shut down the state + qCDebug(audioclient) << "AudioClient::stop(), about to call switchInputToAudioDevice(null)"; switchInputToAudioDevice(QAudioDeviceInfo()); + + qCDebug(audioclient) << "AudioClient::stop(), about to call switchOutputToAudioDevice(null)"; switchOutputToAudioDevice(QAudioDeviceInfo()); } @@ -1402,10 +1348,8 @@ bool AudioClient::outputLocalInjector(AudioInjector* injector) { if (!_activeLocalAudioInjectors.contains(injector)) { qCDebug(audioclient) << "adding new injector"; _activeLocalAudioInjectors.append(injector); - // move local buffer to the LocalAudioThread to avoid dataraces with AudioInjector (like stop()) injectorBuffer->setParent(nullptr); - injectorBuffer->moveToThread(_localInjectorsThread); // update the flag _localInjectorsAvailable.exchange(true, std::memory_order_release); @@ -1428,6 +1372,7 @@ void AudioClient::outputFormatChanged() { } bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo& inputDeviceInfo) { + qCDebug(audioclient) << __FUNCTION__ << "inputDeviceInfo: [" << inputDeviceInfo.deviceName() << "]"; bool supportedFormat = false; // NOTE: device start() uses the Qt internal device list @@ -1542,6 +1487,8 @@ void AudioClient::outputNotify() { } bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDeviceInfo) { + qCDebug(audioclient) << "AudioClient::switchOutputToAudioDevice() outputDeviceInfo: [" << outputDeviceInfo.deviceName() << "]"; + bool supportedFormat = false; // NOTE: device start() uses the Qt internal device list @@ -1678,7 +1625,11 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice } int AudioClient::setOutputBufferSize(int numFrames, bool persist) { + qCDebug(audioclient) << __FUNCTION__ << "numFrames:" << numFrames << "persist:" << persist; + numFrames = std::min(std::max(numFrames, MIN_BUFFER_FRAMES), MAX_BUFFER_FRAMES); + qCDebug(audioclient) << __FUNCTION__ << "clamped numFrames:" << numFrames << "_sessionOutputBufferSizeFrames:" << _sessionOutputBufferSizeFrames; + if (numFrames != _sessionOutputBufferSizeFrames) { qCInfo(audioclient, "Audio output buffer set to %d frames", numFrames); _sessionOutputBufferSizeFrames = numFrames; @@ -1690,6 +1641,7 @@ int AudioClient::setOutputBufferSize(int numFrames, bool persist) { // The buffer size can't be adjusted after QAudioOutput::start() has been called, so // recreate the device by switching to the default. QAudioDeviceInfo outputDeviceInfo = defaultAudioDeviceForMode(QAudio::AudioOutput); + qCDebug(audioclient) << __FUNCTION__ << "about to send changeDevice signal outputDeviceInfo: [" << outputDeviceInfo.deviceName() << "]"; emit changeDevice(outputDeviceInfo); // On correct thread, please, as setOutputBufferSize can be called from main thread. } } @@ -1817,7 +1769,9 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { } // prepare injectors for the next callback - QMetaObject::invokeMethod(_audio->_localInjectorsThread, "prepare", Qt::QueuedConnection); + QtConcurrent::run(QThreadPool::globalInstance(), [this] { + _audio->prepareLocalAudioInjectors(); + }); int samplesPopped = std::max(networkSamplesPopped, injectorSamplesPopped); int framesPopped = samplesPopped / AudioConstants::STEREO; @@ -1877,3 +1831,8 @@ void AudioClient::setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 sca avatarBoundingBoxCorner = corner; avatarBoundingBoxScale = scale; } + + +void AudioClient::startThread() { + moveToNewNamedThread(this, "Audio Thread", [this] { start(); }); +} diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 903b78226e..5be5f31db2 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -104,6 +104,7 @@ public: int _unfulfilledReads; }; + void startThread(); void negotiateAudioFormat(); void selectAudioFormat(const QString& selectedCodecName); @@ -386,8 +387,7 @@ private: RateCounter<> _silentInbound; RateCounter<> _audioInbound; - QThread* _checkDevicesThread { nullptr }; - QThread* _localInjectorsThread { nullptr }; + QTimer* _checkDevicesTimer { nullptr }; }; diff --git a/libraries/audio/src/Sound.cpp b/libraries/audio/src/Sound.cpp index d6607424db..476a8d4d88 100644 --- a/libraries/audio/src/Sound.cpp +++ b/libraries/audio/src/Sound.cpp @@ -13,6 +13,8 @@ #include +#include +#include #include #include #include @@ -49,13 +51,43 @@ Sound::Sound(const QUrl& url, bool isStereo, bool isAmbisonic) : _isAmbisonic(isAmbisonic), _isReady(false) { - } void Sound::downloadFinished(const QByteArray& data) { + // this is a QRunnable, will delete itself after it has finished running + SoundProcessor* soundProcessor = new SoundProcessor(_url, data, _isStereo, _isAmbisonic); + connect(soundProcessor, &SoundProcessor::onSuccess, this, &Sound::soundProcessSuccess); + connect(soundProcessor, &SoundProcessor::onError, this, &Sound::soundProcessError); + QThreadPool::globalInstance()->start(soundProcessor); +} + +void Sound::soundProcessSuccess(QByteArray data, bool stereo, bool ambisonic, float duration) { + + qCDebug(audio) << "Setting ready state for sound file" << _url.toDisplayString(); + + _byteArray = data; + _isStereo = stereo; + _isAmbisonic = ambisonic; + _duration = duration; + _isReady = true; + finishedLoading(true); + + emit ready(); +} + +void Sound::soundProcessError(int error, QString str) { + qCCritical(audio) << "Failed to process sound file" << _url.toDisplayString() << "code =" << error << str; + emit failed(QNetworkReply::UnknownContentError); + finishedLoading(false); +} + +void SoundProcessor::run() { + + qCDebug(audio) << "Processing sound file" << _url.toDisplayString(); + // replace our byte array with the downloaded data - QByteArray rawAudioByteArray = QByteArray(data); - QString fileName = getURL().fileName().toLower(); + QByteArray rawAudioByteArray = QByteArray(_data); + QString fileName = _url.fileName().toLower(); static const QString WAV_EXTENSION = ".wav"; static const QString RAW_EXTENSION = ".raw"; @@ -72,31 +104,28 @@ void Sound::downloadFinished(const QByteArray& data) { // since it's raw the only way for us to know that is if the file was called .stereo.raw if (fileName.toLower().endsWith("stereo.raw")) { _isStereo = true; - qCDebug(audio) << "Processing sound of" << rawAudioByteArray.size() << "bytes from" << getURL() << "as stereo audio file."; + qCDebug(audio) << "Processing sound of" << rawAudioByteArray.size() << "bytes from" << _url << "as stereo audio file."; } // Process as 48khz RAW file downSample(rawAudioByteArray, 48000); } else { qCDebug(audio) << "Unknown sound file type"; + emit onError(300, "Failed to load sound file, reason: unknown sound file type"); + return; } - finishedLoading(true); - - _isReady = true; - emit ready(); + emit onSuccess(_data, _isStereo, _isAmbisonic, _duration); } -void Sound::downSample(const QByteArray& rawAudioByteArray, int sampleRate) { +void SoundProcessor::downSample(const QByteArray& rawAudioByteArray, int sampleRate) { // we want to convert it to the format that the audio-mixer wants // which is signed, 16-bit, 24Khz if (sampleRate == AudioConstants::SAMPLE_RATE) { - // no resampling needed - _byteArray = rawAudioByteArray; - + _data = rawAudioByteArray; } else { int numChannels = _isAmbisonic ? AudioConstants::AMBISONIC : (_isStereo ? AudioConstants::STEREO : AudioConstants::MONO); @@ -106,15 +135,15 @@ void Sound::downSample(const QByteArray& rawAudioByteArray, int sampleRate) { int numSourceFrames = rawAudioByteArray.size() / (numChannels * sizeof(AudioConstants::AudioSample)); int maxDestinationFrames = resampler.getMaxOutput(numSourceFrames); int maxDestinationBytes = maxDestinationFrames * numChannels * sizeof(AudioConstants::AudioSample); - _byteArray.resize(maxDestinationBytes); + _data.resize(maxDestinationBytes); int numDestinationFrames = resampler.render((int16_t*)rawAudioByteArray.data(), - (int16_t*)_byteArray.data(), + (int16_t*)_data.data(), numSourceFrames); // truncate to actual output int numDestinationBytes = numDestinationFrames * numChannels * sizeof(AudioConstants::AudioSample); - _byteArray.resize(numDestinationBytes); + _data.resize(numDestinationBytes); } } @@ -163,7 +192,7 @@ struct WAVEFormat { }; // returns wavfile sample rate, used for resampling -int Sound::interpretAsWav(const QByteArray& inputAudioByteArray, QByteArray& outputAudioByteArray) { +int SoundProcessor::interpretAsWav(const QByteArray& inputAudioByteArray, QByteArray& outputAudioByteArray) { // Create a data stream to analyze the data QDataStream waveStream(const_cast(&inputAudioByteArray), QIODevice::ReadOnly); diff --git a/libraries/audio/src/Sound.h b/libraries/audio/src/Sound.h index 37d5b40e95..69dbf5a913 100644 --- a/libraries/audio/src/Sound.h +++ b/libraries/audio/src/Sound.h @@ -12,6 +12,7 @@ #ifndef hifi_Sound_h #define hifi_Sound_h +#include #include #include #include @@ -28,12 +29,15 @@ public: bool isAmbisonic() const { return _isAmbisonic; } bool isReady() const { return _isReady; } float getDuration() const { return _duration; } - const QByteArray& getByteArray() const { return _byteArray; } signals: void ready(); + +protected slots: + void soundProcessSuccess(QByteArray data, bool stereo, bool ambisonic, float duration); + void soundProcessError(int error, QString str); private: QByteArray _byteArray; @@ -42,10 +46,33 @@ private: bool _isReady; float _duration; // In seconds + virtual void downloadFinished(const QByteArray& data) override; +}; + +class SoundProcessor : public QObject, public QRunnable { + Q_OBJECT + +public: + SoundProcessor(const QUrl& url, const QByteArray& data, bool stereo, bool ambisonic) + : _url(url), _data(data), _isStereo(stereo), _isAmbisonic(ambisonic) + { + } + + virtual void run() override; + void downSample(const QByteArray& rawAudioByteArray, int sampleRate); int interpretAsWav(const QByteArray& inputAudioByteArray, QByteArray& outputAudioByteArray); - - virtual void downloadFinished(const QByteArray& data) override; + +signals: + void onSuccess(QByteArray data, bool stereo, bool ambisonic, float duration); + void onError(int error, QString str); + +private: + QUrl _url; + QByteArray _data; + bool _isStereo; + bool _isAmbisonic; + float _duration; }; typedef QSharedPointer SharedSoundPointer; diff --git a/libraries/avatars-renderer/src/avatars-renderer/Head.cpp b/libraries/avatars-renderer/src/avatars-renderer/Head.cpp index 96ecd86ff4..b8a559027b 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Head.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Head.cpp @@ -304,18 +304,6 @@ glm::quat Head::getEyeRotation(const glm::vec3& eyePosition) const { return rotationBetween(orientation * IDENTITY_FORWARD, lookAtDelta + glm::length(lookAtDelta) * _saccade) * orientation; } -void Head::setFinalPitch(float finalPitch) { - _deltaPitch = glm::clamp(finalPitch, MIN_HEAD_PITCH, MAX_HEAD_PITCH) - _basePitch; -} - -void Head::setFinalYaw(float finalYaw) { - _deltaYaw = glm::clamp(finalYaw, MIN_HEAD_YAW, MAX_HEAD_YAW) - _baseYaw; -} - -void Head::setFinalRoll(float finalRoll) { - _deltaRoll = glm::clamp(finalRoll, MIN_HEAD_ROLL, MAX_HEAD_ROLL) - _baseRoll; -} - float Head::getFinalYaw() const { return glm::clamp(_baseYaw + _deltaYaw, MIN_HEAD_YAW, MAX_HEAD_YAW); } diff --git a/libraries/avatars-renderer/src/avatars-renderer/Head.h b/libraries/avatars-renderer/src/avatars-renderer/Head.h index c5902285b9..e3c8d7d2b5 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Head.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Head.h @@ -71,9 +71,6 @@ public: void setDeltaRoll(float roll) { _deltaRoll = roll; } float getDeltaRoll() const { return _deltaRoll; } - virtual void setFinalYaw(float finalYaw) override; - virtual void setFinalPitch(float finalPitch) override; - virtual void setFinalRoll(float finalRoll) override; virtual float getFinalPitch() const override; virtual float getFinalYaw() const override; virtual float getFinalRoll() const override; diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index d82068b8ac..4407e12295 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -850,7 +850,10 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { glm::quat sensorToWorldQuat; unpackOrientationQuatFromSixBytes(data->sensorToWorldQuat, sensorToWorldQuat); float sensorToWorldScale; - unpackFloatScalarFromSignedTwoByteFixed((int16_t*)&data->sensorToWorldScale, &sensorToWorldScale, SENSOR_TO_WORLD_SCALE_RADIX); + // Grab a local copy of sensorToWorldScale to be able to use the unpack function with a pointer on it, + // a direct pointer on the struct attribute triggers warnings because of potential misalignement. + auto srcSensorToWorldScale = data->sensorToWorldScale; + unpackFloatScalarFromSignedTwoByteFixed((int16_t*)&srcSensorToWorldScale, &sensorToWorldScale, SENSOR_TO_WORLD_SCALE_RADIX); glm::vec3 sensorToWorldTrans(data->sensorToWorldTrans[0], data->sensorToWorldTrans[1], data->sensorToWorldTrans[2]); glm::mat4 sensorToWorldMatrix = createMatFromScaleQuatAndPos(glm::vec3(sensorToWorldScale), sensorToWorldQuat, sensorToWorldTrans); if (_sensorToWorldMatrixCache.get() != sensorToWorldMatrix) { diff --git a/libraries/avatars/src/HeadData.h b/libraries/avatars/src/HeadData.h index 0bb38c1dad..c15714bd73 100644 --- a/libraries/avatars/src/HeadData.h +++ b/libraries/avatars/src/HeadData.h @@ -45,9 +45,6 @@ public: float getBaseRoll() const { return _baseRoll; } void setBaseRoll(float roll) { _baseRoll = glm::clamp(roll, MIN_HEAD_ROLL, MAX_HEAD_ROLL); } - virtual void setFinalYaw(float finalYaw) { _baseYaw = finalYaw; } - virtual void setFinalPitch(float finalPitch) { _basePitch = finalPitch; } - virtual void setFinalRoll(float finalRoll) { _baseRoll = finalRoll; } virtual float getFinalYaw() const { return _baseYaw; } virtual float getFinalPitch() const { return _basePitch; } virtual float getFinalRoll() const { return _baseRoll; } diff --git a/libraries/controllers/src/controllers/Actions.h b/libraries/controllers/src/controllers/Actions.h index 5727d4906e..2cb500c42a 100644 --- a/libraries/controllers/src/controllers/Actions.h +++ b/libraries/controllers/src/controllers/Actions.h @@ -42,8 +42,6 @@ enum class Action { LEFT_HAND = NUM_COMBINED_AXES, RIGHT_HAND, - LEFT_ARM, - RIGHT_ARM, LEFT_FOOT, RIGHT_FOOT, HIPS, @@ -103,6 +101,8 @@ enum class Action { // Bisected aliases for TRANSLATE_CAMERA_Z BOOM_IN, BOOM_OUT, + LEFT_ARM, + RIGHT_ARM, NUM_ACTIONS, diff --git a/libraries/controllers/src/controllers/InputRecorder.cpp b/libraries/controllers/src/controllers/InputRecorder.cpp index 60ff592144..54d1aaf131 100644 --- a/libraries/controllers/src/controllers/InputRecorder.cpp +++ b/libraries/controllers/src/controllers/InputRecorder.cpp @@ -9,7 +9,6 @@ #include "InputRecorder.h" #include -#include #include #include #include @@ -19,13 +18,17 @@ #include #include #include +#include +#include #include #include +#include +#include "UserInputMapper.h" 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 = "json.gz"; +QString COMPRESS_EXTENSION = ".json.gz"; namespace controller { QJsonObject poseToJsonObject(const Pose pose) { @@ -91,23 +94,27 @@ namespace controller { } - void exportToFile(QJsonObject& object) { + void exportToFile(const QJsonObject& object, const QString& fileName) { 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); + QByteArray jsonData = saveData.toJson(QJsonDocument::Indented); + QByteArray jsonDataForFile; + + if (!gzip(jsonData, jsonDataForFile, -1)) { + qCritical("unable to gzip while saving to json."); + return; + } + + saveFile.write(jsonDataForFile); + saveFile.close(); } QJsonObject openFile(const QString& file, bool& status) { @@ -118,10 +125,19 @@ namespace controller { status = false; return object; } - QByteArray compressedData = qUncompress(openFile.readAll()); - QJsonDocument jsonDoc = QJsonDocument::fromJson(compressedData); + QByteArray compressedData = openFile.readAll(); + QByteArray jsonData; + + if (!gunzip(compressedData, jsonData)) { + qCritical() << "json file not in gzip format: " << file; + status = false; + return object; + } + + QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData); object = jsonDoc.object(); status = true; + openFile.close(); return object; } @@ -146,31 +162,47 @@ namespace controller { _actionStateList.clear(); } - void InputRecorder::saveRecording() { + QJsonObject InputRecorder::recordDataToJson() { QJsonObject data; data["frameCount"] = _framesRecorded; - + data["version"] = "0.0"; + QJsonArray actionArrayList; QJsonArray poseArrayList; for(const ActionStates actionState: _actionStateList) { QJsonArray actionArray; - for (const float value: actionState) { - actionArray.append(value); + for (const auto action: actionState) { + QJsonObject actionJson; + actionJson["name"] = action.first; + actionJson["value"] = action.second; + actionArray.append(actionJson); } actionArrayList.append(actionArray); } for (const PoseStates poseState: _poseStateList) { QJsonArray poseArray; - for (const Pose pose: poseState) { - poseArray.append(poseToJsonObject(pose)); + for (const auto pose: poseState) { + QJsonObject poseJson; + poseJson["name"] = pose.first; + poseJson["pose"] = poseToJsonObject(pose.second); + poseArray.append(poseJson); } poseArrayList.append(poseArray); } data["actionList"] = actionArrayList; data["poseList"] = poseArrayList; - exportToFile(data); + + return data; + } + + void InputRecorder::saveRecording() { + QJsonObject jsonData = recordDataToJson(); + QString timeStamp = QDateTime::currentDateTime().toString(Qt::ISODate); + timeStamp.replace(":", "-"); + QString fileName = SAVE_DIRECTORY + FILE_PREFIX_NAME + timeStamp + COMPRESS_EXTENSION; + exportToFile(jsonData, fileName); } void InputRecorder::loadRecording(const QString& path) { @@ -181,17 +213,20 @@ namespace controller { resetFrame(); _poseStateList.clear(); _actionStateList.clear(); - QString filePath = path; - filePath.remove(0,8); + QUrl urlPath(path); + QString filePath = urlPath.toLocalFile(); 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) { + QJsonObject data = openFile(filePath, success); + auto keyValue = data.find("version"); + if (success && keyValue != data.end()) { _framesRecorded = data["frameCount"].toInt(); QJsonArray actionArrayList = data["actionList"].toArray(); QJsonArray poseArrayList = data["poseList"].toArray(); @@ -199,30 +234,31 @@ namespace controller { 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].toDouble(); + QJsonObject actionObject = actionState[index].toObject(); + _currentFrameActions[actionObject["name"].toString()] = actionObject["value"].toDouble(); } _actionStateList.push_back(_currentFrameActions); - _currentFrameActions = ActionStates(toInt(Action::NUM_ACTIONS)); + _currentFrameActions.clear(); } 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()); + QJsonObject poseObject = poseState[index].toObject(); + _currentFramePoses[poseObject["name"].toString()] = jsonObjectToPose(poseObject["pose"].toObject()); } _poseStateList.push_back(_currentFramePoses); - _currentFramePoses = PoseStates(toInt(Action::NUM_ACTIONS)); + _currentFramePoses.clear(); } - } - + } _loading = false; } - + void InputRecorder::stopRecording() { _recording = false; _framesRecorded = (int)_actionStateList.size(); } - + void InputRecorder::startPlayback() { _playback = true; _recording = false; @@ -234,41 +270,36 @@ namespace controller { _playCount = 0; } - void InputRecorder::setActionState(controller::Action action, float value) { + void InputRecorder::setActionState(const QString& action, float value) { if (_recording) { - _currentFrameActions[toInt(action)] += value; + _currentFrameActions[action] += value; } } - void InputRecorder::setActionState(controller::Action action, const controller::Pose pose) { + void InputRecorder::setActionState(const QString& action, const controller::Pose& pose) { if (_recording) { - _currentFramePoses[toInt(action)] = pose; + _currentFramePoses[action] = pose; } } void InputRecorder::resetFrame() { if (_recording) { - for(auto& channel : _currentFramePoses) { - channel = Pose(); - } - - for(auto& channel : _currentFrameActions) { - channel = 0.0f; - } + _currentFramePoses.clear(); + _currentFrameActions.clear(); } } - float InputRecorder::getActionState(controller::Action action) { + float InputRecorder::getActionState(const QString& action) { if (_actionStateList.size() > 0 ) { - return _actionStateList[_playCount][toInt(action)]; + return _actionStateList[_playCount][action]; } return 0.0f; } - controller::Pose InputRecorder::getPoseState(controller::Action action) { + controller::Pose InputRecorder::getPoseState(const QString& action) { if (_poseStateList.size() > 0) { - return _poseStateList[_playCount][toInt(action)]; + return _poseStateList[_playCount][action]; } return Pose(); diff --git a/libraries/controllers/src/controllers/InputRecorder.h b/libraries/controllers/src/controllers/InputRecorder.h index d1cc9a32eb..9adb8e386f 100644 --- a/libraries/controllers/src/controllers/InputRecorder.h +++ b/libraries/controllers/src/controllers/InputRecorder.h @@ -12,8 +12,10 @@ #include #include #include +#include #include +#include #include "Pose.h" #include "Actions.h" @@ -21,8 +23,8 @@ namespace controller { class InputRecorder { public: - using PoseStates = std::vector; - using ActionStates = std::vector; + using PoseStates = std::map; + using ActionStates = std::map; InputRecorder(); ~InputRecorder(); @@ -40,20 +42,21 @@ namespace controller { 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); + void setActionState(const QString& action, float value); + void setActionState(const QString& action, const controller::Pose& pose); + float getActionState(const QString& action); + controller::Pose getPoseState(const QString& action); QString getSaveDirectory(); void frameTick(); private: + QJsonObject recordDataToJson(); 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)); + PoseStates _currentFramePoses; + ActionStates _currentFrameActions; int _framesRecorded { 0 }; int _playCount { 0 }; diff --git a/libraries/controllers/src/controllers/Pose.h b/libraries/controllers/src/controllers/Pose.h index a6d1360f9f..186bbdd733 100644 --- a/libraries/controllers/src/controllers/Pose.h +++ b/libraries/controllers/src/controllers/Pose.h @@ -39,6 +39,7 @@ namespace controller { quat getRotation() const { return rotation; } vec3 getVelocity() const { return velocity; } vec3 getAngularVelocity() const { return angularVelocity; } + mat4 getMatrix() const { return createMatFromQuatAndPos(rotation, translation); } Pose transform(const glm::mat4& mat) const; Pose postTransform(const glm::mat4& mat) const; diff --git a/libraries/controllers/src/controllers/UserInputMapper.cpp b/libraries/controllers/src/controllers/UserInputMapper.cpp index 570081d1f1..79f4325ae6 100755 --- a/libraries/controllers/src/controllers/UserInputMapper.cpp +++ b/libraries/controllers/src/controllers/UserInputMapper.cpp @@ -329,6 +329,16 @@ QString UserInputMapper::getActionName(Action action) const { return QString(); } +QString UserInputMapper::getStandardPoseName(uint16_t pose) { + Locker locker(_lock); + for (auto posePair : getStandardInputs()) { + if (posePair.first.channel == pose && posePair.first.getType() == ChannelType::POSE) { + return posePair.second; + } + } + return QString(); +} + QVector UserInputMapper::getActionNames() const { Locker locker(_lock); QVector result; diff --git a/libraries/controllers/src/controllers/UserInputMapper.h b/libraries/controllers/src/controllers/UserInputMapper.h index 05a286cc10..0c8bb51008 100644 --- a/libraries/controllers/src/controllers/UserInputMapper.h +++ b/libraries/controllers/src/controllers/UserInputMapper.h @@ -80,6 +80,7 @@ namespace controller { QVector getAllActions() const; QString getActionName(Action action) const; + QString getStandardPoseName(uint16_t pose); float getActionState(Action action) const { return _actionStates[toInt(action)]; } Pose getPoseState(Action action) const; int findAction(const QString& actionName) const; diff --git a/libraries/controllers/src/controllers/impl/endpoints/ActionEndpoint.cpp b/libraries/controllers/src/controllers/impl/endpoints/ActionEndpoint.cpp index 6c14533f02..ef9f04402b 100644 --- a/libraries/controllers/src/controllers/impl/endpoints/ActionEndpoint.cpp +++ b/libraries/controllers/src/controllers/impl/endpoints/ActionEndpoint.cpp @@ -17,31 +17,30 @@ using namespace controller; void ActionEndpoint::apply(float newValue, const Pointer& source) { InputRecorder* inputRecorder = InputRecorder::getInstance(); + auto userInputMapper = DependencyManager::get(); + QString actionName = userInputMapper->getActionName(Action(_input.getChannel())); if(inputRecorder->isPlayingback()) { - newValue = inputRecorder->getActionState(Action(_input.getChannel())); + newValue = inputRecorder->getActionState(actionName); } _currentValue += newValue; if (_input != Input::INVALID_INPUT) { - auto userInputMapper = DependencyManager::get(); userInputMapper->deltaActionState(Action(_input.getChannel()), newValue); } - inputRecorder->setActionState(Action(_input.getChannel()), newValue); + inputRecorder->setActionState(actionName, 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())); - } + auto userInputMapper = DependencyManager::get(); + QString actionName = userInputMapper->getActionName(Action(_input.getChannel())); + inputRecorder->setActionState(actionName, _currentPose); if (!_currentPose.isValid()) { return; } if (_input != Input::INVALID_INPUT) { - auto userInputMapper = DependencyManager::get(); userInputMapper->setActionState(Action(_input.getChannel()), _currentPose); } } diff --git a/libraries/controllers/src/controllers/impl/endpoints/StandardEndpoint.h b/libraries/controllers/src/controllers/impl/endpoints/StandardEndpoint.h index dfa728d2b6..2006809fed 100644 --- a/libraries/controllers/src/controllers/impl/endpoints/StandardEndpoint.h +++ b/libraries/controllers/src/controllers/impl/endpoints/StandardEndpoint.h @@ -12,6 +12,11 @@ #include "../Endpoint.h" +#include + +#include "../../InputRecorder.h" +#include "../../UserInputMapper.h" + namespace controller { class StandardEndpoint : public VirtualEndpoint { @@ -40,6 +45,12 @@ public: virtual Pose pose() override { _read = true; + InputRecorder* inputRecorder = InputRecorder::getInstance(); + if (inputRecorder->isPlayingback()) { + auto userInputMapper = DependencyManager::get(); + QString actionName = userInputMapper->getStandardPoseName(_input.getChannel()); + return inputRecorder->getPoseState(actionName); + } return VirtualEndpoint::pose(); } diff --git a/libraries/controllers/src/controllers/impl/filters/LowVelocityFilter.h b/libraries/controllers/src/controllers/impl/filters/LowVelocityFilter.h index d870a5c551..b1c6be1f58 100644 --- a/libraries/controllers/src/controllers/impl/filters/LowVelocityFilter.h +++ b/libraries/controllers/src/controllers/impl/filters/LowVelocityFilter.h @@ -21,9 +21,9 @@ namespace controller { LowVelocityFilter(float rotationConstant, float translationConstant) : _translationConstant(translationConstant), _rotationConstant(rotationConstant) {} - virtual float apply(float value) const override { return value; } - virtual Pose apply(Pose newPose) const; - virtual bool parseParameters(const QJsonValue& parameters) override; + float apply(float value) const override { return value; } + Pose apply(Pose newPose) const override; + bool parseParameters(const QJsonValue& parameters) override; private: float _translationConstant { 0.1f }; diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index f6f7da0ecd..bfd158ffb5 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -339,6 +339,18 @@ void OpenGLDisplayPlugin::deactivate() { Parent::deactivate(); } +bool OpenGLDisplayPlugin::startStandBySession() { + if (!activateStandBySession()) { + return false; + } + return Parent::startStandBySession(); +} + +void OpenGLDisplayPlugin::endSession() { + deactivateSession(); + Parent::endSession(); +} + void OpenGLDisplayPlugin::customizeContext() { auto presentThread = DependencyManager::get(); Q_ASSERT(thread() == presentThread->thread()); diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index e1eea5de6c..10a7558398 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -42,6 +42,8 @@ public: // between the main thread and the presentation thread bool activate() override final; void deactivate() override final; + bool startStandBySession() override final; + void endSession() override final; bool eventFilter(QObject* receiver, QEvent* event) override; bool isDisplayVisible() const override { return true; } @@ -99,6 +101,10 @@ protected: // Returns true on successful activation virtual bool internalActivate() { return true; } virtual void internalDeactivate() {} + + // Returns true on successful activation of standby session + virtual bool activateStandBySession() { return true; } + virtual void deactivateSession() {} // Plugin specific functionality to send the composed scene to the output window or device virtual void internalPresent(); diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index cab96c258b..08c8d4f754 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -221,6 +221,12 @@ void HmdDisplayPlugin::internalPresent() { float shiftLeftBy = getLeftCenterPixel() - (sourceSize.x / 2); float newWidth = sourceSize.x - shiftLeftBy; + // Experimentally adjusted the region presented in preview to avoid seeing the masked pixels and recenter the center... + static float SCALE_WIDTH = 0.9f; + static float SCALE_OFFSET = 2.0f; + newWidth *= SCALE_WIDTH; + shiftLeftBy *= SCALE_OFFSET; + const unsigned int RATIO_Y = 9; const unsigned int RATIO_X = 16; glm::uvec2 originalClippedSize { newWidth, newWidth * RATIO_Y / RATIO_X }; diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index e029ca6ada..1b92adbc37 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -23,7 +23,6 @@ #include #include #include -#include #include "EntityTreeRenderer.h" @@ -141,7 +140,6 @@ void EntityTreeRenderer::clear() { // reset the zone to the default (while we load the next scene) _layeredZones.clear(); - applyZoneAndHasSkybox(nullptr); OctreeRenderer::clear(); } @@ -194,15 +192,7 @@ void EntityTreeRenderer::update() { tree->update(); // Handle enter/leave entity logic - bool updated = checkEnterLeaveEntities(); - - // If we haven't already updated and previously attempted to load a texture, - // check if the texture loaded and apply it - if (!updated && - ((_pendingAmbientTexture && (!_ambientTexture || _ambientTexture->isLoaded())) || - (_pendingSkyboxTexture && (!_skyboxTexture || _skyboxTexture->isLoaded())))) { - applySkyboxAndHasAmbient(); - } + checkEnterLeaveEntities(); // Even if we're not moving the mouse, if we started clicking on an entity and we have // not yet released the hold then this is still considered a holdingClickOnEntity event @@ -371,176 +361,6 @@ bool EntityTreeRenderer::applyLayeredZones() { return true; } - -bool EntityTreeRenderer::applyZoneAndHasSkybox(const std::shared_ptr& zone) { - auto textureCache = DependencyManager::get(); - auto scene = DependencyManager::get(); - auto sceneStage = scene->getStage(); - auto skyStage = scene->getSkyStage(); - auto sceneKeyLight = sceneStage->getKeyLight(); - - // If there is no zone, use the default background - if (!zone) { - _zoneUserData = QString(); - skyStage->getSkybox()->clear(); - - _pendingSkyboxTexture = false; - _skyboxTexture.clear(); - - _pendingAmbientTexture = false; - _ambientTexture.clear(); - - sceneKeyLight->resetAmbientSphere(); - sceneKeyLight->setAmbientMap(nullptr); - - skyStage->setBackgroundMode(model::SunSkyStage::SKY_DEFAULT); - return false; - } - - // Set the keylight - sceneKeyLight->setColor(ColorUtils::toVec3(zone->getKeyLightProperties().getColor())); - sceneKeyLight->setIntensity(zone->getKeyLightProperties().getIntensity()); - sceneKeyLight->setAmbientIntensity(zone->getKeyLightProperties().getAmbientIntensity()); - sceneKeyLight->setDirection(zone->getKeyLightProperties().getDirection()); - - // Set the stage - bool isSunModelEnabled = zone->getStageProperties().getSunModelEnabled(); - sceneStage->setSunModelEnable(isSunModelEnabled); - if (isSunModelEnabled) { - sceneStage->setLocation(zone->getStageProperties().getLongitude(), - zone->getStageProperties().getLatitude(), - zone->getStageProperties().getAltitude()); - - auto sceneTime = sceneStage->getTime(); - sceneTime->setHour(zone->getStageProperties().calculateHour()); - sceneTime->setDay(zone->getStageProperties().calculateDay()); - } - - // Set the ambient texture - _ambientTextureURL = zone->getKeyLightProperties().getAmbientURL(); - if (_ambientTextureURL.isEmpty()) { - _pendingAmbientTexture = false; - _ambientTexture.clear(); - } else { - _pendingAmbientTexture = true; - } - - // Set the skybox texture - return layerZoneAndHasSkybox(zone); -} - -bool EntityTreeRenderer::layerZoneAndHasSkybox(const std::shared_ptr& zone) { - assert(zone); - - auto textureCache = DependencyManager::get(); - auto scene = DependencyManager::get(); - auto skyStage = scene->getSkyStage(); - auto skybox = skyStage->getSkybox(); - - bool hasSkybox = false; - - switch (zone->getBackgroundMode()) { - case BACKGROUND_MODE_SKYBOX: - hasSkybox = true; - - skybox->setColor(zone->getSkyboxProperties().getColorVec3()); - - if (_zoneUserData != zone->getUserData()) { - _zoneUserData = zone->getUserData(); - std::dynamic_pointer_cast(skybox)->parse(_zoneUserData); - } - - _skyboxTextureURL = zone->getSkyboxProperties().getURL(); - if (_skyboxTextureURL.isEmpty()) { - _pendingSkyboxTexture = false; - _skyboxTexture.clear(); - } else { - _pendingSkyboxTexture = true; - } - - applySkyboxAndHasAmbient(); - skyStage->setBackgroundMode(model::SunSkyStage::SKY_BOX); - - break; - - case BACKGROUND_MODE_INHERIT: - default: - // Clear the skybox to release its textures - skybox->clear(); - _zoneUserData = QString(); - - _pendingSkyboxTexture = false; - _skyboxTexture.clear(); - - // Let the application background through - if (applySkyboxAndHasAmbient()) { - skyStage->setBackgroundMode(model::SunSkyStage::SKY_DEFAULT_TEXTURE); - } else { - skyStage->setBackgroundMode(model::SunSkyStage::SKY_DEFAULT_AMBIENT_TEXTURE); - } - - break; - } - - return hasSkybox; -} - -bool EntityTreeRenderer::applySkyboxAndHasAmbient() { - auto textureCache = DependencyManager::get(); - auto scene = DependencyManager::get(); - auto sceneStage = scene->getStage(); - auto skyStage = scene->getSkyStage(); - auto sceneKeyLight = sceneStage->getKeyLight(); - auto skybox = skyStage->getSkybox(); - - bool isAmbientSet = false; - if (_pendingAmbientTexture && !_ambientTexture) { - _ambientTexture = textureCache->getTexture(_ambientTextureURL, image::TextureUsage::CUBE_TEXTURE); - } - if (_ambientTexture && _ambientTexture->isLoaded()) { - _pendingAmbientTexture = false; - - auto texture = _ambientTexture->getGPUTexture(); - if (texture) { - isAmbientSet = true; - sceneKeyLight->setAmbientSphere(texture->getIrradiance()); - sceneKeyLight->setAmbientMap(texture); - } else { - qCDebug(entitiesrenderer) << "Failed to load ambient texture:" << _ambientTexture->getURL(); - } - } - - if (_pendingSkyboxTexture && - (!_skyboxTexture || (_skyboxTexture->getURL() != _skyboxTextureURL))) { - _skyboxTexture = textureCache->getTexture(_skyboxTextureURL, image::TextureUsage::CUBE_TEXTURE); - } - if (_skyboxTexture && _skyboxTexture->isLoaded()) { - _pendingSkyboxTexture = false; - - auto texture = _skyboxTexture->getGPUTexture(); - if (texture) { - skybox->setCubemap(texture); - if (!isAmbientSet) { - sceneKeyLight->setAmbientSphere(texture->getIrradiance()); - sceneKeyLight->setAmbientMap(texture); - isAmbientSet = true; - } - } else { - qCDebug(entitiesrenderer) << "Failed to load skybox texture:" << _skyboxTexture->getURL(); - skybox->setCubemap(nullptr); - } - } else { - skybox->setCubemap(nullptr); - } - - if (!isAmbientSet) { - sceneKeyLight->resetAmbientSphere(); - sceneKeyLight->setAmbientMap(nullptr); - } - - return isAmbientSet; -} - const FBXGeometry* EntityTreeRenderer::getGeometryForEntity(EntityItemPointer entityItem) { const FBXGeometry* result = NULL; @@ -1186,8 +1006,6 @@ std::pair EntityTreeRenderer:: void EntityTreeRenderer::LayeredZones::apply() { assert(_entityTreeRenderer); - - applyPartial(begin()); } void EntityTreeRenderer::LayeredZones::update(std::shared_ptr zone) { @@ -1202,12 +1020,6 @@ void EntityTreeRenderer::LayeredZones::update(std::shared_ptr zo } else { LayeredZone zoneLayer(zone); - // should we update? only if this zone is tighter than the current skybox zone - bool shouldUpdate = false; - if (_skyboxLayer == end() || zoneLayer <= *_skyboxLayer) { - shouldUpdate = true; - } - // find this zone's layer, if it exists iterator layer = end(); auto it = _map.find(zoneLayer.id); @@ -1227,41 +1039,9 @@ void EntityTreeRenderer::LayeredZones::update(std::shared_ptr zo std::tie(layer, std::ignore) = insert(zoneLayer); _map.emplace(layer->id, layer); } - - if (shouldUpdate) { - applyPartial(layer); - } } } -void EntityTreeRenderer::LayeredZones::applyPartial(iterator layer) { - bool hasSkybox = false; - _skyboxLayer = end(); - - if (layer == end()) { - if (empty()) { - _entityTreeRenderer->applyZoneAndHasSkybox(nullptr); - return; - } else { // a layer was removed - reapply from beginning - layer = begin(); - } - } - - if (layer == begin()) { - hasSkybox = _entityTreeRenderer->applyZoneAndHasSkybox(layer->zone); - } else { - hasSkybox = _entityTreeRenderer->layerZoneAndHasSkybox(layer->zone); - } - - if (layer != end()) { - while (!hasSkybox && ++layer != end()) { - hasSkybox = _entityTreeRenderer->layerZoneAndHasSkybox(layer->zone); - } - } - - _skyboxLayer = layer; -} - bool EntityTreeRenderer::LayeredZones::contains(const LayeredZones& other) { bool result = std::equal(other.begin(), other._skyboxLayer, begin()); if (result) { diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index f4717dca51..bf4148212b 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -147,9 +147,6 @@ private: void addEntityToScene(EntityItemPointer entity); bool findBestZoneAndMaybeContainingEntities(QVector* entitiesContainingAvatar = nullptr); - bool applyZoneAndHasSkybox(const std::shared_ptr& zone); - bool layerZoneAndHasSkybox(const std::shared_ptr& zone); - bool applySkyboxAndHasAmbient(); bool applyLayeredZones(); void checkAndCallPreload(const EntityItemID& entityID, bool reload = false, bool unloadFirst = false); diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 36273c1f07..e89646d838 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -605,7 +605,7 @@ bool RenderableModelEntityItem::findDetailedRayIntersection(const glm::vec3& ori QString extraInfo; return _model->findRayIntersectionAgainstSubMeshes(origin, direction, distance, - face, surfaceNormal, extraInfo, precisionPicking); + face, surfaceNormal, extraInfo, precisionPicking, false); } void RenderableModelEntityItem::getCollisionGeometryResource() { diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp index 34dc86d92a..d813a73773 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include "EntitiesRendererLogging.h" @@ -292,6 +293,7 @@ void RenderableParticleEffectEntityItem::createPipelines() { state->setDepthTest(true, false, gpu::LESS_EQUAL); state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + PrepareStencil::testMask(*state); auto vertShader = gpu::Shader::createVertex(std::string(untextured_particle_vert)); auto fragShader = gpu::Shader::createPixel(std::string(untextured_particle_frag)); @@ -305,6 +307,7 @@ void RenderableParticleEffectEntityItem::createPipelines() { state->setDepthTest(true, false, gpu::LESS_EQUAL); state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + PrepareStencil::testMask(*state); auto vertShader = gpu::Shader::createVertex(std::string(textured_particle_vert)); auto fragShader = gpu::Shader::createPixel(std::string(textured_particle_frag)); diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp index a34a1814b4..1e20956301 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -69,6 +70,7 @@ void RenderablePolyLineEntityItem::createPipeline() { gpu::StatePointer state = gpu::StatePointer(new gpu::State()); state->setDepthTest(true, true, gpu::LESS_EQUAL); + PrepareStencil::testMask(*state); state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index fd5346093b..7567566919 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -46,6 +46,9 @@ #endif #include "model/Geometry.h" + +#include "StencilMaskPass.h" + #include "EntityTreeRenderer.h" #include "polyvox_vert.h" #include "polyvox_frag.h" @@ -743,6 +746,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { auto state = std::make_shared(); state->setCullMode(gpu::State::CULL_BACK); state->setDepthTest(true, true, gpu::LESS_EQUAL); + PrepareStencil::testMaskDrawShape(*state); _pipeline = gpu::Pipeline::create(program, state); @@ -750,6 +754,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { wireframeState->setCullMode(gpu::State::CULL_BACK); wireframeState->setDepthTest(true, true, gpu::LESS_EQUAL); wireframeState->setFillMode(gpu::State::FILL_LINE); + PrepareStencil::testMaskDrawShape(*wireframeState); _wireframePipeline = gpu::Pipeline::create(program, wireframeState); } diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 1ad60bf7c6..27dd678d91 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -93,6 +94,7 @@ void RenderableShapeEntityItem::render(RenderArgs* args) { _procedural->_fragmentSource = simple_frag; _procedural->_opaqueState->setCullMode(gpu::State::CULL_NONE); _procedural->_opaqueState->setDepthTest(true, true, gpu::LESS_EQUAL); + PrepareStencil::testMaskDrawShape(*_procedural->_opaqueState); _procedural->_opaqueState->setBlendFunction(false, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 0d286c46eb..3b3480443d 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -127,7 +127,7 @@ bool RenderableWebEntityItem::buildWebSurface(QSharedPointer _webSurface->resume(); _webSurface->getRootItem()->setProperty("url", _sourceUrl); - _webSurface->getRootContext()->setContextProperty("desktop", QVariant()); + _webSurface->getSurfaceContext()->setContextProperty("desktop", QVariant()); // FIXME - Keyboard HMD only: Possibly add "HMDinfo" object to context for WebView.qml. // forward web events to EntityScriptingInterface @@ -271,7 +271,7 @@ void RenderableWebEntityItem::loadSourceURL() { }); _webSurface->getRootItem()->setProperty("url", _sourceUrl); - _webSurface->getRootContext()->setContextProperty("desktop", QVariant()); + _webSurface->getSurfaceContext()->setContextProperty("desktop", QVariant()); } else { _contentType = qmlContent; @@ -284,7 +284,7 @@ void RenderableWebEntityItem::loadSourceURL() { _webSurface->getRootItem(), _webSurface.data()); } } - _webSurface->getRootContext()->setContextProperty("globalPosition", vec3toVariant(getPosition())); + _webSurface->getSurfaceContext()->setContextProperty("globalPosition", vec3toVariant(getPosition())); } @@ -420,7 +420,7 @@ void RenderableWebEntityItem::update(const quint64& now) { if (_webSurface) { // update globalPosition - _webSurface->getRootContext()->setContextProperty("globalPosition", vec3toVariant(getPosition())); + _webSurface->getSurfaceContext()->setContextProperty("globalPosition", vec3toVariant(getPosition())); } auto interval = now - _lastRenderTime; diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp index 6556f18776..d3fd9a0980 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp @@ -13,14 +13,90 @@ #include +#include + #include #include #include #include +#include #include "EntityTreeRenderer.h" #include "RenderableEntityItem.h" +#include +#include "DeferredLightingEffect.h" + + +class RenderableZoneEntityItemMeta { +public: + RenderableZoneEntityItemMeta(EntityItemPointer entity); + ~RenderableZoneEntityItemMeta(); + + typedef render::Payload Payload; + typedef Payload::DataPointer Pointer; + + EntityItemPointer entity; + + void render(RenderArgs* args); + + void setVisible(bool visible) { _isVisible = visible; } + bool isVisible() const { return _isVisible; } + + render::Item::Bound& editBound() { _needUpdate = true; return _bound; } + + model::LightPointer editSunLight() { _needSunUpdate = true; return _sunLight; } + model::LightPointer editAmbientLight() { _needAmbientUpdate = true; return _ambientLight; } + model::SunSkyStagePointer editBackground() { _needBackgroundUpdate = true; return _background; } + model::SkyboxPointer editSkybox() { return editBackground()->getSkybox(); } + + void setAmbientURL(const QString& ambientUrl); + + void setSkyboxURL(const QString& skyboxUrl); + + void setBackgroundMode(BackgroundMode mode); + void setSkyboxColor(const glm::vec3& color); + void setProceduralUserData(QString userData); + +protected: + render::Item::Bound _bound; + + model::LightPointer _sunLight; + model::LightPointer _ambientLight; + model::SunSkyStagePointer _background; + BackgroundMode _backgroundMode { BACKGROUND_MODE_INHERIT }; + + LightStagePointer _stage; + LightStage::Index _sunIndex { LightStage::INVALID_INDEX }; + LightStage::Index _ambientIndex { LightStage::INVALID_INDEX }; + + BackgroundStagePointer _backgroundStage; + BackgroundStage::Index _backgroundIndex { BackgroundStage::INVALID_INDEX }; + + bool _needUpdate { true }; + bool _needSunUpdate { true }; + bool _needAmbientUpdate { true }; + bool _needBackgroundUpdate { true }; + bool _isVisible { true }; + + + void updateAmbientMap(); + void updateSkyboxMap(); + + // More attributes used for rendering: + QString _ambientTextureURL; + NetworkTexturePointer _ambientTexture; + bool _pendingAmbientTexture { false }; + bool _validAmbientTexture { false }; + + QString _skyboxTextureURL; + NetworkTexturePointer _skyboxTexture; + bool _pendingSkyboxTexture { false }; + bool _validSkyboxTexture { false }; + + QString _proceduralUserData; +}; + // Sphere entities should fit inside a cube entity of the same size, so a sphere that has dimensions 1x1x1 // is a half unit sphere. However, the geometry cache renders a UNIT sphere, so we need to scale down. static const float SPHERE_ENTITY_SCALE = 0.5f; @@ -67,8 +143,15 @@ bool RenderableZoneEntityItem::setProperties(const EntityItemProperties& propert void RenderableZoneEntityItem::somethingChangedNotification() { DependencyManager::get()->updateZone(_id); + + // If graphics elements are changed, we need to update the render items + notifyChangedRenderItem(); + + // Poopagate back to parent + ZoneEntityItem::somethingChangedNotification(); } + int RenderableZoneEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args, EntityPropertyFlags& propertyFlags, bool overwriteLocalData, @@ -174,18 +257,157 @@ bool RenderableZoneEntityItem::contains(const glm::vec3& point) const { return false; } -class RenderableZoneEntityItemMeta { -public: - RenderableZoneEntityItemMeta(EntityItemPointer entity) : entity(entity){ } - typedef render::Payload Payload; - typedef Payload::DataPointer Pointer; +bool RenderableZoneEntityItem::addToScene(EntityItemPointer self, const render::ScenePointer& scene, + render::Transaction& transaction) { + _myMetaItem = scene->allocateID(); + + auto renderData = std::make_shared(self); + auto renderPayload = std::make_shared(renderData); + updateKeyZoneItemFromEntity((*renderData)); + updateKeySunFromEntity((*renderData)); + updateKeyAmbientFromEntity((*renderData)); + updateKeyBackgroundFromEntity((*renderData)); + + render::Item::Status::Getters statusGetters; + makeEntityItemStatusGetters(getThisPointer(), statusGetters); + renderPayload->addStatusGetters(statusGetters); + + transaction.resetItem(_myMetaItem, renderPayload); + + return true; +} + +void RenderableZoneEntityItem::removeFromScene(EntityItemPointer self, const render::ScenePointer& scene, + render::Transaction& transaction) { + transaction.removeItem(_myMetaItem); + render::Item::clearID(_myMetaItem); + if (_model) { + _model->removeFromScene(scene, transaction); + } +} + +void RenderableZoneEntityItem::notifyBoundChanged() { + notifyChangedRenderItem(); +} + +void RenderableZoneEntityItem::updateKeySunFromEntity(RenderableZoneEntityItemMeta& keyZonePayload) { + auto sunLight = keyZonePayload.editSunLight(); + sunLight->setType(model::Light::SUN); + + sunLight->setPosition(this->getPosition()); + sunLight->setOrientation(this->getRotation()); + + // Set the keylight + sunLight->setColor(ColorUtils::toVec3(this->getKeyLightProperties().getColor())); + sunLight->setIntensity(this->getKeyLightProperties().getIntensity()); + sunLight->setDirection(this->getKeyLightProperties().getDirection()); +} + +void RenderableZoneEntityItem::updateKeyAmbientFromEntity(RenderableZoneEntityItemMeta& keyZonePayload) { + auto ambientLight = keyZonePayload.editAmbientLight(); + ambientLight->setType(model::Light::AMBIENT); + + ambientLight->setPosition(this->getPosition()); + ambientLight->setOrientation(this->getRotation()); + + + // Set the keylight + ambientLight->setAmbientIntensity(this->getKeyLightProperties().getAmbientIntensity()); + + if (this->getKeyLightProperties().getAmbientURL().isEmpty()) { + keyZonePayload.setAmbientURL(this->getSkyboxProperties().getURL()); + } else { + keyZonePayload.setAmbientURL(this->getKeyLightProperties().getAmbientURL()); + } + +} + +void RenderableZoneEntityItem::updateKeyBackgroundFromEntity(RenderableZoneEntityItemMeta& keyZonePayload) { + auto background = keyZonePayload.editBackground(); + + keyZonePayload.setBackgroundMode(this->getBackgroundMode()); + keyZonePayload.setSkyboxColor(this->getSkyboxProperties().getColorVec3()); + keyZonePayload.setProceduralUserData(this->getUserData()); + keyZonePayload.setSkyboxURL(this->getSkyboxProperties().getURL()); +} + + +void RenderableZoneEntityItem::updateKeyZoneItemFromEntity(RenderableZoneEntityItemMeta& keyZonePayload) { + + keyZonePayload.setVisible(this->getVisible()); + + bool success; + keyZonePayload.editBound() = this->getAABox(success); + if (!success) { + keyZonePayload.editBound() = render::Item::Bound(); + } + + /* TODO: Implement the sun model behavior / Keep this code here for reference, this is how we + { + // Set the stage + bool isSunModelEnabled = this->getStageProperties().getSunModelEnabled(); + sceneStage->setSunModelEnable(isSunModelEnabled); + if (isSunModelEnabled) { + sceneStage->setLocation(this->getStageProperties().getLongitude(), + this->getStageProperties().getLatitude(), + this->getStageProperties().getAltitude()); + + auto sceneTime = sceneStage->getTime(); + sceneTime->setHour(this->getStageProperties().calculateHour()); + sceneTime->setDay(this->getStageProperties().calculateDay()); + } + }*/ +} + + +void RenderableZoneEntityItem::sceneUpdateRenderItemFromEntity(render::Transaction& transaction) { + if (!render::Item::isValidID(_myMetaItem)) { + return; + } + + bool sunChanged = _keyLightPropertiesChanged; + bool backgroundChanged = _backgroundPropertiesChanged; + bool skyboxChanged = _skyboxPropertiesChanged; + + transaction.updateItem(_myMetaItem, [=](RenderableZoneEntityItemMeta& data) { + + updateKeyZoneItemFromEntity(data); + + if (sunChanged) { + updateKeySunFromEntity(data); + } + + if (sunChanged || skyboxChanged) { + updateKeyAmbientFromEntity(data); + } + if (backgroundChanged || skyboxChanged) { + updateKeyBackgroundFromEntity(data); + } + }); +} + +void RenderableZoneEntityItem::notifyChangedRenderItem() { + if (!render::Item::isValidID(_myMetaItem)) { + return; + } + + render::Transaction transaction; + render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); + sceneUpdateRenderItemFromEntity(transaction); + scene->enqueueTransaction(transaction); +} + + + + + + + - EntityItemPointer entity; -}; namespace render { template <> const ItemKey payloadGetKey(const RenderableZoneEntityItemMeta::Pointer& payload) { - return ItemKey::Builder::opaqueShape(); + return ItemKey::Builder().withTypeMeta().build(); } template <> const Item::Bound payloadGetBound(const RenderableZoneEntityItemMeta::Pointer& payload) { @@ -200,51 +422,199 @@ namespace render { return render::Item::Bound(); } template <> void payloadRender(const RenderableZoneEntityItemMeta::Pointer& payload, RenderArgs* args) { - if (args) { - if (payload && payload->entity) { - payload->entity->render(args); + payload->render(args); + } +} + +RenderableZoneEntityItemMeta::RenderableZoneEntityItemMeta(EntityItemPointer entity) : + entity(entity), + _sunLight(std::make_shared()), + _ambientLight(std::make_shared()), + _background(std::make_shared()) +{ + _background->setSkybox(std::make_shared()); +} + + +RenderableZoneEntityItemMeta::~RenderableZoneEntityItemMeta() { + if (_stage) { + if (!LightStage::isIndexInvalid(_sunIndex)) { + _stage->removeLight(_sunIndex); + } + if (!LightStage::isIndexInvalid(_ambientIndex)) { + _stage->removeLight(_ambientIndex); + + } + } + + if (_backgroundStage) { + if (!BackgroundStage::isIndexInvalid(_backgroundIndex)) { + _backgroundStage->removeBackground(_backgroundIndex); + } + } +} + +void RenderableZoneEntityItemMeta::setAmbientURL(const QString& ambientUrl) { + // nothing change if nothing change + if (_ambientTextureURL == ambientUrl) { + return; + } + _ambientTextureURL = ambientUrl; + + if (_ambientTextureURL.isEmpty()) { + _validAmbientTexture = false; + _pendingAmbientTexture = false; + _ambientTexture.clear(); + + _ambientLight->setAmbientMap(nullptr); + _ambientLight->setAmbientSpherePreset(gpu::SphericalHarmonics::BREEZEWAY); + } else { + _pendingAmbientTexture = true; + auto textureCache = DependencyManager::get(); + _ambientTexture = textureCache->getTexture(_ambientTextureURL, image::TextureUsage::CUBE_TEXTURE); + + // keep whatever is assigned on the ambient map/sphere until texture is loaded + } +} + +void RenderableZoneEntityItemMeta::updateAmbientMap() { + if (_pendingAmbientTexture) { + if (_ambientTexture && _ambientTexture->isLoaded()) { + _pendingAmbientTexture = false; + + auto texture = _ambientTexture->getGPUTexture(); + if (texture) { + if (texture->getIrradiance()) { + _ambientLight->setAmbientSphere(*texture->getIrradiance()); + } else { + _ambientLight->setAmbientSpherePreset(gpu::SphericalHarmonics::BREEZEWAY); + } + editAmbientLight()->setAmbientMap(texture); + _validAmbientTexture = true; + } else { + qCDebug(entitiesrenderer) << "Failed to load ambient texture:" << _ambientTexture->getURL(); } } } } -bool RenderableZoneEntityItem::addToScene(EntityItemPointer self, const render::ScenePointer& scene, - render::Transaction& transaction) { - _myMetaItem = scene->allocateID(); - auto renderData = std::make_shared(self); - auto renderPayload = std::make_shared(renderData); - - render::Item::Status::Getters statusGetters; - makeEntityItemStatusGetters(getThisPointer(), statusGetters); - renderPayload->addStatusGetters(statusGetters); - - transaction.resetItem(_myMetaItem, renderPayload); - return true; -} - -void RenderableZoneEntityItem::removeFromScene(EntityItemPointer self, const render::ScenePointer& scene, - render::Transaction& transaction) { - transaction.removeItem(_myMetaItem); - render::Item::clearID(_myMetaItem); - if (_model) { - _model->removeFromScene(scene, transaction); - } -} - - -void RenderableZoneEntityItem::notifyBoundChanged() { - if (!render::Item::isValidID(_myMetaItem)) { +void RenderableZoneEntityItemMeta::setSkyboxURL(const QString& skyboxUrl) { + // nothing change if nothing change + if (_skyboxTextureURL == skyboxUrl) { return; } - render::Transaction transaction; - render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); - if (scene) { - transaction.updateItem(_myMetaItem, [](RenderableZoneEntityItemMeta& data) { - }); + _skyboxTextureURL = skyboxUrl; - scene->enqueueTransaction(transaction); + if (_skyboxTextureURL.isEmpty()) { + _validSkyboxTexture = false; + _pendingSkyboxTexture = false; + _skyboxTexture.clear(); + + editSkybox()->setCubemap(nullptr); } else { - qCWarning(entitiesrenderer) << "RenderableZoneEntityItem::notifyBoundChanged(), Unexpected null scene, possibly during application shutdown"; + _pendingSkyboxTexture = true; + auto textureCache = DependencyManager::get(); + _skyboxTexture = textureCache->getTexture(_skyboxTextureURL, image::TextureUsage::CUBE_TEXTURE); + } +} + +void RenderableZoneEntityItemMeta::updateSkyboxMap() { + if (_pendingSkyboxTexture) { + if (_skyboxTexture && _skyboxTexture->isLoaded()) { + _pendingSkyboxTexture = false; + + auto texture = _skyboxTexture->getGPUTexture(); + if (texture) { + editSkybox()->setCubemap(texture); + _validSkyboxTexture = true; + } else { + qCDebug(entitiesrenderer) << "Failed to load Skybox texture:" << _skyboxTexture->getURL(); + } + } + } +} + +void RenderableZoneEntityItemMeta::setBackgroundMode(BackgroundMode mode) { + _backgroundMode = mode; +} + +void RenderableZoneEntityItemMeta::setSkyboxColor(const glm::vec3& color) { + editSkybox()->setColor(color); +} + +void RenderableZoneEntityItemMeta::setProceduralUserData(QString userData) { + if (_proceduralUserData != userData) { + _proceduralUserData = userData; + std::dynamic_pointer_cast(editSkybox())->parse(_proceduralUserData); + } +} + + + +void RenderableZoneEntityItemMeta::render(RenderArgs* args) { + if (!_stage) { + _stage = DependencyManager::get()->getLightStage(); + } + + if (!_backgroundStage) { + _backgroundStage = DependencyManager::get()->getBackgroundStage(); + } + + { // Sun + // Need an update ? + if (_needSunUpdate) { + // Do we need to allocate the light in the stage ? + if (LightStage::isIndexInvalid(_sunIndex)) { + _sunIndex = _stage->addLight(_sunLight); + } else { + _stage->updateLightArrayBuffer(_sunIndex); + } + _needSunUpdate = false; + } + } + + { // Ambient + updateAmbientMap(); + + // Need an update ? + if (_needAmbientUpdate) { + // Do we need to allocate the light in the stage ? + if (LightStage::isIndexInvalid(_ambientIndex)) { + _ambientIndex = _stage->addLight(_ambientLight); + } else { + _stage->updateLightArrayBuffer(_ambientIndex); + } + _needAmbientUpdate = false; + } + } + + { // Skybox + updateSkyboxMap(); + + if (_needBackgroundUpdate) { + if (BackgroundStage::isIndexInvalid(_backgroundIndex)) { + _backgroundIndex = _backgroundStage->addBackground(_background); + } else { + + } + _needBackgroundUpdate = false; + } + } + + if (isVisible()) { + // FInally, push the light visible in the frame + // THe directional key light for sure + _stage->_currentFrame.pushSunLight(_sunIndex); + + // The ambient light only if it has a valid texture to render with + if (_validAmbientTexture || _validSkyboxTexture) { + _stage->_currentFrame.pushAmbientLight(_ambientIndex); + } + + // The background only if the mode is not inherit + if (_backgroundMode != BACKGROUND_MODE_INHERIT) { + _backgroundStage->_currentFrame.pushBackground(_backgroundIndex); + } } } diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.h b/libraries/entities-renderer/src/RenderableZoneEntityItem.h index 7241e34ce8..d0c7f64fbb 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.h +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.h @@ -16,6 +16,9 @@ #include class NetworkGeometry; +class KeyLightPayload; + +class RenderableZoneEntityItemMeta; class RenderableZoneEntityItem : public ZoneEntityItem { public: @@ -42,7 +45,7 @@ public: virtual void removeFromScene(EntityItemPointer self, const render::ScenePointer& scene, render::Transaction& transaction) override; render::ItemID getRenderItemID() const { return _myMetaItem; } - + private: virtual void locationChanged(bool tellPhysics = true) override { EntityItem::locationChanged(tellPhysics); notifyBoundChanged(); } virtual void dimensionsChanged() override { EntityItem::dimensionsChanged(); notifyBoundChanged(); } @@ -52,10 +55,18 @@ private: template void changeProperties(Lambda functor); - + + void notifyChangedRenderItem(); + void sceneUpdateRenderItemFromEntity(render::Transaction& transaction); + void updateKeyZoneItemFromEntity(RenderableZoneEntityItemMeta& keyZonePayload); + + void updateKeySunFromEntity(RenderableZoneEntityItemMeta& keyZonePayload); + void updateKeyAmbientFromEntity(RenderableZoneEntityItemMeta& keyZonePayload); + void updateKeyBackgroundFromEntity(RenderableZoneEntityItemMeta& keyZonePayload); + ModelPointer _model; bool _needsInitialSimulation; - + render::ItemID _myMetaItem{ render::Item::INVALID_ITEM_ID }; }; diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 431b87cc61..530e356137 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -681,7 +681,9 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef // and pretend that we own it (we assume we'll recover it soon) // However, for now, when the server uses a newer time than what we sent, listen to what we're told. - if (overwriteLocalData) weOwnSimulation = false; + if (overwriteLocalData) { + weOwnSimulation = false; + } } else if (_simulationOwner.set(newSimOwner)) { markDirtyFlags(Simulation::DIRTY_SIMULATOR_ID); somethingChanged = true; @@ -1293,27 +1295,15 @@ void EntityItem::getAllTerseUpdateProperties(EntityItemProperties& properties) c properties._accelerationChanged = true; } -void EntityItem::pokeSimulationOwnership() { - markDirtyFlags(Simulation::DIRTY_SIMULATION_OWNERSHIP_FOR_POKE); +void EntityItem::flagForOwnershipBid(uint8_t priority) { + markDirtyFlags(Simulation::DIRTY_SIMULATION_OWNERSHIP_PRIORITY); auto nodeList = DependencyManager::get(); if (_simulationOwner.matchesValidID(nodeList->getSessionUUID())) { // we already own it - _simulationOwner.promotePriority(SCRIPT_POKE_SIMULATION_PRIORITY); + _simulationOwner.promotePriority(priority); } else { // we don't own it yet - _simulationOwner.setPendingPriority(SCRIPT_POKE_SIMULATION_PRIORITY, usecTimestampNow()); - } -} - -void EntityItem::grabSimulationOwnership() { - markDirtyFlags(Simulation::DIRTY_SIMULATION_OWNERSHIP_FOR_GRAB); - auto nodeList = DependencyManager::get(); - if (_simulationOwner.matchesValidID(nodeList->getSessionUUID())) { - // we already own it - _simulationOwner.promotePriority(SCRIPT_GRAB_SIMULATION_PRIORITY); - } else { - // we don't own it yet - _simulationOwner.setPendingPriority(SCRIPT_GRAB_SIMULATION_PRIORITY, usecTimestampNow()); + _simulationOwner.setPendingPriority(priority, usecTimestampNow()); } } diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 3243f50556..7c08137a1c 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -321,6 +321,7 @@ public: void updateSimulationOwner(const SimulationOwner& owner); void clearSimulationOwnership(); void setPendingOwnershipPriority(quint8 priority, const quint64& timestamp); + uint8_t getPendingOwnershipPriority() const { return _simulationOwner.getPendingPriority(); } void rememberHasSimulationOwnershipBid() const; QString getMarketplaceID() const; @@ -394,8 +395,7 @@ public: void getAllTerseUpdateProperties(EntityItemProperties& properties) const; - void pokeSimulationOwnership(); - void grabSimulationOwnership(); + void flagForOwnershipBid(uint8_t priority); void flagForMotionStateChange() { _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; } QString actionsToDebugString(); diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index b184d648da..90dc6893b4 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -230,6 +230,8 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties } entity->setLastBroadcast(usecTimestampNow()); + // since we're creating this object we will immediately volunteer to own its simulation + entity->flagForOwnershipBid(VOLUNTEER_SIMULATION_PRIORITY); propertiesWithSimID.setLastEdited(entity->getLastEdited()); } else { qCDebug(entities) << "script failed to add new Entity to local Octree"; @@ -440,7 +442,7 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& } else { // we make a bid for simulation ownership properties.setSimulationOwner(myNodeID, SCRIPT_POKE_SIMULATION_PRIORITY); - entity->pokeSimulationOwnership(); + entity->flagForOwnershipBid(SCRIPT_POKE_SIMULATION_PRIORITY); entity->rememberHasSimulationOwnershipBid(); } } @@ -1194,7 +1196,7 @@ QUuid EntityScriptingInterface::addAction(const QString& actionTypeString, } action->setIsMine(true); success = entity->addAction(simulation, action); - entity->grabSimulationOwnership(); + entity->flagForOwnershipBid(SCRIPT_GRAB_SIMULATION_PRIORITY); return false; // Physics will cause a packet to be sent, so don't send from here. }); if (success) { @@ -1210,7 +1212,7 @@ bool EntityScriptingInterface::updateAction(const QUuid& entityID, const QUuid& return actionWorker(entityID, [&](EntitySimulationPointer simulation, EntityItemPointer entity) { bool success = entity->updateAction(simulation, actionID, arguments); if (success) { - entity->grabSimulationOwnership(); + entity->flagForOwnershipBid(SCRIPT_GRAB_SIMULATION_PRIORITY); } return success; }); @@ -1224,7 +1226,7 @@ bool EntityScriptingInterface::deleteAction(const QUuid& entityID, const QUuid& success = entity->removeAction(simulation, actionID); if (success) { // reduce from grab to poke - entity->pokeSimulationOwnership(); + entity->flagForOwnershipBid(SCRIPT_POKE_SIMULATION_PRIORITY); } return false; // Physics will cause a packet to be sent, so don't send from here. }); diff --git a/libraries/entities/src/SimulationFlags.h b/libraries/entities/src/SimulationFlags.h index dbcf51afc0..e2b2224b4a 100644 --- a/libraries/entities/src/SimulationFlags.h +++ b/libraries/entities/src/SimulationFlags.h @@ -26,12 +26,10 @@ namespace Simulation { const uint32_t DIRTY_MATERIAL = 0x00400; const uint32_t DIRTY_PHYSICS_ACTIVATION = 0x0800; // should activate object in physics engine const uint32_t DIRTY_SIMULATOR_ID = 0x1000; // the simulatorID has changed - const uint32_t DIRTY_SIMULATION_OWNERSHIP_FOR_POKE = 0x2000; // bid for simulation ownership at "poke" - const uint32_t DIRTY_SIMULATION_OWNERSHIP_FOR_GRAB = 0x4000; // bid for simulation ownership at "grab" + const uint32_t DIRTY_SIMULATION_OWNERSHIP_PRIORITY = 0x2000; // our own bid priority has changed const uint32_t DIRTY_TRANSFORM = DIRTY_POSITION | DIRTY_ROTATION; const uint32_t DIRTY_VELOCITIES = DIRTY_LINEAR_VELOCITY | DIRTY_ANGULAR_VELOCITY; - const uint32_t DIRTY_SIMULATION_OWNERSHIP_PRIORITY = DIRTY_SIMULATION_OWNERSHIP_FOR_POKE | DIRTY_SIMULATION_OWNERSHIP_FOR_GRAB; }; #endif // hifi_SimulationFlags_h diff --git a/libraries/entities/src/SimulationOwner.cpp b/libraries/entities/src/SimulationOwner.cpp index 77645a9e62..4398582673 100644 --- a/libraries/entities/src/SimulationOwner.cpp +++ b/libraries/entities/src/SimulationOwner.cpp @@ -26,9 +26,9 @@ const int SimulationOwner::NUM_BYTES_ENCODED = NUM_BYTES_RFC4122_UUID + 1; SimulationOwner::SimulationOwner() : _id(), _expiry(0), - _pendingTimestamp(0), + _pendingBidTimestamp(0), _priority(0), - _pendingPriority(0), + _pendingBidPriority(0), _pendingState(PENDING_STATE_NOTHING) { } @@ -36,9 +36,9 @@ SimulationOwner::SimulationOwner() : SimulationOwner::SimulationOwner(const QUuid& id, quint8 priority) : _id(id), _expiry(0), - _pendingTimestamp(0), + _pendingBidTimestamp(0), _priority(priority), - _pendingPriority(0) + _pendingBidPriority(0) { } @@ -61,9 +61,9 @@ bool SimulationOwner::fromByteArray(const QByteArray& data) { void SimulationOwner::clear() { _id = QUuid(); _expiry = 0; - _pendingTimestamp = 0; + _pendingBidTimestamp = 0; _priority = 0; - _pendingPriority = 0; + _pendingBidPriority = 0; _pendingState = PENDING_STATE_NOTHING; } @@ -102,9 +102,9 @@ bool SimulationOwner::set(const SimulationOwner& owner) { } void SimulationOwner::setPendingPriority(quint8 priority, const quint64& timestamp) { - _pendingPriority = priority; - _pendingTimestamp = timestamp; - _pendingState = (_pendingPriority == 0) ? PENDING_STATE_RELEASE : PENDING_STATE_TAKE; + _pendingBidPriority = priority; + _pendingBidTimestamp = timestamp; + _pendingState = (_pendingBidPriority == 0) ? PENDING_STATE_RELEASE : PENDING_STATE_TAKE; } void SimulationOwner::updateExpiry() { @@ -113,11 +113,11 @@ void SimulationOwner::updateExpiry() { } bool SimulationOwner::pendingRelease(const quint64& timestamp) { - return _pendingPriority == 0 && _pendingState == PENDING_STATE_RELEASE && _pendingTimestamp >= timestamp; + return _pendingBidPriority == 0 && _pendingState == PENDING_STATE_RELEASE && _pendingBidTimestamp >= timestamp; } bool SimulationOwner::pendingTake(const quint64& timestamp) { - return _pendingPriority > 0 && _pendingState == PENDING_STATE_TAKE && _pendingTimestamp >= timestamp; + return _pendingBidPriority > 0 && _pendingState == PENDING_STATE_TAKE && _pendingBidTimestamp >= timestamp; } void SimulationOwner::clearCurrentOwner() { diff --git a/libraries/entities/src/SimulationOwner.h b/libraries/entities/src/SimulationOwner.h index 5f940bbe25..94ed1d9a08 100644 --- a/libraries/entities/src/SimulationOwner.h +++ b/libraries/entities/src/SimulationOwner.h @@ -66,6 +66,7 @@ public: bool hasExpired() const { return usecTimestampNow() > _expiry; } + uint8_t getPendingPriority() const { return _pendingBidPriority; } bool pendingRelease(const quint64& timestamp); // return true if valid pending RELEASE bool pendingTake(const quint64& timestamp); // return true if valid pending TAKE void clearCurrentOwner(); @@ -84,9 +85,9 @@ public: private: QUuid _id; // owner quint64 _expiry; // time when ownership can transition at equal priority - quint64 _pendingTimestamp; // time when pending update was set + quint64 _pendingBidTimestamp; // time when pending bid was set quint8 _priority; // priority of current owner - quint8 _pendingPriority; // priority of pendingTake + quint8 _pendingBidPriority; // priority at which we'd like to own it quint8 _pendingState; // NOTHING, TAKE, or RELEASE }; diff --git a/libraries/entities/src/ZoneEntityItem.cpp b/libraries/entities/src/ZoneEntityItem.cpp index 1d73902e8d..074437b6b2 100644 --- a/libraries/entities/src/ZoneEntityItem.cpp +++ b/libraries/entities/src/ZoneEntityItem.cpp @@ -71,22 +71,6 @@ bool ZoneEntityItem::setProperties(const EntityItemProperties& properties) { bool somethingChanged = false; somethingChanged = EntityItem::setProperties(properties); // set the properties in our base class - bool somethingChangedInKeyLight = _keyLightProperties.setProperties(properties); - - bool somethingChangedInStage = _stageProperties.setProperties(properties); - - SET_ENTITY_PROPERTY_FROM_PROPERTIES(shapeType, setShapeType); - SET_ENTITY_PROPERTY_FROM_PROPERTIES(compoundShapeURL, setCompoundShapeURL); - SET_ENTITY_PROPERTY_FROM_PROPERTIES(backgroundMode, setBackgroundMode); - - SET_ENTITY_PROPERTY_FROM_PROPERTIES(flyingAllowed, setFlyingAllowed); - SET_ENTITY_PROPERTY_FROM_PROPERTIES(ghostingAllowed, setGhostingAllowed); - SET_ENTITY_PROPERTY_FROM_PROPERTIES(filterURL, setFilterURL); - - bool somethingChangedInSkybox = _skyboxProperties.setProperties(properties); - - somethingChanged = somethingChanged || somethingChangedInKeyLight || somethingChangedInStage || somethingChangedInSkybox; - if (somethingChanged) { bool wantDebug = false; if (wantDebug) { @@ -101,6 +85,30 @@ bool ZoneEntityItem::setProperties(const EntityItemProperties& properties) { return somethingChanged; } +bool ZoneEntityItem::setSubClassProperties(const EntityItemProperties& properties) { + bool somethingChanged = EntityItem::setSubClassProperties(properties); // set the properties in our base class + + + _keyLightPropertiesChanged = _keyLightProperties.setProperties(properties); + + _stagePropertiesChanged = _stageProperties.setProperties(properties); + + SET_ENTITY_PROPERTY_FROM_PROPERTIES(shapeType, setShapeType); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(compoundShapeURL, setCompoundShapeURL); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(backgroundMode, setBackgroundMode); + + SET_ENTITY_PROPERTY_FROM_PROPERTIES(flyingAllowed, setFlyingAllowed); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(ghostingAllowed, setGhostingAllowed); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(filterURL, setFilterURL); + + _skyboxPropertiesChanged = _skyboxProperties.setProperties(properties); + + somethingChanged = somethingChanged || _keyLightPropertiesChanged || _stagePropertiesChanged || _skyboxPropertiesChanged; + + + return somethingChanged; +} + int ZoneEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args, EntityPropertyFlags& propertyFlags, bool overwriteLocalData, @@ -109,14 +117,14 @@ int ZoneEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, const unsigned char* dataAt = data; int bytesFromKeylight = _keyLightProperties.readEntitySubclassDataFromBuffer(dataAt, (bytesLeftToRead - bytesRead), args, - propertyFlags, overwriteLocalData, somethingChanged); - + propertyFlags, overwriteLocalData, _keyLightPropertiesChanged); + somethingChanged = somethingChanged || _keyLightPropertiesChanged; bytesRead += bytesFromKeylight; dataAt += bytesFromKeylight; int bytesFromStage = _stageProperties.readEntitySubclassDataFromBuffer(dataAt, (bytesLeftToRead - bytesRead), args, - propertyFlags, overwriteLocalData, somethingChanged); - + propertyFlags, overwriteLocalData, _stagePropertiesChanged); + somethingChanged = somethingChanged || _stagePropertiesChanged; bytesRead += bytesFromStage; dataAt += bytesFromStage; @@ -125,7 +133,8 @@ int ZoneEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, READ_ENTITY_PROPERTY(PROP_BACKGROUND_MODE, BackgroundMode, setBackgroundMode); int bytesFromSkybox = _skyboxProperties.readEntitySubclassDataFromBuffer(dataAt, (bytesLeftToRead - bytesRead), args, - propertyFlags, overwriteLocalData, somethingChanged); + propertyFlags, overwriteLocalData, _skyboxPropertiesChanged); + somethingChanged = somethingChanged || _skyboxPropertiesChanged; bytesRead += bytesFromSkybox; dataAt += bytesFromSkybox; @@ -185,6 +194,16 @@ void ZoneEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBits APPEND_ENTITY_PROPERTY(PROP_FILTER_URL, getFilterURL()); } +void ZoneEntityItem::somethingChangedNotification() { + EntityItem::somethingChangedNotification(); + withWriteLock([&] { + _keyLightPropertiesChanged = false; + _backgroundPropertiesChanged = false; + _stagePropertiesChanged = false; + _skyboxPropertiesChanged = false; + }); +} + void ZoneEntityItem::debugDump() const { quint64 now = usecTimestampNow(); qCDebug(entities) << " ZoneEntityItem id:" << getEntityItemID() << "---------------------------------------------"; diff --git a/libraries/entities/src/ZoneEntityItem.h b/libraries/entities/src/ZoneEntityItem.h index c868da0954..bcb690f60e 100644 --- a/libraries/entities/src/ZoneEntityItem.h +++ b/libraries/entities/src/ZoneEntityItem.h @@ -29,10 +29,16 @@ public: // methods for getting/setting all properties of an entity virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const override; virtual bool setProperties(const EntityItemProperties& properties) override; + virtual bool setSubClassProperties(const EntityItemProperties& properties) override; // TODO: eventually only include properties changed since the params.nodeData->getLastTimeBagEmpty() time virtual EntityPropertyFlags getEntityProperties(EncodeBitstreamParams& params) const override; + /// Override this in your derived class if you'd like to be informed when something about the state of the entity + /// has changed. This will be called with properties change or when new data is loaded from a stream + /// Overriding this function to capture the information that a keylight / Ambient / skybox properties has changed + virtual void somethingChangedNotification() override; + virtual void appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, EntityTreeElementExtraEncodeDataPointer modelTreeElementExtraEncodeData, EntityPropertyFlags& requestedProperties, @@ -64,7 +70,7 @@ public: const KeyLightPropertyGroup& getKeyLightProperties() const { return _keyLightProperties; } - void setBackgroundMode(BackgroundMode value) { _backgroundMode = value; } + void setBackgroundMode(BackgroundMode value) { _backgroundMode = value; _backgroundPropertiesChanged = true; } BackgroundMode getBackgroundMode() const { return _backgroundMode; } const SkyboxPropertyGroup& getSkyboxProperties() const { return _skyboxProperties; } @@ -106,6 +112,14 @@ protected: bool _ghostingAllowed { DEFAULT_GHOSTING_ALLOWED }; QString _filterURL { DEFAULT_FILTER_URL }; + // Dirty flags turn true when either keylight properties is changing values. + // This gets back to false in the somethingChangedNotification() call + // Which is called after a setProperties() or a readEntitySubClassFromBUfferCall on the entity. + bool _keyLightPropertiesChanged { false }; + bool _backgroundPropertiesChanged { false }; + bool _skyboxPropertiesChanged { false }; + bool _stagePropertiesChanged { false }; + static bool _drawZoneBoundaries; static bool _zonesArePickable; }; diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 236daf6443..6d4f586c52 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -460,17 +460,11 @@ FBXLight extractLight(const FBXNode& object) { } QByteArray fileOnUrl(const QByteArray& filepath, const QString& url) { - QString path = QFileInfo(url).path(); - QByteArray filename = filepath; - QFileInfo checkFile(path + "/" + filepath); + // in order to match the behaviour when loading models from remote URLs + // we assume that all external textures are right beside the loaded model + // ignoring any relative paths or absolute paths inside of models - // check if the file exists at the RelativeFilename - if (!(checkFile.exists() && checkFile.isFile())) { - // if not, assume it is in the fbx directory - filename = filename.mid(filename.lastIndexOf('/') + 1); - } - - return filename; + return filepath.mid(filepath.lastIndexOf('/') + 1); } FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QString& url) { diff --git a/libraries/gl/CMakeLists.txt b/libraries/gl/CMakeLists.txt index 3e2097e89e..fd3197410b 100644 --- a/libraries/gl/CMakeLists.txt +++ b/libraries/gl/CMakeLists.txt @@ -1,6 +1,6 @@ set(TARGET_NAME gl) setup_hifi_library(OpenGL Qml Quick) -link_hifi_libraries(shared) +link_hifi_libraries(shared networking) target_opengl() diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index 97f120447f..9dcc1d7991 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -33,6 +33,9 @@ #include #include #include +#include +#include +#include #include "OffscreenGLCanvas.h" #include "GLHelpers.h" @@ -41,6 +44,7 @@ Q_LOGGING_CATEGORY(trace_render_qml, "trace.render.qml") Q_LOGGING_CATEGORY(trace_render_qml_gl, "trace.render.qml.gl") +Q_LOGGING_CATEGORY(offscreenFocus, "hifi.offscreen.focus") struct TextureSet { // The number of surfaces with this size @@ -254,8 +258,89 @@ QNetworkAccessManager* QmlNetworkAccessManagerFactory::create(QObject* parent) { return new QmlNetworkAccessManager(parent); } -Q_DECLARE_LOGGING_CATEGORY(offscreenFocus) -Q_LOGGING_CATEGORY(offscreenFocus, "hifi.offscreen.focus") +static QQmlEngine* globalEngine { nullptr }; +static size_t globalEngineRefCount { 0 }; + +QString getEventBridgeJavascript() { + // FIXME: Refactor with similar code in RenderableWebEntityItem + QString javaScriptToInject; + QFile webChannelFile(":qtwebchannel/qwebchannel.js"); + QFile createGlobalEventBridgeFile(PathUtils::resourcesPath() + "/html/createGlobalEventBridge.js"); + if (webChannelFile.open(QFile::ReadOnly | QFile::Text) && + createGlobalEventBridgeFile.open(QFile::ReadOnly | QFile::Text)) { + QString webChannelStr = QTextStream(&webChannelFile).readAll(); + QString createGlobalEventBridgeStr = QTextStream(&createGlobalEventBridgeFile).readAll(); + javaScriptToInject = webChannelStr + createGlobalEventBridgeStr; + } else { + qCWarning(glLogging) << "Unable to find qwebchannel.js or createGlobalEventBridge.js"; + } + return javaScriptToInject; +} + +class EventBridgeWrapper : public QObject { + Q_OBJECT + Q_PROPERTY(QObject* eventBridge READ getEventBridge CONSTANT); + +public: + EventBridgeWrapper(QObject* eventBridge, QObject* parent = nullptr) : QObject(parent), _eventBridge(eventBridge) { + } + + QObject* getEventBridge() { + return _eventBridge; + } + +private: + QObject* _eventBridge; +}; + + + +QQmlEngine* acquireEngine(QQuickWindow* window) { + Q_ASSERT(QThread::currentThread() == qApp->thread()); + if (!globalEngine) { + Q_ASSERT(0 == globalEngineRefCount); + globalEngine = new QQmlEngine(); + globalEngine->setNetworkAccessManagerFactory(new QmlNetworkAccessManagerFactory); + + auto importList = globalEngine->importPathList(); + importList.insert(importList.begin(), PathUtils::resourcesPath()); + globalEngine->setImportPathList(importList); + for (const auto& path : importList) { + qDebug() << path; + } + + if (!globalEngine->incubationController()) { + globalEngine->setIncubationController(window->incubationController()); + } + auto rootContext = globalEngine->rootContext(); + rootContext->setContextProperty("GL", ::getGLContextData()); + rootContext->setContextProperty("urlHandler", new UrlHandler()); + rootContext->setContextProperty("resourceDirectoryUrl", QUrl::fromLocalFile(PathUtils::resourcesPath())); + rootContext->setContextProperty("pathToFonts", "../../"); + rootContext->setContextProperty("ApplicationInterface", qApp); + auto javaScriptToInject = getEventBridgeJavascript(); + if (!javaScriptToInject.isEmpty()) { + rootContext->setContextProperty("eventBridgeJavaScriptToInject", QVariant(javaScriptToInject)); + } + rootContext->setContextProperty("FileTypeProfile", new FileTypeProfile(rootContext)); + rootContext->setContextProperty("HFWebEngineProfile", new HFWebEngineProfile(rootContext)); + rootContext->setContextProperty("HFTabletWebEngineProfile", new HFTabletWebEngineProfile(rootContext)); + + + } + + ++globalEngineRefCount; + return globalEngine; +} + +void releaseEngine() { + Q_ASSERT(QThread::currentThread() == qApp->thread()); + Q_ASSERT(0 != globalEngineRefCount); + if (0 == --globalEngineRefCount) { + globalEngine->deleteLater(); + globalEngine = nullptr; + } +} void OffscreenQmlSurface::cleanup() { _canvas->makeCurrent(); @@ -294,6 +379,7 @@ void OffscreenQmlSurface::render() { GLuint texture = offscreenTextures.getNextTexture(_size); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo); glFramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); _renderControl->render(); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); glBindTexture(GL_TEXTURE_2D, texture); @@ -361,9 +447,8 @@ OffscreenQmlSurface::~OffscreenQmlSurface() { _canvas->deleteLater(); _rootItem->deleteLater(); - _qmlComponent->deleteLater(); - _qmlEngine->deleteLater(); _quickWindow->deleteLater(); + releaseEngine(); } void OffscreenQmlSurface::onAboutToQuit() { @@ -375,6 +460,8 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { qCDebug(glLogging) << "Building QML surface"; _renderControl = new QMyQuickRenderControl(); + connect(_renderControl, &QQuickRenderControl::renderRequested, this, [this] { _render = true; }); + connect(_renderControl, &QQuickRenderControl::sceneChanged, this, [this] { _render = _polish = true; }); QQuickWindow::setDefaultAlphaBuffer(true); @@ -385,7 +472,7 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { // so we wait until after its ctor to move object/context to this thread. _quickWindow = new QQuickWindow(_renderControl); _quickWindow->setColor(QColor(255, 255, 255, 0)); - _quickWindow->setFlags(_quickWindow->flags() | static_cast(Qt::WA_TranslucentBackground)); + _quickWindow->setClearBeforeRendering(false); _renderControl->_renderWindow = _proxyWindow; @@ -398,32 +485,21 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { connect(_quickWindow, &QQuickWindow::focusObjectChanged, this, &OffscreenQmlSurface::onFocusObjectChanged); // Create a QML engine. - _qmlEngine = new QQmlEngine; + auto qmlEngine = acquireEngine(_quickWindow); + _qmlContext = new QQmlContext(qmlEngine->rootContext()); - _qmlEngine->setNetworkAccessManagerFactory(new QmlNetworkAccessManagerFactory); + _qmlContext->setContextProperty("offscreenWindow", QVariant::fromValue(getWindow())); + _qmlContext->setContextProperty("eventBridge", this); + _qmlContext->setContextProperty("webEntity", this); - auto importList = _qmlEngine->importPathList(); - importList.insert(importList.begin(), PathUtils::resourcesPath()); - _qmlEngine->setImportPathList(importList); - if (!_qmlEngine->incubationController()) { - _qmlEngine->setIncubationController(_quickWindow->incubationController()); - } - - // FIXME - _glData = ::getGLContextData(); // Initialize JSON structure so that it can be filled in later and then used in QML. - _qmlEngine->rootContext()->setContextProperty("GL", _glData); - _qmlEngine->rootContext()->setContextProperty("offscreenWindow", QVariant::fromValue(getWindow())); - _qmlComponent = new QQmlComponent(_qmlEngine); - - - connect(_renderControl, &QQuickRenderControl::renderRequested, this, [this] { _render = true; }); - connect(_renderControl, &QQuickRenderControl::sceneChanged, this, [this] { _render = _polish = true; }); + // FIXME Compatibility mechanism for existing HTML and JS that uses eventBridgeWrapper + // Find a way to flag older scripts using this mechanism and wanr that this is deprecated + _qmlContext->setContextProperty("eventBridgeWrapper", new EventBridgeWrapper(this, _qmlContext)); if (!_canvas->makeCurrent()) { qWarning("Failed to make context current for QML Renderer"); return; } - _glData = ::getGLContextData(); _renderControl->initialize(_canvas->getContext()); // When Quick says there is a need to render, we will not render immediately. Instead, @@ -433,11 +509,6 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { _updateTimer.setTimerType(Qt::PreciseTimer); _updateTimer.setInterval(MIN_TIMER_MS); // 5ms, Qt::PreciseTimer required _updateTimer.start(); - - auto rootContext = getRootContext(); - rootContext->setContextProperty("urlHandler", new UrlHandler()); - rootContext->setContextProperty("resourceDirectoryUrl", QUrl::fromLocalFile(PathUtils::resourcesPath())); - rootContext->setContextProperty("pathToFonts", "../../"); } static uvec2 clampSize(const uvec2& size, uint32_t maxDimension) { @@ -460,7 +531,7 @@ void OffscreenQmlSurface::resize(const QSize& newSize_, bool forceResize) { return; } - _qmlEngine->rootContext()->setContextProperty("surfaceSize", newSize); + _qmlContext->setContextProperty("surfaceSize", newSize); if (_rootItem) { _rootItem->setSize(newSize); @@ -520,73 +591,82 @@ QQuickItem* OffscreenQmlSurface::getRootItem() { } void OffscreenQmlSurface::setBaseUrl(const QUrl& baseUrl) { - _qmlEngine->setBaseUrl(baseUrl); + _qmlContext->setBaseUrl(baseUrl); } -QObject* OffscreenQmlSurface::load(const QUrl& qmlSource, std::function f) { +QObject* OffscreenQmlSurface::load(const QUrl& qmlSource, bool createNewContext, std::function f) { // Synchronous loading may take a while; restart the deadlock timer QMetaObject::invokeMethod(qApp, "updateHeartbeat", Qt::DirectConnection); - qDebug() << qmlSource; - _qmlComponent->loadUrl(qmlSource, QQmlComponent::PreferSynchronous); + QQmlContext* targetContext = _qmlContext; + if (_rootItem && createNewContext) { + targetContext = new QQmlContext(targetContext); + } - if (_qmlComponent->isLoading()) { - connect(_qmlComponent, &QQmlComponent::statusChanged, this, - [this, f](QQmlComponent::Status){ - finishQmlLoad(f); - }); + QUrl finalQmlSource = qmlSource; + if ((qmlSource.isRelative() && !qmlSource.isEmpty()) || qmlSource.scheme() == QLatin1String("file")) { + finalQmlSource = _qmlContext->resolvedUrl(qmlSource); + } + + auto qmlComponent = new QQmlComponent(_qmlContext->engine(), finalQmlSource, QQmlComponent::PreferSynchronous); + if (qmlComponent->isLoading()) { + connect(qmlComponent, &QQmlComponent::statusChanged, this, + [this, qmlComponent, targetContext, f](QQmlComponent::Status) { + finishQmlLoad(qmlComponent, targetContext, f); + }); return nullptr; } - return finishQmlLoad(f); + return finishQmlLoad(qmlComponent, targetContext, f); +} + +QObject* OffscreenQmlSurface::loadInNewContext(const QUrl& qmlSource, std::function f) { + return load(qmlSource, true, f); +} + +QObject* OffscreenQmlSurface::load(const QUrl& qmlSource, std::function f) { + return load(qmlSource, false, f); } void OffscreenQmlSurface::clearCache() { - getRootContext()->engine()->clearComponentCache(); + _qmlContext->engine()->clearComponentCache(); } -QObject* OffscreenQmlSurface::finishQmlLoad(std::function f) { - disconnect(_qmlComponent, &QQmlComponent::statusChanged, this, 0); - if (_qmlComponent->isError()) { - QList errorList = _qmlComponent->errors(); - foreach(const QQmlError& error, errorList) { - qWarning() << error.url() << error.line() << error; +QObject* OffscreenQmlSurface::finishQmlLoad(QQmlComponent* qmlComponent, QQmlContext* qmlContext, std::function f) { + disconnect(qmlComponent, &QQmlComponent::statusChanged, this, 0); + if (qmlComponent->isError()) { + for (const auto& error : qmlComponent->errors()) { + qCWarning(glLogging) << error.url() << error.line() << error; } + qmlComponent->deleteLater(); return nullptr; } - // FIXME: Refactor with similar code in RenderableWebEntityItem - QString javaScriptToInject; - QFile webChannelFile(":qtwebchannel/qwebchannel.js"); - QFile createGlobalEventBridgeFile(PathUtils::resourcesPath() + "/html/createGlobalEventBridge.js"); - if (webChannelFile.open(QFile::ReadOnly | QFile::Text) && - createGlobalEventBridgeFile.open(QFile::ReadOnly | QFile::Text)) { - QString webChannelStr = QTextStream(&webChannelFile).readAll(); - QString createGlobalEventBridgeStr = QTextStream(&createGlobalEventBridgeFile).readAll(); - javaScriptToInject = webChannelStr + createGlobalEventBridgeStr; - } else { - qCWarning(glLogging) << "Unable to find qwebchannel.js or createGlobalEventBridge.js"; - } - QQmlContext* newContext = new QQmlContext(_qmlEngine, qApp); - QObject* newObject = _qmlComponent->beginCreate(newContext); - if (_qmlComponent->isError()) { - QList errorList = _qmlComponent->errors(); - foreach(const QQmlError& error, errorList) + QObject* newObject = qmlComponent->beginCreate(qmlContext); + if (qmlComponent->isError()) { + for (const auto& error : qmlComponent->errors()) { qCWarning(glLogging) << error.url() << error.line() << error; + } if (!_rootItem) { qFatal("Unable to finish loading QML root"); } + qmlComponent->deleteLater(); return nullptr; } - _qmlEngine->setObjectOwnership(this, QQmlEngine::CppOwnership); - newObject->setProperty("eventBridge", QVariant::fromValue(this)); + qmlContext->engine()->setObjectOwnership(this, QQmlEngine::CppOwnership); + f(qmlContext, newObject); - newContext->setContextProperty("eventBridgeJavaScriptToInject", QVariant(javaScriptToInject)); + QObject* eventBridge = qmlContext->contextProperty("eventBridge").value(); + if (qmlContext != _qmlContext && eventBridge && eventBridge != this) { + // FIXME Compatibility mechanism for existing HTML and JS that uses eventBridgeWrapper + // Find a way to flag older scripts using this mechanism and wanr that this is deprecated + qmlContext->setContextProperty("eventBridgeWrapper", new EventBridgeWrapper(eventBridge, qmlContext)); + } - f(newContext, newObject); - _qmlComponent->completeCreate(); + qmlComponent->completeCreate(); + qmlComponent->deleteLater(); // All quick items should be focusable @@ -736,7 +816,7 @@ bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* even mouseEvent->screenPos(), mouseEvent->button(), mouseEvent->buttons(), mouseEvent->modifiers()); if (event->type() == QEvent::MouseMove) { - _qmlEngine->rootContext()->setContextProperty("lastMousePosition", transformedPos); + _qmlContext->setContextProperty("lastMousePosition", transformedPos); } mappedEvent.ignore(); if (QCoreApplication::sendEvent(_quickWindow, &mappedEvent)) { @@ -763,9 +843,6 @@ void OffscreenQmlSurface::resume() { if (getRootItem()) { getRootItem()->setProperty("eventBridge", QVariant::fromValue(this)); } - if (getRootContext()) { - getRootContext()->setContextProperty("webEntity", this); - } } bool OffscreenQmlSurface::isPaused() const { @@ -791,8 +868,8 @@ QSize OffscreenQmlSurface::size() const { return _quickWindow->geometry().size(); } -QQmlContext* OffscreenQmlSurface::getRootContext() { - return _qmlEngine->rootContext(); +QQmlContext* OffscreenQmlSurface::getSurfaceContext() { + return _qmlContext; } Q_DECLARE_METATYPE(std::function); diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.h b/libraries/gl/src/gl/OffscreenQmlSurface.h index efd35fce8b..ae81ae48b4 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.h +++ b/libraries/gl/src/gl/OffscreenQmlSurface.h @@ -48,6 +48,8 @@ public: void resize(const QSize& size, bool forceResize = false); QSize size() const; + Q_INVOKABLE QObject* load(const QUrl& qmlSource, bool createNewContext, std::function f = [](QQmlContext*, QObject*) {}); + Q_INVOKABLE QObject* loadInNewContext(const QUrl& qmlSource, std::function f = [](QQmlContext*, QObject*) {}); Q_INVOKABLE QObject* load(const QUrl& qmlSource, std::function f = [](QQmlContext*, QObject*) {}); Q_INVOKABLE QObject* load(const QString& qmlSourceFile, std::function f = [](QQmlContext*, QObject*) {}) { return load(QUrl(qmlSourceFile), f); @@ -73,7 +75,7 @@ public: QQuickItem* getRootItem(); QQuickWindow* getWindow(); QObject* getEventHandler(); - QQmlContext* getRootContext(); + QQmlContext* getSurfaceContext(); QPointF mapToVirtualScreen(const QPointF& originalPoint, QObject* originalWidget); bool eventFilter(QObject* originalDestination, QEvent* event) override; @@ -118,7 +120,7 @@ protected: void setFocusText(bool newFocusText); private: - QObject* finishQmlLoad(std::function f); + QObject* finishQmlLoad(QQmlComponent* qmlComponent, QQmlContext* qmlContext, std::function f); QPointF mapWindowToUi(const QPointF& sourcePosition, QObject* sourceObject); void setupFbo(); bool allowNewFrame(uint8_t fps); @@ -133,11 +135,9 @@ private slots: private: QQuickWindow* _quickWindow { nullptr }; QMyQuickRenderControl* _renderControl{ nullptr }; - QQmlEngine* _qmlEngine { nullptr }; - QQmlComponent* _qmlComponent { nullptr }; + QQmlContext* _qmlContext { nullptr }; QQuickItem* _rootItem { nullptr }; OffscreenGLCanvas* _canvas { nullptr }; - QJsonObject _glData; QTimer _updateTimer; uint32_t _fbo { 0 }; diff --git a/libraries/gl/src/gl/OffscreenQmlSurfaceCache.cpp b/libraries/gl/src/gl/OffscreenQmlSurfaceCache.cpp index ad370a1f43..c047738e77 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurfaceCache.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurfaceCache.cpp @@ -49,7 +49,6 @@ QSharedPointer OffscreenQmlSurfaceCache::buildSurface(const surface->create(currentContext); surface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/")); surface->load(rootSource); - surface->getRootContext()->setContextProperty("ApplicationInterface", qApp); surface->resize(QSize(100, 100)); currentContext->makeCurrent(currentSurface); return surface; diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendOutput.cpp index 1e6691538b..5c6a18d7af 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendOutput.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendOutput.cpp @@ -63,11 +63,17 @@ void GLBackend::do_clearFramebuffer(const Batch& batch, size_t paramOffset) { int useScissor = batch._params[paramOffset + 0]._int; GLuint glmask = 0; + bool restoreStencilMask = false; + uint8_t cacheStencilMask = 0xFF; if (masks & Framebuffer::BUFFER_STENCIL) { glClearStencil(stencil); glmask |= GL_STENCIL_BUFFER_BIT; - // TODO: we will probably need to also check the write mask of stencil like we do - // for depth buffer, but as would say a famous Fez owner "We'll cross that bridge when we come to it" + + cacheStencilMask = _pipeline._stateCache.stencilActivation.getWriteMaskFront(); + if (cacheStencilMask != 0xFF) { + restoreStencilMask = true; + glStencilMask( 0xFF); + } } bool restoreDepthMask = false; @@ -121,6 +127,11 @@ void GLBackend::do_clearFramebuffer(const Batch& batch, size_t paramOffset) { glDisable(GL_SCISSOR_TEST); } + // Restore Stencil write mask + if (restoreStencilMask) { + glStencilMask(cacheStencilMask); + } + // Restore write mask meaning turn back off if (restoreDepthMask) { glDepthMask(GL_FALSE); diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp b/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp index 26ce56b387..ef9b6c4297 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp @@ -211,6 +211,7 @@ GLenum GLTexelFormat::evalGLTexelFormatInternal(const gpu::Element& dstFormat) { break; case gpu::NUINT32: case gpu::NINT32: + case gpu::COMPRESSED: case gpu::NUM_TYPES: // quiet compiler Q_UNREACHABLE(); } @@ -484,6 +485,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E texel.internalFormat = GL_R8_SNORM; break; } + case gpu::COMPRESSED: case gpu::NUM_TYPES: { // quiet compiler Q_UNREACHABLE(); } @@ -527,6 +529,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E texel.internalFormat = GL_DEPTH_COMPONENT24; break; } + case gpu::COMPRESSED: case gpu::NUM_TYPES: { // quiet compiler Q_UNREACHABLE(); } @@ -641,6 +644,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E break; case gpu::NUINT32: case gpu::NINT32: + case gpu::COMPRESSED: case gpu::NUM_TYPES: // quiet compiler Q_UNREACHABLE(); } diff --git a/libraries/gpu/src/gpu/DrawTransformVertexPosition.slv b/libraries/gpu/src/gpu/DrawTransformVertexPosition.slv new file mode 100644 index 0000000000..cf66a615f5 --- /dev/null +++ b/libraries/gpu/src/gpu/DrawTransformVertexPosition.slv @@ -0,0 +1,28 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// Draw and transform the fed vertex position with the standard MVP stack +// Output the clip position +// +// Created by Sam Gateau on 5/30/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 gpu/Transform.slh@> + +<$declareStandardTransform()$> + +layout(location = 0) in vec4 inPosition; + +out vec3 varWorldPos; + +void main(void) { + // standard transform + TransformCamera cam = getTransformCamera(); + TransformObject obj = getTransformObject(); + <$transformModelToClipPos(cam, obj, inPosition, gl_Position)$> +} diff --git a/libraries/render-utils/src/hit_effect.slv b/libraries/gpu/src/gpu/DrawVertexPosition.slv similarity index 51% rename from libraries/render-utils/src/hit_effect.slv rename to libraries/gpu/src/gpu/DrawVertexPosition.slv index b9b0638f9e..b12280d577 100644 --- a/libraries/render-utils/src/hit_effect.slv +++ b/libraries/gpu/src/gpu/DrawVertexPosition.slv @@ -2,25 +2,18 @@ <$VERSION_HEADER$> // Generated on <$_SCRIBE_DATE$> // -// hit_effect.vert -// vertex shader +// Draw the fed vertex position, pass straight as clip pos +// Output the clip position // -// Created by Eric Levin on 7/20/15. -// Copyright 2015 High Fidelity, Inc. +// Created by Sam Gateau on 5/30/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 gpu/Inputs.slh@> - -<@include gpu/Transform.slh@> - -<$declareStandardTransform()$> - -out vec2 varQuadPosition; +layout(location = 0) in vec4 inPosition; void main(void) { - varQuadPosition = inPosition.xy; gl_Position = inPosition; -} \ No newline at end of file +} diff --git a/libraries/render-utils/src/drawOpaqueStencil.slf b/libraries/gpu/src/gpu/DrawWhite.slf similarity index 63% rename from libraries/render-utils/src/drawOpaqueStencil.slf rename to libraries/gpu/src/gpu/DrawWhite.slf index 43e9c5c27a..bdecc0c5c5 100644 --- a/libraries/render-utils/src/drawOpaqueStencil.slf +++ b/libraries/gpu/src/gpu/DrawWhite.slf @@ -2,15 +2,17 @@ <$VERSION_HEADER$> // Generated on <$_SCRIBE_DATE$> // -// drawOpaqueStencil.frag -// fragment shader +// Draw white // -// Created by Sam Gateau on 9/29/15. -// Copyright 2015 High Fidelity, Inc. +// Created by Sam Gateau on 5/30/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 // +out vec4 outFragColor; + void main(void) { + outFragColor = vec4(1.0); } diff --git a/libraries/gpu/src/gpu/StandardShaderLib.cpp b/libraries/gpu/src/gpu/StandardShaderLib.cpp index 81500347fd..756070ff68 100755 --- a/libraries/gpu/src/gpu/StandardShaderLib.cpp +++ b/libraries/gpu/src/gpu/StandardShaderLib.cpp @@ -16,6 +16,12 @@ #include "DrawTransformUnitQuad_vert.h" #include "DrawTexcoordRectTransformUnitQuad_vert.h" #include "DrawViewportQuadTransformTexcoord_vert.h" +#include "DrawVertexPosition_vert.h" +#include "DrawTransformVertexPosition_vert.h" + +const char DrawNada_frag[] = "void main(void) {}"; // DrawNada is really simple... + +#include "DrawWhite_frag.h" #include "DrawTexture_frag.h" #include "DrawTextureOpaque_frag.h" #include "DrawColoredTexture_frag.h" @@ -26,6 +32,10 @@ ShaderPointer StandardShaderLib::_drawUnitQuadTexcoordVS; ShaderPointer StandardShaderLib::_drawTransformUnitQuadVS; ShaderPointer StandardShaderLib::_drawTexcoordRectTransformUnitQuadVS; ShaderPointer StandardShaderLib::_drawViewportQuadTransformTexcoordVS; +ShaderPointer StandardShaderLib::_drawVertexPositionVS; +ShaderPointer StandardShaderLib::_drawTransformVertexPositionVS; +ShaderPointer StandardShaderLib::_drawNadaPS; +ShaderPointer StandardShaderLib::_drawWhitePS; ShaderPointer StandardShaderLib::_drawTexturePS; ShaderPointer StandardShaderLib::_drawTextureOpaquePS; ShaderPointer StandardShaderLib::_drawColoredTexturePS; @@ -85,6 +95,34 @@ ShaderPointer StandardShaderLib::getDrawViewportQuadTransformTexcoordVS() { return _drawViewportQuadTransformTexcoordVS; } +ShaderPointer StandardShaderLib::getDrawVertexPositionVS() { + if (!_drawVertexPositionVS) { + _drawVertexPositionVS = gpu::Shader::createVertex(std::string(DrawVertexPosition_vert)); + } + return _drawVertexPositionVS; +} + +ShaderPointer StandardShaderLib::getDrawTransformVertexPositionVS() { + if (!_drawTransformVertexPositionVS) { + _drawTransformVertexPositionVS = gpu::Shader::createVertex(std::string(DrawTransformVertexPosition_vert)); + } + return _drawTransformVertexPositionVS; +} + +ShaderPointer StandardShaderLib::getDrawNadaPS() { + if (!_drawNadaPS) { + _drawNadaPS = gpu::Shader::createPixel(std::string(DrawNada_frag)); + } + return _drawNadaPS; +} + +ShaderPointer StandardShaderLib::getDrawWhitePS() { + if (!_drawWhitePS) { + _drawWhitePS = gpu::Shader::createPixel(std::string(DrawWhite_frag)); + } + return _drawWhitePS; +} + ShaderPointer StandardShaderLib::getDrawTexturePS() { if (!_drawTexturePS) { _drawTexturePS = gpu::Shader::createPixel(std::string(DrawTexture_frag)); @@ -99,8 +137,6 @@ ShaderPointer StandardShaderLib::getDrawTextureOpaquePS() { return _drawTextureOpaquePS; } - - ShaderPointer StandardShaderLib::getDrawColoredTexturePS() { if (!_drawColoredTexturePS) { _drawColoredTexturePS = gpu::Shader::createPixel(std::string(DrawColoredTexture_frag)); diff --git a/libraries/gpu/src/gpu/StandardShaderLib.h b/libraries/gpu/src/gpu/StandardShaderLib.h index 12ea9045c2..a21d4dea9a 100755 --- a/libraries/gpu/src/gpu/StandardShaderLib.h +++ b/libraries/gpu/src/gpu/StandardShaderLib.h @@ -37,6 +37,15 @@ public: // Shader draws the unit quad in the full viewport clipPos = ([(-1,-1),(1,1)]) and transform the texcoord = [(0,0),(1,1)] by the model transform. static ShaderPointer getDrawViewportQuadTransformTexcoordVS(); + // Shader draw the fed vertex position and transform it by the full model transform stack (Model, View, Proj). + // simply output the world pos and the clip pos to the next stage + static ShaderPointer getDrawVertexPositionVS(); + static ShaderPointer getDrawTransformVertexPositionVS(); + + // PShader does nothing, no really nothing, but still needed for defining a program triggering rasterization + static ShaderPointer getDrawNadaPS(); + + static ShaderPointer getDrawWhitePS(); static ShaderPointer getDrawTexturePS(); static ShaderPointer getDrawTextureOpaquePS(); static ShaderPointer getDrawColoredTexturePS(); @@ -51,6 +60,12 @@ protected: static ShaderPointer _drawTransformUnitQuadVS; static ShaderPointer _drawTexcoordRectTransformUnitQuadVS; static ShaderPointer _drawViewportQuadTransformTexcoordVS; + + static ShaderPointer _drawVertexPositionVS; + static ShaderPointer _drawTransformVertexPositionVS; + + static ShaderPointer _drawNadaPS; + static ShaderPointer _drawWhitePS; static ShaderPointer _drawTexturePS; static ShaderPointer _drawTextureOpaquePS; static ShaderPointer _drawColoredTexturePS; diff --git a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.h b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.h index 3570ec7193..b94acb8b71 100644 --- a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.h +++ b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.h @@ -70,8 +70,6 @@ public: bool isSupported() const override { return true; } const QString getName() const override { return NAME; } - bool isHandController() const override { return false; } - void pluginFocusOutEvent() override { _inputDevice->focusOutEvent(); } void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h index 65e771e8f0..02dcbe4664 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h +++ b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h @@ -39,8 +39,6 @@ public: virtual bool isSupported() const override; virtual const QString getName() const override { return NAME; } - bool isHandController() const override { return false; } - virtual void pluginFocusOutEvent() override { _inputDevice->focusOutEvent(); } virtual void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; diff --git a/libraries/ktx/src/khronos/KHR.h b/libraries/ktx/src/khronos/KHR.h index a98f2cc0d4..98cc1a4736 100644 --- a/libraries/ktx/src/khronos/KHR.h +++ b/libraries/ktx/src/khronos/KHR.h @@ -211,6 +211,8 @@ namespace khronos { template inline uint32_t evalAlignedCompressedBlockCount(uint32_t value) { + enum { val = ALIGNMENT && !(ALIGNMENT & (ALIGNMENT - 1)) }; + static_assert(val, "template parameter ALIGNMENT must be a power of 2"); // FIXME add static assert that ALIGNMENT is a power of 2 static uint32_t ALIGNMENT_REMAINDER = ALIGNMENT - 1; return (value + ALIGNMENT_REMAINDER) / ALIGNMENT; diff --git a/libraries/ktx/src/ktx/Writer.cpp b/libraries/ktx/src/ktx/Writer.cpp index c94856e598..6d6cfa81a2 100644 --- a/libraries/ktx/src/ktx/Writer.cpp +++ b/libraries/ktx/src/ktx/Writer.cpp @@ -229,7 +229,7 @@ namespace ktx { } else { Image::FaceBytes faceBytes(NUM_CUBEMAPFACES); auto faceSize = srcImages[l]._faceSize; - for (int face = 0; face < NUM_CUBEMAPFACES; face++) { + for (uint32_t face = 0; face < NUM_CUBEMAPFACES; face++) { memcpy(currentPtr, srcImages[l]._faceBytes[face], faceSize); faceBytes[face] = currentPtr; currentPtr += faceSize; diff --git a/libraries/model-networking/src/model-networking/KTXCache.cpp b/libraries/model-networking/src/model-networking/KTXCache.cpp index e0447af8e6..6443748920 100644 --- a/libraries/model-networking/src/model-networking/KTXCache.cpp +++ b/libraries/model-networking/src/model-networking/KTXCache.cpp @@ -11,14 +11,28 @@ #include "KTXCache.h" +#include #include using File = cache::File; using FilePointer = cache::FilePointer; +// Whenever a change is made to the serialized format for the KTX cache that isn't backward compatible, +// this value should be incremented. This will force the KTX cache to be wiped +const int KTXCache::CURRENT_VERSION = 0x01; +const int KTXCache::INVALID_VERSION = 0x00; +const char* KTXCache::SETTING_VERSION_NAME = "hifi.ktx.cache_version"; + KTXCache::KTXCache(const std::string& dir, const std::string& ext) : FileCache(dir, ext) { initialize(); + + Setting::Handle cacheVersionHandle(SETTING_VERSION_NAME, INVALID_VERSION); + auto cacheVersion = cacheVersionHandle.get(); + if (cacheVersion != CURRENT_VERSION) { + wipe(); + cacheVersionHandle.set(CURRENT_VERSION); + } } KTXFilePointer KTXCache::writeFile(const char* data, Metadata&& metadata) { diff --git a/libraries/model-networking/src/model-networking/KTXCache.h b/libraries/model-networking/src/model-networking/KTXCache.h index bbf7ceadea..5617019c52 100644 --- a/libraries/model-networking/src/model-networking/KTXCache.h +++ b/libraries/model-networking/src/model-networking/KTXCache.h @@ -27,6 +27,12 @@ class KTXCache : public cache::FileCache { Q_OBJECT public: + // Whenever a change is made to the serialized format for the KTX cache that isn't backward compatible, + // this value should be incremented. This will force the KTX cache to be wiped + static const int CURRENT_VERSION; + static const int INVALID_VERSION; + static const char* SETTING_VERSION_NAME; + KTXCache(const std::string& dir, const std::string& ext); KTXFilePointer writeFile(const char* data, Metadata&& metadata); diff --git a/libraries/model/src/model/Geometry.cpp b/libraries/model/src/model/Geometry.cpp index 16608ab63e..f88c8233ea 100755 --- a/libraries/model/src/model/Geometry.cpp +++ b/libraries/model/src/model/Geometry.cpp @@ -241,6 +241,42 @@ void Mesh::forEach(std::function vertexFunc, } } +MeshPointer Mesh::createIndexedTriangles_P3F(uint32_t numVertices, uint32_t numIndices, const glm::vec3* vertices, const uint32_t* indices) { + MeshPointer mesh; + if (numVertices == 0) { return mesh; } + if (numIndices < 3) { return mesh; } + + mesh = std::make_shared(); + + // Vertex buffer + mesh->setVertexBuffer(gpu::BufferView(new gpu::Buffer(numVertices * sizeof(glm::vec3), (gpu::Byte*) vertices), gpu::Element::VEC3F_XYZ)); + + // trim down the indices to shorts if possible + if (numIndices < std::numeric_limits::max()) { + Indices16 shortIndicesVector; + int16_t* shortIndices = nullptr; + if (indices) { + shortIndicesVector.resize(numIndices); + for (uint32_t i = 0; i < numIndices; i++) { + shortIndicesVector[i] = indices[i]; + } + shortIndices = shortIndicesVector.data(); + } + + mesh->setIndexBuffer(gpu::BufferView(new gpu::Buffer(numIndices * sizeof(uint16_t), (gpu::Byte*) shortIndices), gpu::Element::INDEX_UINT16)); + } else { + + mesh->setIndexBuffer(gpu::BufferView(new gpu::Buffer(numIndices * sizeof(uint32_t), (gpu::Byte*) indices), gpu::Element::INDEX_INT32)); + } + + + std::vector parts; + parts.push_back(model::Mesh::Part(0, numIndices, 0, model::Mesh::TRIANGLES)); + mesh->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(model::Mesh::Part), (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL)); + + return mesh; +} + Geometry::Geometry() { } @@ -256,3 +292,5 @@ Geometry::~Geometry() { void Geometry::setMesh(const MeshPointer& mesh) { _mesh = mesh; } + + diff --git a/libraries/model/src/model/Geometry.h b/libraries/model/src/model/Geometry.h index 7ba3e83407..2375944f04 100755 --- a/libraries/model/src/model/Geometry.h +++ b/libraries/model/src/model/Geometry.h @@ -65,6 +65,9 @@ public: const gpu::BufferStream& getVertexStream() const { return _vertexStream; } // Index Buffer + using Indices16 = std::vector; + using Indices32 = std::vector; + void setIndexBuffer(const BufferView& buffer); const BufferView& getIndexBuffer() const { return _indexBuffer; } size_t getNumIndices() const { return _indexBuffer.getNumElements(); } @@ -127,6 +130,9 @@ public: std::function normalFunc, std::function indexFunc); + + static MeshPointer createIndexedTriangles_P3F(uint32_t numVertices, uint32_t numTriangles, const glm::vec3* vertices = nullptr, const uint32_t* indices = nullptr); + protected: gpu::Stream::FormatPointer _vertexFormat; diff --git a/libraries/model/src/model/Light.cpp b/libraries/model/src/model/Light.cpp index 11b13606b8..19da084f84 100755 --- a/libraries/model/src/model/Light.cpp +++ b/libraries/model/src/model/Light.cpp @@ -145,7 +145,7 @@ void Light::setAmbientSpherePreset(gpu::SphericalHarmonics::Preset preset) { _ambientSchemaBuffer.edit().ambientSphere.assignPreset(preset); } -void Light::setAmbientMap(gpu::TexturePointer ambientMap) { +void Light::setAmbientMap(const gpu::TexturePointer& ambientMap) { _ambientMap = ambientMap; if (ambientMap) { setAmbientMapNumMips(_ambientMap->getNumMips()); diff --git a/libraries/model/src/model/Light.h b/libraries/model/src/model/Light.h index 947474bbfd..4c82eb5d77 100755 --- a/libraries/model/src/model/Light.h +++ b/libraries/model/src/model/Light.h @@ -68,7 +68,8 @@ public: enum Type { - SUN = 0, + AMBIENT = 0, + SUN, POINT, SPOT, @@ -112,7 +113,6 @@ public: void setIntensity(float intensity); bool isRanged() const { return (getType() == POINT) || (getType() == SPOT); } - bool hasAmbient() const { return (getType() == SUN); } // FalloffRradius is the physical radius of the light sphere through which energy shines, // expressed in meters. It is used only to calculate the falloff curve of the light. @@ -143,7 +143,7 @@ public: const gpu::SphericalHarmonics& getAmbientSphere() const { return _ambientSchemaBuffer->ambientSphere; } void setAmbientSpherePreset(gpu::SphericalHarmonics::Preset preset); - void setAmbientMap(gpu::TexturePointer ambientMap); + void setAmbientMap(const gpu::TexturePointer& ambientMap); gpu::TexturePointer getAmbientMap() const { return _ambientMap; } void setAmbientMapNumMips(uint16_t numMips); diff --git a/libraries/model/src/model/Skybox.cpp b/libraries/model/src/model/Skybox.cpp index 4901a3c61b..d327593573 100755 --- a/libraries/model/src/model/Skybox.cpp +++ b/libraries/model/src/model/Skybox.cpp @@ -97,7 +97,7 @@ void Skybox::render(gpu::Batch& batch, const ViewFrustum& viewFrustum, const Sky } auto skyState = std::make_shared(); - skyState->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); + skyState->setStencilTest(true, 0xFF, gpu::State::StencilTest(1, 0xFF, gpu::EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); thePipeline = gpu::Pipeline::create(skyShader, skyState); } diff --git a/libraries/model/src/model/Skybox.h b/libraries/model/src/model/Skybox.h index d7d95fbe9e..90896fd8c6 100755 --- a/libraries/model/src/model/Skybox.h +++ b/libraries/model/src/model/Skybox.h @@ -25,6 +25,8 @@ typedef glm::vec3 Color; class Skybox { public: + typedef gpu::BufferView UniformBufferView; + Skybox(); Skybox& operator= (const Skybox& skybox); virtual ~Skybox() {}; @@ -43,6 +45,8 @@ public: static void render(gpu::Batch& batch, const ViewFrustum& frustum, const Skybox& skybox); + const UniformBufferView& getSchemaBuffer() const { return _schemaBuffer; } + protected: static const int SKYBOX_SKYMAP_SLOT { 0 }; static const int SKYBOX_CONSTANTS_SLOT { 0 }; diff --git a/libraries/networking/CMakeLists.txt b/libraries/networking/CMakeLists.txt index 288e98d5a5..ff9a05b959 100644 --- a/libraries/networking/CMakeLists.txt +++ b/libraries/networking/CMakeLists.txt @@ -1,5 +1,5 @@ set(TARGET_NAME networking) -setup_hifi_library(Network) +setup_hifi_library(Network WebEngine) link_hifi_libraries(shared) target_include_directories(${TARGET_NAME} PRIVATE "${CMAKE_BINARY_DIR}/includes") diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 6266ad0f89..fa6b49597d 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -45,7 +45,6 @@ Q_DECLARE_METATYPE(QNetworkAccessManager::Operation) Q_DECLARE_METATYPE(JSONCallbackParameters) const QString ACCOUNTS_GROUP = "accounts"; -static const auto METAVERSE_SESSION_ID_HEADER = QString("HFM-SessionID").toLocal8Bit(); JSONCallbackParameters::JSONCallbackParameters(QObject* jsonCallbackReceiver, const QString& jsonCallbackMethod, QObject* errorCallbackReceiver, const QString& errorCallbackMethod, @@ -201,6 +200,13 @@ void AccountManager::setAuthURL(const QUrl& authURL) { } } +void AccountManager::setSessionID(const QUuid& sessionID) { + if (_sessionID != sessionID) { + qCDebug(networking) << "Metaverse session ID changed to" << uuidStringWithoutCurlyBraces(sessionID); + _sessionID = sessionID; + } +} + void AccountManager::sendRequest(const QString& path, AccountManagerAuth::Type authType, QNetworkAccessManager::Operation operation, diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index dd2216957f..b37846ec1b 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -52,6 +52,7 @@ namespace AccountManagerAuth { Q_DECLARE_METATYPE(AccountManagerAuth::Type); const QByteArray ACCESS_TOKEN_AUTHORIZATION_HEADER = "Authorization"; +const auto METAVERSE_SESSION_ID_HEADER = QString("HFM-SessionID").toLocal8Bit(); using UserAgentGetter = std::function; @@ -90,7 +91,7 @@ public: static QJsonObject dataObjectFromResponse(QNetworkReply& requestReply); QUuid getSessionID() const { return _sessionID; } - void setSessionID(const QUuid& sessionID) { _sessionID = sessionID; } + void setSessionID(const QUuid& sessionID); void setTemporaryDomain(const QUuid& domainID, const QString& key); const QString& getTemporaryDomainKey(const QUuid& domainID) { return _accountInfo.getTemporaryDomainKey(domainID); } diff --git a/libraries/networking/src/AssetRequest.cpp b/libraries/networking/src/AssetRequest.cpp index 920c7ae036..00fa3d9f2f 100644 --- a/libraries/networking/src/AssetRequest.cpp +++ b/libraries/networking/src/AssetRequest.cpp @@ -15,11 +15,13 @@ #include +#include +#include + #include "AssetClient.h" #include "NetworkLogging.h" #include "NodeList.h" #include "ResourceCache.h" -#include static int requestID = 0; @@ -62,9 +64,12 @@ void AssetRequest::start() { _data = loadFromCache(getUrl()); if (!_data.isNull()) { _error = NoError; - + + _loadedFromCache = true; + _state = Finished; emit finished(this); + return; } diff --git a/libraries/networking/src/AssetRequest.h b/libraries/networking/src/AssetRequest.h index b808ae0ca6..df5cf80ecd 100644 --- a/libraries/networking/src/AssetRequest.h +++ b/libraries/networking/src/AssetRequest.h @@ -52,6 +52,8 @@ public: QUrl getUrl() const { return ::getATPUrl(_hash); } QString getHash() const { return _hash; } + bool loadedFromCache() const { return _loadedFromCache; } + signals: void finished(AssetRequest* thisRequest); void progress(qint64 totalReceived, qint64 total); @@ -66,6 +68,7 @@ private: int _numPendingRequests { 0 }; MessageID _assetRequestID { INVALID_MESSAGE_ID }; const ByteRange _byteRange; + bool _loadedFromCache { false }; }; #endif diff --git a/libraries/networking/src/AssetResourceRequest.cpp b/libraries/networking/src/AssetResourceRequest.cpp index a4d5d66923..f4a3b20fd5 100644 --- a/libraries/networking/src/AssetResourceRequest.cpp +++ b/libraries/networking/src/AssetResourceRequest.cpp @@ -13,12 +13,14 @@ #include +#include +#include +#include + #include "AssetClient.h" #include "AssetUtils.h" #include "MappingRequest.h" #include "NetworkLogging.h" -#include -#include static const int DOWNLOAD_PROGRESS_LOG_INTERVAL_SECONDS = 5; @@ -48,6 +50,8 @@ bool AssetResourceRequest::urlIsAssetHash(const QUrl& url) { } void AssetResourceRequest::doSend() { + DependencyManager::get()->incrementStat(STAT_ATP_REQUEST_STARTED); + // We'll either have a hash or an ATP path to a file (that maps to a hash) if (urlIsAssetHash(_url)) { // We've detected that this is a hash - simply use AssetClient to request that asset @@ -65,11 +69,16 @@ void AssetResourceRequest::doSend() { } void AssetResourceRequest::requestMappingForPath(const AssetPath& path) { + auto statTracker = DependencyManager::get(); + statTracker->incrementStat(STAT_ATP_MAPPING_REQUEST_STARTED); + auto assetClient = DependencyManager::get(); _assetMappingRequest = assetClient->createGetMappingRequest(path); // make sure we'll hear about the result of the get mapping request connect(_assetMappingRequest, &GetMappingRequest::finished, this, [this, path](GetMappingRequest* request){ + auto statTracker = DependencyManager::get(); + Q_ASSERT(_state == InProgress); Q_ASSERT(request == _assetMappingRequest); @@ -80,6 +89,8 @@ void AssetResourceRequest::requestMappingForPath(const AssetPath& path) { requestHash(request->getHash()); + statTracker->incrementStat(STAT_ATP_MAPPING_REQUEST_SUCCESS); + break; default: { switch (request->getError()) { @@ -100,6 +111,9 @@ void AssetResourceRequest::requestMappingForPath(const AssetPath& path) { _state = Finished; emit finished(); + statTracker->incrementStat(STAT_ATP_MAPPING_REQUEST_FAILED); + statTracker->incrementStat(STAT_ATP_REQUEST_FAILED); + break; } } @@ -140,10 +154,26 @@ void AssetResourceRequest::requestHash(const AssetHash& hash) { _result = Error; break; } - + + auto statTracker = DependencyManager::get(); + + if (_assetRequest->loadedFromCache()) { + _loadedFromCache = true; + } + _state = Finished; emit finished(); + if (_result == Success) { + statTracker->incrementStat(STAT_ATP_REQUEST_SUCCESS); + + if (loadedFromCache()) { + statTracker->incrementStat(STAT_ATP_REQUEST_CACHE); + } + } else { + statTracker->incrementStat(STAT_ATP_REQUEST_FAILED); + } + _assetRequest->deleteLater(); _assetRequest = nullptr; }); diff --git a/libraries/networking/src/FileCache.cpp b/libraries/networking/src/FileCache.cpp index 8f3509d8f3..95304e3866 100644 --- a/libraries/networking/src/FileCache.cpp +++ b/libraries/networking/src/FileCache.cpp @@ -11,52 +11,81 @@ #include "FileCache.h" -#include -#include -#include -#include -#include -#include +#include +#include +#include + +#include +#include +#include +#include #include +#include +#ifdef Q_OS_WIN +#include +#else +#include +#endif + +#ifdef NDEBUG Q_LOGGING_CATEGORY(file_cache, "hifi.file_cache", QtWarningMsg) - +#else +Q_LOGGING_CATEGORY(file_cache, "hifi.file_cache") +#endif using namespace cache; -static const std::string MANIFEST_NAME = "manifest"; +static const char DIR_SEP = '/'; +static const char EXT_SEP = '.'; -static const size_t BYTES_PER_MEGABYTES = 1024 * 1024; -static const size_t BYTES_PER_GIGABYTES = 1024 * BYTES_PER_MEGABYTES; -const size_t FileCache::DEFAULT_UNUSED_MAX_SIZE = 5 * BYTES_PER_GIGABYTES; // 5GB -const size_t FileCache::MAX_UNUSED_MAX_SIZE = 100 * BYTES_PER_GIGABYTES; // 100GB -const size_t FileCache::DEFAULT_OFFLINE_MAX_SIZE = 2 * BYTES_PER_GIGABYTES; // 2GB +const size_t FileCache::DEFAULT_MAX_SIZE { GB_TO_BYTES(5) }; +const size_t FileCache::MAX_MAX_SIZE { GB_TO_BYTES(100) }; +const size_t FileCache::DEFAULT_MIN_FREE_STORAGE_SPACE { GB_TO_BYTES(1) }; -void FileCache::setUnusedFileCacheSize(size_t unusedFilesMaxSize) { - _unusedFilesMaxSize = std::min(unusedFilesMaxSize, MAX_UNUSED_MAX_SIZE); - reserve(0); + +std::string getCacheName(const std::string& dirname_str) { + QString dirname { dirname_str.c_str() }; + QDir dir(dirname); + if (!dir.isAbsolute()) { + return dirname_str; + } + return dir.dirName().toStdString(); +} + +std::string getCachePath(const std::string& dirname_str) { + QString dirname { dirname_str.c_str() }; + QDir dir(dirname); + if (dir.isAbsolute()) { + return dirname_str; + } + return PathUtils::getAppLocalDataFilePath(dirname).toStdString(); +} + +void FileCache::setMinFreeSize(size_t size) { + _minFreeSpaceSize = size; + clean(); emit dirty(); } -void FileCache::setOfflineFileCacheSize(size_t offlineFilesMaxSize) { - _offlineFilesMaxSize = std::min(offlineFilesMaxSize, MAX_UNUSED_MAX_SIZE); +void FileCache::setMaxSize(size_t maxSize) { + _maxSize = std::min(maxSize, MAX_MAX_SIZE); + clean(); + emit dirty(); } FileCache::FileCache(const std::string& dirname, const std::string& ext, QObject* parent) : QObject(parent), _ext(ext), - _dirname(dirname), - _dirpath(PathUtils::getAppLocalDataFilePath(dirname.c_str()).toStdString()) {} + _dirname(getCacheName(dirname)), + _dirpath(getCachePath(dirname)) { +} FileCache::~FileCache() { clear(); } -void fileDeleter(File* file) { - file->deleter(); -} - void FileCache::initialize() { QDir dir(_dirpath.c_str()); @@ -84,7 +113,7 @@ void FileCache::initialize() { } FilePointer FileCache::addFile(Metadata&& metadata, const std::string& filepath) { - FilePointer file(createFile(std::move(metadata), filepath).release(), &fileDeleter); + FilePointer file(createFile(std::move(metadata), filepath).release(), std::mem_fn(&File::deleter)); if (file) { _numTotalFiles += 1; _totalFilesSize += file->getLength(); @@ -141,6 +170,7 @@ FilePointer FileCache::getFile(const Key& key) { if (it != _files.cend()) { file = it->second.lock(); if (file) { + file->touch(); // if it exists, it is active - remove it from the cache removeUnusedFile(file); qCDebug(file_cache, "[%s] Found %s", _dirname.c_str(), key.c_str()); @@ -155,82 +185,127 @@ FilePointer FileCache::getFile(const Key& key) { } std::string FileCache::getFilepath(const Key& key) { - return _dirpath + '/' + key + '.' + _ext; + return _dirpath + DIR_SEP + key + EXT_SEP + _ext; } -void FileCache::addUnusedFile(const FilePointer file) { +void FileCache::addUnusedFile(const FilePointer& file) { { Lock lock(_filesMutex); _files[file->getKey()] = file; } - reserve(file->getLength()); - file->_LRUKey = ++_lastLRUKey; - { Lock lock(_unusedFilesMutex); - _unusedFiles.insert({ file->_LRUKey, file }); + _unusedFiles.insert(file); _numUnusedFiles += 1; _unusedFilesSize += file->getLength(); } + clean(); emit dirty(); } -void FileCache::removeUnusedFile(const FilePointer file) { +void FileCache::removeUnusedFile(const FilePointer& file) { Lock lock(_unusedFilesMutex); - const auto it = _unusedFiles.find(file->_LRUKey); - if (it != _unusedFiles.cend()) { - _unusedFiles.erase(it); + if (_unusedFiles.erase(file)) { _numUnusedFiles -= 1; _unusedFilesSize -= file->getLength(); } } -void FileCache::reserve(size_t length) { - Lock unusedLock(_unusedFilesMutex); - while (!_unusedFiles.empty() && - _unusedFilesSize + length > _unusedFilesMaxSize) { - auto it = _unusedFiles.begin(); - auto file = it->second; - auto length = file->getLength(); +size_t FileCache::getOverbudgetAmount() const { + size_t result = 0; - unusedLock.unlock(); - { - file->_cache = nullptr; - Lock lock(_filesMutex); - _files.erase(file->getKey()); + size_t currentFreeSpace = QStorageInfo(_dirpath.c_str()).bytesFree(); + if (_minFreeSpaceSize > currentFreeSpace) { + result = _minFreeSpaceSize - currentFreeSpace; + } + + if (_totalFilesSize > _maxSize) { + result = std::max(_totalFilesSize - _maxSize, result); + } + + return result; +} + +namespace cache { + struct FilePointerComparator { + bool operator()(const FilePointer& a, const FilePointer& b) { + return a->_modified > b->_modified; } - unusedLock.lock(); + }; +} - _unusedFiles.erase(it); - _numTotalFiles -= 1; - _numUnusedFiles -= 1; - _totalFilesSize -= length; - _unusedFilesSize -= length; +void FileCache::eject(const FilePointer& file) { + file->_cache = nullptr; + const auto& length = file->getLength(); + const auto& key = file->getKey(); + + { + Lock lock(_filesMutex); + if (0 != _files.erase(key)) { + _numTotalFiles -= 1; + _totalFilesSize -= length; + } + } + + { + Lock unusedLock(_unusedFilesMutex); + if (0 != _unusedFiles.erase(file)) { + _numUnusedFiles -= 1; + _unusedFilesSize -= length; + } + } +} + +void FileCache::clean() { + size_t overbudgetAmount = getOverbudgetAmount(); + + // Avoid sorting the unused files by LRU if we're not over budget / under free space + if (0 == overbudgetAmount) { + return; + } + + Lock unusedLock(_unusedFilesMutex); + using Queue = std::priority_queue, FilePointerComparator>; + Queue queue; + for (const auto& file : _unusedFiles) { + queue.push(file); + } + + while (!queue.empty() && overbudgetAmount > 0) { + auto file = queue.top(); + queue.pop(); + eject(file); + auto length = file->getLength(); + overbudgetAmount -= std::min(length, overbudgetAmount); + } +} + +void FileCache::wipe() { + Lock unusedFilesLock(_unusedFilesMutex); + while (!_unusedFiles.empty()) { + eject(*_unusedFiles.begin()); } } void FileCache::clear() { - Lock unusedFilesLock(_unusedFilesMutex); - for (const auto& pair : _unusedFiles) { - auto& file = pair.second; - file->_cache = nullptr; + // Eliminate any overbudget files + clean(); - if (_totalFilesSize > _offlineFilesMaxSize) { - _totalFilesSize -= file->getLength(); - } else { - file->_shouldPersist = true; - qCDebug(file_cache, "[%s] Persisting %s", _dirname.c_str(), file->getKey().c_str()); - } + // Mark everything remaining as persisted + Lock unusedFilesLock(_unusedFilesMutex); + for (auto& file : _unusedFiles) { + file->_shouldPersist = true; + file->_cache = nullptr; + qCDebug(file_cache, "[%s] Persisting %s", _dirname.c_str(), file->getKey().c_str()); } _unusedFiles.clear(); } void File::deleter() { if (_cache) { - FilePointer self(this, &fileDeleter); - _cache->addUnusedFile(self); + _cache->addUnusedFile(FilePointer(this, std::mem_fn(&File::deleter))); } else { deleteLater(); } @@ -239,7 +314,9 @@ void File::deleter() { File::File(Metadata&& metadata, const std::string& filepath) : _key(std::move(metadata.key)), _length(metadata.length), - _filepath(filepath) {} + _filepath(filepath), + _modified(QFileInfo(_filepath.c_str()).lastRead().toMSecsSinceEpoch()) { +} File::~File() { QFile file(getFilepath().c_str()); @@ -248,3 +325,8 @@ File::~File() { file.remove(); } } + +void File::touch() { + utime(_filepath.c_str(), nullptr); + _modified = std::max(QFileInfo(_filepath.c_str()).lastRead().toMSecsSinceEpoch(), _modified); +} \ No newline at end of file diff --git a/libraries/networking/src/FileCache.h b/libraries/networking/src/FileCache.h index 908ddcd285..f29d75f779 100644 --- a/libraries/networking/src/FileCache.h +++ b/libraries/networking/src/FileCache.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -35,24 +36,31 @@ class FileCache : public QObject { Q_PROPERTY(size_t sizeTotal READ getSizeTotalFiles NOTIFY dirty) Q_PROPERTY(size_t sizeCached READ getSizeCachedFiles NOTIFY dirty) - static const size_t DEFAULT_UNUSED_MAX_SIZE; - static const size_t MAX_UNUSED_MAX_SIZE; - static const size_t DEFAULT_OFFLINE_MAX_SIZE; + static const size_t DEFAULT_MAX_SIZE; + static const size_t MAX_MAX_SIZE; + static const size_t DEFAULT_MIN_FREE_STORAGE_SPACE; public: + // You can initialize the FileCache with a directory name (ex.: "temp_jpgs") that will be relative to the application local data, OR with a full path + // The file cache will ignore any file that doesn't match the extension provided + FileCache(const std::string& dirname, const std::string& ext, QObject* parent = nullptr); + virtual ~FileCache(); + + // Remove all unlocked items from the cache + void wipe(); + size_t getNumTotalFiles() const { return _numTotalFiles; } size_t getNumCachedFiles() const { return _numUnusedFiles; } size_t getSizeTotalFiles() const { return _totalFilesSize; } size_t getSizeCachedFiles() const { return _unusedFilesSize; } - void setUnusedFileCacheSize(size_t unusedFilesMaxSize); - size_t getUnusedFileCacheSize() const { return _unusedFilesSize; } + // Set the maximum amount of disk space to use on disk + void setMaxSize(size_t maxCacheSize); - void setOfflineFileCacheSize(size_t offlineFilesMaxSize); - - // initialize FileCache with a directory name (not a path, ex.: "temp_jpgs") and an ext (ex.: "jpg") - FileCache(const std::string& dirname, const std::string& ext, QObject* parent = nullptr); - virtual ~FileCache(); + // Set the minumum amount of free disk space to retain. This supercedes the max size, + // so if the cache is consuming all but 500 MB of the drive, unused entries will be ejected + // to free up more space, regardless of the cache max size + void setMinFreeSize(size_t size); using Key = std::string; struct Metadata { @@ -76,10 +84,11 @@ public: signals: void dirty(); -protected: +public: /// must be called after construction to create the cache on the fs and restore persisted files void initialize(); + // Add file to the cache and return the cache entry. FilePointer writeFile(const char* data, Metadata&& metadata, bool overwrite = false); FilePointer getFile(const Key& key); @@ -89,17 +98,28 @@ protected: private: using Mutex = std::recursive_mutex; using Lock = std::unique_lock; + using Map = std::unordered_map>; + using Set = std::unordered_set; + using KeySet = std::unordered_set; friend class File; std::string getFilepath(const Key& key); FilePointer addFile(Metadata&& metadata, const std::string& filepath); - void addUnusedFile(const FilePointer file); - void removeUnusedFile(const FilePointer file); - void reserve(size_t length); + void addUnusedFile(const FilePointer& file); + void removeUnusedFile(const FilePointer& file); + void clean(); void clear(); + // Remove a file from the cache + void eject(const FilePointer& file); + size_t getOverbudgetAmount() const; + + // FIXME it might be desirable to have the min free space variable be static so it can be + // shared among multiple instances of FileCache + std::atomic _minFreeSpaceSize { DEFAULT_MIN_FREE_STORAGE_SPACE }; + std::atomic _maxSize { DEFAULT_MAX_SIZE }; std::atomic _numTotalFiles { 0 }; std::atomic _numUnusedFiles { 0 }; std::atomic _totalFilesSize { 0 }; @@ -110,15 +130,11 @@ private: std::string _dirpath; bool _initialized { false }; - std::unordered_map> _files; + Map _files; Mutex _filesMutex; - std::map _unusedFiles; + Set _unusedFiles; Mutex _unusedFilesMutex; - size_t _unusedFilesMaxSize { DEFAULT_UNUSED_MAX_SIZE }; - int _lastLRUKey { 0 }; - - size_t _offlineFilesMaxSize { DEFAULT_OFFLINE_MAX_SIZE }; }; class File : public QObject { @@ -128,8 +144,8 @@ public: using Key = FileCache::Key; using Metadata = FileCache::Metadata; - Key getKey() const { return _key; } - size_t getLength() const { return _length; } + const Key& getKey() const { return _key; } + const size_t& getLength() const { return _length; } std::string getFilepath() const { return _filepath; } virtual ~File(); @@ -142,13 +158,15 @@ protected: private: friend class FileCache; + friend struct FilePointerComparator; const Key _key; const size_t _length; const std::string _filepath; - FileCache* _cache; - int _LRUKey { 0 }; + void touch(); + FileCache* _cache { nullptr }; + int64_t _modified { 0 }; bool _shouldPersist { false }; }; diff --git a/libraries/networking/src/FileResourceRequest.cpp b/libraries/networking/src/FileResourceRequest.cpp index 1e549e5fa3..d0e2721679 100644 --- a/libraries/networking/src/FileResourceRequest.cpp +++ b/libraries/networking/src/FileResourceRequest.cpp @@ -15,7 +15,12 @@ #include +#include + void FileResourceRequest::doSend() { + auto statTracker = DependencyManager::get(); + statTracker->incrementStat(STAT_FILE_REQUEST_STARTED); + QString filename = _url.toLocalFile(); // sometimes on windows, we see the toLocalFile() return null, @@ -60,4 +65,10 @@ void FileResourceRequest::doSend() { _state = Finished; emit finished(); + + if (_result == ResourceRequest::Success) { + statTracker->incrementStat(STAT_FILE_REQUEST_SUCCESS); + } else { + statTracker->incrementStat(STAT_FILE_REQUEST_FAILED); + } } diff --git a/interface/src/networking/FileTypeProfile.cpp b/libraries/networking/src/FileTypeProfile.cpp similarity index 100% rename from interface/src/networking/FileTypeProfile.cpp rename to libraries/networking/src/FileTypeProfile.cpp diff --git a/interface/src/networking/FileTypeProfile.h b/libraries/networking/src/FileTypeProfile.h similarity index 100% rename from interface/src/networking/FileTypeProfile.h rename to libraries/networking/src/FileTypeProfile.h diff --git a/interface/src/networking/FileTypeRequestInterceptor.cpp b/libraries/networking/src/FileTypeRequestInterceptor.cpp similarity index 100% rename from interface/src/networking/FileTypeRequestInterceptor.cpp rename to libraries/networking/src/FileTypeRequestInterceptor.cpp diff --git a/interface/src/networking/FileTypeRequestInterceptor.h b/libraries/networking/src/FileTypeRequestInterceptor.h similarity index 100% rename from interface/src/networking/FileTypeRequestInterceptor.h rename to libraries/networking/src/FileTypeRequestInterceptor.h diff --git a/interface/src/networking/HFTabletWebEngineProfile.cpp b/libraries/networking/src/HFTabletWebEngineProfile.cpp similarity index 95% rename from interface/src/networking/HFTabletWebEngineProfile.cpp rename to libraries/networking/src/HFTabletWebEngineProfile.cpp index 46634299bb..a3e3906497 100644 --- a/interface/src/networking/HFTabletWebEngineProfile.cpp +++ b/libraries/networking/src/HFTabletWebEngineProfile.cpp @@ -19,6 +19,7 @@ HFTabletWebEngineProfile::HFTabletWebEngineProfile(QObject* parent) : QQuickWebE static const QString WEB_ENGINE_USER_AGENT = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Mobile Safari/537.36"; setHttpUserAgent(WEB_ENGINE_USER_AGENT); + setStorageName(QML_WEB_ENGINE_NAME); auto requestInterceptor = new HFTabletWebEngineRequestInterceptor(this); setRequestInterceptor(requestInterceptor); diff --git a/interface/src/networking/HFTabletWebEngineProfile.h b/libraries/networking/src/HFTabletWebEngineProfile.h similarity index 100% rename from interface/src/networking/HFTabletWebEngineProfile.h rename to libraries/networking/src/HFTabletWebEngineProfile.h diff --git a/interface/src/networking/HFTabletWebEngineRequestInterceptor.cpp b/libraries/networking/src/HFTabletWebEngineRequestInterceptor.cpp similarity index 98% rename from interface/src/networking/HFTabletWebEngineRequestInterceptor.cpp rename to libraries/networking/src/HFTabletWebEngineRequestInterceptor.cpp index fd79fc1cb6..6ee8589615 100644 --- a/interface/src/networking/HFTabletWebEngineRequestInterceptor.cpp +++ b/libraries/networking/src/HFTabletWebEngineRequestInterceptor.cpp @@ -11,7 +11,7 @@ #include "HFTabletWebEngineRequestInterceptor.h" #include -#include +#include "AccountManager.h" bool isTabletAuthableHighFidelityURL(const QUrl& url) { static const QStringList HF_HOSTS = { diff --git a/interface/src/networking/HFTabletWebEngineRequestInterceptor.h b/libraries/networking/src/HFTabletWebEngineRequestInterceptor.h similarity index 100% rename from interface/src/networking/HFTabletWebEngineRequestInterceptor.h rename to libraries/networking/src/HFTabletWebEngineRequestInterceptor.h diff --git a/interface/src/networking/HFWebEngineProfile.cpp b/libraries/networking/src/HFWebEngineProfile.cpp similarity index 94% rename from interface/src/networking/HFWebEngineProfile.cpp rename to libraries/networking/src/HFWebEngineProfile.cpp index 6b377fa900..a69d4d653b 100644 --- a/interface/src/networking/HFWebEngineProfile.cpp +++ b/libraries/networking/src/HFWebEngineProfile.cpp @@ -20,6 +20,7 @@ HFWebEngineProfile::HFWebEngineProfile(QObject* parent) : { static const QString WEB_ENGINE_USER_AGENT = "Chrome/48.0 (HighFidelityInterface)"; setHttpUserAgent(WEB_ENGINE_USER_AGENT); + setStorageName(QML_WEB_ENGINE_STORAGE_NAME); // we use the HFWebEngineRequestInterceptor to make sure that web requests are authenticated for the interface user auto requestInterceptor = new HFWebEngineRequestInterceptor(this); diff --git a/interface/src/networking/HFWebEngineProfile.h b/libraries/networking/src/HFWebEngineProfile.h similarity index 100% rename from interface/src/networking/HFWebEngineProfile.h rename to libraries/networking/src/HFWebEngineProfile.h diff --git a/interface/src/networking/HFWebEngineRequestInterceptor.cpp b/libraries/networking/src/HFWebEngineRequestInterceptor.cpp similarity index 95% rename from interface/src/networking/HFWebEngineRequestInterceptor.cpp rename to libraries/networking/src/HFWebEngineRequestInterceptor.cpp index 59897d427f..eaf0de7245 100644 --- a/interface/src/networking/HFWebEngineRequestInterceptor.cpp +++ b/libraries/networking/src/HFWebEngineRequestInterceptor.cpp @@ -13,7 +13,7 @@ #include -#include +#include "AccountManager.h" #include "RequestFilters.h" void HFWebEngineRequestInterceptor::interceptRequest(QWebEngineUrlRequestInfo& info) { diff --git a/interface/src/networking/HFWebEngineRequestInterceptor.h b/libraries/networking/src/HFWebEngineRequestInterceptor.h similarity index 100% rename from interface/src/networking/HFWebEngineRequestInterceptor.h rename to libraries/networking/src/HFWebEngineRequestInterceptor.h diff --git a/libraries/networking/src/HTTPResourceRequest.cpp b/libraries/networking/src/HTTPResourceRequest.cpp index c6a4b93e51..266ea429a0 100644 --- a/libraries/networking/src/HTTPResourceRequest.cpp +++ b/libraries/networking/src/HTTPResourceRequest.cpp @@ -17,6 +17,7 @@ #include #include +#include #include "NetworkAccessManager.h" #include "NetworkLogging.h" @@ -49,6 +50,8 @@ void HTTPResourceRequest::cleanupTimer() { } void HTTPResourceRequest::doSend() { + DependencyManager::get()->incrementStat(STAT_HTTP_REQUEST_STARTED); + QNetworkRequest networkRequest(_url); networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); @@ -178,6 +181,17 @@ void HTTPResourceRequest::onRequestFinished() { _state = Finished; emit finished(); + + auto statTracker = DependencyManager::get(); + if (_result == Success) { + statTracker->incrementStat(STAT_HTTP_REQUEST_SUCCESS); + + if (loadedFromCache()) { + statTracker->incrementStat(STAT_HTTP_REQUEST_CACHE); + } + } else { + statTracker->incrementStat(STAT_HTTP_REQUEST_FAILED); + } } void HTTPResourceRequest::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal) { @@ -202,4 +216,6 @@ void HTTPResourceRequest::onTimeout() { _result = Timeout; _state = Finished; emit finished(); + + DependencyManager::get()->incrementStat(STAT_HTTP_REQUEST_FAILED); } diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 8feb695c79..93ae941f1e 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -22,13 +22,12 @@ #include #include -#include - #include #include #include #include #include +#include #include #include "AccountManager.h" @@ -37,7 +36,6 @@ #include "HifiSockAddr.h" #include "NetworkLogging.h" #include "udt/Packet.h" -#include static Setting::Handle LIMITED_NODELIST_LOCAL_PORT("LimitedNodeList.LocalPort", 0); @@ -1044,10 +1042,10 @@ void LimitedNodeList::setLocalSocket(const HifiSockAddr& sockAddr) { qCInfo(networking) << "Local socket is" << sockAddr; } else { qCInfo(networking) << "Local socket has changed from" << _localSockAddr << "to" << sockAddr; + DependencyManager::get()->incrementStat(LOCAL_SOCKET_CHANGE_STAT); } _localSockAddr = sockAddr; - emit localSockAddrChanged(_localSockAddr); } } diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index 3eb898463a..554386f786 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -31,7 +31,7 @@ #include #include -#include +#include #include #include @@ -66,9 +66,10 @@ const QHostAddress DEFAULT_ASSIGNMENT_CLIENT_MONITOR_HOSTNAME = QHostAddress::Lo const QString USERNAME_UUID_REPLACEMENT_STATS_KEY = "$username"; -using namespace tbb; +const QString LOCAL_SOCKET_CHANGE_STAT = "LocalSocketChanges"; + typedef std::pair UUIDNodePair; -typedef concurrent_unordered_map NodeHash; +typedef tbb::concurrent_unordered_map NodeHash; typedef quint8 PingType_t; namespace PingType { diff --git a/libraries/networking/src/MessagesClient.cpp b/libraries/networking/src/MessagesClient.cpp index 333552db4e..2302c22a48 100644 --- a/libraries/networking/src/MessagesClient.cpp +++ b/libraries/networking/src/MessagesClient.cpp @@ -16,6 +16,8 @@ #include #include +#include + #include "NetworkLogging.h" #include "NodeList.h" #include "PacketReceiver.h" @@ -30,12 +32,6 @@ MessagesClient::MessagesClient() { connect(nodeList.data(), &LimitedNodeList::nodeActivated, this, &MessagesClient::handleNodeActivated); } -void MessagesClient::init() { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "init", Qt::BlockingQueuedConnection); - } -} - void MessagesClient::decodeMessagesPacket(QSharedPointer receivedMessage, QString& channel, bool& isText, QString& message, QByteArray& data, QUuid& senderID) { quint16 channelLength; @@ -185,3 +181,7 @@ void MessagesClient::handleNodeActivated(SharedNodePointer node) { } } } + +void MessagesClient::startThread() { + moveToNewNamedThread(this, "Messages Client Thread"); +} diff --git a/libraries/networking/src/MessagesClient.h b/libraries/networking/src/MessagesClient.h index 51b468d646..6d0483fe9d 100644 --- a/libraries/networking/src/MessagesClient.h +++ b/libraries/networking/src/MessagesClient.h @@ -28,7 +28,7 @@ class MessagesClient : public QObject, public Dependency { public: MessagesClient(); - Q_INVOKABLE void init(); + void startThread(); Q_INVOKABLE void sendMessage(QString channel, QString message, bool localOnly = false); Q_INVOKABLE void sendLocalMessage(QString channel, QString message); diff --git a/libraries/networking/src/Node.h b/libraries/networking/src/Node.h index d1bbffd817..1092fcc7fa 100644 --- a/libraries/networking/src/Node.h +++ b/libraries/networking/src/Node.h @@ -24,7 +24,7 @@ #include #include -#include +#include #include "HifiSockAddr.h" #include "NetworkPeer.h" diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 868128f093..2aa30b84aa 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -1115,3 +1116,8 @@ void NodeList::setRequestsDomainListData(bool isRequesting) { }); _requestsDomainListData = isRequesting; } + + +void NodeList::startThread() { + moveToNewNamedThread(this, "NodeList Thread", QThread::TimeCriticalPriority); +} \ No newline at end of file diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index 293b0942d6..6db760b3ca 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -20,7 +20,7 @@ #include // not on windows, not needed for mac or windows #endif -#include +#include #include #include @@ -52,6 +52,7 @@ class NodeList : public LimitedNodeList { SINGLETON_DEPENDENCY public: + void startThread(); NodeType_t getOwnerType() const { return _ownerType.load(); } void setOwnerType(NodeType_t ownerType) { _ownerType.store(ownerType); } diff --git a/interface/src/networking/RequestFilters.cpp b/libraries/networking/src/RequestFilters.cpp similarity index 98% rename from interface/src/networking/RequestFilters.cpp rename to libraries/networking/src/RequestFilters.cpp index fedde94f15..3e72b8a8bd 100644 --- a/interface/src/networking/RequestFilters.cpp +++ b/libraries/networking/src/RequestFilters.cpp @@ -14,7 +14,7 @@ #include -#include +#include "AccountManager.h" namespace { diff --git a/interface/src/networking/RequestFilters.h b/libraries/networking/src/RequestFilters.h similarity index 100% rename from interface/src/networking/RequestFilters.h rename to libraries/networking/src/RequestFilters.h diff --git a/libraries/networking/src/ResourceRequest.h b/libraries/networking/src/ResourceRequest.h index ef40cb3455..39bcb3fe93 100644 --- a/libraries/networking/src/ResourceRequest.h +++ b/libraries/networking/src/ResourceRequest.h @@ -19,6 +19,21 @@ #include "ByteRange.h" +const QString STAT_ATP_REQUEST_STARTED = "StartedATPRequest"; +const QString STAT_HTTP_REQUEST_STARTED = "StartedHTTPRequest"; +const QString STAT_FILE_REQUEST_STARTED = "StartedFileRequest"; +const QString STAT_ATP_REQUEST_SUCCESS = "SuccessfulATPRequest"; +const QString STAT_HTTP_REQUEST_SUCCESS = "SuccessfulHTTPRequest"; +const QString STAT_FILE_REQUEST_SUCCESS = "SuccessfulFileRequest"; +const QString STAT_ATP_REQUEST_FAILED = "FailedATPRequest"; +const QString STAT_HTTP_REQUEST_FAILED = "FailedHTTPRequest"; +const QString STAT_FILE_REQUEST_FAILED = "FailedFileRequest"; +const QString STAT_ATP_REQUEST_CACHE = "CacheATPRequest"; +const QString STAT_HTTP_REQUEST_CACHE = "CacheHTTPRequest"; +const QString STAT_ATP_MAPPING_REQUEST_STARTED = "StartedATPMappingRequest"; +const QString STAT_ATP_MAPPING_REQUEST_FAILED = "FailedATPMappingRequest"; +const QString STAT_ATP_MAPPING_REQUEST_SUCCESS = "SuccessfulATPMappingRequest"; + class ResourceRequest : public QObject { Q_OBJECT public: diff --git a/libraries/networking/src/SandboxUtils.cpp b/libraries/networking/src/SandboxUtils.cpp index d816f7ebee..4a348b0662 100644 --- a/libraries/networking/src/SandboxUtils.cpp +++ b/libraries/networking/src/SandboxUtils.cpp @@ -52,9 +52,8 @@ bool readStatus(QByteArray statusData) { return false; } -void runLocalSandbox(QString contentPath, bool autoShutdown, QString runningMarkerName, bool noUpdater) { +void runLocalSandbox(QString contentPath, bool autoShutdown, bool noUpdater) { QString serverPath = "./server-console/server-console.exe"; - qCDebug(networking) << "Running marker path is: " << runningMarkerName; qCDebug(networking) << "Server path is: " << serverPath; qCDebug(networking) << "autoShutdown: " << autoShutdown; qCDebug(networking) << "noUpdater: " << noUpdater; @@ -74,8 +73,8 @@ void runLocalSandbox(QString contentPath, bool autoShutdown, QString runningMark } if (autoShutdown) { - QString interfaceRunningStateFile = RunningMarker::getMarkerFilePath(runningMarkerName); - args << "--shutdownWatcher" << interfaceRunningStateFile; + auto pid = QCoreApplication::applicationPid(); + args << "--shutdownWith" << QString::number(pid); } if (noUpdater) { diff --git a/libraries/networking/src/SandboxUtils.h b/libraries/networking/src/SandboxUtils.h index 42484b8edf..370b28e1b0 100644 --- a/libraries/networking/src/SandboxUtils.h +++ b/libraries/networking/src/SandboxUtils.h @@ -21,7 +21,7 @@ namespace SandboxUtils { QNetworkReply* getStatus(); bool readStatus(QByteArray statusData); - void runLocalSandbox(QString contentPath, bool autoShutdown, QString runningMarkerName, bool noUpdater); + void runLocalSandbox(QString contentPath, bool autoShutdown, bool noUpdater); }; #endif // hifi_SandboxUtils_h diff --git a/libraries/networking/src/UserActivityLogger.cpp b/libraries/networking/src/UserActivityLogger.cpp index e2dd110cfd..0cfd1e09e7 100644 --- a/libraries/networking/src/UserActivityLogger.cpp +++ b/libraries/networking/src/UserActivityLogger.cpp @@ -20,7 +20,9 @@ #include #include "AddressManager.h" -static const QString USER_ACTIVITY_URL = "/api/v1/user_activities"; +UserActivityLogger::UserActivityLogger() { + _timer.start(); +} UserActivityLogger& UserActivityLogger::getInstance() { static UserActivityLogger sharedInstance; @@ -44,6 +46,12 @@ void UserActivityLogger::logAction(QString action, QJsonObject details, JSONCall actionPart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"action_name\""); actionPart.setBody(QByteArray().append(action)); multipart->append(actionPart); + + // Log the local-time that this event was logged + QHttpPart elapsedPart; + elapsedPart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"elapsed_ms\""); + elapsedPart.setBody(QString::number(_timer.elapsed()).toLocal8Bit()); + multipart->append(elapsedPart); // If there are action details, add them to the multipart if (!details.isEmpty()) { diff --git a/libraries/networking/src/UserActivityLogger.h b/libraries/networking/src/UserActivityLogger.h index b41960a8ad..179e8e6e66 100644 --- a/libraries/networking/src/UserActivityLogger.h +++ b/libraries/networking/src/UserActivityLogger.h @@ -18,10 +18,13 @@ #include #include #include +#include #include #include "AddressManager.h" +const QString USER_ACTIVITY_URL = "/api/v1/user_activities"; + class UserActivityLogger : public QObject { Q_OBJECT @@ -49,8 +52,10 @@ private slots: void requestError(QNetworkReply& errorReply); private: - UserActivityLogger() {}; + UserActivityLogger(); Setting::Handle _disabled { "UserActivityLoggerDisabled", false }; + + QElapsedTimer _timer; }; #endif // hifi_UserActivityLogger_h diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp index ff69363570..61f2071c5f 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp @@ -56,7 +56,7 @@ void UserActivityLoggerScriptingInterface::palAction(QString action, QString tar } void UserActivityLoggerScriptingInterface::palOpened(float secondsOpened) { - doLogAction("pal_opened", { + doLogAction("pal_opened", { { "seconds_opened", secondsOpened } }); } @@ -71,6 +71,14 @@ void UserActivityLoggerScriptingInterface::makeUserConnection(QString otherID, b doLogAction("makeUserConnection", payload); } +void UserActivityLoggerScriptingInterface::bubbleToggled(bool newValue) { + doLogAction(newValue ? "bubbleOn" : "bubbleOff"); +} + +void UserActivityLoggerScriptingInterface::bubbleActivated() { + doLogAction("bubbleActivated"); +} + void UserActivityLoggerScriptingInterface::logAction(QString action, QVariantMap details) { doLogAction(action, QJsonObject::fromVariantMap(details)); } diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.h b/libraries/networking/src/UserActivityLoggerScriptingInterface.h index b141e930f2..885f637a62 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.h +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.h @@ -30,6 +30,8 @@ public: 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 bubbleToggled(bool newValue); + Q_INVOKABLE void bubbleActivated(); Q_INVOKABLE void logAction(QString action, QVariantMap details = QVariantMap{}); private: void doLogAction(QString action, QJsonObject details = {}); diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 9d970fa318..d59da2f726 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -56,7 +56,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::AvatarData: case PacketType::BulkAvatarData: case PacketType::KillAvatar: - return static_cast(AvatarMixerPacketVersion::AvatarIdentitySequenceId); + return static_cast(AvatarMixerPacketVersion::MannequinDefaultAvatar); case PacketType::MessagesData: return static_cast(MessageDataVersion::TextOrBinaryData); case PacketType::ICEServerHeartbeat: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 2cc3a2c42e..fa1151e0a6 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -236,7 +236,8 @@ enum class AvatarMixerPacketVersion : PacketVersion { AvatarAsChildFixes, StickAndBallDefaultAvatar, IdentityPacketsIncludeUpdateTime, - AvatarIdentitySequenceId + AvatarIdentitySequenceId, + MannequinDefaultAvatar }; enum class DomainConnectRequestVersion : PacketVersion { diff --git a/libraries/networking/src/udt/SendQueue.cpp b/libraries/networking/src/udt/SendQueue.cpp index c14ae0a39c..0c029751aa 100644 --- a/libraries/networking/src/udt/SendQueue.cpp +++ b/libraries/networking/src/udt/SendQueue.cpp @@ -241,6 +241,9 @@ void SendQueue::handshakeACK(SequenceNumber initialSequenceNumber) { std::lock_guard locker { _handshakeMutex }; _hasReceivedHandshakeACK = true; } + + _lastReceiverResponse = QDateTime::currentMSecsSinceEpoch(); + // Notify on the handshake ACK condition _handshakeACKCondition.notify_one(); } diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp index ee240a6aac..bd4d1201c7 100755 --- a/libraries/physics/src/CharacterController.cpp +++ b/libraries/physics/src/CharacterController.cpp @@ -152,6 +152,7 @@ bool CharacterController::checkForSupport(btCollisionWorld* collisionWorld) { btDispatcher* dispatcher = collisionWorld->getDispatcher(); int numManifolds = dispatcher->getNumManifolds(); bool hasFloor = false; + bool isStuck = false; btTransform rotation = _rigidBody->getWorldTransform(); rotation.setOrigin(btVector3(0.0f, 0.0f, 0.0f)); // clear translation part @@ -169,10 +170,18 @@ bool CharacterController::checkForSupport(btCollisionWorld* collisionWorld) { btVector3 pointOnCharacter = characterIsFirst ? contact.m_localPointA : contact.m_localPointB; // object-local-frame btVector3 normal = characterIsFirst ? contact.m_normalWorldOnB : -contact.m_normalWorldOnB; // points toward character btScalar hitHeight = _halfHeight + _radius + pointOnCharacter.dot(_currentUp); + // If there's non-trivial penetration with a big impulse for several steps, we're probably stuck. + // Note it here in the controller, and let MyAvatar figure out what to do about it. + const float STUCK_PENETRATION = -0.05f; // always negative into the object. + const float STUCK_IMPULSE = 500.0f; + const int STUCK_LIFETIME = 3; + if ((contact.getDistance() < STUCK_PENETRATION) && (contact.getAppliedImpulse() > STUCK_IMPULSE) && (contact.getLifeTime() > STUCK_LIFETIME)) { + isStuck = true; // latch on + } if (hitHeight < _maxStepHeight && normal.dot(_currentUp) > _minFloorNormalDotUp) { hasFloor = true; - if (!pushing) { - // we're not pushing against anything so we can early exit + if (!pushing && isStuck) { + // we're not pushing against anything and we're stuck so we can early exit // (all we need to know is that there is a floor) break; } @@ -198,12 +207,13 @@ bool CharacterController::checkForSupport(btCollisionWorld* collisionWorld) { _stepHeight = highestStep; _stepPoint = rotation * pointOnCharacter; // rotate into world-frame } - if (hasFloor && !(pushing && _stepUpEnabled)) { + if (hasFloor && isStuck && !(pushing && _stepUpEnabled)) { // early exit since all we need to know is that we're on a floor break; } } } + _isStuck = isStuck; return hasFloor; } diff --git a/libraries/physics/src/CharacterController.h b/libraries/physics/src/CharacterController.h index 6790495ff8..bf84d318d4 100644 --- a/libraries/physics/src/CharacterController.h +++ b/libraries/physics/src/CharacterController.h @@ -110,6 +110,7 @@ public: void setLocalBoundingBox(const glm::vec3& minCorner, const glm::vec3& scale); bool isEnabledAndReady() const { return _dynamicsWorld; } + bool isStuck() const { return _isStuck; } void setCollisionless(bool collisionless); int16_t computeCollisionGroup() const; @@ -192,6 +193,7 @@ protected: State _state; bool _isPushingUp; + bool _isStuck { false }; btDynamicsWorld* _dynamicsWorld { nullptr }; btRigidBody* _rigidBody { nullptr }; diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index fdd290bfca..ecfb2b55e4 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -65,8 +65,7 @@ EntityMotionState::EntityMotionState(btCollisionShape* shape, EntityItemPointer _lastStep(0), _loopsWithoutOwner(0), _accelerationNearlyGravityCount(0), - _numInactiveUpdates(1), - _outgoingPriority(0) + _numInactiveUpdates(1) { _type = MOTIONSTATE_TYPE_ENTITY; assert(_entity); @@ -75,6 +74,8 @@ EntityMotionState::EntityMotionState(btCollisionShape* shape, EntityItemPointer // we need the side-effects of EntityMotionState::setShape() so we call it explicitly here // rather than pass the legit shape pointer to the ObjectMotionState ctor above. setShape(shape); + + _outgoingPriority = _entity->getPendingOwnershipPriority(); } EntityMotionState::~EntityMotionState() { @@ -84,7 +85,7 @@ EntityMotionState::~EntityMotionState() { void EntityMotionState::updateServerPhysicsVariables() { assert(entityTreeIsLocked()); - if (_entity->getSimulatorID() == Physics::getSessionUUID()) { + if (isLocallyOwned()) { // don't slam these values if we are the simulation owner return; } @@ -114,6 +115,7 @@ void EntityMotionState::handleDeactivation() { // virtual void EntityMotionState::handleEasyChanges(uint32_t& flags) { + assert(_entity); assert(entityTreeIsLocked()); updateServerPhysicsVariables(); ObjectMotionState::handleEasyChanges(flags); @@ -135,23 +137,23 @@ void EntityMotionState::handleEasyChanges(uint32_t& flags) { _nextOwnershipBid = usecTimestampNow() + USECS_BETWEEN_OWNERSHIP_BIDS; } _loopsWithoutOwner = 0; - } else if (_entity->getSimulatorID() == Physics::getSessionUUID()) { + _numInactiveUpdates = 0; + } else if (isLocallyOwned()) { // we just inherited ownership, make sure our desired priority matches what we have upgradeOutgoingPriority(_entity->getSimulationPriority()); } else { _outgoingPriority = 0; _nextOwnershipBid = usecTimestampNow() + USECS_BETWEEN_OWNERSHIP_BIDS; + _numInactiveUpdates = 0; } } if (flags & Simulation::DIRTY_SIMULATION_OWNERSHIP_PRIORITY) { - // The DIRTY_SIMULATOR_OWNERSHIP_PRIORITY bits really mean "we should bid for ownership because - // a local script has been changing physics properties, or we should adjust our own ownership priority". - // The desired priority is determined by which bits were set. - if (flags & Simulation::DIRTY_SIMULATION_OWNERSHIP_FOR_GRAB) { - _outgoingPriority = SCRIPT_GRAB_SIMULATION_PRIORITY; - } else { - _outgoingPriority = SCRIPT_POKE_SIMULATION_PRIORITY; - } + // The DIRTY_SIMULATOR_OWNERSHIP_PRIORITY bit means one of the following: + // (1) we own it but may need to change the priority OR... + // (2) we don't own it but should bid (because a local script has been changing physics properties) + uint8_t newPriority = isLocallyOwned() ? _entity->getSimulationOwner().getPriority() : _entity->getSimulationOwner().getPendingPriority(); + _outgoingPriority = glm::max(_outgoingPriority, newPriority); + // reset bid expiry so that we bid ASAP _nextOwnershipBid = 0; } @@ -170,6 +172,7 @@ void EntityMotionState::handleEasyChanges(uint32_t& flags) { // virtual bool EntityMotionState::handleHardAndEasyChanges(uint32_t& flags, PhysicsEngine* engine) { + assert(_entity); updateServerPhysicsVariables(); return ObjectMotionState::handleHardAndEasyChanges(flags, engine); } @@ -315,7 +318,7 @@ bool EntityMotionState::isCandidateForOwnership() const { assert(_entity); assert(entityTreeIsLocked()); return _outgoingPriority != 0 - || Physics::getSessionUUID() == _entity->getSimulatorID() + || isLocallyOwned() || _entity->dynamicDataNeedsTransmit(); } @@ -489,7 +492,7 @@ bool EntityMotionState::shouldSendUpdate(uint32_t simulationStep) { return true; } - if (_entity->getSimulatorID() != Physics::getSessionUUID()) { + if (!isLocallyOwned()) { // we don't own the simulation // NOTE: we do not volunteer to own kinematic or static objects @@ -597,7 +600,7 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ properties.clearSimulationOwner(); _outgoingPriority = 0; _entity->setPendingOwnershipPriority(_outgoingPriority, now); - } else if (Physics::getSessionUUID() != _entity->getSimulatorID()) { + } else if (!isLocallyOwned()) { // we don't own the simulation for this entity yet, but we're sending a bid for it quint8 bidPriority = glm::max(_outgoingPriority, VOLUNTEER_SIMULATION_PRIORITY); properties.setSimulationOwner(Physics::getSessionUUID(), bidPriority); @@ -786,6 +789,10 @@ void EntityMotionState::computeCollisionGroupAndMask(int16_t& group, int16_t& ma _entity->computeCollisionGroupAndFinalMask(group, mask); } +bool EntityMotionState::isLocallyOwned() const { + return _entity->getSimulatorID() == Physics::getSessionUUID(); +} + bool EntityMotionState::shouldBeLocallyOwned() const { return (_outgoingPriority > VOLUNTEER_SIMULATION_PRIORITY && _outgoingPriority > _entity->getSimulationPriority()) || _entity->getSimulatorID() == Physics::getSessionUUID(); diff --git a/libraries/physics/src/EntityMotionState.h b/libraries/physics/src/EntityMotionState.h index 380edf3927..541ad7c93c 100644 --- a/libraries/physics/src/EntityMotionState.h +++ b/libraries/physics/src/EntityMotionState.h @@ -79,6 +79,7 @@ public: virtual void computeCollisionGroupAndMask(int16_t& group, int16_t& mask) const override; + bool isLocallyOwned() const override; bool shouldBeLocallyOwned() const override; friend class PhysicalEntitySimulation; diff --git a/libraries/physics/src/ObjectMotionState.cpp b/libraries/physics/src/ObjectMotionState.cpp index a877887840..b11e21366e 100644 --- a/libraries/physics/src/ObjectMotionState.cpp +++ b/libraries/physics/src/ObjectMotionState.cpp @@ -202,6 +202,7 @@ void ObjectMotionState::setShape(const btCollisionShape* shape) { } void ObjectMotionState::handleEasyChanges(uint32_t& flags) { + assert(_body && _shape); if (flags & Simulation::DIRTY_POSITION) { btTransform worldTrans = _body->getWorldTransform(); btVector3 newPosition = glmToBullet(getObjectPosition()); @@ -282,6 +283,7 @@ void ObjectMotionState::handleEasyChanges(uint32_t& flags) { } bool ObjectMotionState::handleHardAndEasyChanges(uint32_t& flags, PhysicsEngine* engine) { + assert(_body && _shape); if (flags & Simulation::DIRTY_SHAPE) { // make sure the new shape is valid if (!isReadyToComputeShape()) { diff --git a/libraries/physics/src/ObjectMotionState.h b/libraries/physics/src/ObjectMotionState.h index 1e582ea854..81bfbc72b4 100644 --- a/libraries/physics/src/ObjectMotionState.h +++ b/libraries/physics/src/ObjectMotionState.h @@ -79,7 +79,7 @@ public: static ShapeManager* getShapeManager(); ObjectMotionState(const btCollisionShape* shape); - ~ObjectMotionState(); + virtual ~ObjectMotionState(); virtual void handleEasyChanges(uint32_t& flags); virtual bool handleHardAndEasyChanges(uint32_t& flags, PhysicsEngine* engine); @@ -146,6 +146,7 @@ public: void dirtyInternalKinematicChanges() { _hasInternalKinematicChanges = true; } void clearInternalKinematicChanges() { _hasInternalKinematicChanges = false; } + virtual bool isLocallyOwned() const { return false; } virtual bool shouldBeLocallyOwned() const { return false; } friend class PhysicsEngine; diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index 2e69ff987c..fe507ed1ee 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -130,7 +130,7 @@ void PhysicalEntitySimulation::clearEntitiesInternal() { } // then remove the objects (aka MotionStates) from physics - _physicsEngine->removeObjects(_physicalObjects); + _physicsEngine->removeSetOfObjects(_physicalObjects); // delete the MotionStates // TODO: after we invert the entities/physics lib dependencies we will let EntityItem delete diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index 3a02e95e7c..a64796308e 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -129,6 +129,9 @@ void PhysicsEngine::addObjectToDynamicsWorld(ObjectMotionState* motionState) { } body->setCollisionFlags(btCollisionObject::CF_STATIC_OBJECT); body->updateInertiaTensor(); + if (motionState->isLocallyOwned()) { + _activeStaticBodies.insert(body); + } break; } } @@ -174,19 +177,9 @@ void PhysicsEngine::removeObjects(const VectorOfMotionStates& objects) { // frame (because the framerate is faster than our physics simulation rate). When this happens we must scan // _activeStaticBodies for objects that were recently deleted so we don't try to access a dangling pointer. for (auto object : objects) { - btRigidBody* body = object->getRigidBody(); - - std::vector::reverse_iterator itr = _activeStaticBodies.rbegin(); - while (itr != _activeStaticBodies.rend()) { - if (body == *itr) { - if (*itr != *(_activeStaticBodies.rbegin())) { - // swap with rbegin - *itr = *(_activeStaticBodies.rbegin()); - } - _activeStaticBodies.pop_back(); - break; - } - ++itr; + std::set::iterator itr = _activeStaticBodies.find(object->getRigidBody()); + if (itr != _activeStaticBodies.end()) { + _activeStaticBodies.erase(itr); } } } @@ -207,7 +200,7 @@ void PhysicsEngine::removeObjects(const VectorOfMotionStates& objects) { } // Same as above, but takes a Set instead of a Vector. Should only be called during teardown. -void PhysicsEngine::removeObjects(const SetOfMotionStates& objects) { +void PhysicsEngine::removeSetOfObjects(const SetOfMotionStates& objects) { _contactMap.clear(); for (auto object : objects) { btRigidBody* body = object->getRigidBody(); @@ -245,14 +238,16 @@ VectorOfMotionStates PhysicsEngine::changeObjects(const VectorOfMotionStates& ob object->clearIncomingDirtyFlags(); } if (object->getMotionType() == MOTION_TYPE_STATIC && object->isActive()) { - _activeStaticBodies.push_back(object->getRigidBody()); + _activeStaticBodies.insert(object->getRigidBody()); } } // active static bodies have changed (in an Easy way) and need their Aabbs updated // but we've configured Bullet to NOT update them automatically (for improved performance) // so we must do it ourselves - for (size_t i = 0; i < _activeStaticBodies.size(); ++i) { - _dynamicsWorld->updateSingleAabb(_activeStaticBodies[i]); + std::set::const_iterator itr = _activeStaticBodies.begin(); + while (itr != _activeStaticBodies.end()) { + _dynamicsWorld->updateSingleAabb(*itr); + ++itr; } return stillNeedChange; } @@ -496,13 +491,23 @@ const CollisionEvents& PhysicsEngine::getCollisionEvents() { const VectorOfMotionStates& PhysicsEngine::getChangedMotionStates() { BT_PROFILE("copyOutgoingChanges"); + + _dynamicsWorld->synchronizeMotionStates(); + // Bullet will not deactivate static objects (it doesn't expect them to be active) // so we must deactivate them ourselves - for (size_t i = 0; i < _activeStaticBodies.size(); ++i) { - _activeStaticBodies[i]->forceActivationState(ISLAND_SLEEPING); + std::set::const_iterator itr = _activeStaticBodies.begin(); + while (itr != _activeStaticBodies.end()) { + btRigidBody* body = *itr; + body->forceActivationState(ISLAND_SLEEPING); + ObjectMotionState* motionState = static_cast(body->getUserPointer()); + if (motionState) { + _dynamicsWorld->addChangedMotionState(motionState); + } + ++itr; } _activeStaticBodies.clear(); - _dynamicsWorld->synchronizeMotionStates(); + _hasOutgoingChanges = false; return _dynamicsWorld->getChangedMotionStates(); } diff --git a/libraries/physics/src/PhysicsEngine.h b/libraries/physics/src/PhysicsEngine.h index e9b29a43a4..3063a4a89a 100644 --- a/libraries/physics/src/PhysicsEngine.h +++ b/libraries/physics/src/PhysicsEngine.h @@ -13,6 +13,7 @@ #define hifi_PhysicsEngine_h #include +#include #include #include @@ -53,7 +54,7 @@ public: uint32_t getNumSubsteps(); void removeObjects(const VectorOfMotionStates& objects); - void removeObjects(const SetOfMotionStates& objects); // only called during teardown + void removeSetOfObjects(const SetOfMotionStates& objects); // only called during teardown void addObjects(const VectorOfMotionStates& objects); VectorOfMotionStates changeObjects(const VectorOfMotionStates& objects); @@ -114,7 +115,7 @@ private: CollisionEvents _collisionEvents; QHash _objectDynamics; QHash> _objectDynamicsByBody; - std::vector _activeStaticBodies; + std::set _activeStaticBodies; glm::vec3 _originOffset; diff --git a/libraries/physics/src/ThreadSafeDynamicsWorld.h b/libraries/physics/src/ThreadSafeDynamicsWorld.h index b4fcca8cdb..54c3ddb756 100644 --- a/libraries/physics/src/ThreadSafeDynamicsWorld.h +++ b/libraries/physics/src/ThreadSafeDynamicsWorld.h @@ -51,6 +51,8 @@ public: const VectorOfMotionStates& getChangedMotionStates() const { return _changedMotionStates; } const VectorOfMotionStates& getDeactivatedMotionStates() const { return _deactivatedStates; } + void addChangedMotionState(ObjectMotionState* motionState) { _changedMotionStates.push_back(motionState); } + private: // call this instead of non-virtual btDiscreteDynamicsWorld::synchronizeSingleMotionState() void synchronizeMotionState(btRigidBody* body); diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index 754c919fd4..297bdb2cca 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -200,6 +200,7 @@ public: virtual float newFramePresentRate() const { return -1.0f; } // Rate at which rendered frames are being skipped virtual float droppedFrameRate() const { return -1.0f; } + virtual bool getSupportsAutoSwitch() { return false; } // Hardware specific stats virtual QJsonObject getHardwareStats() const { return QJsonObject(); } diff --git a/libraries/plugins/src/plugins/InputPlugin.h b/libraries/plugins/src/plugins/InputPlugin.h index 0db0b24420..2a4737b5a1 100644 --- a/libraries/plugins/src/plugins/InputPlugin.h +++ b/libraries/plugins/src/plugins/InputPlugin.h @@ -24,6 +24,7 @@ public: // Some input plugins are comprised of multiple subdevices (SDL2, for instance). // If an input plugin is only a single device, it will only return it's primary name. virtual QStringList getSubdeviceNames() { return { getName() }; }; - virtual bool isHandController() const = 0; + virtual bool isHandController() const { return false; } + virtual bool isHeadController() const { return false; } }; diff --git a/libraries/plugins/src/plugins/Plugin.h b/libraries/plugins/src/plugins/Plugin.h index 15588fafa4..2d4a24a1fe 100644 --- a/libraries/plugins/src/plugins/Plugin.h +++ b/libraries/plugins/src/plugins/Plugin.h @@ -53,6 +53,18 @@ public: virtual bool isActive() { return _active; } + virtual bool startStandBySession() { + _sessionStatus = true; + return _sessionStatus; + } + + virtual void endSession() { + _sessionStatus = false; + } + + virtual bool isSessionActive() { + return _sessionStatus; + } /** * Called by the application during it's idle phase. If the plugin needs to do @@ -73,6 +85,7 @@ signals: protected: bool _active { false }; + bool _sessionStatus { false }; PluginContainer* _container { nullptr }; static const char* UNKNOWN_PLUGIN_ID; diff --git a/libraries/plugins/src/plugins/PluginUtils.cpp b/libraries/plugins/src/plugins/PluginUtils.cpp index 20dc011f25..ce67f7c585 100644 --- a/libraries/plugins/src/plugins/PluginUtils.cpp +++ b/libraries/plugins/src/plugins/PluginUtils.cpp @@ -24,6 +24,15 @@ bool PluginUtils::isHMDAvailable(const QString& pluginName) { return false; } +bool PluginUtils::isHeadControllerAvailable(const QString& pluginName) { + for (auto& inputPlugin : PluginManager::getInstance()->getInputPlugins()) { + if (inputPlugin->isHeadController() && (pluginName.isEmpty() || inputPlugin->getName() == pluginName)) { + return true; + } + } + return false; +}; + bool PluginUtils::isHandControllerAvailable(const QString& pluginName) { for (auto& inputPlugin : PluginManager::getInstance()->getInputPlugins()) { if (inputPlugin->isHandController() && (pluginName.isEmpty() || inputPlugin->getName() == pluginName)) { diff --git a/libraries/plugins/src/plugins/PluginUtils.h b/libraries/plugins/src/plugins/PluginUtils.h index 351c9e7e58..2b6206fe63 100644 --- a/libraries/plugins/src/plugins/PluginUtils.h +++ b/libraries/plugins/src/plugins/PluginUtils.h @@ -16,6 +16,7 @@ class PluginUtils { public: static bool isHMDAvailable(const QString& pluginName = ""); static bool isHandControllerAvailable(const QString& pluginName = ""); + static bool isHeadControllerAvailable(const QString& pluginName = ""); static bool isSubdeviceContainingNameAvailable(QString name); static bool isViveControllerAvailable(); static bool isOculusTouchControllerAvailable(); diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.cpp b/libraries/procedural/src/procedural/ProceduralSkybox.cpp index 0275a875ec..f5feb434fa 100644 --- a/libraries/procedural/src/procedural/ProceduralSkybox.cpp +++ b/libraries/procedural/src/procedural/ProceduralSkybox.cpp @@ -23,7 +23,7 @@ ProceduralSkybox::ProceduralSkybox() : model::Skybox() { _procedural._fragmentSource = skybox_frag; // Adjust the pipeline state for background using the stencil test _procedural.setDoesFade(false); - _procedural._opaqueState->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); + _procedural._opaqueState->setStencilTest(true, 0xFF, gpu::State::StencilTest(1, 0xFF, gpu::EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); } bool ProceduralSkybox::empty() { diff --git a/libraries/render-utils/CMakeLists.txt b/libraries/render-utils/CMakeLists.txt index 454097233a..c359924e0d 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 image) +link_hifi_libraries(shared ktx gpu model model-networking render animation fbx entities image procedural) if (NOT ANDROID) target_nsight() diff --git a/libraries/render-utils/src/AnimDebugDraw.cpp b/libraries/render-utils/src/AnimDebugDraw.cpp index 162745e76f..4f7f9ef5c4 100644 --- a/libraries/render-utils/src/AnimDebugDraw.cpp +++ b/libraries/render-utils/src/AnimDebugDraw.cpp @@ -260,7 +260,7 @@ static void addLink(const AnimPose& rootPose, const AnimPose& pose, const AnimPo // there is room, so lets draw a nice bone glm::vec3 uAxis, vAxis, wAxis; - generateBasisVectors(boneAxis0, glm::vec3(1, 0, 0), uAxis, vAxis, wAxis); + generateBasisVectors(boneAxis0, glm::vec3(1.0f, 0.0f, 0.0f), uAxis, vAxis, wAxis); glm::vec3 boneBaseCorners[NUM_BASE_CORNERS]; boneBaseCorners[0] = pose0 * ((uAxis * radius) + (vAxis * radius) + (wAxis * radius)); diff --git a/libraries/render-utils/src/AntialiasingEffect.cpp b/libraries/render-utils/src/AntialiasingEffect.cpp index cd378d4e5b..f7881b0333 100644 --- a/libraries/render-utils/src/AntialiasingEffect.cpp +++ b/libraries/render-utils/src/AntialiasingEffect.cpp @@ -17,6 +17,7 @@ #include #include "AntialiasingEffect.h" +#include "StencilMaskPass.h" #include "TextureCache.h" #include "FramebufferCache.h" #include "DependencyManager.h" @@ -70,6 +71,8 @@ const gpu::PipelinePointer& Antialiasing::getAntialiasingPipeline() { gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + PrepareStencil::testMask(*state); + state->setDepthTest(false, false, gpu::LESS_EQUAL); // Good to go add the brand new pipeline @@ -93,6 +96,7 @@ const gpu::PipelinePointer& Antialiasing::getBlendPipeline() { gpu::StatePointer state = gpu::StatePointer(new gpu::State()); state->setDepthTest(false, false, gpu::LESS_EQUAL); + PrepareStencil::testMask(*state); // Good to go add the brand new pipeline _blendPipeline = gpu::Pipeline::create(program, state); @@ -106,10 +110,6 @@ void Antialiasing::run(const render::RenderContextPointer& renderContext, const RenderArgs* args = renderContext->args; - if (args->_renderMode == RenderArgs::MIRROR_RENDER_MODE) { - return; - } - gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { batch.enableStereo(false); batch.setViewportTransform(args->_viewport); diff --git a/libraries/render-utils/src/BackgroundStage.cpp b/libraries/render-utils/src/BackgroundStage.cpp new file mode 100644 index 0000000000..5c2f55954a --- /dev/null +++ b/libraries/render-utils/src/BackgroundStage.cpp @@ -0,0 +1,140 @@ +// +// BackgroundStage.cpp +// +// Created by Sam Gateau on 5/9/2017. +// 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 "BackgroundStage.h" + +#include "DeferredLightingEffect.h" + +#include + +BackgroundStage::Index BackgroundStage::findBackground(const BackgroundPointer& background) const { + auto found = _backgroundMap.find(background); + if (found != _backgroundMap.end()) { + return INVALID_INDEX; + } else { + return (*found).second; + } + +} + +BackgroundStage::Index BackgroundStage::addBackground(const BackgroundPointer& background) { + + auto found = _backgroundMap.find(background); + if (found == _backgroundMap.end()) { + auto backgroundId = _backgrounds.newElement(background); + // Avoid failing to allocate a background, just pass + if (backgroundId != INVALID_INDEX) { + + // Insert the background and its index in the reverse map + _backgroundMap.insert(BackgroundMap::value_type(background, backgroundId)); + } + return backgroundId; + } else { + return (*found).second; + } +} + + +BackgroundStage::BackgroundPointer BackgroundStage::removeBackground(Index index) { + BackgroundPointer removed = _backgrounds.freeElement(index); + + if (removed) { + _backgroundMap.erase(removed); + } + return removed; +} + + +void DrawBackgroundStage::run(const render::RenderContextPointer& renderContext, const Inputs& inputs) { + + const auto& lightingModel = inputs; + if (!lightingModel->isBackgroundEnabled()) { + return; + } + + + // Background rendering decision + auto backgroundStage = DependencyManager::get()->getBackgroundStage(); + model::SunSkyStagePointer background; + model::SkyboxPointer skybox; + if (backgroundStage->_currentFrame._backgrounds.size()) { + auto backgroundId = backgroundStage->_currentFrame._backgrounds.front(); + auto background = backgroundStage->getBackground(backgroundId); + if (background) { + skybox = background->getSkybox(); + } + } else { + skybox = DependencyManager::get()->getDefaultSkybox(); + } + + /* auto backgroundMode = skyStage->getBackgroundMode(); + + switch (backgroundMode) { + case model::SunSkyStage::SKY_DEFAULT: { + auto scene = DependencyManager::get()->getStage(); + auto sceneKeyLight = scene->getKeyLight(); + + scene->setSunModelEnable(false); + sceneKeyLight->setColor(ColorUtils::toVec3(KeyLightPropertyGroup::DEFAULT_KEYLIGHT_COLOR)); + sceneKeyLight->setIntensity(KeyLightPropertyGroup::DEFAULT_KEYLIGHT_INTENSITY); + sceneKeyLight->setAmbientIntensity(KeyLightPropertyGroup::DEFAULT_KEYLIGHT_AMBIENT_INTENSITY); + sceneKeyLight->setDirection(KeyLightPropertyGroup::DEFAULT_KEYLIGHT_DIRECTION); + // fall through: render a skybox (if available), or the defaults (if requested) + } + + case model::SunSkyStage::SKY_BOX: {*/ + if (skybox && !skybox->empty()) { + PerformanceTimer perfTimer("skybox"); + auto args = renderContext->args; + + gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { + args->_batch = &batch; + + batch.enableSkybox(true); + + batch.setViewportTransform(args->_viewport); + batch.setStateScissorRect(args->_viewport); + + glm::mat4 projMat; + Transform viewMat; + args->getViewFrustum().evalProjectionMatrix(projMat); + args->getViewFrustum().evalViewTransform(viewMat); + + batch.setProjectionTransform(projMat); + batch.setViewTransform(viewMat); + + skybox->render(batch, args->getViewFrustum()); + }); + args->_batch = nullptr; + + // break; + } + // fall through: render defaults (if requested) +// } +/* + case model::SunSkyStage::SKY_DEFAULT_AMBIENT_TEXTURE: { + if (Menu::getInstance()->isOptionChecked(MenuOption::DefaultSkybox)) { + auto scene = DependencyManager::get()->getStage(); + auto sceneKeyLight = scene->getKeyLight(); + auto defaultSkyboxAmbientTexture = qApp->getDefaultSkyboxAmbientTexture(); + if (defaultSkyboxAmbientTexture) { + sceneKeyLight->setAmbientSphere(defaultSkyboxAmbientTexture->getIrradiance()); + sceneKeyLight->setAmbientMap(defaultSkyboxAmbientTexture); + } else { + static QString repeatedMessage = LogHandler::getInstance().addRepeatedMessageRegex( + "Failed to get a valid Default Skybox Ambient Texture ? probably because it couldn't be find during initialization step"); + } + // fall through: render defaults skybox + } else { + break; + } + } + */ + +} \ No newline at end of file diff --git a/libraries/render-utils/src/BackgroundStage.h b/libraries/render-utils/src/BackgroundStage.h new file mode 100644 index 0000000000..1dd1651c98 --- /dev/null +++ b/libraries/render-utils/src/BackgroundStage.h @@ -0,0 +1,80 @@ +// +// BackgroundStage.h + +// Created by Sam Gateau on 5/9/2017. +// 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_render_utils_BackgroundStage_h +#define hifi_render_utils_BackgroundStage_h + +#include +#include +#include +#include + +#include "LightingModel.h" + + +// Background stage to set up background-related rendering tasks +class BackgroundStage { +public: + using Index = render::indexed_container::Index; + static const Index INVALID_INDEX { render::indexed_container::INVALID_INDEX }; + static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; } + + using BackgroundPointer = model::SunSkyStagePointer; + using Backgrounds = render::indexed_container::IndexedPointerVector; + using BackgroundMap = std::unordered_map; + + using BackgroundIndices = std::vector; + + + Index findBackground(const BackgroundPointer& background) const; + Index addBackground(const BackgroundPointer& background); + + BackgroundPointer removeBackground(Index index); + + bool checkBackgroundId(Index index) const { return _backgrounds.checkIndex(index); } + + Index getNumBackgrounds() const { return _backgrounds.getNumElements(); } + Index getNumFreeBackgrounds() const { return _backgrounds.getNumFreeIndices(); } + Index getNumAllocatedBackgrounds() const { return _backgrounds.getNumAllocatedIndices(); } + + BackgroundPointer getBackground(Index backgroundId) const { + return _backgrounds.get(backgroundId); + } + + Backgrounds _backgrounds; + BackgroundMap _backgroundMap; + + class Frame { + public: + Frame() {} + + void clear() { _backgrounds.clear(); } + + void pushBackground(BackgroundStage::Index index) { _backgrounds.emplace_back(index); } + + BackgroundStage::BackgroundIndices _backgrounds; + }; + + Frame _currentFrame; +}; +using BackgroundStagePointer = std::shared_ptr; + + +class DrawBackgroundStage { +public: + using Inputs = LightingModelPointer; + using JobModel = render::Job::ModelI; + + void run(const render::RenderContextPointer& renderContext, const Inputs& inputs); + +protected: +}; + +#endif diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 93a176f4f3..4b3ee9fec7 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -18,6 +18,7 @@ #include #include +#include "StencilMaskPass.h" #include "AbstractViewStateInterface.h" #include "GeometryCache.h" #include "TextureCache.h" @@ -27,18 +28,15 @@ #include "deferred_light_point_vert.h" #include "deferred_light_spot_vert.h" -#include "directional_light_frag.h" #include "directional_ambient_light_frag.h" #include "directional_skybox_light_frag.h" -#include "directional_light_shadow_frag.h" #include "directional_ambient_light_shadow_frag.h" #include "directional_skybox_light_shadow_frag.h" #include "local_lights_shading_frag.h" #include "local_lights_drawOutline_frag.h" -#include "point_light_frag.h" -#include "spot_light_frag.h" + using namespace render; @@ -82,48 +80,26 @@ enum DeferredShader_BufferSlot { }; static void loadLightProgram(const char* vertSource, const char* fragSource, bool lightVolume, gpu::PipelinePointer& program, LightLocationsPtr& locations); -static void loadLightVolumeProgram(const char* vertSource, const char* fragSource, bool front, gpu::PipelinePointer& program, LightLocationsPtr& locations); - -const char no_light_frag[] = -R"SCRIBE( -out vec4 _fragColor; - -void main(void) { - _fragColor = vec4(1.0, 1.0, 1.0, 1.0); -} -)SCRIBE" -; void DeferredLightingEffect::init() { - _directionalLightLocations = std::make_shared(); _directionalAmbientSphereLightLocations = std::make_shared(); _directionalSkyboxLightLocations = std::make_shared(); - _directionalLightShadowLocations = std::make_shared(); _directionalAmbientSphereLightShadowLocations = std::make_shared(); _directionalSkyboxLightShadowLocations = std::make_shared(); _localLightLocations = std::make_shared(); _localLightOutlineLocations = std::make_shared(); - _pointLightLocations = std::make_shared(); - _spotLightLocations = std::make_shared(); - loadLightProgram(deferred_light_vert, directional_light_frag, false, _directionalLight, _directionalLightLocations); loadLightProgram(deferred_light_vert, directional_ambient_light_frag, false, _directionalAmbientSphereLight, _directionalAmbientSphereLightLocations); loadLightProgram(deferred_light_vert, directional_skybox_light_frag, false, _directionalSkyboxLight, _directionalSkyboxLightLocations); - loadLightProgram(deferred_light_vert, directional_light_shadow_frag, false, _directionalLightShadow, _directionalLightShadowLocations); loadLightProgram(deferred_light_vert, directional_ambient_light_shadow_frag, false, _directionalAmbientSphereLightShadow, _directionalAmbientSphereLightShadowLocations); loadLightProgram(deferred_light_vert, directional_skybox_light_shadow_frag, false, _directionalSkyboxLightShadow, _directionalSkyboxLightShadowLocations); loadLightProgram(deferred_light_vert, local_lights_shading_frag, true, _localLight, _localLightLocations); loadLightProgram(deferred_light_vert, local_lights_drawOutline_frag, true, _localLightOutline, _localLightOutlineLocations); - loadLightVolumeProgram(deferred_light_point_vert, no_light_frag, false, _pointLightBack, _pointLightLocations); - loadLightVolumeProgram(deferred_light_point_vert, no_light_frag, true, _pointLightFront, _pointLightLocations); - loadLightVolumeProgram(deferred_light_spot_vert, no_light_frag, false, _spotLightBack, _spotLightLocations); - loadLightVolumeProgram(deferred_light_spot_vert, no_light_frag, true, _spotLightFront, _spotLightLocations); - // Light Stage and clusters _lightStage = std::make_shared(); @@ -141,79 +117,69 @@ void DeferredLightingEffect::init() { _globalLights.push_back(_lightStage->addLight(lp)); _lightStage->addShadow(_globalLights[0]); -} -void DeferredLightingEffect::addLight(const model::LightPointer& light) { - assert(light); - auto lightID = _lightStage->addLight(light); - if (light->getType() == model::Light::POINT) { - _pointLights.push_back(lightID); - } else { - _spotLights.push_back(lightID); + _backgroundStage = std::make_shared(); + + auto textureCache = DependencyManager::get(); + + { + PROFILE_RANGE(render, "Process Default Skybox"); + auto textureCache = DependencyManager::get(); + + auto skyboxUrl = PathUtils::resourcesPath().toStdString() + "images/Default-Sky-9-cubemap.ktx"; + + _defaultSkyboxTexture = gpu::Texture::unserialize(skyboxUrl); + _defaultSkyboxAmbientTexture = _defaultSkyboxTexture; + + _defaultSkybox->setCubemap(_defaultSkyboxTexture); } -} -void DeferredLightingEffect::addPointLight(const glm::vec3& position, float radius, const glm::vec3& color, - float intensity, float falloffRadius) { - addSpotLight(position, radius, color, intensity, falloffRadius); -} - -void DeferredLightingEffect::addSpotLight(const glm::vec3& position, float radius, const glm::vec3& color, - float intensity, float falloffRadius, const glm::quat& orientation, float exponent, float cutoff) { - - unsigned int lightID = (unsigned int)(_pointLights.size() + _spotLights.size() + _globalLights.size()); - if (lightID >= _allocatedLights.size()) { - _allocatedLights.push_back(std::make_shared()); - } - model::LightPointer lp = _allocatedLights[lightID]; - - lp->setPosition(position); - lp->setMaximumRadius(radius); - lp->setColor(color); - lp->setIntensity(intensity); - lp->setFalloffRadius(falloffRadius); - - if (exponent == 0.0f && cutoff == PI) { - lp->setType(model::Light::POINT); - _pointLights.push_back(lightID); - - } else { - lp->setOrientation(orientation); - lp->setSpotAngle(cutoff); - lp->setSpotExponent(exponent); - lp->setType(model::Light::SPOT); - _spotLights.push_back(lightID); + lp->setAmbientIntensity(0.5f); + lp->setAmbientMap(_defaultSkyboxAmbientTexture); + auto irradianceSH = _defaultSkyboxAmbientTexture->getIrradiance(); + if (irradianceSH) { + lp->setAmbientSphere((*irradianceSH)); } } void DeferredLightingEffect::setupKeyLightBatch(gpu::Batch& batch, int lightBufferUnit, int ambientBufferUnit, int skyboxCubemapUnit) { PerformanceTimer perfTimer("DLE->setupBatch()"); - auto keyLight = _allocatedLights[_globalLights.front()]; + model::LightPointer keySunLight; + if (_lightStage && _lightStage->_currentFrame._sunLights.size()) { + keySunLight = _lightStage->getLight(_lightStage->_currentFrame._sunLights.front()); + } else { + keySunLight = _allocatedLights[_globalLights.front()]; + } + + model::LightPointer keyAmbiLight; + if (_lightStage && _lightStage->_currentFrame._ambientLights.size()) { + keyAmbiLight = _lightStage->getLight(_lightStage->_currentFrame._ambientLights.front()); + } else { + keyAmbiLight = _allocatedLights[_globalLights.front()]; + } if (lightBufferUnit >= 0) { - batch.setUniformBuffer(lightBufferUnit, keyLight->getLightSchemaBuffer()); + batch.setUniformBuffer(lightBufferUnit, keySunLight->getLightSchemaBuffer()); } - if (keyLight->hasAmbient() && (ambientBufferUnit >= 0)) { - batch.setUniformBuffer(ambientBufferUnit, keyLight->getAmbientSchemaBuffer()); + if (ambientBufferUnit >= 0) { + batch.setUniformBuffer(ambientBufferUnit, keyAmbiLight->getAmbientSchemaBuffer()); } - if (keyLight->getAmbientMap() && (skyboxCubemapUnit >= 0)) { - batch.setResourceTexture(skyboxCubemapUnit, keyLight->getAmbientMap()); + if (keyAmbiLight->getAmbientMap() && (skyboxCubemapUnit >= 0)) { + batch.setResourceTexture(skyboxCubemapUnit, keyAmbiLight->getAmbientMap()); } } void DeferredLightingEffect::unsetKeyLightBatch(gpu::Batch& batch, int lightBufferUnit, int ambientBufferUnit, int skyboxCubemapUnit) { - auto keyLight = _allocatedLights[_globalLights.front()]; - if (lightBufferUnit >= 0) { batch.setUniformBuffer(lightBufferUnit, nullptr); } - if (keyLight->hasAmbient() && (ambientBufferUnit >= 0)) { + if ((ambientBufferUnit >= 0)) { batch.setUniformBuffer(ambientBufferUnit, nullptr); } - if (keyLight->getAmbientMap() && (skyboxCubemapUnit >= 0)) { + if ((skyboxCubemapUnit >= 0)) { batch.setResourceTexture(skyboxCubemapUnit, nullptr); } } @@ -277,7 +243,7 @@ static void loadLightProgram(const char* vertSource, const char* fragSource, boo state->setColorWriteMask(true, true, true, false); if (lightVolume) { - state->setStencilTest(true, 0x00, gpu::State::StencilTest(1, 0xFF, gpu::LESS_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); + PrepareStencil::testShape(*state); state->setCullMode(gpu::State::CULL_BACK); // state->setCullMode(gpu::State::CULL_FRONT); @@ -290,7 +256,7 @@ static void loadLightProgram(const char* vertSource, const char* fragSource, boo } else { // Stencil test all the light passes for objects pixels only, not the background - state->setStencilTest(true, 0x00, gpu::State::StencilTest(0, 0x01, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); + PrepareStencil::testShape(*state); state->setCullMode(gpu::State::CULL_BACK); // additive blending @@ -300,49 +266,21 @@ static void loadLightProgram(const char* vertSource, const char* fragSource, boo } - -static void loadLightVolumeProgram(const char* vertSource, const char* fragSource, bool front, gpu::PipelinePointer& pipeline, LightLocationsPtr& locations) { - gpu::ShaderPointer program = makeLightProgram(vertSource, fragSource, locations); - - auto state = std::make_shared(); - - // Stencil test all the light passes for objects pixels only, not the background - - if (front) { - state->setCullMode(gpu::State::CULL_BACK); - state->setDepthTest(true, false, gpu::LESS_EQUAL); - state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_DECR, gpu::State::STENCIL_OP_KEEP)); - - // state->setDepthClampEnable(true); - // TODO: We should use DepthClamp and avoid changing geometry for inside /outside cases - // additive blending - // state->setBlendFunction(true, gpu::State::ONE, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - - //state->setColorWriteMask(true, true, true, false); - state->setColorWriteMask(false, false, false, false); - } else { - state->setCullMode(gpu::State::CULL_FRONT); - state->setDepthTest(true, false, gpu::LESS_EQUAL); - state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_INCR, gpu::State::STENCIL_OP_KEEP)); - // additive blending - // state->setBlendFunction(true, gpu::State::ONE, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - // state->setColorWriteMask(true, true, true, false); - state->setColorWriteMask(false, false, false, false); - } - pipeline = gpu::Pipeline::create(program, state); - -} - void DeferredLightingEffect::setGlobalLight(const model::LightPointer& light) { - auto globalLight = _allocatedLights.front(); + /* auto globalLight = _allocatedLights.front(); globalLight->setDirection(light->getDirection()); globalLight->setColor(light->getColor()); globalLight->setIntensity(light->getIntensity()); globalLight->setAmbientIntensity(light->getAmbientIntensity()); globalLight->setAmbientSphere(light->getAmbientSphere()); - globalLight->setAmbientMap(light->getAmbientMap()); + globalLight->setAmbientMap(light->getAmbientMap());*/ } +const model::LightPointer& DeferredLightingEffect::getGlobalLight() const { + return _allocatedLights.front(); +} + + #include model::MeshPointer DeferredLightingEffect::getPointLightMesh() { @@ -540,7 +478,7 @@ void PrepareDeferred::run(const RenderContextPointer& renderContext, const Input gpu::Framebuffer::BUFFER_COLOR0 | gpu::Framebuffer::BUFFER_COLOR1 | gpu::Framebuffer::BUFFER_COLOR2 | gpu::Framebuffer::BUFFER_COLOR3 | gpu::Framebuffer::BUFFER_DEPTH | gpu::Framebuffer::BUFFER_STENCIL, - vec4(vec3(0), 0), 1.0, 0.0, true); + vec4(vec3(0), 0), 1.0, 1, true); // For the rest of the rendering, bind the lighting model batch.setUniformBuffer(LIGHTING_MODEL_BUFFER_SLOT, lightingModel->getParametersBuffer()); @@ -566,10 +504,7 @@ void RenderDeferredSetup::run(const render::RenderContextPointer& renderContext, { // Framebuffer copy operations cannot function as multipass stereo operations. batch.enableStereo(false); - - // perform deferred lighting, rendering to free fbo - auto framebufferCache = DependencyManager::get(); - + auto textureCache = DependencyManager::get(); auto deferredLightingEffect = DependencyManager::get(); @@ -624,8 +559,8 @@ void RenderDeferredSetup::run(const render::RenderContextPointer& renderContext, batch.setResourceTexture(SHADOW_MAP_UNIT, globalShadow->map); } - auto& program = deferredLightingEffect->_shadowMapEnabled ? deferredLightingEffect->_directionalLightShadow : deferredLightingEffect->_directionalLight; - LightLocationsPtr locations = deferredLightingEffect->_shadowMapEnabled ? deferredLightingEffect->_directionalLightShadowLocations : deferredLightingEffect->_directionalLightLocations; + auto& program = deferredLightingEffect->_directionalSkyboxLight; + LightLocationsPtr locations = deferredLightingEffect->_directionalSkyboxLightLocations; const auto& keyLight = deferredLightingEffect->_allocatedLights[deferredLightingEffect->_globalLights.front()]; // Setup the global directional pass pipeline @@ -772,16 +707,6 @@ void RenderDeferredCleanup::run(const render::RenderContextPointer& renderContex batch.setUniformBuffer(LIGHT_CLUSTER_GRID_CLUSTER_CONTENT_SLOT, nullptr); } - - auto deferredLightingEffect = DependencyManager::get(); - - // End of the Lighting pass - if (!deferredLightingEffect->_pointLights.empty()) { - deferredLightingEffect->_pointLights.clear(); - } - if (!deferredLightingEffect->_spotLights.empty()) { - deferredLightingEffect->_spotLights.clear(); - } } RenderDeferred::RenderDeferred() { diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h index dcf0c84622..c171973216 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.h +++ b/libraries/render-utils/src/DeferredLightingEffect.h @@ -20,6 +20,8 @@ #include "model/Light.h" #include "model/Geometry.h" +#include + #include #include "DeferredFrameTransform.h" @@ -28,11 +30,13 @@ #include "LightStage.h" #include "LightClusters.h" +#include "BackgroundStage.h" #include "SurfaceGeometryPass.h" #include "SubsurfaceScattering.h" #include "AmbientOcclusionEffect.h" + class RenderArgs; struct LightLocations; using LightLocationsPtr = std::shared_ptr; @@ -43,34 +47,30 @@ class DeferredLightingEffect : public Dependency { public: void init(); - - void addLight(const model::LightPointer& light); - - /// Adds a point light to render for the current frame. - void addPointLight(const glm::vec3& position, float radius, const glm::vec3& color = glm::vec3(0.0f, 0.0f, 0.0f), - float intensity = 0.5f, float falloffRadius = 0.01f); - - /// Adds a spot light to render for the current frame. - void addSpotLight(const glm::vec3& position, float radius, const glm::vec3& color = glm::vec3(1.0f, 1.0f, 1.0f), - float intensity = 0.5f, float falloffRadius = 0.01f, - const glm::quat& orientation = glm::quat(), float exponent = 0.0f, float cutoff = PI); - + void setupKeyLightBatch(gpu::Batch& batch, int lightBufferUnit, int ambientBufferUnit, int skyboxCubemapUnit); void unsetKeyLightBatch(gpu::Batch& batch, int lightBufferUnit, int ambientBufferUnit, int skyboxCubemapUnit); // update global lighting void setGlobalLight(const model::LightPointer& light); + const model::LightPointer& getGlobalLight() const; - const LightStagePointer getLightStage() { return _lightStage; } + const LightStagePointer& getLightStage() { return _lightStage; } + const BackgroundStagePointer& getBackgroundStage() { return _backgroundStage; } void setShadowMapEnabled(bool enable) { _shadowMapEnabled = enable; }; void setAmbientOcclusionEnabled(bool enable) { _ambientOcclusionEnabled = enable; } bool isAmbientOcclusionEnabled() const { return _ambientOcclusionEnabled; } + model::SkyboxPointer getDefaultSkybox() const { return _defaultSkybox; } + gpu::TexturePointer getDefaultSkyboxTexture() const { return _defaultSkyboxTexture; } + gpu::TexturePointer getDefaultSkyboxAmbientTexture() const { return _defaultSkyboxAmbientTexture; } + private: DeferredLightingEffect() = default; LightStagePointer _lightStage; + BackgroundStagePointer _backgroundStage; bool _shadowMapEnabled{ false }; bool _ambientOcclusionEnabled{ false }; @@ -82,39 +82,30 @@ private: gpu::PipelinePointer _directionalSkyboxLight; gpu::PipelinePointer _directionalAmbientSphereLight; - gpu::PipelinePointer _directionalLight; gpu::PipelinePointer _directionalSkyboxLightShadow; gpu::PipelinePointer _directionalAmbientSphereLightShadow; - gpu::PipelinePointer _directionalLightShadow; gpu::PipelinePointer _localLight; gpu::PipelinePointer _localLightOutline; - gpu::PipelinePointer _pointLightBack; - gpu::PipelinePointer _pointLightFront; - gpu::PipelinePointer _spotLightBack; - gpu::PipelinePointer _spotLightFront; - LightLocationsPtr _directionalSkyboxLightLocations; LightLocationsPtr _directionalAmbientSphereLightLocations; - LightLocationsPtr _directionalLightLocations; LightLocationsPtr _directionalSkyboxLightShadowLocations; LightLocationsPtr _directionalAmbientSphereLightShadowLocations; - LightLocationsPtr _directionalLightShadowLocations; LightLocationsPtr _localLightLocations; LightLocationsPtr _localLightOutlineLocations; - LightLocationsPtr _pointLightLocations; - LightLocationsPtr _spotLightLocations; using Lights = std::vector; Lights _allocatedLights; std::vector _globalLights; - std::vector _pointLights; - std::vector _spotLights; + + model::SkyboxPointer _defaultSkybox { new ProceduralSkybox() }; + gpu::TexturePointer _defaultSkyboxTexture; + gpu::TexturePointer _defaultSkyboxAmbientTexture; friend class LightClusteringPass; friend class RenderDeferredSetup; diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index f1c995b943..dcf90012c1 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -24,6 +24,7 @@ #include "TextureCache.h" #include "RenderUtilsLogging.h" +#include "StencilMaskPass.h" #include "gpu/StandardShaderLib.h" @@ -68,7 +69,7 @@ std::vector polygon() { std::vector result; result.reserve(SIDES); double angleIncrement = 2.0 * M_PI / SIDES; - for (size_t i = 0; i < SIDES; ++i) { + for (size_t i = 0; i < SIDES; i++) { double angle = (double)i * angleIncrement; result.push_back(vec3{ cos(angle) * 0.5, 0.0, sin(angle) * 0.5 }); } @@ -171,20 +172,20 @@ void setupFlatShape(GeometryCache::ShapeData& shapeData, const geometry::Solid& face = shape.faces[f]; // Compute the face normal vec3 faceNormal = shape.getFaceNormal(f); // Create the vertices for the face - for (Index i = 0; i < N; ++i) { + for (Index i = 0; i < N; i++) { Index originalIndex = face[i]; vertices.push_back(shape.vertices[originalIndex]); vertices.push_back(faceNormal); } // Create the wire indices for unseen edges - for (Index i = 0; i < N; ++i) { + for (Index i = 0; i < N; i++) { Index a = i; Index b = (i + 1) % N; auto token = indexToken(face[a], face[b]); @@ -196,7 +197,7 @@ void setupFlatShape(GeometryCache::ShapeData& shapeData, const geometry::Solid& face = shape.faces[f]; // Create the wire indices for unseen edges - for (Index i = 0; i < N; ++i) { + for (Index i = 0; i < N; i++) { Index a = face[i]; Index b = face[(i + 1) % N]; auto token = indexToken(a, b); @@ -243,7 +244,7 @@ void setupSmoothShape(GeometryCache::ShapeData& shapeData, const geometry::Solid } // Create the solid face indices - for (Index i = 0; i < N - 2; ++i) { + for (Index i = 0; i < N - 2; i++) { solidIndices.push_back(face[i] + baseVertex); solidIndices.push_back(face[i + 1] + baseVertex); solidIndices.push_back(face[i + 2] + baseVertex); @@ -255,23 +256,30 @@ void setupSmoothShape(GeometryCache::ShapeData& shapeData, const geometry::Solid } template -void extrudePolygon(GeometryCache::ShapeData& shapeData, gpu::BufferPointer& vertexBuffer, gpu::BufferPointer& indexBuffer) { +void extrudePolygon(GeometryCache::ShapeData& shapeData, gpu::BufferPointer& vertexBuffer, gpu::BufferPointer& indexBuffer, bool isConical = false) { using namespace geometry; Index baseVertex = (Index)(vertexBuffer->getSize() / SHAPE_VERTEX_STRIDE); VertexVector vertices; IndexVector solidIndices, wireIndices; - // Top and bottom faces + // Top (if not conical) and bottom faces std::vector shape = polygon(); - for (const vec3& v : shape) { - vertices.push_back(vec3(v.x, 0.5f, v.z)); - vertices.push_back(vec3(0, 1, 0)); + if (isConical) { + for (uint32_t i = 0; i < N; i++) { + vertices.push_back(vec3(0.0f, 0.5f, 0.0f)); + vertices.push_back(vec3(0.0f, 1.0f, 0.0f)); + } + } else { + for (const vec3& v : shape) { + vertices.push_back(vec3(v.x, 0.5f, v.z)); + vertices.push_back(vec3(0.0f, 1.0f, 0.0f)); + } } for (const vec3& v : shape) { vertices.push_back(vec3(v.x, -0.5f, v.z)); - vertices.push_back(vec3(0, -1, 0)); + vertices.push_back(vec3(0.0f, -1.0f, 0.0f)); } - for (uint32_t i = 2; i < N; ++i) { + for (uint32_t i = 2; i < N; i++) { solidIndices.push_back(baseVertex + 0); solidIndices.push_back(baseVertex + i); solidIndices.push_back(baseVertex + i - 1); @@ -279,7 +287,7 @@ void extrudePolygon(GeometryCache::ShapeData& shapeData, gpu::BufferPointer& ver solidIndices.push_back(baseVertex + i + N - 1); solidIndices.push_back(baseVertex + i + N); } - for (uint32_t i = 1; i <= N; ++i) { + for (uint32_t i = 1; i <= N; i++) { wireIndices.push_back(baseVertex + (i % N)); wireIndices.push_back(baseVertex + i - 1); wireIndices.push_back(baseVertex + (i % N) + N); @@ -289,12 +297,12 @@ void extrudePolygon(GeometryCache::ShapeData& shapeData, gpu::BufferPointer& ver // Now do the sides baseVertex += 2 * N; - for (uint32_t i = 0; i < N; ++i) { + for (uint32_t i = 0; i < N; i++) { vec3 left = shape[i]; vec3 right = shape[(i + 1) % N]; vec3 normal = glm::normalize(left + right); - vec3 topLeft = vec3(left.x, 0.5f, left.z); - vec3 topRight = vec3(right.x, 0.5f, right.z); + vec3 topLeft = (isConical ? vec3(0.0f, 0.5f, 0.0f) : vec3(left.x, 0.5f, left.z)); + vec3 topRight = (isConical ? vec3(0.0f, 0.5f, 0.0f) : vec3(right.x, 0.5f, right.z)); vec3 bottomLeft = vec3(left.x, -0.5f, left.z); vec3 bottomRight = vec3(right.x, -0.5f, right.z); @@ -324,6 +332,41 @@ void extrudePolygon(GeometryCache::ShapeData& shapeData, gpu::BufferPointer& ver shapeData.setupIndices(indexBuffer, solidIndices, wireIndices); } +void drawCircle(GeometryCache::ShapeData& shapeData, gpu::BufferPointer& vertexBuffer, gpu::BufferPointer& indexBuffer) { + // Draw a circle with radius 1/4th the size of the bounding box + using namespace geometry; + + Index baseVertex = (Index)(vertexBuffer->getSize() / SHAPE_VERTEX_STRIDE); + VertexVector vertices; + IndexVector solidIndices, wireIndices; + const int NUM_CIRCLE_VERTICES = 64; + + std::vector shape = polygon(); + for (const vec3& v : shape) { + vertices.push_back(vec3(v.x, 0.0f, v.z)); + vertices.push_back(vec3(0.0f, 0.0f, 0.0f)); + } + + for (uint32_t i = 2; i < NUM_CIRCLE_VERTICES; i++) { + solidIndices.push_back(baseVertex + 0); + solidIndices.push_back(baseVertex + i); + solidIndices.push_back(baseVertex + i - 1); + solidIndices.push_back(baseVertex + NUM_CIRCLE_VERTICES); + solidIndices.push_back(baseVertex + i + NUM_CIRCLE_VERTICES - 1); + solidIndices.push_back(baseVertex + i + NUM_CIRCLE_VERTICES); + } + + for (uint32_t i = 1; i <= NUM_CIRCLE_VERTICES; i++) { + wireIndices.push_back(baseVertex + (i % NUM_CIRCLE_VERTICES)); + wireIndices.push_back(baseVertex + i - 1); + wireIndices.push_back(baseVertex + (i % NUM_CIRCLE_VERTICES) + NUM_CIRCLE_VERTICES); + wireIndices.push_back(baseVertex + (i - 1) + NUM_CIRCLE_VERTICES); + } + + shapeData.setupVertices(vertexBuffer, vertices); + shapeData.setupIndices(indexBuffer, solidIndices, wireIndices); +} + // FIXME solids need per-face vertices, but smooth shaded // components do not. Find a way to support using draw elements // or draw arrays as appropriate @@ -356,8 +399,8 @@ void GeometryCache::buildShapes() { Index baseVertex = (Index)(_shapeVertices->getSize() / SHAPE_VERTEX_STRIDE); ShapeData& shapeData = _shapes[Line]; shapeData.setupVertices(_shapeVertices, VertexVector { - vec3(-0.5, 0, 0), vec3(-0.5f, 0, 0), - vec3(0.5f, 0, 0), vec3(0.5f, 0, 0) + vec3(-0.5f, 0.0f, 0.0f), vec3(-0.5f, 0.0f, 0.0f), + vec3(0.5f, 0.0f, 0.0f), vec3(0.5f, 0.0f, 0.0f) }); IndexVector wireIndices; // Only two indices @@ -366,20 +409,22 @@ void GeometryCache::buildShapes() { shapeData.setupIndices(_shapeIndices, IndexVector(), wireIndices); } - // Not implememented yet: - //Triangle, extrudePolygon<3>(_shapes[Triangle], _shapeVertices, _shapeIndices); //Hexagon, extrudePolygon<6>(_shapes[Hexagon], _shapeVertices, _shapeIndices); //Octagon, extrudePolygon<8>(_shapes[Octagon], _shapeVertices, _shapeIndices); - - //Quad, - //Circle, - //Torus, - //Cone, //Cylinder, + extrudePolygon<64>(_shapes[Cylinder], _shapeVertices, _shapeIndices); + //Cone, + extrudePolygon<64>(_shapes[Cone], _shapeVertices, _shapeIndices, true); + //Circle + drawCircle(_shapes[Circle], _shapeVertices, _shapeIndices); + // Not implememented yet: + //Quad, + //Torus, + } gpu::Stream::FormatPointer& getSolidStreamFormat() { @@ -596,7 +641,7 @@ void GeometryCache::updateVertices(int id, const QVector& points, con auto pointCount = points.size(); auto colorCount = colors.size(); int compactColor = 0; - for (auto i = 0; i < pointCount; ++i) { + for (auto i = 0; i < pointCount; i++) { const auto& point = points[i]; *(vertex++) = point.x; *(vertex++) = point.y; @@ -673,7 +718,7 @@ void GeometryCache::updateVertices(int id, const QVector& points, con const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f); auto pointCount = points.size(); auto colorCount = colors.size(); - for (auto i = 0; i < pointCount; ++i) { + for (auto i = 0; i < pointCount; i++) { const glm::vec3& point = points[i]; if (i < colorCount) { const glm::vec4& color = colors[i]; @@ -1610,6 +1655,9 @@ void GeometryCache::renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + + PrepareStencil::testMask(*state); + gpu::Shader::BindingSet slotBindings; slotBindings.insert(gpu::Shader::Binding(std::string("lineData"), LINE_DATA_SLOT)); gpu::Shader::makeProgram(*program, slotBindings); @@ -1663,11 +1711,14 @@ void GeometryCache::useSimpleDrawPipeline(gpu::Batch& batch, bool noBlend) { // enable decal blend state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA); + PrepareStencil::testMask(*state); _standardDrawPipeline = gpu::Pipeline::create(program, state); auto stateNoBlend = std::make_shared(); + PrepareStencil::testMaskDrawShape(*state); + auto noBlendPS = gpu::StandardShaderLib::getDrawTextureOpaquePS(); auto programNoBlend = gpu::Shader::createProgram(vs, noBlendPS); gpu::Shader::makeProgram((*programNoBlend)); @@ -1690,12 +1741,14 @@ void GeometryCache::useGridPipeline(gpu::Batch& batch, GridBuffer gridBuffer, bo auto stateLayered = std::make_shared(); stateLayered->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA); + PrepareStencil::testMask(*stateLayered); _gridPipelineLayered = gpu::Pipeline::create(program, stateLayered); auto state = std::make_shared(stateLayered->getValues()); const float DEPTH_BIAS = 0.001f; state->setDepthBias(DEPTH_BIAS); state->setDepthTest(true, false, gpu::LESS_EQUAL); + PrepareStencil::testMaskDrawShape(*state); _gridPipeline = gpu::Pipeline::create(program, state); } @@ -1773,6 +1826,11 @@ static void buildWebShader(const std::string& vertShaderText, const std::string& state->setBlendFunction(blendEnable, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + if (blendEnable) { + PrepareStencil::testMask(*state); + } else { + PrepareStencil::testMaskDrawShape(*state); + } pipelinePointerOut = gpu::Pipeline::create(shaderPointerOut, state); } @@ -1858,6 +1916,12 @@ gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool transp gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + if (config.isTransparent()) { + PrepareStencil::testMask(*state); + } else { + PrepareStencil::testMaskDrawShape(*state); + } + gpu::ShaderPointer program = (config.isUnlit()) ? _unlitShader : _simpleShader; gpu::PipelinePointer pipeline = gpu::Pipeline::create(program, state); _simplePrograms.insert(config, pipeline); diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index e0a610a095..9853269280 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -142,8 +142,8 @@ public: Dodecahedron, Icosahedron, Torus, // not yet implemented - Cone, // not yet implemented - Cylinder, // not yet implemented + Cone, + Cylinder, NUM_SHAPES, }; diff --git a/libraries/render-utils/src/HitEffect.cpp b/libraries/render-utils/src/HitEffect.cpp deleted file mode 100644 index 319e94384f..0000000000 --- a/libraries/render-utils/src/HitEffect.cpp +++ /dev/null @@ -1,94 +0,0 @@ -// -// HitEffect.cpp -// interface/src/renderer -// -// Created by Andrzej Kapolka on 7/14/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 this before QOpenGLFramebufferObject, which includes an earlier version of OpenGL - - -#include - -#include -#include -#include - -#include "AbstractViewStateInterface.h" -#include "HitEffect.h" - -#include "TextureCache.h" -#include "DependencyManager.h" -#include "ViewFrustum.h" -#include "GeometryCache.h" - -#include - -#include "hit_effect_vert.h" -#include "hit_effect_frag.h" - - -HitEffect::HitEffect() { - _geometryId = DependencyManager::get()->allocateID(); -} - -HitEffect::~HitEffect() { - auto geometryCache = DependencyManager::get(); - if (_geometryId && geometryCache) { - geometryCache->releaseID(_geometryId); - } -} - -const gpu::PipelinePointer& HitEffect::getHitEffectPipeline() { - if (!_hitEffectPipeline) { - auto vs = gpu::Shader::createVertex(std::string(hit_effect_vert)); - auto ps = gpu::Shader::createPixel(std::string(hit_effect_frag)); - gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); - - - gpu::Shader::BindingSet slotBindings; - gpu::Shader::makeProgram(*program, slotBindings); - - - gpu::StatePointer state = gpu::StatePointer(new gpu::State()); - - state->setDepthTest(false, false, gpu::LESS_EQUAL); - - // Blend on transparent - state->setBlendFunction(true, - gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA); - - // Good to go add the brand new pipeline - _hitEffectPipeline = gpu::Pipeline::create(program, state); - } - return _hitEffectPipeline; -} - -void HitEffect::run(const render::RenderContextPointer& renderContext) { - assert(renderContext->args); - assert(renderContext->args->hasViewFrustum()); - RenderArgs* args = renderContext->args; - - gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { - - glm::mat4 projMat; - Transform viewMat; - args->getViewFrustum().evalProjectionMatrix(projMat); - args->getViewFrustum().evalViewTransform(viewMat); - batch.setProjectionTransform(projMat); - batch.setViewTransform(viewMat); - batch.setModelTransform(Transform()); - - batch.setPipeline(getHitEffectPipeline()); - - static const glm::vec4 color(0.0f, 0.0f, 0.0f, 1.0f); - static const glm::vec2 bottomLeft(-1.0f, -1.0f); - static const glm::vec2 topRight(1.0f, 1.0f); - DependencyManager::get()->renderQuad(batch, bottomLeft, topRight, color, _geometryId); - }); -} - diff --git a/libraries/render-utils/src/HitEffect.h b/libraries/render-utils/src/HitEffect.h deleted file mode 100644 index d025d2d980..0000000000 --- a/libraries/render-utils/src/HitEffect.h +++ /dev/null @@ -1,38 +0,0 @@ -// -// hitEffect.h -// hifi -// -// Created by eric levin on 7/17/15. -// -// - -#ifndef hifi_hitEffect_h -#define hifi_hitEffect_h - -#include - -class HitEffectConfig : public render::Job::Config { - Q_OBJECT - Q_PROPERTY(bool enabled MEMBER enabled) -public: - HitEffectConfig() : render::Job::Config(false) {} -}; - -class HitEffect { -public: - using Config = HitEffectConfig; - using JobModel = render::Job::Model; - - HitEffect(); - ~HitEffect(); - void configure(const Config& config) {} - void run(const render::RenderContextPointer& renderContext); - - const gpu::PipelinePointer& getHitEffectPipeline(); - -private: - int _geometryId { 0 }; - gpu::PipelinePointer _hitEffectPipeline; -}; - -#endif diff --git a/libraries/render-utils/src/LightClusters.cpp b/libraries/render-utils/src/LightClusters.cpp index 4145264b2d..7e04b1c2a4 100644 --- a/libraries/render-utils/src/LightClusters.cpp +++ b/libraries/render-utils/src/LightClusters.cpp @@ -16,6 +16,8 @@ #include +#include "StencilMaskPass.h" + #include "lightClusters_drawGrid_vert.h" #include "lightClusters_drawGrid_frag.h" diff --git a/libraries/render-utils/src/LightClusters.h b/libraries/render-utils/src/LightClusters.h index 105d6fb139..f495dabebb 100644 --- a/libraries/render-utils/src/LightClusters.h +++ b/libraries/render-utils/src/LightClusters.h @@ -152,9 +152,9 @@ public: int numInputLights { 0 }; int numClusteredLights { 0 }; - void setNumClusteredLightReferences(int numRefs) { numClusteredLightReferences = numRefs; emit dirty(); } - void setNumInputLights(int numLights) { numInputLights = numLights; emit dirty(); } - void setNumClusteredLights(int numLights) { numClusteredLights = numLights; emit dirty(); } + void setNumClusteredLightReferences(int numRefs) { numClusteredLightReferences = numRefs; } + void setNumInputLights(int numLights) { numInputLights = numLights; } + void setNumClusteredLights(int numLights) { numClusteredLights = numLights; } int numSceneLights { 0 }; int numFreeSceneLights { 0 }; diff --git a/libraries/render-utils/src/LightPayload.cpp b/libraries/render-utils/src/LightPayload.cpp index a670c9f620..dbdf7129ef 100644 --- a/libraries/render-utils/src/LightPayload.cpp +++ b/libraries/render-utils/src/LightPayload.cpp @@ -81,3 +81,71 @@ void LightPayload::render(RenderArgs* args) { } } + +namespace render { + template <> const ItemKey payloadGetKey(const KeyLightPayload::Pointer& payload) { + ItemKey::Builder builder; + builder.withTypeLight(); + if (!payload || !payload->isVisible()) { + builder.withInvisible(); + } + return builder.build(); + } + + template <> const Item::Bound payloadGetBound(const KeyLightPayload::Pointer& payload) { + if (payload) { + return payload->editBound(); + } + return render::Item::Bound(); + } + template <> void payloadRender(const KeyLightPayload::Pointer& payload, RenderArgs* args) { + if (args) { + if (payload) { + payload->render(args); + } + } + } +} + +KeyLightPayload::KeyLightPayload() : +_light(std::make_shared()) +{ +} + + +KeyLightPayload::~KeyLightPayload() { + if (!LightStage::isIndexInvalid(_index)) { + if (_stage) { + _stage->removeLight(_index); + } + } +} + +void KeyLightPayload::render(RenderArgs* args) { + if (!_stage) { + _stage = DependencyManager::get()->getLightStage(); + } + // Do we need to allocate the light in the stage ? + if (LightStage::isIndexInvalid(_index)) { + _index = _stage->addLight(_light); + _needUpdate = false; + } + // Need an update ? + if (_needUpdate) { + _stage->updateLightArrayBuffer(_index); + _needUpdate = false; + } + + if (isVisible()) { + // FInally, push the light visible in the frame + _stage->_currentFrame.pushLight(_index, _light->getType()); + +#ifdef WANT_DEBUG + Q_ASSERT(args->_batch); + gpu::Batch& batch = *args->_batch; + batch.setModelTransform(getTransformToCenter()); + DependencyManager::get()->renderWireSphere(batch, 0.5f, 15, 15, glm::vec4(color, 1.0f)); +#endif + } +} + diff --git a/libraries/render-utils/src/LightPayload.h b/libraries/render-utils/src/LightPayload.h index 6b5fe83f07..0cf8f8e2de 100644 --- a/libraries/render-utils/src/LightPayload.h +++ b/libraries/render-utils/src/LightPayload.h @@ -15,6 +15,7 @@ #include #include #include "LightStage.h" +#include "TextureCache.h" class LightPayload { public: @@ -46,4 +47,41 @@ namespace render { template <> void payloadRender(const LightPayload::Pointer& payload, RenderArgs* args); } +class KeyLightPayload { +public: + using Payload = render::Payload; + using Pointer = Payload::DataPointer; + + KeyLightPayload(); + ~KeyLightPayload(); + void render(RenderArgs* args); + + model::LightPointer editLight() { _needUpdate = true; return _light; } + render::Item::Bound& editBound() { _needUpdate = true; return _bound; } + + void setVisible(bool visible) { _isVisible = visible; } + bool isVisible() const { return _isVisible; } + + + // More attributes used for rendering: + NetworkTexturePointer _ambientTexture; + QString _ambientTextureURL; + bool _pendingAmbientTexture { false }; + bool _validAmbientTextureURL { false }; + +protected: + model::LightPointer _light; + render::Item::Bound _bound; + LightStagePointer _stage; + LightStage::Index _index { LightStage::INVALID_INDEX }; + bool _needUpdate { true }; + bool _isVisible { true }; +}; + +namespace render { + template <> const ItemKey payloadGetKey(const KeyLightPayload::Pointer& payload); + template <> const Item::Bound payloadGetBound(const KeyLightPayload::Pointer& payload); + template <> void payloadRender(const KeyLightPayload::Pointer& payload, RenderArgs* args); +} + #endif \ No newline at end of file diff --git a/libraries/render-utils/src/LightStage.h b/libraries/render-utils/src/LightStage.h index c2293ac099..edbdff28fd 100644 --- a/libraries/render-utils/src/LightStage.h +++ b/libraries/render-utils/src/LightStage.h @@ -77,7 +77,6 @@ public: }; using Descs = std::vector; - Index findLight(const LightPointer& light) const; Index addLight(const LightPointer& light); @@ -118,19 +117,25 @@ public: public: Frame() {} - void clear() { _pointLights.clear(); _spotLights.clear(); } + void clear() { _pointLights.clear(); _spotLights.clear(); _sunLights.clear(); _ambientLights.clear(); } void pushLight(LightStage::Index index, model::Light::Type type) { switch (type) { case model::Light::POINT: { pushPointLight(index); break; } case model::Light::SPOT: { pushSpotLight(index); break; } + case model::Light::SUN: { pushSunLight(index); break; } + case model::Light::AMBIENT: { pushAmbientLight(index); break; } default: { break; } } } void pushPointLight(LightStage::Index index) { _pointLights.emplace_back(index); } void pushSpotLight(LightStage::Index index) { _spotLights.emplace_back(index); } - + void pushSunLight(LightStage::Index index) { _sunLights.emplace_back(index); } + void pushAmbientLight(LightStage::Index index) { _ambientLights.emplace_back(index); } + LightStage::LightIndices _pointLights; LightStage::LightIndices _spotLights; + LightStage::LightIndices _sunLights; + LightStage::LightIndices _ambientLights; }; Frame _currentFrame; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index e5a25d733e..447d0e37bd 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -332,7 +332,7 @@ void Model::initJointStates() { bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - QString& extraInfo, bool pickAgainstTriangles) { + QString& extraInfo, bool pickAgainstTriangles, bool allowBackface) { bool intersectedSomething = false; @@ -381,7 +381,7 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g float triangleSetDistance = 0.0f; BoxFace triangleSetFace; glm::vec3 triangleSetNormal; - if (triangleSet.findRayIntersection(meshFrameOrigin, meshFrameDirection, triangleSetDistance, triangleSetFace, triangleSetNormal, pickAgainstTriangles)) { + if (triangleSet.findRayIntersection(meshFrameOrigin, meshFrameDirection, triangleSetDistance, triangleSetFace, triangleSetNormal, pickAgainstTriangles, allowBackface)) { glm::vec3 meshIntersectionPoint = meshFrameOrigin + (meshFrameDirection * triangleSetDistance); glm::vec3 worldIntersectionPoint = glm::vec3(meshToWorldMatrix * glm::vec4(meshIntersectionPoint, 1.0f)); diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index d718145d66..53d446d306 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -156,7 +156,7 @@ public: bool findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - QString& extraInfo, bool pickAgainstTriangles = false); + QString& extraInfo, bool pickAgainstTriangles = false, bool allowBackface = false); void setOffset(const glm::vec3& offset); const glm::vec3& getOffset() const { return _offset; } diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 313b176f19..1b99fe92ee 100644 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -27,12 +27,12 @@ #include #include "LightingModel.h" +#include "StencilMaskPass.h" #include "DebugDeferredBuffer.h" #include "DeferredFramebuffer.h" #include "DeferredLightingEffect.h" #include "SurfaceGeometryPass.h" #include "FramebufferCache.h" -#include "HitEffect.h" #include "TextureCache.h" #include "ZoneRenderer.h" @@ -43,8 +43,6 @@ #include -#include "drawOpaqueStencil_frag.h" - using namespace render; extern void initOverlay3DPipelines(render::ShapePlumber& plumber); @@ -75,7 +73,6 @@ void RenderDeferredTask::build(JobModel& task, const render::Varying& input, ren const auto deferredFrameTransform = task.addJob("DeferredFrameTransform"); const auto lightingModel = task.addJob("LightingModel"); - // GPU jobs: Start preparing the primary, deferred and lighting buffer const auto primaryFramebuffer = task.addJob("PreparePrimaryBuffer"); @@ -86,13 +83,13 @@ void RenderDeferredTask::build(JobModel& task, const render::Varying& input, ren const auto deferredFramebuffer = prepareDeferredOutputs.getN(0); const auto lightingFramebuffer = prepareDeferredOutputs.getN(1); + // draw a stencil mask in hidden regions of the framebuffer. + task.addJob("PrepareStencil", primaryFramebuffer); + // Render opaque objects in DeferredBuffer const auto opaqueInputs = DrawStateSortDeferred::Inputs(opaques, lightingModel).hasVarying(); task.addJob("DrawOpaqueDeferred", opaqueInputs, shapePlumber); - // Once opaque is all rendered create stencil background - task.addJob("DrawOpaqueStencil", deferredFramebuffer); - task.addJob("OpaqueRangeTimer", opaqueRangeTimer); @@ -124,6 +121,9 @@ void RenderDeferredTask::build(JobModel& task, const render::Varying& input, ren // Draw Lights just add the lights to the current list of lights to deal with. NOt really gpu job for now. task.addJob("DrawLight", lights); + // Filter zones from the general metas bucket + const auto zones = task.addJob("ZoneRenderer", metas); + // Light Clustering // Create the cluster grid of lights, cpu job for now const auto lightClusteringPassInputs = LightClusteringPass::Inputs(deferredFrameTransform, lightingModel, linearDepthTarget).hasVarying(); @@ -136,11 +136,9 @@ void RenderDeferredTask::build(JobModel& task, const render::Varying& input, ren task.addJob("RenderDeferred", deferredLightingInputs); - // Use Stencil and draw background in Lighting buffer to complete filling in the opaque - const auto backgroundInputs = DrawBackgroundDeferred::Inputs(background, lightingModel).hasVarying(); - task.addJob("DrawBackgroundDeferred", backgroundInputs); - - + // Similar to light stage, background stage has been filled by several potential render items and resolved for the frame in this job + task.addJob("DrawBackgroundDeferred", lightingModel); + // Render transparent objects forward in LightingBuffer const auto transparentsInputs = DrawDeferred::Inputs(transparents, lightingModel).hasVarying(); task.addJob("DrawTransparentDeferred", transparentsInputs, shapePlumber); @@ -162,7 +160,8 @@ void RenderDeferredTask::build(JobModel& task, const render::Varying& input, ren task.addJob("DrawOpaqueBounds", opaques); task.addJob("DrawTransparentBounds", transparents); - task.addJob("ZoneRenderer", opaques); + task.addJob("DrawLightBounds", lights); + task.addJob("DrawZones", zones); } // Overlays @@ -202,6 +201,8 @@ void RenderDeferredTask::build(JobModel& task, const render::Varying& input, ren auto statusIconMap = DependencyManager::get()->getImageTexture(iconMapPath, image::TextureUsage::STRICT_TEXTURE); task.addJob("DrawStatus", opaques, DrawStatus(statusIconMap)); } + + task.addJob("DrawZoneStack", deferredFrameTransform); } @@ -384,88 +385,6 @@ void DrawOverlay3D::run(const RenderContextPointer& renderContext, const Inputs& } } - -gpu::PipelinePointer DrawStencilDeferred::getOpaquePipeline() { - if (!_opaquePipeline) { - const gpu::int8 STENCIL_OPAQUE = 1; - auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); - auto ps = gpu::Shader::createPixel(std::string(drawOpaqueStencil_frag)); - auto program = gpu::Shader::createProgram(vs, ps); - gpu::Shader::makeProgram((*program)); - - auto state = std::make_shared(); - state->setDepthTest(true, false, gpu::LESS_EQUAL); - state->setStencilTest(true, 0xFF, gpu::State::StencilTest(STENCIL_OPAQUE, 0xFF, gpu::ALWAYS, gpu::State::STENCIL_OP_REPLACE, gpu::State::STENCIL_OP_REPLACE, gpu::State::STENCIL_OP_KEEP)); - state->setColorWriteMask(0); - - _opaquePipeline = gpu::Pipeline::create(program, state); - } - return _opaquePipeline; -} - -void DrawStencilDeferred::run(const RenderContextPointer& renderContext, const DeferredFramebufferPointer& deferredFramebuffer) { - assert(renderContext->args); - assert(renderContext->args->hasViewFrustum()); - - // from the touched pixel generate the stencil buffer - RenderArgs* args = renderContext->args; - doInBatch(args->_context, [&](gpu::Batch& batch) { - args->_batch = &batch; - - auto deferredFboColorDepthStencil = deferredFramebuffer->getDeferredFramebufferDepthColor(); - - - batch.enableStereo(false); - - batch.setFramebuffer(deferredFboColorDepthStencil); - batch.setViewportTransform(args->_viewport); - batch.setStateScissorRect(args->_viewport); - - batch.setPipeline(getOpaquePipeline()); - - batch.draw(gpu::TRIANGLE_STRIP, 4); - batch.setResourceTexture(0, nullptr); - - }); - args->_batch = nullptr; -} - -void DrawBackgroundDeferred::run(const RenderContextPointer& renderContext, const Inputs& inputs) { - assert(renderContext->args); - assert(renderContext->args->hasViewFrustum()); - - const auto& inItems = inputs.get0(); - const auto& lightingModel = inputs.get1(); - if (!lightingModel->isBackgroundEnabled()) { - return; - } - - RenderArgs* args = renderContext->args; - doInBatch(args->_context, [&](gpu::Batch& batch) { - args->_batch = &batch; - // _gpuTimer.begin(batch); - - batch.enableSkybox(true); - - batch.setViewportTransform(args->_viewport); - batch.setStateScissorRect(args->_viewport); - - glm::mat4 projMat; - Transform viewMat; - args->getViewFrustum().evalProjectionMatrix(projMat); - args->getViewFrustum().evalViewTransform(viewMat); - - batch.setProjectionTransform(projMat); - batch.setViewTransform(viewMat); - - renderItems(renderContext, inItems); - // _gpuTimer.end(batch); - }); - args->_batch = nullptr; - - // std::static_pointer_cast(renderContext->jobConfig)->gpuTime = _gpuTimer.getAverage(); -} - void Blit::run(const RenderContextPointer& renderContext, const gpu::FramebufferPointer& srcFramebuffer) { assert(renderContext->args); assert(renderContext->args->_context); @@ -535,3 +454,4 @@ void Blit::run(const RenderContextPointer& renderContext, const gpu::Framebuffer } }); } + diff --git a/libraries/render-utils/src/RenderDeferredTask.h b/libraries/render-utils/src/RenderDeferredTask.h index 12ecd5ecaf..fd7c5eb23b 100644 --- a/libraries/render-utils/src/RenderDeferredTask.h +++ b/libraries/render-utils/src/RenderDeferredTask.h @@ -120,35 +120,6 @@ protected: bool _stateSort; }; -class DeferredFramebuffer; -class DrawStencilDeferred { -public: - using JobModel = render::Job::ModelI>; - - void run(const render::RenderContextPointer& renderContext, const std::shared_ptr& deferredFramebuffer); - -protected: - gpu::PipelinePointer _opaquePipeline; - - gpu::PipelinePointer getOpaquePipeline(); -}; - -using DrawBackgroundDeferredConfig = render::GPUJobConfig; - -class DrawBackgroundDeferred { -public: - using Inputs = render::VaryingSet2 ; - - using Config = DrawBackgroundDeferredConfig; - using JobModel = render::Job::ModelI; - - void configure(const Config& config) {} - void run(const render::RenderContextPointer& renderContext, const Inputs& inputs); - -protected: - gpu::RangeTimerPointer _gpuTimer; -}; - class DrawOverlay3DConfig : public render::Job::Config { Q_OBJECT Q_PROPERTY(int numDrawn READ getNumDrawn NOTIFY numDrawnChanged) diff --git a/libraries/render-utils/src/RenderPipelines.cpp b/libraries/render-utils/src/RenderPipelines.cpp index da264cbf7d..42ed0bdad9 100644 --- a/libraries/render-utils/src/RenderPipelines.cpp +++ b/libraries/render-utils/src/RenderPipelines.cpp @@ -15,6 +15,7 @@ #include #include +#include "StencilMaskPass.h" #include "DeferredLightingEffect.h" #include "TextureCache.h" #include "render/DrawTask.h" @@ -330,6 +331,7 @@ void addPlumberPipeline(ShapePlumber& plumber, bool isWireframed = (i & 4); auto state = std::make_shared(); + PrepareStencil::testMaskDrawShape(*state); // Depth test depends on transparency state->setDepthTest(true, !key.isTranslucent(), gpu::LESS_EQUAL); diff --git a/libraries/render-utils/src/RenderShadowTask.cpp b/libraries/render-utils/src/RenderShadowTask.cpp index d7ec087174..ddb64bc69e 100644 --- a/libraries/render-utils/src/RenderShadowTask.cpp +++ b/libraries/render-utils/src/RenderShadowTask.cpp @@ -48,6 +48,7 @@ void RenderShadowMap::run(const render::RenderContextPointer& renderContext, RenderArgs* args = renderContext->args; gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { args->_batch = &batch; + batch.enableStereo(false); glm::ivec4 viewport{0, 0, fbo->getWidth(), fbo->getHeight()}; batch.setViewportTransform(viewport); @@ -114,7 +115,7 @@ void RenderShadowTask::build(JobModel& task, const render::Varying& input, rende skinProgram, state); } - const auto cachedMode = task.addJob("Setup"); + const auto cachedMode = task.addJob("ShadowSetup"); // CPU jobs: // Fetch and cull the items from the scene @@ -129,7 +130,7 @@ void RenderShadowTask::build(JobModel& task, const render::Varying& input, rende // GPU jobs: Render to shadow map task.addJob("RenderShadowMap", sortedShapes, shapePlumber); - task.addJob("Teardown", cachedMode); + task.addJob("ShadowTeardown", cachedMode); } void RenderShadowTask::configure(const Config& configuration) { diff --git a/libraries/render-utils/src/RenderViewTask.cpp b/libraries/render-utils/src/RenderViewTask.cpp new file mode 100644 index 0000000000..fceaf7b5b9 --- /dev/null +++ b/libraries/render-utils/src/RenderViewTask.cpp @@ -0,0 +1,33 @@ +// +// RenderViewTask.cpp +// render-utils/src/ +// +// Created by Sam Gateau on 5/25/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 "RenderViewTask.h" + +#include "RenderShadowTask.h" +#include "RenderDeferredTask.h" +#include "RenderForwardTask.h" + + + +void RenderViewTask::build(JobModel& task, const render::Varying& input, render::Varying& output, render::CullFunctor cullFunctor, bool isDeferred) { + // auto items = input.get(); + + task.addJob("RenderShadowTask", cullFunctor); + + const auto items = task.addJob("FetchCullSort", cullFunctor); + assert(items.canCast()); + + if (isDeferred) { + task.addJob("RenderDeferredTask", items); + } else { + task.addJob("Forward", items); + } +} + diff --git a/libraries/render-utils/src/RenderViewTask.h b/libraries/render-utils/src/RenderViewTask.h new file mode 100644 index 0000000000..eb61f56eab --- /dev/null +++ b/libraries/render-utils/src/RenderViewTask.h @@ -0,0 +1,31 @@ +// +// RenderViewTask.h +// render-utils/src/ +// +// Created by Sam Gateau on 5/25/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 +// +#pragma once +#ifndef hifi_RenderViewTask_h +#define hifi_RenderViewTask_h + +#include +#include + + +class RenderViewTask { +public: + using Input = RenderFetchCullSortTask::Output; + using JobModel = render::Task::ModelI; + + RenderViewTask() {} + + void build(JobModel& task, const render::Varying& inputs, render::Varying& outputs, render::CullFunctor cullFunctor, bool isDeferred); + +}; + + +#endif // hifi_RenderViewTask_h diff --git a/libraries/render-utils/src/StencilMaskPass.cpp b/libraries/render-utils/src/StencilMaskPass.cpp new file mode 100644 index 0000000000..2374f24211 --- /dev/null +++ b/libraries/render-utils/src/StencilMaskPass.cpp @@ -0,0 +1,126 @@ +// +// StencilMaskPass.cpp +// render-utils/src/ +// +// Created by Sam Gateau on 5/31/17. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "StencilMaskPass.h" + +#include +#include +#include + + +#include + +#include "stencil_drawMask_frag.h" + +using namespace render; + +void PrepareStencil::configure(const Config& config) { + _maskMode = config.maskMode; + _forceDraw = config.forceDraw; +} + +model::MeshPointer PrepareStencil::getMesh() { + if (!_mesh) { + + std::vector vertices { + { -1.0f, -1.0f, 0.0f }, { -1.0f, 0.0f, 0.0f }, + { -1.0f, 1.0f, 0.0f }, { 0.0f, 1.0f, 0.0f }, + { 1.0f, 1.0f, 0.0f }, { 1.0f, 0.0f, 0.0f }, + { 1.0f, -1.0f, 0.0f }, { 0.0f, -1.0f, 0.0f } }; + + std::vector indices { 0, 7, 1, 1, 3, 2, 3, 5, 4, 5, 7, 6 }; + _mesh = model::Mesh::createIndexedTriangles_P3F((uint32_t) vertices.size(), (uint32_t) indices.size(), vertices.data(), indices.data()); + } + return _mesh; +} + +gpu::PipelinePointer PrepareStencil::getMeshStencilPipeline() { + if (!_meshStencilPipeline) { + auto vs = gpu::StandardShaderLib::getDrawVertexPositionVS(); + auto ps = gpu::StandardShaderLib::getDrawNadaPS(); + auto program = gpu::Shader::createProgram(vs, ps); + gpu::Shader::makeProgram((*program)); + + auto state = std::make_shared(); + drawMask(*state); + state->setColorWriteMask(0); + + _meshStencilPipeline = gpu::Pipeline::create(program, state); + } + return _meshStencilPipeline; +} + +gpu::PipelinePointer PrepareStencil::getPaintStencilPipeline() { + if (!_paintStencilPipeline) { + auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); + auto ps = gpu::Shader::createPixel(std::string(stencil_drawMask_frag)); + auto program = gpu::Shader::createProgram(vs, ps); + gpu::Shader::makeProgram((*program)); + + auto state = std::make_shared(); + drawMask(*state); + state->setColorWriteMask(0); + + _paintStencilPipeline = gpu::Pipeline::create(program, state); + } + return _paintStencilPipeline; +} + +void PrepareStencil::run(const RenderContextPointer& renderContext, const gpu::FramebufferPointer& srcFramebuffer) { + RenderArgs* args = renderContext->args; + + // Only draw the stencil mask if in HMD mode or not forced. + if (!_forceDraw && (args->_displayMode != RenderArgs::STEREO_HMD)) { + return; + } + + doInBatch(args->_context, [&](gpu::Batch& batch) { + batch.enableStereo(false); + + batch.setViewportTransform(args->_viewport); + + if (_maskMode < 0) { + batch.setPipeline(getMeshStencilPipeline()); + + auto mesh = getMesh(); + batch.setIndexBuffer(mesh->getIndexBuffer()); + batch.setInputFormat((mesh->getVertexFormat())); + batch.setInputStream(0, mesh->getVertexStream()); + + // Draw + auto part = mesh->getPartBuffer().get(0); + batch.drawIndexed(gpu::TRIANGLES, part._numIndices, part._startIndex); + } else { + batch.setPipeline(getPaintStencilPipeline()); + batch.draw(gpu::TRIANGLE_STRIP, 4); + } + }); +} + +void PrepareStencil::drawMask(gpu::State& state) { + state.setStencilTest(true, 0xFF, gpu::State::StencilTest(PrepareStencil::STENCIL_MASK, 0xFF, gpu::ALWAYS, gpu::State::STENCIL_OP_REPLACE, gpu::State::STENCIL_OP_REPLACE, gpu::State::STENCIL_OP_REPLACE)); +} + +void PrepareStencil::testMask(gpu::State& state) { + state.setStencilTest(true, 0x00, gpu::State::StencilTest(PrepareStencil::STENCIL_MASK, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); +} + +void PrepareStencil::testBackground(gpu::State& state) { + state.setStencilTest(true, 0x00, gpu::State::StencilTest(PrepareStencil::STENCIL_BACKGROUND, 0xFF, gpu::EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); +} + +void PrepareStencil::testMaskDrawShape(gpu::State& state) { + state.setStencilTest(true, 0xFF, gpu::State::StencilTest(PrepareStencil::STENCIL_MASK, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_ZERO)); +} + +void PrepareStencil::testShape(gpu::State& state) { + state.setStencilTest(true, 0x00, gpu::State::StencilTest(PrepareStencil::STENCIL_SHAPE, 0xFF, gpu::EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); +} \ No newline at end of file diff --git a/libraries/render-utils/src/StencilMaskPass.h b/libraries/render-utils/src/StencilMaskPass.h new file mode 100644 index 0000000000..01601d1ae6 --- /dev/null +++ b/libraries/render-utils/src/StencilMaskPass.h @@ -0,0 +1,71 @@ +// +// StencilMaskPass.h +// render-utils/src/ +// +// Created by Sam Gateau on 5/31/17. +// Copyright 20154 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#pragma once +#ifndef hifi_StencilMaskPass_h +#define hifi_StencilMaskPass_h + +#include +#include +#include + +class PrepareStencilConfig : public render::Job::Config { + Q_OBJECT + Q_PROPERTY(int maskMode MEMBER maskMode NOTIFY dirty) + Q_PROPERTY(bool forceDraw MEMBER forceDraw NOTIFY dirty) + +public: + PrepareStencilConfig(bool enabled = true) : JobConfig(enabled) {} + + int maskMode { 0 }; + bool forceDraw { false }; + +signals: + void dirty(); +}; + +class PrepareStencil { +public: + using Config = PrepareStencilConfig; + + using JobModel = render::Job::ModelI; + + void configure(const Config& config); + + void run(const render::RenderContextPointer& renderContext, const gpu::FramebufferPointer& dstFramebuffer); + + static const gpu::int8 STENCIL_MASK = 2; + static const gpu::int8 STENCIL_BACKGROUND = 1; + static const gpu::int8 STENCIL_SHAPE = 0; + + + static void drawMask(gpu::State& state); + static void testMask(gpu::State& state); + static void testBackground(gpu::State& state); + static void testMaskDrawShape(gpu::State& state); + static void testShape(gpu::State& state); + + +private: + gpu::PipelinePointer _meshStencilPipeline; + gpu::PipelinePointer getMeshStencilPipeline(); + + gpu::PipelinePointer _paintStencilPipeline; + gpu::PipelinePointer getPaintStencilPipeline(); + + model::MeshPointer _mesh; + model::MeshPointer getMesh(); + + int _maskMode { 0 }; + bool _forceDraw { false }; +}; + + +#endif // hifi_StencilMaskPass_h diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index ef50960b7d..1941766353 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -14,7 +14,7 @@ #include #include - +#include "StencilMaskPass.h" const int DepthLinearPass_FrameTransformSlot = 0; const int DepthLinearPass_DepthMapSlot = 0; @@ -224,7 +224,7 @@ const gpu::PipelinePointer& LinearDepthPass::getLinearDepthPipeline() { gpu::StatePointer state = gpu::StatePointer(new gpu::State()); // Stencil test the curvature pass for objects pixels only, not the background - state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); + PrepareStencil::testShape(*state); state->setColorWriteMask(true, false, false, false); @@ -250,6 +250,7 @@ const gpu::PipelinePointer& LinearDepthPass::getDownsamplePipeline() { gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + PrepareStencil::testShape(*state); state->setColorWriteMask(true, true, true, false); @@ -554,7 +555,7 @@ const gpu::PipelinePointer& SurfaceGeometryPass::getCurvaturePipeline() { #ifdef USE_STENCIL_TEST // Stencil test the curvature pass for objects pixels only, not the background - state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); + PrepareStencil::testShape(*state); #endif // Good to go add the brand new pipeline _curvaturePipeline = gpu::Pipeline::create(program, state); diff --git a/libraries/render-utils/src/ToneMappingEffect.cpp b/libraries/render-utils/src/ToneMappingEffect.cpp index d54481d246..ce41cf16fa 100644 --- a/libraries/render-utils/src/ToneMappingEffect.cpp +++ b/libraries/render-utils/src/ToneMappingEffect.cpp @@ -15,7 +15,7 @@ #include #include - +#include "StencilMaskPass.h" #include "FramebufferCache.h" #include "toneMapping_frag.h" diff --git a/libraries/render-utils/src/ZoneRenderer.cpp b/libraries/render-utils/src/ZoneRenderer.cpp index 2cabe58c33..8f04265226 100644 --- a/libraries/render-utils/src/ZoneRenderer.cpp +++ b/libraries/render-utils/src/ZoneRenderer.cpp @@ -10,18 +10,203 @@ // #include "ZoneRenderer.h" + +#include +#include + #include #include +#include "StencilMaskPass.h" +#include "DeferredLightingEffect.h" + +#include "zone_drawKeyLight_frag.h" +#include "zone_drawAmbient_frag.h" +#include "zone_drawSkybox_frag.h" + + using namespace render; +class SetupZones { +public: + using Inputs = render::ItemBounds; + using JobModel = render::Job::ModelI; + + SetupZones() {} + + void run(const RenderContextPointer& context, const Inputs& inputs); + +protected: +}; + const Selection::Name ZoneRendererTask::ZONES_SELECTION { "RankedZones" }; void ZoneRendererTask::build(JobModel& task, const Varying& input, Varying& ouput) { - + // Filter out the sorted list of zones const auto zoneItems = task.addJob("FilterZones", input, ZONES_SELECTION.c_str()); - // just draw them... - task.addJob("DrawZones", zoneItems); + // just setup the current zone env + task.addJob("SetupZones", zoneItems); + + ouput = zoneItems; } +void SetupZones::run(const RenderContextPointer& context, const Inputs& inputs) { + + auto backgroundStage = DependencyManager::get()->getBackgroundStage(); + backgroundStage->_currentFrame.clear(); + + // call render in the correct order first... + render::renderItems(context, inputs); + +} + +const gpu::PipelinePointer& DebugZoneLighting::getKeyLightPipeline() { + if (!_keyLightPipeline) { + auto vs = gpu::StandardShaderLib::getDrawTransformUnitQuadVS(); + auto ps = gpu::Shader::createPixel(std::string(zone_drawKeyLight_frag)); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + + gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), ZONE_DEFERRED_TRANSFORM_BUFFER)); + slotBindings.insert(gpu::Shader::Binding(std::string("lightBuffer"), ZONE_KEYLIGHT_BUFFER)); + + gpu::Shader::makeProgram(*program, slotBindings); + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + PrepareStencil::testMask(*state); + state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA); + _keyLightPipeline = gpu::Pipeline::create(program, state); + } + return _keyLightPipeline; +} + +const gpu::PipelinePointer& DebugZoneLighting::getAmbientPipeline() { + if (!_ambientPipeline) { + auto vs = gpu::StandardShaderLib::getDrawTransformUnitQuadVS(); + auto ps = gpu::Shader::createPixel(std::string(zone_drawAmbient_frag)); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + + gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), ZONE_DEFERRED_TRANSFORM_BUFFER)); + slotBindings.insert(gpu::Shader::Binding(std::string("lightAmbientBuffer"), ZONE_AMBIENT_BUFFER)); + slotBindings.insert(gpu::Shader::Binding(std::string("skyboxMap"), ZONE_AMBIENT_MAP)); + + gpu::Shader::makeProgram(*program, slotBindings); + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + PrepareStencil::testMask(*state); + state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA); + _ambientPipeline = gpu::Pipeline::create(program, state); + } + return _ambientPipeline; +} +const gpu::PipelinePointer& DebugZoneLighting::getBackgroundPipeline() { + if (!_backgroundPipeline) { + auto vs = gpu::StandardShaderLib::getDrawTransformUnitQuadVS(); + auto ps = gpu::Shader::createPixel(std::string(zone_drawSkybox_frag)); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + + gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), ZONE_DEFERRED_TRANSFORM_BUFFER)); + slotBindings.insert(gpu::Shader::Binding(std::string("skyboxMap"), ZONE_SKYBOX_MAP)); + slotBindings.insert(gpu::Shader::Binding(std::string("skyboxBuffer"), ZONE_SKYBOX_BUFFER)); + + gpu::Shader::makeProgram(*program, slotBindings); + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + PrepareStencil::testMask(*state); + state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA); + _backgroundPipeline = gpu::Pipeline::create(program, state); + } + return _backgroundPipeline; +} + +void DebugZoneLighting::run(const render::RenderContextPointer& context, const Inputs& inputs) { + RenderArgs* args = context->args; + + auto deferredTransform = inputs; + + auto lightStage = DependencyManager::get()->getLightStage(); + std::vector keyLightStack; + if (lightStage && lightStage->_currentFrame._sunLights.size()) { + for (auto index : lightStage->_currentFrame._sunLights) { + keyLightStack.push_back(lightStage->getLight(index)); + } + } + keyLightStack.push_back(DependencyManager::get()->getGlobalLight()); + + std::vector ambientLightStack; + if (lightStage && lightStage->_currentFrame._ambientLights.size()) { + for (auto index : lightStage->_currentFrame._ambientLights) { + ambientLightStack.push_back(lightStage->getLight(index)); + } + } + ambientLightStack.push_back(DependencyManager::get()->getGlobalLight()); + + + auto backgroundStage = DependencyManager::get()->getBackgroundStage(); + std::vector skyboxStack; + if (backgroundStage && backgroundStage->_currentFrame._backgrounds.size()) { + for (auto index : backgroundStage->_currentFrame._backgrounds) { + auto background = backgroundStage->getBackground(index); + if (background) { + skyboxStack.push_back(background->getSkybox()); + } + } + } + skyboxStack.push_back(DependencyManager::get()->getDefaultSkybox()); + + + gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { + + batch.setViewportTransform(args->_viewport); + auto viewFrustum = args->getViewFrustum(); + batch.setProjectionTransform(viewFrustum.getProjection()); + batch.resetViewTransform(); + + Transform model; + + batch.setUniformBuffer(ZONE_DEFERRED_TRANSFORM_BUFFER, deferredTransform->getFrameTransformBuffer()); + + batch.setPipeline(getKeyLightPipeline()); + auto numKeys = (int) keyLightStack.size(); + for (int i = numKeys - 1; i >= 0; i--) { + model.setTranslation(glm::vec3(-4.0, -3.0 + (i * 1.0), -10.0 - (i * 3.0))); + batch.setModelTransform(model); + if (keyLightStack[i]) { + batch.setUniformBuffer(ZONE_KEYLIGHT_BUFFER, keyLightStack[i]->getLightSchemaBuffer()); + batch.draw(gpu::TRIANGLE_STRIP, 4); + } + } + + batch.setPipeline(getAmbientPipeline()); + auto numAmbients = (int) ambientLightStack.size(); + for (int i = numAmbients - 1; i >= 0; i--) { + model.setTranslation(glm::vec3(0.0, -3.0 + (i * 1.0), -10.0 - (i * 3.0))); + batch.setModelTransform(model); + if (ambientLightStack[i]) { + batch.setUniformBuffer(ZONE_AMBIENT_BUFFER, ambientLightStack[i]->getAmbientSchemaBuffer()); + if (ambientLightStack[i]->getAmbientMap()) { + batch.setResourceTexture(ZONE_AMBIENT_MAP, ambientLightStack[i]->getAmbientMap()); + } + batch.draw(gpu::TRIANGLE_STRIP, 4); + } + } + + batch.setPipeline(getBackgroundPipeline()); + auto numBackgrounds = (int) skyboxStack.size(); + for (int i = numBackgrounds - 1; i >= 0; i--) { + model.setTranslation(glm::vec3(4.0, -3.0 + (i * 1.0), -10.0 - (i * 3.0))); + batch.setModelTransform(model); + if (skyboxStack[i]) { + batch.setResourceTexture(ZONE_SKYBOX_MAP, skyboxStack[i]->getCubemap()); + batch.setUniformBuffer(ZONE_SKYBOX_BUFFER, skyboxStack[i]->getSchemaBuffer()); + batch.draw(gpu::TRIANGLE_STRIP, 4); + } + } + }); +} diff --git a/libraries/render-utils/src/ZoneRenderer.h b/libraries/render-utils/src/ZoneRenderer.h index 1218608400..5737499270 100644 --- a/libraries/render-utils/src/ZoneRenderer.h +++ b/libraries/render-utils/src/ZoneRenderer.h @@ -14,12 +14,15 @@ #include "render/Engine.h" +#include "DeferredFrameTransform.h" + class ZoneRendererConfig : public render::Task::Config { Q_OBJECT Q_PROPERTY(int maxDrawn MEMBER maxDrawn NOTIFY dirty) public: - ZoneRendererConfig() : render::Task::Config(false) {} + ZoneRendererConfig() : render::Task::Config( + ) {} int maxDrawn { -1 }; @@ -49,4 +52,40 @@ protected: int _maxDrawn; // initialized by Config }; +class DebugZoneLighting { +public: + class Config : public render::JobConfig { + public: + Config(bool enabled = false) : JobConfig(enabled) {} + }; + + using Inputs = DeferredFrameTransformPointer; + using JobModel = render::Job::ModelI; + + DebugZoneLighting() {} + + void configure(const Config& configuration) {} + void run(const render::RenderContextPointer& context, const Inputs& inputs); + +protected: + + enum Slots { + ZONE_DEFERRED_TRANSFORM_BUFFER = 0, + ZONE_KEYLIGHT_BUFFER, + ZONE_AMBIENT_BUFFER, + ZONE_AMBIENT_MAP, + ZONE_SKYBOX_BUFFER, + ZONE_SKYBOX_MAP, + }; + + gpu::PipelinePointer _keyLightPipeline; + gpu::PipelinePointer _ambientPipeline; + gpu::PipelinePointer _backgroundPipeline; + + const gpu::PipelinePointer& getKeyLightPipeline(); + const gpu::PipelinePointer& getAmbientPipeline(); + const gpu::PipelinePointer& getBackgroundPipeline(); +}; + + #endif \ No newline at end of file diff --git a/libraries/render-utils/src/directional_light.slf b/libraries/render-utils/src/directional_light.slf deleted file mode 100644 index cda03b0779..0000000000 --- a/libraries/render-utils/src/directional_light.slf +++ /dev/null @@ -1,47 +0,0 @@ -<@include gpu/Config.slh@> -<$VERSION_HEADER$> -// Generated on <$_SCRIBE_DATE$> -// -// directional_light.frag -// fragment shader -// -// Created by Andrzej Kapolka on 9/3/14. -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -<@include DeferredBufferRead.slh@> -<@include DeferredGlobalLight.slh@> - -<$declareEvalLightmappedColor()$> -<$declareEvalAmbientGlobalColor()$> - -in vec2 _texCoord0; -out vec4 _fragColor; - -void main(void) { - DeferredFrameTransform deferredTransform = getDeferredFrameTransform(); - DeferredFragment frag = unpackDeferredFragment(deferredTransform, _texCoord0); - - float shadowAttenuation = 1.0; - - if (frag.mode == FRAG_MODE_UNLIT) { - discard; - } else if (frag.mode == FRAG_MODE_LIGHTMAPPED) { - discard; - } else { - vec3 color = evalAmbientGlobalColor( - getViewInverse(), - shadowAttenuation, - frag.obscurance, - frag.position.xyz, - frag.normal, - frag.albedo, - frag.fresnel, - frag.metallic, - frag.roughness); - _fragColor = vec4(color, 1.0); - } -} diff --git a/libraries/render-utils/src/directional_light_shadow.slf b/libraries/render-utils/src/directional_light_shadow.slf deleted file mode 100644 index 7f98330f84..0000000000 --- a/libraries/render-utils/src/directional_light_shadow.slf +++ /dev/null @@ -1,49 +0,0 @@ -<@include gpu/Config.slh@> -<$VERSION_HEADER$> -// Generated on <$_SCRIBE_DATE$> -// -// directional_light_shadow.frag -// fragment shader -// -// Created by Zach Pomerantz on 1/18/2016. -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -<@include Shadow.slh@> -<@include DeferredBufferRead.slh@> -<@include DeferredGlobalLight.slh@> - -<$declareEvalLightmappedColor()$> -<$declareEvalAmbientGlobalColor()$> - -in vec2 _texCoord0; -out vec4 _fragColor; - -void main(void) { - DeferredFrameTransform deferredTransform = getDeferredFrameTransform(); - DeferredFragment frag = unpackDeferredFragment(deferredTransform, _texCoord0); - - vec4 worldPos = getViewInverse() * vec4(frag.position.xyz, 1.0); - float shadowAttenuation = evalShadowAttenuation(worldPos); - - if (frag.mode == FRAG_MODE_UNLIT) { - discard; - } else if (frag.mode == FRAG_MODE_LIGHTMAPPED) { - discard; - } else { - vec3 color = evalAmbientGlobalColor( - getViewInverse(), - shadowAttenuation, - frag.obscurance, - frag.position.xyz, - frag.normal, - frag.albedo, - frag.fresnel, - frag.metallic, - frag.roughness); - _fragColor = vec4(color, 1.0); - } -} diff --git a/libraries/render-utils/src/hit_effect.slf b/libraries/render-utils/src/hit_effect.slf deleted file mode 100644 index cc4484442f..0000000000 --- a/libraries/render-utils/src/hit_effect.slf +++ /dev/null @@ -1,27 +0,0 @@ -<@include gpu/Config.slh@> -<$VERSION_HEADER$> -// Generated on <$_SCRIBE_DATE$> -// - -// hit_effect.frag -// fragment shader -// -// Created by Eric Levin on 7/20 -// 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 DeferredBufferWrite.slh@> - -in vec2 varQuadPosition; -out vec4 outFragColor; - -void main(void) { - vec2 center = vec2(0.0, 0.0); - float distFromCenter = distance( vec2(0.0, 0.0), varQuadPosition); - float alpha = mix(0.0, 0.5, pow(distFromCenter,5.)); - outFragColor = vec4(1.0, 0.0, 0.0, alpha); -} \ No newline at end of file diff --git a/libraries/render-utils/src/point_light.slf b/libraries/render-utils/src/point_light.slf deleted file mode 100644 index e646db5069..0000000000 --- a/libraries/render-utils/src/point_light.slf +++ /dev/null @@ -1,85 +0,0 @@ -<@include gpu/Config.slh@> -<$VERSION_HEADER$> -// Generated on <$_SCRIBE_DATE$> -// -// point_light.frag -// fragment shader -// -// Created by Sam Gateau on 9/18/15. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - - -<$declareDeferredCurvature()$> - -// Everything about light -<@include model/Light.slh@> -<$declareLightBuffer()$> - -<@include LightingModel.slh@> - -<@include LightPoint.slh@> -<$declareLightingPoint(supportScattering)$> - - -uniform vec4 texcoordFrameTransform; - -in vec4 _texCoord0;!> -out vec4 _fragColor; - -void main(void) { - _fragColor = vec4(1.0, 1.0, 1.0, 1.0); - - -} diff --git a/libraries/render-utils/src/spot_light.slf b/libraries/render-utils/src/spot_light.slf deleted file mode 100644 index 4cacff86c4..0000000000 --- a/libraries/render-utils/src/spot_light.slf +++ /dev/null @@ -1,115 +0,0 @@ -<@include gpu/Config.slh@> -<$VERSION_HEADER$> -// Generated on <$_SCRIBE_DATE$> -// -// spot_light.frag -// fragment shader -// -// Created by Sam Gateau on 9/18/15. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -// Everything about deferred buffer - - -<$declareDeferredCurvature()$> - -// Everything about light -<@include model/Light.slh@> -<$declareLightBuffer(256)$> -uniform lightIndexBuffer { - int lightIndex[256]; -}; -<@include LightingModel.slh@> - -<@include LightPoint.slh@> -<$declareLightingPoint(supportScattering)$> -<@include LightSpot.slh@> -<$declareLightingSpot(supportScattering)$> - -//uniform vec4 texcoordFrameTransform; -!> - - -//in vec4 _texCoord0; -//flat in int instanceID; -out vec4 _fragColor; - -void main(void) { - _fragColor = vec4(1.0, 1.0, 1.0, 1.0); - -// DeferredFrameTransform deferredTransform = getDeferredFrameTransform(); - - // Grab the fragment data from the uv - //vec2 texCoord = _texCoord0.st;/* / _texCoord0.q; - /*texCoord *= texcoordFrameTransform.zw; - texCoord += texcoordFrameTransform.xy;*/ - /* - vec4 fragPosition = unpackDeferredPositionFromZeye(texCoord); - DeferredFragment frag = unpackDeferredFragmentNoPosition(texCoord); - - if (frag.mode == FRAG_MODE_UNLIT) { - discard; - } - - - // frag.depthVal = depthValue; - frag.position = fragPosition; - - vec4 midNormalCurvature; - vec4 lowNormalCurvature; - if (frag.mode == FRAG_MODE_SCATTERING) { - unpackMidLowNormalCurvature(texCoord, midNormalCurvature, lowNormalCurvature); - } - - // Frag pos in world - mat4 invViewMat = getViewInverse(); - vec4 fragPos = invViewMat * fragPosition; - - // Frag to eye vec - vec4 fragEyeVector = invViewMat * vec4(-frag.position.xyz, 0.0); - vec3 fragEyeDir = normalize(fragEyeVector.xyz); - - int numLights = lightIndex[0]; - for (int i = 0; i < numLights; i++) { - // Need the light now - Light light = getLight(lightIndex[i + 1]); - bool isSpot = light_isSpot(light); - // Clip againgst the light volume and Make the Light vector going from fragment to light center in world space - vec4 fragLightVecLen2; - vec4 fragLightDirLen; - float cosSpotAngle; - if (isSpot) { - if (!clipFragToLightVolumeSpot(light, fragPos.xyz, fragLightVecLen2, fragLightDirLen, cosSpotAngle)) { - continue; - } - } else { - if (!clipFragToLightVolumePoint(light, fragPos.xyz, fragLightVecLen2)) { - continue; - } - } - - vec3 diffuse; - vec3 specular; - - if (isSpot) { - evalLightingSpot(diffuse, specular, light, - fragLightDirLen.xyzw, cosSpotAngle, fragEyeDir, frag.normal, frag.roughness, - frag.metallic, frag.fresnel, frag.albedo, 1.0, - frag.scattering, midNormalCurvature, lowNormalCurvature); - } else { - evalLightingPoint(diffuse, specular, light, - fragLightVecLen2.xyz, fragEyeDir, frag.normal, frag.roughness, - frag.metallic, frag.fresnel, frag.albedo, 1.0, - frag.scattering, midNormalCurvature, lowNormalCurvature); - } - - _fragColor.rgb += diffuse; - _fragColor.rgb += specular; - } - */ -} - diff --git a/libraries/render-utils/src/stencil_drawMask.slf b/libraries/render-utils/src/stencil_drawMask.slf new file mode 100644 index 0000000000..3eedeecb82 --- /dev/null +++ b/libraries/render-utils/src/stencil_drawMask.slf @@ -0,0 +1,23 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// stencil_drawMask.slf +// fragment shader +// +// Created by Sam Gateau on 5/31/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 +// + +in vec2 varTexCoord0; + +float aspectRatio = 0.95; + +void main(void) { + vec2 pos = varTexCoord0 * 2.0 - vec2(1.0); + pos.x = aspectRatio * (pos.x * (pos.x > 0.0 ? 2.0 : -2.0) - 1.0); + if (1.0 - dot(pos.xy, pos.xy) > 0.0 ) discard; +} diff --git a/libraries/render-utils/src/text/Font.cpp b/libraries/render-utils/src/text/Font.cpp index c6a7da3a1a..f51a779066 100644 --- a/libraries/render-utils/src/text/Font.cpp +++ b/libraries/render-utils/src/text/Font.cpp @@ -14,6 +14,8 @@ #include "../RenderUtilsLogging.h" #include "FontFamilies.h" +static std::mutex fontMutex; + struct TextureVertex { glm::vec2 pos; glm::vec2 tex; @@ -56,6 +58,7 @@ Font::Pointer Font::load(QIODevice& fontFile) { } Font::Pointer Font::load(const QString& family) { + std::lock_guard lock(fontMutex); if (!LOADED_FONTS.contains(family)) { static const QString SDFF_COURIER_PRIME_FILENAME{ ":/CourierPrime.sdff" }; diff --git a/libraries/render-utils/src/text/Font.h b/libraries/render-utils/src/text/Font.h index 5b6b4f2a43..2b61f19492 100644 --- a/libraries/render-utils/src/text/Font.h +++ b/libraries/render-utils/src/text/Font.h @@ -31,10 +31,10 @@ public: const glm::vec4* color, EffectType effectType, const glm::vec2& bound, bool layered = false); - static Pointer load(QIODevice& fontFile); static Pointer load(const QString& family); private: + static Pointer load(QIODevice& fontFile); QStringList tokenizeForWrapping(const QString& str) const; QStringList splitLines(const QString& str) const; glm::vec2 computeTokenExtent(const QString& str) const; diff --git a/libraries/render-utils/src/zone_draw.slh b/libraries/render-utils/src/zone_draw.slh new file mode 100644 index 0000000000..2cc8fc99f2 --- /dev/null +++ b/libraries/render-utils/src/zone_draw.slh @@ -0,0 +1,37 @@ + +// Generated on <$_SCRIBE_DATE$> +// +// Created by Sam Gateau on 5/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 +// + +<@include DeferredTransform.slh@> +<$declareDeferredFrameTransform()$> + +<@func evalGlobeWidget()@> + const float SCOPE_RADIUS = 1.0; + const float SCOPE_RADIUS2 = SCOPE_RADIUS * SCOPE_RADIUS; + const float EDGE_HALFWIDTH = 0.025; + const float EDGE_HALFWIDTH2 = EDGE_HALFWIDTH * EDGE_HALFWIDTH; + const float OUT_RADIUS = SCOPE_RADIUS + EDGE_HALFWIDTH; + + vec2 sphereUV = (varTexCoord0.xy * 2.0 - vec2(1.0)) * OUT_RADIUS; + float sphereR2 = dot(sphereUV.xy, sphereUV.xy); + if (sphereR2 > OUT_RADIUS * OUT_RADIUS) { + discard; + } + float sphereR = sqrt(sphereR2); + + float edgeFalloff = (SCOPE_RADIUS - sphereR) / (EDGE_HALFWIDTH); + float edgeFalloff2 = min(1.0, edgeFalloff * edgeFalloff); + + vec4 base = vec4(0.0, 0.0, 0.0, 1.0 - edgeFalloff2); + if (sphereR2 > SCOPE_RADIUS2) { + _fragColor = base; + return; + } +<@endfunc@> + diff --git a/libraries/render-utils/src/zone_drawAmbient.slf b/libraries/render-utils/src/zone_drawAmbient.slf new file mode 100644 index 0000000000..f104e5be44 --- /dev/null +++ b/libraries/render-utils/src/zone_drawAmbient.slf @@ -0,0 +1,54 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// Created by Sam Gateau on 5/16/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +<@include zone_draw.slh@> + +<@include model/Light.slh@> + +<@include LightingModel.slh@> +<$declareLightAmbientBuffer()$> + +<@include LightAmbient.slh@> + +<$declareLightingAmbient(_SCRIBE_NULL, 1, _SCRIBE_NULL, _SCRIBE_NULL)$> + +in vec2 varTexCoord0; +out vec4 _fragColor; + +void main(void) { + + <$evalGlobeWidget()$> + + vec3 spherePos = normalize(vec3(sphereUV, sqrt(1.0 - sphereR2))); + + + vec3 fragNormal = vec3(getViewInverse() * vec4(spherePos, 0.0)); + + + LightAmbient lightAmbient = getLightAmbient(); + + + float roughness = 0.1; + float levels = getLightAmbientMapNumMips(lightAmbient); + float lod = min(((roughness)* levels), levels); + vec3 ambientMap = evalSkyboxLight(fragNormal, lod).xyz; + vec3 ambientSH = sphericalHarmonics_evalSphericalLight(getLightAmbientSphere(lightAmbient), fragNormal).xyz; + + // vec3 ambient = sphericalHarmonics_evalSphericalLight(getLightAmbientSphere(lightAmbient), fragNormal).xyz; + // _fragColor = vec4( 0.5 * (fragNormal + vec3(1.0)), 1.0); + + vec3 color = (sphereUV.x > 0 ? ambientMap : ambientSH); + + color = color * 1.0 - base.w + base.xyz * base.w; + const float INV_GAMMA_22 = 1.0 / 2.2; + _fragColor = vec4(pow(color, vec3(INV_GAMMA_22)), 1.0); +} + + diff --git a/libraries/render-utils/src/zone_drawKeyLight.slf b/libraries/render-utils/src/zone_drawKeyLight.slf new file mode 100644 index 0000000000..e96239b6dc --- /dev/null +++ b/libraries/render-utils/src/zone_drawKeyLight.slf @@ -0,0 +1,57 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// Created by Sam Gateau on 5/16/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +<@include zone_draw.slh@> + +<@include model/Light.slh@> + +<@include LightingModel.slh@> +<$declareLightBuffer()$> + +<@include LightDirectional.slh@> +<$declareLightingDirectional(_SCRIBE_NULL)$> + +in vec2 varTexCoord0; +out vec4 _fragColor; + +void main(void) { + + <$evalGlobeWidget()$> + + Light light = getLight(); + vec3 lightDirection = normalize(getLightDirection(light)); + vec3 lightIrradiance = getLightIrradiance(light); + vec3 color = vec3(0.0); + + const float INOUT_RATIO = 0.4; + const float SUN_THRESHOLD = 0.99; + + vec3 outSpherePos = normalize(vec3(sphereUV, -sqrt(1.0 - sphereR2))); + vec3 outNormal = vec3(getViewInverse() * vec4(outSpherePos, 0.0)); + + float val = step(SUN_THRESHOLD, dot(-lightDirection, outNormal)); + + color = lightIrradiance * vec3(val); + + if (sphereR2 < INOUT_RATIO * INOUT_RATIO * SCOPE_RADIUS2) { + vec2 inSphereUV = sphereUV / INOUT_RATIO; + vec3 inSpherePos = normalize(vec3(inSphereUV, sqrt(1.0 - dot(inSphereUV.xy, inSphereUV.xy)))); + vec3 inNormal = vec3(getViewInverse() * vec4(inSpherePos, 0.0)); + + vec3 marbleColor = max(lightIrradiance * vec3(dot(-lightDirection, inNormal)), vec3(0.01)); + color += marbleColor; + } + + color = color * 1.0 - base.w + base.xyz * base.w; + const float INV_GAMMA_22 = 1.0 / 2.2; + _fragColor = vec4(pow(color, vec3(INV_GAMMA_22)), 1.0); +} + + diff --git a/libraries/render-utils/src/zone_drawSkybox.slf b/libraries/render-utils/src/zone_drawSkybox.slf new file mode 100644 index 0000000000..fd6976365e --- /dev/null +++ b/libraries/render-utils/src/zone_drawSkybox.slf @@ -0,0 +1,48 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// Created by Sam Gateau on 5/16/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +<@include zone_draw.slh@> + +uniform samplerCube skyboxMap; + +struct Skybox { + vec4 color; +}; + +uniform skyboxBuffer { + Skybox skybox; +}; + +in vec2 varTexCoord0; +out vec4 _fragColor; + +void main(void) { + <$evalGlobeWidget()$> + + vec3 spherePos = normalize(vec3(sphereUV, -sqrt(1.0 - sphereR2))); + + vec3 direction = vec3(getViewInverse() * vec4(spherePos, 0.0)); + + vec3 color = skybox.color.rgb; + + // blend is only set if there is a cubemap + if (skybox.color.a > 0.0) { + color = texture(skyboxMap, direction).rgb; + if (skybox.color.a < 1.0) { + color *= skybox.color.rgb; + } + } + + color = color * 1.0 - base.w + base.xyz * base.w; + const float INV_GAMMA_22 = 1.0 / 2.2; + _fragColor = vec4(pow(color, vec3(INV_GAMMA_22)), 1.0); +} + + diff --git a/libraries/render/src/render/EngineStats.cpp b/libraries/render/src/render/EngineStats.cpp index 9e45be5dbd..ae1467ac0f 100644 --- a/libraries/render/src/render/EngineStats.cpp +++ b/libraries/render/src/render/EngineStats.cpp @@ -63,6 +63,4 @@ void EngineStats::run(const RenderContextPointer& renderContext) { config->frameSetPipelineCount = _gpuStats._PSNumSetPipelines; config->frameSetInputFormatCount = _gpuStats._ISNumFormatChanges; - - config->emitDirty(); } diff --git a/libraries/render/src/task/Config.cpp b/libraries/render/src/task/Config.cpp index cb2c4f1e3c..0e630311f6 100644 --- a/libraries/render/src/task/Config.cpp +++ b/libraries/render/src/task/Config.cpp @@ -34,6 +34,7 @@ void TaskConfig::connectChildConfig(QConfigPointer childConfig, const std::strin if (childConfig->metaObject()->indexOfSignal(DIRTY_SIGNAL) != -1) { // Connect dirty->refresh if defined QObject::connect(childConfig.get(), SIGNAL(dirty()), this, SLOT(refresh())); + QObject::connect(childConfig.get(), SIGNAL(dirtyEnabled()), this, SLOT(refresh())); } } @@ -50,6 +51,7 @@ void TaskConfig::transferChildrenConfigs(QConfigPointer source) { if (child->metaObject()->indexOfSignal(DIRTY_SIGNAL) != -1) { // Connect dirty->refresh if defined QObject::connect(child, SIGNAL(dirty()), this, SLOT(refresh())); + QObject::connect(child, SIGNAL(dirtyEnabled()), this, SLOT(refresh())); } } } diff --git a/libraries/render/src/task/Config.h b/libraries/render/src/task/Config.h index c78a3f3bfe..40a3abbd18 100644 --- a/libraries/render/src/task/Config.h +++ b/libraries/render/src/task/Config.h @@ -89,7 +89,7 @@ protected: class JobConfig : public QObject { Q_OBJECT Q_PROPERTY(double cpuRunTime READ getCPURunTime NOTIFY newStats()) //ms - Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled) + Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled NOTIFY dirtyEnabled()) double _msCPURunTime{ 0.0 }; public: @@ -99,7 +99,7 @@ public: JobConfig(bool enabled) : alwaysEnabled{ false }, enabled{ enabled } {} bool isEnabled() { return alwaysEnabled || enabled; } - void setEnabled(bool enable) { enabled = alwaysEnabled || enable; } + void setEnabled(bool enable) { enabled = alwaysEnabled || enable; emit dirtyEnabled(); } bool alwaysEnabled{ true }; bool enabled{ true }; @@ -121,6 +121,7 @@ public slots: signals: void loaded(); void newStats(); + void dirtyEnabled(); }; class TaskConfig : public JobConfig { diff --git a/libraries/render/src/task/Task.h b/libraries/render/src/task/Task.h index ed335150a7..f76ba92546 100644 --- a/libraries/render/src/task/Task.h +++ b/libraries/render/src/task/Task.h @@ -170,6 +170,7 @@ protected: std::string _name = ""; }; + // A task is a specialized job to run a collection of other jobs // It can be created on any type T by aliasing the type JobModel in the class T // using JobModel = Task::Model diff --git a/libraries/script-engine/src/TabletScriptingInterface.cpp b/libraries/script-engine/src/TabletScriptingInterface.cpp index ab45ebfd77..b41c548e57 100644 --- a/libraries/script-engine/src/TabletScriptingInterface.cpp +++ b/libraries/script-engine/src/TabletScriptingInterface.cpp @@ -542,7 +542,7 @@ void TabletProxy::gotoWebScreen(const QString& url, const QString& injectedJavaS QObject* TabletProxy::addButton(const QVariant& properties) { auto tabletButtonProxy = QSharedPointer(new TabletButtonProxy(properties.toMap())); - std::lock_guard guard(_tabletMutex); + std::unique_lock guard(_tabletMutex); _tabletButtonProxies.push_back(tabletButtonProxy); if (!_toolbarMode && _qmlTabletRoot) { auto tablet = getQmlTablet(); @@ -552,7 +552,6 @@ QObject* TabletProxy::addButton(const QVariant& properties) { qCCritical(scriptengine) << "Could not find tablet in TabletRoot.qml"; } } else if (_toolbarMode) { - auto tabletScriptingInterface = DependencyManager::get(); QObject* toolbarProxy = tabletScriptingInterface->getSystemToolbarProxy(); @@ -561,6 +560,8 @@ QObject* TabletProxy::addButton(const QVariant& properties) { connectionType = Qt::BlockingQueuedConnection; } + guard.unlock(); + // copy properties from tablet button proxy to toolbar button proxy. QObject* toolbarButtonProxy = nullptr; bool hasResult = QMetaObject::invokeMethod(toolbarProxy, "addButton", connectionType, Q_RETURN_ARG(QObject*, toolbarButtonProxy), Q_ARG(QVariant, tabletButtonProxy->getProperties())); @@ -578,31 +579,38 @@ bool TabletProxy::onHomeScreen() { } void TabletProxy::removeButton(QObject* tabletButtonProxy) { - std::lock_guard guard(_tabletMutex); + std::unique_lock guard(_tabletMutex); auto tablet = getQmlTablet(); if (!tablet) { qCCritical(scriptengine) << "Could not find tablet in TabletRoot.qml"; } - auto iter = std::find(_tabletButtonProxies.begin(), _tabletButtonProxies.end(), tabletButtonProxy); - if (iter != _tabletButtonProxies.end()) { - if (!_toolbarMode && _qmlTabletRoot) { - (*iter)->setQmlButton(nullptr); - if (tablet) { - QMetaObject::invokeMethod(tablet, "removeButtonProxy", Qt::AutoConnection, Q_ARG(QVariant, (*iter)->getProperties())); - } - } else if (_toolbarMode) { - auto tabletScriptingInterface = DependencyManager::get(); - QObject* toolbarProxy = tabletScriptingInterface->getSystemToolbarProxy(); - - // remove button from toolbarProxy - QMetaObject::invokeMethod(toolbarProxy, "removeButton", Qt::AutoConnection, Q_ARG(QVariant, (*iter)->getUuid().toString())); - (*iter)->setToolbarButtonProxy(nullptr); + QSharedPointer buttonProxy; + { + auto iter = std::find(_tabletButtonProxies.begin(), _tabletButtonProxies.end(), tabletButtonProxy); + if (iter == _tabletButtonProxies.end()) { + qCWarning(scriptengine) << "TabletProxy::removeButton() could not find button " << tabletButtonProxy; + return; } + buttonProxy = *iter; _tabletButtonProxies.erase(iter); - } else { - qCWarning(scriptengine) << "TabletProxy::removeButton() could not find button " << tabletButtonProxy; + } + + if (!_toolbarMode && _qmlTabletRoot) { + buttonProxy->setQmlButton(nullptr); + if (tablet) { + guard.unlock(); + QMetaObject::invokeMethod(tablet, "removeButtonProxy", Qt::AutoConnection, Q_ARG(QVariant, buttonProxy->getProperties())); + } + } else if (_toolbarMode) { + auto tabletScriptingInterface = DependencyManager::get(); + QObject* toolbarProxy = tabletScriptingInterface->getSystemToolbarProxy(); + + // remove button from toolbarProxy + guard.unlock(); + QMetaObject::invokeMethod(toolbarProxy, "removeButton", Qt::AutoConnection, Q_ARG(QVariant, buttonProxy->getUuid().toString())); + buttonProxy->setToolbarButtonProxy(nullptr); } } diff --git a/libraries/shared/src/CubicHermiteSpline.h b/libraries/shared/src/CubicHermiteSpline.h new file mode 100644 index 0000000000..da2ed26de4 --- /dev/null +++ b/libraries/shared/src/CubicHermiteSpline.h @@ -0,0 +1,113 @@ +// +// CubicHermiteSpline.h +// +// 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_CubicHermiteSpline_h +#define hifi_CubicHermiteSpline_h + +#include "GLMHelpers.h" + +class CubicHermiteSplineFunctor { +public: + CubicHermiteSplineFunctor() : _p0(), _m0(), _p1(), _m1() {} + CubicHermiteSplineFunctor(const glm::vec3& p0, const glm::vec3& m0, const glm::vec3& p1, const glm::vec3& m1) : _p0(p0), _m0(m0), _p1(p1), _m1(m1) {} + + CubicHermiteSplineFunctor(const CubicHermiteSplineFunctor& orig) : _p0(orig._p0), _m0(orig._m0), _p1(orig._p1), _m1(orig._m1) {} + + virtual ~CubicHermiteSplineFunctor() {} + + // evalute the hermite curve at parameter t (0..1) + glm::vec3 operator()(float t) const { + float t2 = t * t; + float t3 = t2 * t; + float w0 = 2.0f * t3 - 3.0f * t2 + 1.0f; + float w1 = t3 - 2.0f * t2 + t; + float w2 = -2.0f * t3 + 3.0f * t2; + float w3 = t3 - t2; + return w0 * _p0 + w1 * _m0 + w2 * _p1 + w3 * _m1; + } + + // evaulate the first derivative of the hermite curve at parameter t (0..1) + glm::vec3 d(float t) const { + float t2 = t * t; + float w0 = -6.0f * t + 6.0f * t2; + float w1 = 1.0f - 4.0f * t + 3.0f * t2; + float w2 = 6.0f * t - 6.0f * t2; + float w3 = -2.0f * t + 3.0f * t2; + return w0 * _p0 + w1 * _m0 + w2 * _p1 + w3 * _m1; + } + + // evaulate the second derivative of the hermite curve at paramter t (0..1) + glm::vec3 d2(float t) const { + float w0 = -6.0f + 12.0f * t; + float w1 = -4.0f + 6.0f * t; + float w2 = 6.0f - 12.0f * t; + float w3 = -2.0f + 6.0f * t; + return w0 + _p0 + w1 * _m0 + w2 * _p1 + w3 * _m1; + } + +protected: + glm::vec3 _p0; + glm::vec3 _m0; + glm::vec3 _p1; + glm::vec3 _m1; +}; + +class CubicHermiteSplineFunctorWithArcLength : public CubicHermiteSplineFunctor { +public: + enum Constants { NUM_SUBDIVISIONS = 30 }; + + CubicHermiteSplineFunctorWithArcLength() : CubicHermiteSplineFunctor() { + memset(_values, 0, sizeof(float) * (NUM_SUBDIVISIONS + 1)); + } + CubicHermiteSplineFunctorWithArcLength(const glm::vec3& p0, const glm::vec3& m0, const glm::vec3& p1, const glm::vec3& m1) : CubicHermiteSplineFunctor(p0, m0, p1, m1) { + // initialize _values with the accumulated arcLength along the spline. + const float DELTA = 1.0f / NUM_SUBDIVISIONS; + float alpha = 0.0f; + float accum = 0.0f; + _values[0] = 0.0f; + for (int i = 1; i < NUM_SUBDIVISIONS + 1; i++) { + accum += glm::distance(this->operator()(alpha), + this->operator()(alpha + DELTA)); + alpha += DELTA; + _values[i] = accum; + } + } + + CubicHermiteSplineFunctorWithArcLength(const CubicHermiteSplineFunctorWithArcLength& orig) : CubicHermiteSplineFunctor(orig) { + memcpy(_values, orig._values, sizeof(float) * (NUM_SUBDIVISIONS + 1)); + } + + // given the spline parameter (0..1) output the arcLength of the spline up to that point. + float arcLength(float t) const { + float index = t * NUM_SUBDIVISIONS; + int prevIndex = std::min(std::max(0, (int)glm::floor(index)), (int)NUM_SUBDIVISIONS); + int nextIndex = std::min(std::max(0, (int)glm::ceil(index)), (int)NUM_SUBDIVISIONS); + float alpha = glm::fract(index); + return lerp(_values[prevIndex], _values[nextIndex], alpha); + } + + // given an arcLength compute the spline parameter (0..1) that cooresponds to that arcLength. + float arcLengthInverse(float s) const { + // find first item in _values that is > s. + int nextIndex; + for (nextIndex = 0; nextIndex < NUM_SUBDIVISIONS; nextIndex++) { + if (_values[nextIndex] > s) { + break; + } + } + int prevIndex = std::min(std::max(0, nextIndex - 1), (int)NUM_SUBDIVISIONS); + float alpha = glm::clamp((s - _values[prevIndex]) / (_values[nextIndex] - _values[prevIndex]), 0.0f, 1.0f); + const float DELTA = 1.0f / NUM_SUBDIVISIONS; + return lerp(prevIndex * DELTA, nextIndex * DELTA, alpha); + } +protected: + float _values[NUM_SUBDIVISIONS + 1]; +}; + +#endif // hifi_CubicHermiteSpline_h diff --git a/libraries/shared/src/Extents.h b/libraries/shared/src/Extents.h index 850735dd5d..444575b7d3 100644 --- a/libraries/shared/src/Extents.h +++ b/libraries/shared/src/Extents.h @@ -69,7 +69,7 @@ public: glm::vec3 size() const { return maximum - minimum; } float largestDimension() const { return glm::compMax(size()); } - /// \return new Extents which is original rotated around orign by rotation + /// \return new Extents which is original rotated around origin by rotation Extents getRotated(const glm::quat& rotation) const { Extents temp(minimum, maximum); temp.rotate(rotation); diff --git a/libraries/shared/src/GLMHelpers.cpp b/libraries/shared/src/GLMHelpers.cpp index 70237e8ff6..394517aac4 100644 --- a/libraries/shared/src/GLMHelpers.cpp +++ b/libraries/shared/src/GLMHelpers.cpp @@ -49,7 +49,7 @@ const mat4 Matrices::Z_180 { createMatFromQuatAndPos(Quaternions::Z_180, Vectors glm::quat safeMix(const glm::quat& q1, const glm::quat& q2, float proportion) { float cosa = q1.x * q2.x + q1.y * q2.y + q1.z * q2.z + q1.w * q2.w; float ox = q2.x, oy = q2.y, oz = q2.z, ow = q2.w, s0, s1; - + // adjust signs if necessary if (cosa < 0.0f) { cosa = -cosa; @@ -58,19 +58,19 @@ glm::quat safeMix(const glm::quat& q1, const glm::quat& q2, float proportion) { oz = -oz; ow = -ow; } - + // calculate coefficients; if the angle is too close to zero, we must fall back // to linear interpolation if ((1.0f - cosa) > EPSILON) { float angle = acosf(cosa), sina = sinf(angle); s0 = sinf((1.0f - proportion) * angle) / sina; s1 = sinf(proportion * angle) / sina; - + } else { s0 = 1.0f - proportion; s1 = proportion; } - + return glm::normalize(glm::quat(s0 * q1.w + s1 * ow, s0 * q1.x + s1 * ox, s0 * q1.y + s1 * oy, s0 * q1.z + s1 * oz)); } @@ -105,10 +105,10 @@ int unpackFloatVec3FromSignedTwoByteFixed(const unsigned char* sourceBuffer, glm int packFloatAngleToTwoByte(unsigned char* buffer, float degrees) { const float ANGLE_CONVERSION_RATIO = (std::numeric_limits::max() / 360.0f); - + uint16_t angleHolder = floorf((degrees + 180.0f) * ANGLE_CONVERSION_RATIO); memcpy(buffer, &angleHolder, sizeof(uint16_t)); - + return sizeof(uint16_t); } @@ -125,7 +125,7 @@ int packOrientationQuatToBytes(unsigned char* buffer, const glm::quat& quatInput quatParts[1] = floorf((quatNormalized.y + 1.0f) * QUAT_PART_CONVERSION_RATIO); quatParts[2] = floorf((quatNormalized.z + 1.0f) * QUAT_PART_CONVERSION_RATIO); quatParts[3] = floorf((quatNormalized.w + 1.0f) * QUAT_PART_CONVERSION_RATIO); - + memcpy(buffer, &quatParts, sizeof(quatParts)); return sizeof(quatParts); } @@ -133,12 +133,12 @@ int packOrientationQuatToBytes(unsigned char* buffer, const glm::quat& quatInput int unpackOrientationQuatFromBytes(const unsigned char* buffer, glm::quat& quatOutput) { uint16_t quatParts[4]; memcpy(&quatParts, buffer, sizeof(quatParts)); - + quatOutput.x = ((quatParts[0] / (float) std::numeric_limits::max()) * 2.0f) - 1.0f; quatOutput.y = ((quatParts[1] / (float) std::numeric_limits::max()) * 2.0f) - 1.0f; quatOutput.z = ((quatParts[2] / (float) std::numeric_limits::max()) * 2.0f) - 1.0f; quatOutput.w = ((quatParts[3] / (float) std::numeric_limits::max()) * 2.0f) - 1.0f; - + return sizeof(quatParts); } @@ -235,7 +235,7 @@ glm::vec3 safeEulerAngles(const glm::quat& q) { atan2f(q.y * q.z + q.x * q.w, 0.5f - (q.x * q.x + q.y * q.y)), asinf(sy), atan2f(q.x * q.y + q.z * q.w, 0.5f - (q.y * q.y + q.z * q.z))); - + } else { // not a unique solution; x + z = atan2(-m21, m11) eulers = glm::vec3( @@ -250,7 +250,7 @@ glm::vec3 safeEulerAngles(const glm::quat& q) { PI_OVER_TWO, -atan2f(q.x * q.w - q.y * q.z, 0.5f - (q.x * q.x + q.z * q.z))); } - + // adjust so that z, rather than y, is in [-pi/2, pi/2] if (eulers.z < -PI_OVER_TWO) { if (eulers.x < 0.0f) { @@ -265,7 +265,7 @@ glm::vec3 safeEulerAngles(const glm::quat& q) { eulers.y -= PI; } eulers.z += PI; - + } else if (eulers.z > PI_OVER_TWO) { if (eulers.x < 0.0f) { eulers.x += PI; @@ -320,7 +320,7 @@ glm::quat extractRotation(const glm::mat4& matrix, bool assumeOrthogonal) { for (int i = 0; i < 10; i++) { // store the results of the previous iteration glm::mat3 previous = upper; - + // compute average of the matrix with its inverse transpose float sd00 = previous[1][1] * previous[2][2] - previous[2][1] * previous[1][2]; float sd10 = previous[0][1] * previous[2][2] - previous[2][1] * previous[0][2]; @@ -334,15 +334,15 @@ glm::quat extractRotation(const glm::mat4& matrix, bool assumeOrthogonal) { upper[0][0] = +sd00 * hrdet + previous[0][0] * 0.5f; upper[1][0] = -sd10 * hrdet + previous[1][0] * 0.5f; upper[2][0] = +sd20 * hrdet + previous[2][0] * 0.5f; - + upper[0][1] = -(previous[1][0] * previous[2][2] - previous[2][0] * previous[1][2]) * hrdet + previous[0][1] * 0.5f; upper[1][1] = +(previous[0][0] * previous[2][2] - previous[2][0] * previous[0][2]) * hrdet + previous[1][1] * 0.5f; upper[2][1] = -(previous[0][0] * previous[1][2] - previous[1][0] * previous[0][2]) * hrdet + previous[2][1] * 0.5f; - + upper[0][2] = +(previous[1][0] * previous[2][1] - previous[2][0] * previous[1][1]) * hrdet + previous[0][2] * 0.5f; upper[1][2] = -(previous[0][0] * previous[2][1] - previous[2][0] * previous[0][1]) * hrdet + previous[1][2] * 0.5f; upper[2][2] = +(previous[0][0] * previous[1][1] - previous[1][0] * previous[0][1]) * hrdet + previous[2][2] * 0.5f; - + // compute the difference; if it's small enough, we're done glm::mat3 diff = upper - previous; if (diff[0][0] * diff[0][0] + diff[1][0] * diff[1][0] + diff[2][0] * diff[2][0] + diff[0][1] * diff[0][1] + @@ -352,7 +352,7 @@ glm::quat extractRotation(const glm::mat4& matrix, bool assumeOrthogonal) { } } } - + // now that we have a nice orthogonal matrix, we can extract the rotation quaternion // using the method described in http://en.wikipedia.org/wiki/Rotation_matrix#Conversions float x2 = fabs(1.0f + upper[0][0] - upper[1][1] - upper[2][2]); @@ -473,7 +473,7 @@ glm::mat4 createMatFromScaleQuatAndPos(const glm::vec3& scale, const glm::quat& glm::vec4(zAxis, 0.0f), glm::vec4(trans, 1.0f)); } -// cancel out roll +// cancel out roll glm::quat cancelOutRoll(const glm::quat& q) { glm::vec3 forward = q * Vectors::FRONT; return glm::quat_cast(glm::inverse(glm::lookAt(Vectors::ZERO, forward, Vectors::UP))); @@ -538,17 +538,16 @@ void generateBasisVectors(const glm::vec3& primaryAxis, const glm::vec3& seconda uAxisOut = glm::normalize(primaryAxis); glm::vec3 normSecondary = glm::normalize(secondaryAxis); - // if secondaryAxis is parallel with the primaryAxis, pick another axis. + // if normSecondary is parallel with the primaryAxis, pick another secondary. const float EPSILON = 1.0e-4f; - if (fabsf(fabsf(glm::dot(uAxisOut, secondaryAxis)) - 1.0f) > EPSILON) { - // pick a better secondaryAxis. - normSecondary = glm::vec3(1.0f, 0.0f, 0.0f); - if (fabsf(fabsf(glm::dot(uAxisOut, secondaryAxis)) - 1.0f) > EPSILON) { - normSecondary = glm::vec3(0.0f, 1.0f, 0.0f); + if (fabsf(fabsf(glm::dot(uAxisOut, normSecondary)) - 1.0f) < EPSILON) { + normSecondary = Vectors::UNIT_X; + if (fabsf(fabsf(glm::dot(uAxisOut, normSecondary)) - 1.0f) < EPSILON) { + normSecondary = Vectors::UNIT_Y; } } - wAxisOut = glm::normalize(glm::cross(uAxisOut, secondaryAxis)); + wAxisOut = glm::normalize(glm::cross(uAxisOut, normSecondary)); vAxisOut = glm::cross(wAxisOut, uAxisOut); } diff --git a/libraries/shared/src/GeometryUtil.cpp b/libraries/shared/src/GeometryUtil.cpp index c137ebd438..f853240fe3 100644 --- a/libraries/shared/src/GeometryUtil.cpp +++ b/libraries/shared/src/GeometryUtil.cpp @@ -290,12 +290,12 @@ glm::vec3 Triangle::getNormal() const { } bool findRayTriangleIntersection(const glm::vec3& origin, const glm::vec3& direction, - const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2, float& distance) { + const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2, float& distance, bool allowBackface) { glm::vec3 firstSide = v0 - v1; glm::vec3 secondSide = v2 - v1; glm::vec3 normal = glm::cross(secondSide, firstSide); float dividend = glm::dot(normal, v1) - glm::dot(origin, normal); - if (dividend > 0.0f) { + if (!allowBackface && dividend > 0.0f) { return false; // origin below plane } float divisor = glm::dot(normal, direction); diff --git a/libraries/shared/src/GeometryUtil.h b/libraries/shared/src/GeometryUtil.h index 2fdc1aa25f..857d423896 100644 --- a/libraries/shared/src/GeometryUtil.h +++ b/libraries/shared/src/GeometryUtil.h @@ -83,7 +83,7 @@ bool findRayRectangleIntersection(const glm::vec3& origin, const glm::vec3& dire const glm::vec3& position, const glm::vec2& dimensions, float& distance); bool findRayTriangleIntersection(const glm::vec3& origin, const glm::vec3& direction, - const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2, float& distance); + const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2, float& distance, bool allowBackface = false); /// \brief decomposes rotation into its components such that: rotation = swing * twist /// \param rotation[in] rotation to decompose @@ -104,8 +104,8 @@ public: }; inline bool findRayTriangleIntersection(const glm::vec3& origin, const glm::vec3& direction, - const Triangle& triangle, float& distance) { - return findRayTriangleIntersection(origin, direction, triangle.v0, triangle.v1, triangle.v2, distance); + const Triangle& triangle, float& distance, bool allowBackface = false) { + return findRayTriangleIntersection(origin, direction, triangle.v0, triangle.v1, triangle.v2, distance, allowBackface); } diff --git a/libraries/shared/src/NumericalConstants.h b/libraries/shared/src/NumericalConstants.h index d37e1e31c5..e2d2409d56 100644 --- a/libraries/shared/src/NumericalConstants.h +++ b/libraries/shared/src/NumericalConstants.h @@ -50,10 +50,13 @@ const int KILO_PER_MEGA = 1000; #define KB_TO_BYTES_SHIFT 10 #define MB_TO_BYTES_SHIFT 20 +#define GB_TO_BYTES_SHIFT 30 +#define GB_TO_BYTES(X) ((size_t)(X) << GB_TO_BYTES_SHIFT) #define MB_TO_BYTES(X) ((size_t)(X) << MB_TO_BYTES_SHIFT) #define KB_TO_BYTES(X) ((size_t)(X) << KB_TO_BYTES_SHIFT) +#define BYTES_TO_GB(X) (X >> GB_TO_BYTES_SHIFT) #define BYTES_TO_MB(X) (X >> MB_TO_BYTES_SHIFT) #define BYTES_TO_KB(X) (X >> KB_TO_BYTES_SHIFT) diff --git a/libraries/shared/src/RenderArgs.h b/libraries/shared/src/RenderArgs.h index f44d736e1a..d4d88c26a8 100644 --- a/libraries/shared/src/RenderArgs.h +++ b/libraries/shared/src/RenderArgs.h @@ -76,7 +76,7 @@ public: class RenderArgs { public: enum RenderMode { DEFAULT_RENDER_MODE, SHADOW_RENDER_MODE, DIFFUSE_RENDER_MODE, NORMAL_RENDER_MODE, MIRROR_RENDER_MODE }; - enum RenderSide { MONO, STEREO_LEFT, STEREO_RIGHT }; + enum DisplayMode { MONO, STEREO_MONITOR, STEREO_HMD }; enum DebugFlags { RENDER_DEBUG_NONE = 0, RENDER_DEBUG_HULLS = 1 @@ -87,7 +87,7 @@ public: float sizeScale = 1.0f, int boundaryLevelAdjust = 0, RenderMode renderMode = DEFAULT_RENDER_MODE, - RenderSide renderSide = MONO, + DisplayMode displayMode = MONO, DebugFlags debugFlags = RENDER_DEBUG_NONE, gpu::Batch* batch = nullptr) : _context(context), @@ -95,7 +95,7 @@ public: _sizeScale(sizeScale), _boundaryLevelAdjust(boundaryLevelAdjust), _renderMode(renderMode), - _renderSide(renderSide), + _displayMode(displayMode), _debugFlags(debugFlags), _batch(batch) { } @@ -121,7 +121,7 @@ public: float _sizeScale = 1.0f; int _boundaryLevelAdjust = 0; RenderMode _renderMode = DEFAULT_RENDER_MODE; - RenderSide _renderSide = MONO; + DisplayMode _displayMode = MONO; DebugFlags _debugFlags = RENDER_DEBUG_NONE; gpu::Batch* _batch = nullptr; diff --git a/libraries/shared/src/RunningMarker.cpp b/libraries/shared/src/RunningMarker.cpp index 0c1fd06df8..cb7b39320c 100644 --- a/libraries/shared/src/RunningMarker.cpp +++ b/libraries/shared/src/RunningMarker.cpp @@ -13,44 +13,16 @@ #include #include -#include -#include -#include "NumericalConstants.h" #include "PathUtils.h" -RunningMarker::RunningMarker(QObject* parent, QString name) : - _parent(parent), +RunningMarker::RunningMarker(QString name) : _name(name) { } -void RunningMarker::startRunningMarker() { - static const int RUNNING_STATE_CHECK_IN_MSECS = MSECS_PER_SECOND; - - // start the nodeThread so its event loop is running - _runningMarkerThread = new QThread(_parent); - _runningMarkerThread->setObjectName("Running Marker Thread"); - _runningMarkerThread->start(); - - writeRunningMarkerFile(); // write the first file, even before timer - - _runningMarkerTimer = new QTimer(); - QObject::connect(_runningMarkerTimer, &QTimer::timeout, [=](){ - writeRunningMarkerFile(); - }); - _runningMarkerTimer->start(RUNNING_STATE_CHECK_IN_MSECS); - - // put the time on the thread - _runningMarkerTimer->moveToThread(_runningMarkerThread); -} - RunningMarker::~RunningMarker() { deleteRunningMarkerFile(); - QMetaObject::invokeMethod(_runningMarkerTimer, "stop", Qt::BlockingQueuedConnection); - _runningMarkerThread->quit(); - _runningMarkerTimer->deleteLater(); - _runningMarkerThread->deleteLater(); } bool RunningMarker::fileExists() const { @@ -77,8 +49,3 @@ void RunningMarker::deleteRunningMarkerFile() { QString RunningMarker::getFilePath() const { return QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/" + _name; } - -QString RunningMarker::getMarkerFilePath(QString name) { - return QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/" + name; -} - diff --git a/libraries/shared/src/RunningMarker.h b/libraries/shared/src/RunningMarker.h index f9c8e72d37..ae7c550660 100644 --- a/libraries/shared/src/RunningMarker.h +++ b/libraries/shared/src/RunningMarker.h @@ -12,21 +12,14 @@ #ifndef hifi_RunningMarker_h #define hifi_RunningMarker_h -#include #include -class QThread; -class QTimer; - class RunningMarker { public: - RunningMarker(QObject* parent, QString name); + RunningMarker(QString name); ~RunningMarker(); - void startRunningMarker(); - QString getFilePath() const; - static QString getMarkerFilePath(QString name); bool fileExists() const; @@ -34,10 +27,7 @@ public: void deleteRunningMarkerFile(); private: - QObject* _parent { nullptr }; QString _name; - QThread* _runningMarkerThread { nullptr }; - QTimer* _runningMarkerTimer { nullptr }; }; #endif // hifi_RunningMarker_h diff --git a/libraries/shared/src/TBBHelpers.h b/libraries/shared/src/TBBHelpers.h new file mode 100644 index 0000000000..6b5c4d416b --- /dev/null +++ b/libraries/shared/src/TBBHelpers.h @@ -0,0 +1,28 @@ +// +// Created by Bradley Austin Davis on 2017/06/06 +// 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 +// + +#pragma once +#ifndef hifi_TBBHelpers_h +#define hifi_TBBHelpers_h + +#ifdef _WIN32 +#pragma warning( push ) +#pragma warning( disable : 4334 ) +#endif + +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#pragma warning( pop ) +#endif + +#endif // hifi_TBBHelpers_h diff --git a/libraries/shared/src/ThreadHelpers.cpp b/libraries/shared/src/ThreadHelpers.cpp new file mode 100644 index 0000000000..14ae35762b --- /dev/null +++ b/libraries/shared/src/ThreadHelpers.cpp @@ -0,0 +1,39 @@ +// +// Created by Bradley Austin Davis on 2017/06/06 +// 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 "ThreadHelpers.h" + +#include + + +void moveToNewNamedThread(QObject* object, const QString& name, std::function startCallback, QThread::Priority priority) { + Q_ASSERT(QThread::currentThread() == object->thread()); + // setup a thread for the NodeList and its PacketReceiver + QThread* thread = new QThread(); + thread->setObjectName(name); + + if (priority != QThread::InheritPriority) { + thread->setPriority(priority); + } + + QString tempName = name; + QObject::connect(thread, &QThread::started, [startCallback] { + startCallback(); + }); + // Make sure the thread will be destroyed and cleaned up + QObject::connect(object, &QObject::destroyed, thread, &QThread::quit); + QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); + + // put the object on the thread + object->moveToThread(thread); + thread->start(); +} + +void moveToNewNamedThread(QObject* object, const QString& name, QThread::Priority priority) { + moveToNewNamedThread(object, name, [] {}, priority); +} diff --git a/libraries/shared/src/ThreadHelpers.h b/libraries/shared/src/ThreadHelpers.h index 0a92d36b80..6461fa5724 100644 --- a/libraries/shared/src/ThreadHelpers.h +++ b/libraries/shared/src/ThreadHelpers.h @@ -12,8 +12,13 @@ #define hifi_ThreadHelpers_h #include -#include -#include +#include + +#include +#include +#include +#include +#include template void withLock(L lock, F function) { @@ -26,4 +31,7 @@ void withLock(QMutex& lock, F function) { function(); } +void moveToNewNamedThread(QObject* object, const QString& name, std::function startCallback, QThread::Priority priority = QThread::InheritPriority); +void moveToNewNamedThread(QObject* object, const QString& name, QThread::Priority priority = QThread::InheritPriority); + #endif diff --git a/libraries/shared/src/TriangleSet.cpp b/libraries/shared/src/TriangleSet.cpp index aa21aa5cc0..68c99a9753 100644 --- a/libraries/shared/src/TriangleSet.cpp +++ b/libraries/shared/src/TriangleSet.cpp @@ -31,7 +31,7 @@ void TriangleSet::clear() { } bool TriangleSet::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision) { + float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, bool allowBackface) { // reset our distance to be the max possible, lower level tests will store best distance here distance = std::numeric_limits::max(); @@ -41,7 +41,7 @@ bool TriangleSet::findRayIntersection(const glm::vec3& origin, const glm::vec3& } int trianglesTouched = 0; - auto result = _triangleOctree.findRayIntersection(origin, direction, distance, face, surfaceNormal, precision, trianglesTouched); + auto result = _triangleOctree.findRayIntersection(origin, direction, distance, face, surfaceNormal, precision, trianglesTouched, allowBackface); #if WANT_DEBUGGING if (precision) { @@ -95,7 +95,7 @@ void TriangleSet::balanceOctree() { // Determine of the given ray (origin/direction) in model space intersects with any triangles // in the set. If an intersection occurs, the distance and surface normal will be provided. bool TriangleSet::TriangleOctreeCell::findRayIntersectionInternal(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched) { + float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched, bool allowBackface) { bool intersectedSomething = false; float boxDistance = distance; @@ -114,7 +114,7 @@ bool TriangleSet::TriangleOctreeCell::findRayIntersectionInternal(const glm::vec const auto& triangle = _allTriangles[triangleIndex]; float thisTriangleDistance; trianglesTouched++; - if (findRayTriangleIntersection(origin, direction, triangle, thisTriangleDistance)) { + if (findRayTriangleIntersection(origin, direction, triangle, thisTriangleDistance, allowBackface)) { if (thisTriangleDistance < bestDistance) { bestDistance = thisTriangleDistance; intersectedSomething = true; @@ -203,7 +203,7 @@ void TriangleSet::TriangleOctreeCell::insert(size_t triangleIndex) { } bool TriangleSet::TriangleOctreeCell::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched) { + float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched, bool allowBackface) { if (_population < 1) { return false; // no triangles below here, so we can't intersect @@ -247,7 +247,7 @@ bool TriangleSet::TriangleOctreeCell::findRayIntersection(const glm::vec3& origi } } // also check our local triangle set - if (findRayIntersectionInternal(origin, direction, childDistance, childFace, childNormal, precision, trianglesTouched)) { + if (findRayIntersectionInternal(origin, direction, childDistance, childFace, childNormal, precision, trianglesTouched, allowBackface)) { if (childDistance < bestLocalDistance) { bestLocalDistance = childDistance; bestLocalFace = childFace; diff --git a/libraries/shared/src/TriangleSet.h b/libraries/shared/src/TriangleSet.h index 6cedc4da7e..3b0b33d7d5 100644 --- a/libraries/shared/src/TriangleSet.h +++ b/libraries/shared/src/TriangleSet.h @@ -27,7 +27,7 @@ class TriangleSet { void clear(); bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched); + float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched, bool allowBackface = false); const AABox& getBounds() const { return _bounds; } @@ -38,7 +38,7 @@ class TriangleSet { // checks our internal list of triangles bool findRayIntersectionInternal(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched); + float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched, bool allowBackface = false); std::vector& _allTriangles; std::map _children; @@ -60,7 +60,7 @@ public: void insert(const Triangle& t); bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision); + float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, bool allowBackface = false); void balanceOctree(); diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 84812b4f60..a80105293b 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -91,14 +91,14 @@ QObject* OffscreenUi::getFlags() { void OffscreenUi::create(QOpenGLContext* context) { OffscreenQmlSurface::create(context); - auto rootContext = getRootContext(); + auto myContext = getSurfaceContext(); - rootContext->setContextProperty("OffscreenUi", this); - rootContext->setContextProperty("offscreenFlags", offscreenFlags = new OffscreenFlags()); - rootContext->setContextProperty("fileDialogHelper", new FileDialogHelper()); + myContext->setContextProperty("OffscreenUi", this); + myContext->setContextProperty("offscreenFlags", offscreenFlags = new OffscreenFlags()); + myContext->setContextProperty("fileDialogHelper", new FileDialogHelper()); auto tabletScriptingInterface = DependencyManager::get(); TabletProxy* tablet = tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system"); - rootContext->engine()->setObjectOwnership(tablet, QQmlEngine::CppOwnership); + myContext->engine()->setObjectOwnership(tablet, QQmlEngine::CppOwnership); } void OffscreenUi::show(const QUrl& url, const QString& name, std::function f) { @@ -547,14 +547,14 @@ void OffscreenUi::createDesktop(const QUrl& url) { } #ifdef DEBUG - getRootContext()->setContextProperty("DebugQML", QVariant(true)); + getSurfaceContext()->setContextProperty("DebugQML", QVariant(true)); #else - getRootContext()->setContextProperty("DebugQML", QVariant(false)); + getSurfaceContext()->setContextProperty("DebugQML", QVariant(false)); #endif _desktop = dynamic_cast(load(url)); Q_ASSERT(_desktop); - getRootContext()->setContextProperty("desktop", _desktop); + getSurfaceContext()->setContextProperty("desktop", _desktop); _toolWindow = _desktop->findChild("ToolWindow"); diff --git a/libraries/ui/src/QmlWindowClass.cpp b/libraries/ui/src/QmlWindowClass.cpp index c0e94058ae..58d39448ac 100644 --- a/libraries/ui/src/QmlWindowClass.cpp +++ b/libraries/ui/src/QmlWindowClass.cpp @@ -104,9 +104,9 @@ void QmlWindowClass::initQml(QVariantMap properties) { Q_ASSERT(invokeResult); } else { // Build the event bridge and wrapper on the main thread - offscreenUi->load(qmlSource(), [&](QQmlContext* context, QObject* object) { + offscreenUi->loadInNewContext(qmlSource(), [&](QQmlContext* context, QObject* object) { _qmlWindow = object; - _qmlWindow->setProperty("eventBridge", QVariant::fromValue(this)); + context->setContextProperty("eventBridge", this); context->engine()->setObjectOwnership(this, QQmlEngine::CppOwnership); context->engine()->setObjectOwnership(object, QQmlEngine::CppOwnership); if (properties.contains(TITLE_PROPERTY)) { diff --git a/libraries/ui/src/VrMenu.cpp b/libraries/ui/src/VrMenu.cpp index 878514dd41..3959e950e9 100644 --- a/libraries/ui/src/VrMenu.cpp +++ b/libraries/ui/src/VrMenu.cpp @@ -115,7 +115,7 @@ private: VrMenu::VrMenu(OffscreenUi* parent) : QObject(parent) { _rootMenu = parent->getRootItem()->findChild("rootMenu"); - parent->getRootContext()->setContextProperty("rootMenu", _rootMenu); + parent->getSurfaceContext()->setContextProperty("rootMenu", _rootMenu); } QObject* VrMenu::findMenuObject(const QString& menuOption) { diff --git a/plugins/hifiKinect/src/KinectPlugin.cpp b/plugins/hifiKinect/src/KinectPlugin.cpp index 3a36be0982..92c578e9aa 100644 --- a/plugins/hifiKinect/src/KinectPlugin.cpp +++ b/plugins/hifiKinect/src/KinectPlugin.cpp @@ -273,7 +273,7 @@ bool KinectPlugin::activate() { return false; } -bool KinectPlugin::isHandController() const { +bool KinectPlugin::isHandController() const { bool sensorAvailable = false; #ifdef HAVE_KINECT if (_kinectSensor) { @@ -285,6 +285,10 @@ bool KinectPlugin::isHandController() const { return _enabled && _initialized && sensorAvailable; } +bool KinectPlugin::isHeadController() const { + return isHandController(); +} + bool KinectPlugin::initializeDefaultSensor() const { #ifdef HAVE_KINECT @@ -654,4 +658,4 @@ void KinectPlugin::InputDevice::clearState() { int poseIndex = KinectJointIndexToPoseIndex((KinectJointIndex)i); _poseStateMap[poseIndex] = controller::Pose(); } -} \ No newline at end of file +} diff --git a/plugins/hifiKinect/src/KinectPlugin.h b/plugins/hifiKinect/src/KinectPlugin.h index 158e66ee62..e8f0745200 100644 --- a/plugins/hifiKinect/src/KinectPlugin.h +++ b/plugins/hifiKinect/src/KinectPlugin.h @@ -43,6 +43,7 @@ class KinectPlugin : public InputPlugin { Q_OBJECT public: bool isHandController() const override; + bool isHeadController() const override; // Plugin functions virtual void init() override; diff --git a/plugins/hifiNeuron/src/NeuronPlugin.h b/plugins/hifiNeuron/src/NeuronPlugin.h index 43b27d14dd..34d084160f 100644 --- a/plugins/hifiNeuron/src/NeuronPlugin.h +++ b/plugins/hifiNeuron/src/NeuronPlugin.h @@ -25,8 +25,6 @@ class NeuronPlugin : public InputPlugin { public: friend void FrameDataReceivedCallback(void* context, void* sender, _BvhDataHeaderEx* header, float* data); - bool isHandController() const override { return false; } - // Plugin functions virtual void init() override; virtual bool isSupported() const override; diff --git a/plugins/hifiSdl2/src/SDL2Manager.h b/plugins/hifiSdl2/src/SDL2Manager.h index 4501d0792b..9cb4d268c0 100644 --- a/plugins/hifiSdl2/src/SDL2Manager.h +++ b/plugins/hifiSdl2/src/SDL2Manager.h @@ -27,7 +27,6 @@ public: const QString getName() const override { return NAME; } QStringList getSubdeviceNames() override; - bool isHandController() const override { return false; } void init() override; void deinit() override; diff --git a/plugins/hifiSixense/src/SixenseManager.h b/plugins/hifiSixense/src/SixenseManager.h index 5237dba791..5b2c140868 100644 --- a/plugins/hifiSixense/src/SixenseManager.h +++ b/plugins/hifiSixense/src/SixenseManager.h @@ -33,7 +33,7 @@ public: // Sixense always seems to initialize even if the hydras are not present. Is there // a way we can properly detect whether the hydras are present? - bool isHandController() const override { return false; } + // bool isHandController() const override { return true; } virtual bool activate() override; virtual void deactivate() override; diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp index 26906ef2fb..93f4787f0f 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp @@ -123,10 +123,19 @@ bool OculusBaseDisplayPlugin::internalActivate() { void OculusBaseDisplayPlugin::internalDeactivate() { Parent::internalDeactivate(); +} + +bool OculusBaseDisplayPlugin::activateStandBySession() { + _session = acquireOculusSession(); + if (!_session) { + return false; + } + return true; +} +void OculusBaseDisplayPlugin::deactivateSession() { releaseOculusSession(); _session = nullptr; } - void OculusBaseDisplayPlugin::updatePresentPose() { //mat4 sensorResetMat; //_currentPresentFrameInfo.sensorSampleTime = ovr_GetTimeInSeconds(); diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.h b/plugins/oculus/src/OculusBaseDisplayPlugin.h index e5dc75095d..5230b11681 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.h +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.h @@ -26,6 +26,7 @@ public: void resetSensors() override final; bool beginFrameRender(uint32_t frameIndex) override; float getTargetFrameRate() const override { return _hmdDesc.DisplayRefreshRate; } + bool getSupportsAutoSwitch() override final { return true; } protected: @@ -33,6 +34,8 @@ protected: void uncustomizeContext() override; bool internalActivate() override; void internalDeactivate() override; + bool activateStandBySession() override; + void deactivateSession() override; void updatePresentPose() override; protected: diff --git a/plugins/oculus/src/OculusControllerManager.cpp b/plugins/oculus/src/OculusControllerManager.cpp index 6445c3c891..6f7be26554 100644 --- a/plugins/oculus/src/OculusControllerManager.cpp +++ b/plugins/oculus/src/OculusControllerManager.cpp @@ -19,6 +19,8 @@ #include #include +#include +#include #include @@ -208,13 +210,18 @@ void OculusControllerManager::RemoteDevice::focusOutEvent() { _buttonPressedMap.clear(); } -void OculusControllerManager::TouchDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { - _buttonPressedMap.clear(); +bool OculusControllerManager::isHeadControllerMounted() const { ovrSessionStatus status; - if (!OVR_SUCCESS(ovr_GetSessionStatus(_parent._session, &status)) || (ovrFalse == status.HmdMounted)) { - // if the HMD isn't on someone's head, don't take input from the controllers - return; + bool success = OVR_SUCCESS(ovr_GetSessionStatus(_session, &status)); + if (!success) { + return false; } + return status.HmdMounted == ovrTrue; +} + +void OculusControllerManager::TouchDevice::update(float deltaTime, + const controller::InputCalibrationData& inputCalibrationData) { + _buttonPressedMap.clear(); int numTrackedControllers = 0; quint64 currentTime = usecTimestampNow(); @@ -230,14 +237,14 @@ void OculusControllerManager::TouchDevice::update(float deltaTime, const control _lastControllerPose[controller] = tracking.HandPoses[hand]; return; } - + if (_lostTracking[controller]) { if (currentTime > _regainTrackingDeadline[controller]) { _poseStateMap.erase(controller); _poseStateMap[controller].valid = false; return; } - + } else { quint64 deadlineToRegainTracking = currentTime + LOST_TRACKING_DELAY; _regainTrackingDeadline[controller] = deadlineToRegainTracking; @@ -245,6 +252,13 @@ void OculusControllerManager::TouchDevice::update(float deltaTime, const control } handleRotationForUntrackedHand(inputCalibrationData, hand, tracking.HandPoses[hand]); }); + + if (_parent.isHeadControllerMounted()) { + handleHeadPose(deltaTime, inputCalibrationData, tracking.HeadPose); + } else { + _poseStateMap[controller::HEAD].valid = false; + } + using namespace controller; // Axes const auto& inputState = _parent._inputState; @@ -269,7 +283,7 @@ void OculusControllerManager::TouchDevice::update(float deltaTime, const control if (inputState.Touches & pair.first) { _buttonPressedMap.insert(pair.second); } - } + } // Haptics { @@ -292,16 +306,38 @@ void OculusControllerManager::TouchDevice::focusOutEvent() { _buttonPressedMap.clear(); }; -void OculusControllerManager::TouchDevice::handlePose(float deltaTime, - const controller::InputCalibrationData& inputCalibrationData, ovrHandType hand, - const ovrPoseStatef& handPose) { +void OculusControllerManager::TouchDevice::handlePose(float deltaTime, + const controller::InputCalibrationData& inputCalibrationData, + ovrHandType hand, const ovrPoseStatef& handPose) { auto poseId = hand == ovrHand_Left ? controller::LEFT_HAND : controller::RIGHT_HAND; auto& pose = _poseStateMap[poseId]; pose = ovrControllerPoseToHandPose(hand, handPose); // transform into avatar frame glm::mat4 controllerToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat; pose = pose.transform(controllerToAvatar); +} +void OculusControllerManager::TouchDevice::handleHeadPose(float deltaTime, + const controller::InputCalibrationData& inputCalibrationData, + const ovrPoseStatef& headPose) { + glm::mat4 mat = createMatFromQuatAndPos(toGlm(headPose.ThePose.Orientation), + toGlm(headPose.ThePose.Position)); + + //perform a 180 flip to make the HMD face the +z instead of -z, beacuse the head faces +z + glm::mat4 matYFlip = mat * Matrices::Y_180; + controller::Pose pose(extractTranslation(matYFlip), + glmExtractRotation(matYFlip), + toGlm(headPose.LinearVelocity), // XXX * matYFlip ? + toGlm(headPose.AngularVelocity)); + + glm::mat4 sensorToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat; + glm::mat4 defaultHeadOffset = glm::inverse(inputCalibrationData.defaultCenterEyeMat) * + inputCalibrationData.defaultHeadMat; + + controller::Pose hmdHeadPose = pose.transform(sensorToAvatar); + + pose.valid = true; + _poseStateMap[controller::HEAD] = hmdHeadPose.postTransform(defaultHeadOffset); } void OculusControllerManager::TouchDevice::handleRotationForUntrackedHand(const controller::InputCalibrationData& inputCalibrationData, @@ -382,6 +418,7 @@ controller::Input::NamedVector OculusControllerManager::TouchDevice::getAvailabl makePair(LEFT_HAND, "LeftHand"), makePair(RIGHT_HAND, "RightHand"), + makePair(HEAD, "Head"), makePair(LEFT_PRIMARY_THUMB_TOUCH, "LeftPrimaryThumbTouch"), makePair(LEFT_SECONDARY_THUMB_TOUCH, "LeftSecondaryThumbTouch"), diff --git a/plugins/oculus/src/OculusControllerManager.h b/plugins/oculus/src/OculusControllerManager.h index 11d699ca8e..69187f94a6 100644 --- a/plugins/oculus/src/OculusControllerManager.h +++ b/plugins/oculus/src/OculusControllerManager.h @@ -28,6 +28,8 @@ public: const QString getName() const override { return NAME; } bool isHandController() const override { return _touch != nullptr; } + bool isHeadController() const override { return true; } + bool isHeadControllerMounted() const; QStringList getSubdeviceNames() override; bool activate() override; @@ -75,8 +77,13 @@ private: private: void stopHapticPulse(bool leftHand); - void handlePose(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, ovrHandType hand, const ovrPoseStatef& handPose); - void handleRotationForUntrackedHand(const controller::InputCalibrationData& inputCalibrationData, ovrHandType hand, const ovrPoseStatef& handPose); + void handlePose(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, + ovrHandType hand, const ovrPoseStatef& handPose); + void handleRotationForUntrackedHand(const controller::InputCalibrationData& inputCalibrationData, + ovrHandType hand, const ovrPoseStatef& handPose); + void handleHeadPose(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, + const ovrPoseStatef& headPose); + int _trackedControllers { 0 }; // perform an action when the TouchDevice mutex is acquired. diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp index b759a06aee..80c8698bb6 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp @@ -24,7 +24,7 @@ #include #include #include -#include +#include #include diff --git a/plugins/openvr/CMakeLists.txt b/plugins/openvr/CMakeLists.txt index bc62117e70..6a95ef6d76 100644 --- a/plugins/openvr/CMakeLists.txt +++ b/plugins/openvr/CMakeLists.txt @@ -10,10 +10,10 @@ if (WIN32) # we're using static GLEW, so define GLEW_STATIC add_definitions(-DGLEW_STATIC) set(TARGET_NAME openvr) - setup_hifi_plugin(OpenGL Script Qml Widgets) + setup_hifi_plugin(OpenGL Script Qml Widgets Multimedia) 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 ktx image) + audio-client render-utils model gpu gpu-gl render model-networking fbx ktx image procedural) include_hifi_library_headers(octree) @@ -21,4 +21,5 @@ if (WIN32) find_package(OpenVR REQUIRED) target_include_directories(${TARGET_NAME} PRIVATE ${OPENVR_INCLUDE_DIRS}) target_link_libraries(${TARGET_NAME} ${OPENVR_LIBRARIES}) + target_link_libraries(${TARGET_NAME} Winmm.lib) endif() diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 8105de7a13..15fb7d72c9 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -7,6 +7,9 @@ // #include "OpenVrDisplayPlugin.h" +// Odd ordering of header is required to avoid 'macro redinition warnings' +#include + #include #include #include @@ -713,3 +716,30 @@ bool OpenVrDisplayPlugin::isKeyboardVisible() { int OpenVrDisplayPlugin::getRequiredThreadCount() const { return Parent::getRequiredThreadCount() + (_threadedSubmit ? 1 : 0); } + +QString OpenVrDisplayPlugin::getPreferredAudioInDevice() const { + QString device = getVrSettingString(vr::k_pch_audio_Section, vr::k_pch_audio_OnPlaybackDevice_String); + if (!device.isEmpty()) { + static const WCHAR INIT = 0; + size_t size = device.size() + 1; + std::vector deviceW; + deviceW.assign(size, INIT); + device.toWCharArray(deviceW.data()); + device = AudioClient::friendlyNameForAudioDevice(deviceW.data()); + } + return device; +} + +QString OpenVrDisplayPlugin::getPreferredAudioOutDevice() const { + QString device = getVrSettingString(vr::k_pch_audio_Section, vr::k_pch_audio_OnRecordDevice_String); + if (!device.isEmpty()) { + static const WCHAR INIT = 0; + size_t size = device.size() + 1; + std::vector deviceW; + deviceW.assign(size, INIT); + device.toWCharArray(deviceW.data()); + device = AudioClient::friendlyNameForAudioDevice(deviceW.data()); + } + return device; +} + diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.h b/plugins/openvr/src/OpenVrDisplayPlugin.h index a60c21a606..a1bbed8754 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.h +++ b/plugins/openvr/src/OpenVrDisplayPlugin.h @@ -56,7 +56,10 @@ public: bool isKeyboardVisible() override; // Possibly needs an additional thread for VR submission - int getRequiredThreadCount() const override; + int getRequiredThreadCount() const override; + + QString getPreferredAudioInDevice() const override; + QString getPreferredAudioOutDevice() const override; protected: bool internalActivate() override; diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index d9db757b2f..7e287a16c3 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -72,6 +72,21 @@ bool openVrSupported() { return (enableDebugOpenVR || !isOculusPresent()) && vr::VR_IsHmdPresent(); } +QString getVrSettingString(const char* section, const char* setting) { + QString result; + static const uint32_t BUFFER_SIZE = 1024; + static char BUFFER[BUFFER_SIZE]; + vr::IVRSettings * vrSettings = vr::VRSettings(); + if (vrSettings) { + vr::EVRSettingsError error = vr::VRSettingsError_None; + vrSettings->GetString(vr::k_pch_audio_Section, vr::k_pch_audio_OnPlaybackDevice_String, BUFFER, BUFFER_SIZE, &error); + if (error == vr::VRSettingsError_None) { + result = BUFFER; + } + } + return result; +} + vr::IVRSystem* acquireOpenVrSystem() { bool hmdPresent = vr::VR_IsHmdPresent(); if (hmdPresent) { @@ -82,6 +97,7 @@ vr::IVRSystem* acquireOpenVrSystem() { #endif vr::EVRInitError eError = vr::VRInitError_None; activeHmd = vr::VR_Init(&eError, vr::VRApplication_Scene); + #if DEV_BUILD qCDebug(displayplugins) << "OpenVR display: HMD is " << activeHmd << " error is " << eError; #endif diff --git a/plugins/openvr/src/OpenVrHelpers.h b/plugins/openvr/src/OpenVrHelpers.h index f00cd9e117..f4253899a2 100644 --- a/plugins/openvr/src/OpenVrHelpers.h +++ b/plugins/openvr/src/OpenVrHelpers.h @@ -25,6 +25,7 @@ bool openVrQuitRequested(); void enableOpenVrKeyboard(PluginContainer* container); void disableOpenVrKeyboard(); bool isOpenVrKeyboardShown(); +QString getVrSettingString(const char* section, const char* setting); template diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 53500a3353..7330daf228 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -50,11 +50,15 @@ static const char* RENDER_CONTROLLERS = "Render Hand Controllers"; static const int MIN_PUCK_COUNT = 2; static const int MIN_FEET_AND_HIPS = 3; static const int MIN_FEET_HIPS_CHEST = 4; +static const int MIN_FEET_HIPS_HEAD = 4; static const int MIN_FEET_HIPS_SHOULDERS = 5; +static const int MIN_FEET_HIPS_CHEST_HEAD = 5; static const int FIRST_FOOT = 0; static const int SECOND_FOOT = 1; static const int HIP = 2; static const int CHEST = 3; +static float HEAD_PUCK_Y_OFFSET = -0.0254f; +static float HEAD_PUCK_Z_OFFSET = -0.152f; const char* ViveControllerManager::NAME { "OpenVR" }; @@ -80,6 +84,28 @@ static bool sortPucksXPosition(PuckPosePair firstPuck, PuckPosePair secondPuck) return (firstPuck.second.translation.x < secondPuck.second.translation.x); } +static bool determineFeetOrdering(const controller::Pose& poseA, const controller::Pose& poseB, glm::vec3 axis, glm::vec3 axisOrigin) { + glm::vec3 poseAPosition = poseA.getTranslation(); + glm::vec3 poseBPosition = poseB.getTranslation(); + + glm::vec3 poseAVector = poseAPosition - axisOrigin; + glm::vec3 poseBVector = poseBPosition - axisOrigin; + + float poseAProjection = glm::dot(poseAVector, axis); + float poseBProjection = glm::dot(poseBVector, axis); + return (poseAProjection > poseBProjection); +} + +static glm::vec3 getReferenceHeadXAxis(glm::mat4 defaultToReferenceMat, glm::mat4 defaultHead) { + glm::mat4 finalHead = defaultToReferenceMat * defaultHead; + return glmExtractRotation(finalHead) * Vectors::UNIT_X; +} + +static glm::vec3 getReferenceHeadPosition(glm::mat4 defaultToReferenceMat, glm::mat4 defaultHead) { + glm::mat4 finalHead = defaultToReferenceMat * defaultHead; + return extractTranslation(finalHead); +} + static QString deviceTrackingResultToString(vr::ETrackingResult trackingResult) { QString result; auto iterator = TRACKING_RESULT_TO_STRING.find(trackingResult); @@ -97,15 +123,18 @@ bool ViveControllerManager::isSupported() const { bool ViveControllerManager::activate() { InputPlugin::activate(); - _container->addMenu(MENU_PATH); - _container->addMenuItem(PluginType::INPUT_PLUGIN, MENU_PATH, RENDER_CONTROLLERS, - [this] (bool clicked) { this->setRenderControllers(clicked); }, - true, true); - if (!_system) { _system = acquireOpenVrSystem(); } - Q_ASSERT(_system); + + if (!_system) { + return false; + } + + _container->addMenu(MENU_PATH); + _container->addMenuItem(PluginType::INPUT_PLUGIN, MENU_PATH, RENDER_CONTROLLERS, + [this](bool clicked) { this->setRenderControllers(clicked); }, + true, true); enableOpenVrKeyboard(_container); @@ -138,6 +167,14 @@ void ViveControllerManager::deactivate() { _registeredWithInputMapper = false; } +bool ViveControllerManager::isHeadControllerMounted() const { + if (_inputDevice && _inputDevice->isHeadControllerMounted()) { + return true; + } + vr::EDeviceActivityLevel activityLevel = _system->GetTrackedDeviceActivityLevel(vr::k_unTrackedDeviceIndex_Hmd); + return activityLevel == vr::k_EDeviceActivityLevel_UserInteraction; +} + void ViveControllerManager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { if (!_system) { @@ -175,6 +212,8 @@ ViveControllerManager::InputDevice::InputDevice(vr::IVRSystem*& system) : contro _configStringMap[Config::FeetAndHips] = QString("FeetAndHips"); _configStringMap[Config::FeetHipsAndChest] = QString("FeetHipsAndChest"); _configStringMap[Config::FeetHipsAndShoulders] = QString("FeetHipsAndShoulders"); + _configStringMap[Config::FeetHipsChestAndHead] = QString("FeetHipsChestAndHead"); + _configStringMap[Config::FeetHipsAndHead] = QString("FeetHipsAndHead"); if (openVrSupported()) { createPreferences(); @@ -355,6 +394,23 @@ void ViveControllerManager::InputDevice::calibrate(const controller::InputCalibr int firstShoulderIndex = 3; int secondShoulderIndex = 4; calibrateShoulders(defaultToReferenceMat, inputCalibration, firstShoulderIndex, secondShoulderIndex); + } else if (_config == Config::FeetHipsAndHead && puckCount == MIN_FEET_HIPS_HEAD) { + glm::mat4 headPuckDefaultToReferenceMat = recalculateDefaultToReferenceForHeadPuck(inputCalibration); + glm::vec3 headXAxis = getReferenceHeadXAxis(headPuckDefaultToReferenceMat, inputCalibration.defaultHeadMat); + glm::vec3 headPosition = getReferenceHeadPosition(headPuckDefaultToReferenceMat, inputCalibration.defaultHeadMat); + calibrateFeet(headPuckDefaultToReferenceMat, inputCalibration, headXAxis, headPosition); + calibrateHips(headPuckDefaultToReferenceMat, inputCalibration); + calibrateHead(headPuckDefaultToReferenceMat, inputCalibration); + _overrideHead = true; + } else if (_config == Config::FeetHipsChestAndHead && puckCount == MIN_FEET_HIPS_CHEST_HEAD) { + glm::mat4 headPuckDefaultToReferenceMat = recalculateDefaultToReferenceForHeadPuck(inputCalibration); + glm::vec3 headXAxis = getReferenceHeadXAxis(headPuckDefaultToReferenceMat, inputCalibration.defaultHeadMat); + glm::vec3 headPosition = getReferenceHeadPosition(headPuckDefaultToReferenceMat, inputCalibration.defaultHeadMat); + calibrateFeet(headPuckDefaultToReferenceMat, inputCalibration, headXAxis, headPosition); + calibrateHips(headPuckDefaultToReferenceMat, inputCalibration); + calibrateChest(headPuckDefaultToReferenceMat, inputCalibration); + calibrateHead(headPuckDefaultToReferenceMat, inputCalibration); + _overrideHead = true; } else { qDebug() << "Puck Calibration: " << configToString(_config) << " Config Failed: Could not meet the minimal # of pucks"; uncalibrate(); @@ -369,6 +425,7 @@ void ViveControllerManager::InputDevice::uncalibrate() { _pucksOffset.clear(); _jointToPuckMap.clear(); _calibrated = false; + _overrideHead = false; } void ViveControllerManager::InputDevice::updateCalibratedLimbs() { @@ -378,6 +435,10 @@ void ViveControllerManager::InputDevice::updateCalibratedLimbs() { _poseStateMap[controller::SPINE2] = addOffsetToPuckPose(controller::SPINE2); _poseStateMap[controller::RIGHT_ARM] = addOffsetToPuckPose(controller::RIGHT_ARM); _poseStateMap[controller::LEFT_ARM] = addOffsetToPuckPose(controller::LEFT_ARM); + + if (_overrideHead) { + _poseStateMap[controller::HEAD] = addOffsetToPuckPose(controller::HEAD); + } } controller::Pose ViveControllerManager::InputDevice::addOffsetToPuckPose(int joint) const { @@ -443,6 +504,43 @@ void ViveControllerManager::InputDevice::handleHandController(float deltaTime, u } } +glm::mat4 ViveControllerManager::InputDevice::recalculateDefaultToReferenceForHeadPuck(const controller::InputCalibrationData& inputCalibration) { + glm::mat4 avatarToSensorMat = glm::inverse(inputCalibration.sensorToWorldMat) * inputCalibration.avatarMat; + glm::mat4 sensorToAvatarMat = glm::inverse(inputCalibration.avatarMat) * inputCalibration.sensorToWorldMat; + size_t headPuckIndex = _validTrackedObjects.size() - 1; + controller::Pose headPuckPose = _validTrackedObjects[headPuckIndex].second; + glm::mat4 headPuckAvatarMat = createMatFromQuatAndPos(headPuckPose.getRotation(), headPuckPose.getTranslation()) * Matrices::Y_180; + glm::vec3 headPuckTranslation = extractTranslation(headPuckAvatarMat); + glm::vec3 headPuckZAxis = cancelOutRollAndPitch(glmExtractRotation(headPuckAvatarMat)) * glm::vec3(0.0f, 0.0f, 1.0f); + glm::vec3 worldUp = glm::vec3(0.0f, 1.0f, 0.0f); + + // check that the head puck z axis is not parrallel to the world up + const float EPSILON = 1.0e-4f; + glm::vec3 zAxis = glmExtractRotation(headPuckAvatarMat) * glm::vec3(0.0f, 0.0f, 1.0f); + if (fabsf(fabsf(glm::dot(glm::normalize(worldUp), glm::normalize(zAxis))) - 1.0f) < EPSILON) { + headPuckZAxis = glm::vec3(1.0f, 0.0f, 0.0f); + } + + glm::vec3 yPrime = glm::vec3(0.0f, 1.0f, 0.0f); + glm::vec3 xPrime = glm::normalize(glm::cross(worldUp, headPuckZAxis)); + glm::vec3 zPrime = glm::normalize(glm::cross(xPrime, yPrime)); + glm::mat4 newHeadPuck = glm::mat4(glm::vec4(xPrime, 0.0f), glm::vec4(yPrime, 0.0f), + glm::vec4(zPrime, 0.0f), glm::vec4(headPuckTranslation, 1.0f)); + + glm::mat4 headPuckOffset = glm::mat4(glm::vec4(1.0f, 0.0f, 0.0f, 0.0f), glm::vec4(0.0f, 1.0f, 0.0f, 0.0f), + glm::vec4(0.0f, 0.0f, 1.0f, 0.0f), glm::vec4(0.0f, HEAD_PUCK_Y_OFFSET, HEAD_PUCK_Z_OFFSET, 1.0f)); + + glm::mat4 finalHeadPuck = newHeadPuck * headPuckOffset; + + glm::mat4 defaultHeadOffset = glm::inverse(inputCalibration.defaultCenterEyeMat) * inputCalibration.defaultHeadMat; + + glm::mat4 currentHead = finalHeadPuck * defaultHeadOffset; + + // calculate the defaultToRefrenceXform + glm::mat4 defaultToReferenceMat = currentHead * glm::inverse(inputCalibration.defaultHeadMat); + return defaultToReferenceMat; +} + void ViveControllerManager::InputDevice::partitionTouchpad(int sButton, int xAxis, int yAxis, int centerPseudoButton, int xPseudoButton, int yPseudoButton) { // Populate the L/RS_CENTER/OUTER pseudo buttons, corresponding to a partition of the L/RS space based on the X/Y values. const float CENTER_DEADBAND = 0.6f; @@ -630,6 +728,26 @@ void ViveControllerManager::InputDevice::calibrateFeet(glm::mat4& defaultToRefer } } + +void ViveControllerManager::InputDevice::calibrateFeet(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration, glm::vec3 headXAxis, glm::vec3 headPosition) { + auto& firstFoot = _validTrackedObjects[FIRST_FOOT]; + auto& secondFoot = _validTrackedObjects[SECOND_FOOT]; + controller::Pose& firstFootPose = firstFoot.second; + controller::Pose& secondFootPose = secondFoot.second; + + if (determineFeetOrdering(firstFootPose, secondFootPose, headXAxis, headPosition)) { + _jointToPuckMap[controller::LEFT_FOOT] = firstFoot.first; + _pucksOffset[firstFoot.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultLeftFoot, firstFootPose); + _jointToPuckMap[controller::RIGHT_FOOT] = secondFoot.first; + _pucksOffset[secondFoot.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultRightFoot, secondFootPose); + } else { + _jointToPuckMap[controller::LEFT_FOOT] = secondFoot.first; + _pucksOffset[secondFoot.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultLeftFoot, secondFootPose); + _jointToPuckMap[controller::RIGHT_FOOT] = firstFoot.first; + _pucksOffset[firstFoot.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultRightFoot, firstFootPose); + } +} + void ViveControllerManager::InputDevice::calibrateHips(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration) { _jointToPuckMap[controller::HIPS] = _validTrackedObjects[HIP].first; _pucksOffset[_validTrackedObjects[HIP].first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultHips, _validTrackedObjects[HIP].second); @@ -658,7 +776,19 @@ void ViveControllerManager::InputDevice::calibrateShoulders(glm::mat4& defaultTo _jointToPuckMap[controller::RIGHT_ARM] = firstShoulder.first; _pucksOffset[firstShoulder.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultRightArm, firstShoulder.second); } -} +} + +void ViveControllerManager::InputDevice::calibrateHead(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration) { + size_t headIndex = _validTrackedObjects.size() - 1; + const PuckPosePair& head = _validTrackedObjects[headIndex]; + + // assume the person is wearing the head puck on his/her forehead + glm::mat4 defaultHeadOffset = glm::inverse(inputCalibration.defaultCenterEyeMat) * inputCalibration.defaultHeadMat; + controller::Pose newHead = head.second.postTransform(defaultHeadOffset); + + _jointToPuckMap[controller::HEAD] = head.first; + _pucksOffset[head.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultHeadMat, newHead); +} void ViveControllerManager::InputDevice::loadSettings() { @@ -694,6 +824,10 @@ void ViveControllerManager::InputDevice::setConfigFromString(const QString& valu _preferedConfig = Config::FeetHipsAndChest; } else if (value == "FeetHipsAndShoulders") { _preferedConfig = Config::FeetHipsAndShoulders; + } else if (value == "FeetHipsChestAndHead") { + _preferedConfig = Config::FeetHipsChestAndHead; + } else if (value == "FeetHipsAndHead") { + _preferedConfig = Config::FeetHipsAndHead; } } @@ -702,11 +836,43 @@ void ViveControllerManager::InputDevice::createPreferences() { auto preferences = DependencyManager::get(); static const QString VIVE_PUCKS_CONFIG = "Vive Pucks Configuration"; + { + static const float MIN_VALUE = -3.0f; + static const float MAX_VALUE = 3.0f; + static const float STEP = 0.01f; + + auto getter = [this]()->float { return HEAD_PUCK_Y_OFFSET; }; + auto setter = [this](const float& value) { HEAD_PUCK_Y_OFFSET = value; }; + + auto preference = new SpinnerPreference(VIVE_PUCKS_CONFIG, "HeadPuckYOffset", getter, setter); + preference->setMin(MIN_VALUE); + preference->setMax(MAX_VALUE); + preference->setDecimals(3); + preference->setStep(STEP); + preferences->addPreference(preference); + } + + { + static const float MIN_VALUE = -3.0f; + static const float MAX_VALUE = 3.0f; + static const float STEP = 0.01f; + + auto getter = [this]()->float { return HEAD_PUCK_Z_OFFSET; }; + auto setter = [this](const float& value) { HEAD_PUCK_Z_OFFSET = value; }; + + auto preference = new SpinnerPreference(VIVE_PUCKS_CONFIG, "HeadPuckXOffset", getter, setter); + preference->setMin(MIN_VALUE); + preference->setMax(MAX_VALUE); + preference->setStep(STEP); + preference->setDecimals(3); + preferences->addPreference(preference); + } + { auto getter = [this]()->QString { return _configStringMap[_preferedConfig]; }; auto setter = [this](const QString& value) { setConfigFromString(value); saveSettings(); }; auto preference = new ComboBoxPreference(VIVE_PUCKS_CONFIG, "Configuration", getter, setter); - QStringList list = {"Auto", "Feet", "FeetAndHips", "FeetHipsAndChest", "FeetHipsAndShoulders"}; + QStringList list = {"Auto", "Feet", "FeetAndHips", "FeetHipsAndChest", "FeetHipsAndShoulders", "FeetHipsAndHead"}; preference->setItems(list); preferences->addPreference(preference); diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index a76adaa8f9..9a27543e8f 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -41,6 +41,8 @@ public: const QString getName() const override { return NAME; } bool isHandController() const override { return true; } + bool isHeadController() const override { return true; } + bool isHeadControllerMounted() const; bool activate() override; void deactivate() override; @@ -54,6 +56,7 @@ private: class InputDevice : public controller::InputDevice { public: InputDevice(vr::IVRSystem*& system); + bool isHeadControllerMounted() const { return _overrideHead; } private: // Device functions controller::Input::NamedVector getAvailableInputs() const override; @@ -67,6 +70,7 @@ private: void calibrate(const controller::InputCalibrationData& inputCalibration); void uncalibrate(); controller::Pose addOffsetToPuckPose(int joint) const; + glm::mat4 recalculateDefaultToReferenceForHeadPuck(const controller::InputCalibrationData& inputCalibration); void updateCalibratedLimbs(); bool checkForCalibrationEvent(); void handleHandController(float deltaTime, uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData, bool isLeftHand); @@ -84,11 +88,14 @@ private: void loadSettings(); void saveSettings() const; void calibrateFeet(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration); + void calibrateFeet(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration, glm::vec3 headXAxis, glm::vec3 headPosition); void calibrateHips(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration); void calibrateChest(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration); void calibrateShoulders(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration, int firstShoulderIndex, int secondShoulderIndex); + void calibrateHead(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration); + class FilteredStick { public: @@ -119,6 +126,8 @@ private: FeetAndHips, FeetHipsAndChest, FeetHipsAndShoulders, + FeetHipsChestAndHead, + FeetHipsAndHead }; Config _config { Config::Auto }; Config _preferedConfig { Config::Auto }; @@ -137,7 +146,7 @@ private: int _trackedControllers { 0 }; vr::IVRSystem*& _system; - quint64 _timeTilCalibration { 0.0f }; + quint64 _timeTilCalibration { 0 }; float _leftHapticStrength { 0.0f }; float _leftHapticDuration { 0.0f }; float _rightHapticStrength { 0.0f }; @@ -145,6 +154,7 @@ private: bool _triggersPressedHandled { false }; bool _calibrated { false }; bool _timeTilCalibrationSet { false }; + bool _overrideHead { false }; mutable std::recursive_mutex _lock; QString configToString(Config config); diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index b3ea5f2ea8..e368354b56 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -31,7 +31,8 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/tablet-ui/tabletUI.js" ]; var DEFAULT_SCRIPTS_SEPARATE = [ - "system/controllers/controllerScripts.js" + "system/controllers/controllerScripts.js", + "system/chat.js" ]; // add a menu item for debugging diff --git a/scripts/developer/tests/avatarToWorldTests.js b/scripts/developer/tests/avatarToWorldTests.js new file mode 100644 index 0000000000..c6e23fc81b --- /dev/null +++ b/scripts/developer/tests/avatarToWorldTests.js @@ -0,0 +1,127 @@ +var AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}"; + +var debugSphereBaseProperties = { + type: "Sphere", + dimensions: { x: 0.2, y: 0.2, z: 0.2 }, + dynamic: false, + collisionless: true, + gravity: { x: 0, y: 0, z: 0 }, + lifetime: 10.0, + userData: "{ \"grabbableKey\": { \"grabbable\": false, \"kinematic\": false } }" +}; + +var debugBoxBaseProperties = { + type: "Box", + dimensions: { x: 0.2, y: 0.2, z: 0.2 }, + dynamic: false, + collisionless: true, + gravity: { x: 0, y: 0, z: 0 }, + lifetime: 10.0, + userData: "{ \"grabbableKey\": { \"grabbable\": false, \"kinematic\": false } }" +}; + +//jointToWorldPoint +// create sphere for finger on left hand +// each frame, calculate world position of finger, with some offset +// update sphere to match this position +var jointToWorldPointTest_sphereEntity; +function jointToWorldPointTest() { + var jointIndex = MyAvatar.getJointIndex("LeftHandPinky4"); + var jointOffset_WorldSpace = { x: 0.1, y: 0, z: 0 }; + var worldPos = MyAvatar.jointToWorldPoint(jointOffset_WorldSpace, jointIndex); + + var jointSphereProps = Object.create(debugSphereBaseProperties); + jointSphereProps.name = "jointToWorldPointTest_Sphere"; + jointSphereProps.color = { blue: 240, green: 150, red: 150 }; + jointSphereProps.position = worldPos; + jointToWorldPointTest_sphereEntity = Entities.addEntity(jointSphereProps); +} +function jointToWorldPointTest_update(deltaTime) { + var jointIndex = MyAvatar.getJointIndex("LeftHandPinky4"); + var jointOffset_WorldSpace = { x: 0.1, y: 0, z: 0 }; + var worldPos = MyAvatar.jointToWorldPoint(jointOffset_WorldSpace, jointIndex); + var newProperties = { position: worldPos }; + Entities.editEntity(jointToWorldPointTest_sphereEntity, newProperties); +} + +//jointToWorldDirection +// create line in world space +// each frame calculate world space direction of players head z axis +// update line to match +var jointToWorldDirectionTest_lineEntity; +function jointToWorldDirectionTest() { + var jointIndex = MyAvatar.getJointIndex("Head"); + var avatarPos = MyAvatar.getJointPosition(jointIndex); + + var jointDir = { x: 1, y: 0, z: 1 }; + var worldDir = MyAvatar.jointToWorldDirection(jointDir, jointIndex); + print(worldDir.x); + print(worldDir.y); + print(worldDir.z); + jointToWorldDirectionTest_lineEntity = Entities.addEntity({ + type: "Line", + color: {red: 250, green: 0, blue: 0}, + dimensions: {x: 5, y: 5, z: 5}, + lifetime: 10.0, + linePoints: [{ + x: 0, + y: 0, + z: 0 + }, worldDir + ], + position : avatarPos, + }); +} +function jointToWorldDirection_update(deltaTime) { + var jointIndex = MyAvatar.getJointIndex("Head"); + var avatarPos = MyAvatar.getJointPosition(jointIndex); + var jointDir = { x: 1, y: 0, z: 0 }; + var worldDir = MyAvatar.jointToWorldDirection(jointDir, jointIndex); + var newProperties = { + linePoints: [{ + x: 0, + y: 0, + z: 0 + }, worldDir + ], + position : avatarPos + }; + + Entities.editEntity(jointToWorldDirectionTest_lineEntity, newProperties); +} + +//jointToWorldRotation +// create box in world space +// each frame calculate world space rotation of players head +// update box rotation to match +var jointToWorldRotationTest_boxEntity; +function jointToWorldRotationTest() { + var jointIndex = MyAvatar.getJointIndex("Head"); + var jointPosition_WorldSpace = MyAvatar.getJointPosition(jointIndex); + var jointRot = MyAvatar.getJointRotation(jointIndex); + var jointRot_WorldSpace = MyAvatar.jointToWorldRotation(jointRot, jointIndex); + + var boxProps = Object.create(debugBoxBaseProperties); + boxProps.name = "jointToWorldRotationTest_Box"; + boxProps.color = { blue: 250, green: 250, red: 250 }; + boxProps.position = jointPosition_WorldSpace; + boxProps.rotation = jointRot_WorldSpace; + jointToWorldRotationTest_boxEntity = Entities.addEntity(boxProps); +} +function jointToWorldRotationTest_update(deltaTime) { + var jointIndex = MyAvatar.getJointIndex("Head"); + var jointPosition_WorldSpace = MyAvatar.getJointPosition(jointIndex); + var jointRot = MyAvatar.getJointRotation(jointIndex); + var jointRot_WorldSpace = MyAvatar.jointToWorldRotation(jointRot, jointIndex); + var newProperties = { position: jointPosition_WorldSpace, rotation: jointRot_WorldSpace }; + Entities.editEntity(jointToWorldRotationTest_boxEntity, newProperties); +} + +jointToWorldPointTest(); +Script.update.connect(jointToWorldPointTest_update); + +jointToWorldDirectionTest(); +Script.update.connect(jointToWorldDirection_update); + +jointToWorldRotationTest(); +Script.update.connect(jointToWorldRotationTest_update); diff --git a/scripts/developer/tests/worldToAvatarTests.js b/scripts/developer/tests/worldToAvatarTests.js new file mode 100644 index 0000000000..6f0b19dc2d --- /dev/null +++ b/scripts/developer/tests/worldToAvatarTests.js @@ -0,0 +1,134 @@ +var AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}"; + +var debugSphereBaseProperties = { + type: "Sphere", + dimensions: { x: 0.2, y: 0.2, z: 0.2 }, + dynamic: false, + collisionless: true, + gravity: { x: 0, y: 0, z: 0 }, + lifetime: 10.0, + userData: "{ \"grabbableKey\": { \"grabbable\": false, \"kinematic\": false } }" +}; + +var debugBoxBaseProperties = { + type: "Box", + dimensions: { x: 0.2, y: 0.2, z: 0.2 }, + dynamic: false, + collisionless: true, + gravity: { x: 0, y: 0, z: 0 }, + lifetime: 10.0, + userData: "{ \"grabbableKey\": { \"grabbable\": false, \"kinematic\": false } }" +}; + +//worldToJointPoint +// calculate position offset from joint using getJointPosition +// pass through worldToJointPoint to get offset in joint space of players joint +// create a blue sphere and attach it to players joint using the joint space offset +// The two spheres should appear in the same place, but the blue sphere will follow the avatar +function worldToJointPointTest() { + var jointIndex = MyAvatar.getJointIndex("LeftHandPinky4"); + var avatarPos = MyAvatar.position; + + var jointPosition_WorldSpace = MyAvatar.getJointPosition(jointIndex); + var jointOffset_WorldSpace = { x: 0.1, y: 0, z: 0 }; + var jointPosition_WorldSpaceOffset = Vec3.sum(jointPosition_WorldSpace, jointOffset_WorldSpace); + var jointPosition_JointSpaceOffset = MyAvatar.worldToJointPoint(jointPosition_WorldSpaceOffset, jointIndex); + + var jointSphereProps = Object.create(debugSphereBaseProperties); + jointSphereProps.name = "worldToJointPointTest_Joint"; + jointSphereProps.color = { blue: 240, green: 150, red: 150 }; + jointSphereProps.localPosition = jointPosition_JointSpaceOffset; + jointSphereProps.parentID = AVATAR_SELF_ID; + jointSphereProps.parentJointIndex = jointIndex; + Entities.addEntity(jointSphereProps); + + var worldSphereProps = Object.create(debugSphereBaseProperties); + worldSphereProps.name = "worldToJointPointTest_World"; + worldSphereProps.color = { blue: 150, green: 250, red: 150 }; + worldSphereProps.position = jointPosition_WorldSpaceOffset; + Entities.addEntity(worldSphereProps); +} + +//worldToJointDirection +// create line and attach to avatars head +// each frame calculate direction of world x axis in joint space of players head +// update arrow orientation to match +var worldToJointDirectionTest_lineEntity; +function worldToJointDirectionTest() { + var jointIndex = MyAvatar.getJointIndex("Head"); + + var jointPosition_WorldSpace = MyAvatar.getJointPosition(jointIndex); + var jointOffset_WorldSpace = { x: 0, y: 0, z: 0 }; + var jointPosition_WorldSpaceOffset = Vec3.sum(jointPosition_WorldSpace, jointOffset_WorldSpace); + var jointPosition_JointSpaceOffset = MyAvatar.worldToJointPoint(jointPosition_WorldSpaceOffset, jointIndex); + + var worldDir = { x: 1, y: 0, z: 0 }; + var avatarDir = MyAvatar.worldToJointDirection(worldDir, jointIndex); + + worldToJointDirectionTest_lineEntity = Entities.addEntity({ + type: "Line", + color: {red: 200, green: 250, blue: 0}, + dimensions: {x: 5, y: 5, z: 5}, + lifetime: 10.0, + linePoints: [{ + x: 0, + y: 0, + z: 0 + }, avatarDir + ], + localPosition : jointOffset_WorldSpace, + parentID : AVATAR_SELF_ID, + parentJointIndex : jointIndex + }); +} + +function worldToJointDirectionTest_update(deltaTime) { + var jointIndex = MyAvatar.getJointIndex("Head"); + var worldDir = { x: 1, y: 0, z: 0 }; + var avatarDir = MyAvatar.worldToJointDirection(worldDir, jointIndex); + var newProperties = { linePoints: [{ + x: 0, + y: 0, + z: 0 + }, avatarDir + ]}; + + Entities.editEntity(worldToJointDirectionTest_lineEntity, newProperties); +} + +//worldToJointRotation +// create box and parent to some player joint +// convert world identity rotation to joint space rotation +// each frame, update box with new orientation +var worldToJointRotationTest_boxEntity; +function worldToJointRotationTest() { + var jointIndex = MyAvatar.getJointIndex("RightHandPinky4"); + var avatarPos = MyAvatar.position; + + var jointPosition_WorldSpace = MyAvatar.getJointPosition(jointIndex); + var jointOffset_WorldSpace = { x: 0.0, y: 0, z: 0 }; + var jointPosition_WorldSpaceOffset = Vec3.sum(jointPosition_WorldSpace, jointOffset_WorldSpace); + var jointPosition_JointSpaceOffset = MyAvatar.worldToJointPoint(jointPosition_WorldSpaceOffset, jointIndex); + + var jointBoxProps = Object.create(debugBoxBaseProperties); + jointBoxProps.name = "worldToJointRotationTest_Box"; + jointBoxProps.color = { blue: 0, green: 0, red: 250 }; + jointBoxProps.localPosition = jointPosition_JointSpaceOffset; + jointBoxProps.parentID = AVATAR_SELF_ID; + jointBoxProps.parentJointIndex = jointIndex; + worldToJointRotationTest_boxEntity = Entities.addEntity(jointBoxProps); +} +function worldToJointRotationTest_update(deltaTime) { + var jointIndex = MyAvatar.getJointIndex("RightHandPinky4"); + var worldRot = Quat.fromPitchYawRollDegrees(0,0,0); + var avatarRot = MyAvatar.worldToJointRotation(worldRot, jointIndex); + var newProperties = { localRotation: avatarRot }; + Entities.editEntity(worldToJointRotationTest_boxEntity, newProperties); +} + +worldToJointPointTest(); +worldToJointDirectionTest(); +worldToJointRotationTest(); + +Script.update.connect(worldToJointDirectionTest_update); +Script.update.connect(worldToJointRotationTest_update); diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index 229a2d1c3b..ff4621a87a 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -64,6 +64,7 @@ Column { "Point:LightingModel:enablePointLight", "Spot:LightingModel:enableSpotLight", "Light Contour:LightingModel:showLightContour", + "Zone Stack:DrawZoneStack:enabled", "Shadow:RenderShadowTask:enabled" ] CheckBox { @@ -162,13 +163,9 @@ Column { } } + Row { Column { - id: metas - CheckBox { - text: "Metas" - checked: Render.getConfig("DrawMetaBounds")["enabled"] - onCheckedChanged: { Render.getConfig("DrawMetaBounds")["enabled"] = checked } - } + CheckBox { text: "Opaques" checked: Render.getConfig("DrawOpaqueBounds")["enabled"] @@ -189,11 +186,24 @@ Column { checked: Render.getConfig("DrawOverlayTransparentBounds")["enabled"] onCheckedChanged: { Render.getConfig("DrawOverlayTransparentBounds")["enabled"] = checked } } + } + Column { + CheckBox { + text: "Metas" + checked: Render.getConfig("DrawMetaBounds")["enabled"] + onCheckedChanged: { Render.getConfig("DrawMetaBounds")["enabled"] = checked } + } + CheckBox { + text: "Lights" + checked: Render.getConfig("DrawLightBounds")["enabled"] + onCheckedChanged: { Render.getConfig("DrawLightBounds")["enabled"] = checked; } + } CheckBox { text: "Zones" checked: Render.getConfig("DrawZones")["enabled"] onCheckedChanged: { Render.getConfig("ZoneRenderer")["enabled"] = checked; Render.getConfig("DrawZones")["enabled"] = checked; } } } + } } diff --git a/scripts/developer/utilities/render/stats.qml b/scripts/developer/utilities/render/stats.qml index 7acf678570..54e0dc4ce8 100644 --- a/scripts/developer/utilities/render/stats.qml +++ b/scripts/developer/utilities/render/stats.qml @@ -20,9 +20,9 @@ Item { id: stats spacing: 8 anchors.fill:parent - + property var config: Render.getConfig("Stats") - + function evalEvenHeight() { // Why do we have to do that manually ? cannot seem to find a qml / anchor / layout mode that does that ? return (height - spacing * (children.length - 1)) / children.length @@ -81,7 +81,7 @@ Item { color: "#1AC567" }, { - prop: "textureGPUTransferCount", + prop: "texturePendingGPUTransferCount", label: "Transfer", color: "#9495FF" } @@ -158,7 +158,7 @@ Item { } ] } - + PlotPerf { title: "State Changes" height: parent.evalEvenHeight() @@ -180,7 +180,7 @@ Item { color: "#1AC567" } ] - } + } property var drawOpaqueConfig: Render.getConfig("DrawOpaqueDeferred") property var drawTransparentConfig: Render.getConfig("DrawTransparentDeferred") @@ -211,7 +211,7 @@ Item { color: "#FED959" } ] - } + } PlotPerf { title: "Timing" @@ -250,4 +250,3 @@ Item { } } - diff --git a/scripts/system/assets/sounds/snap.wav b/scripts/system/assets/sounds/snap.wav deleted file mode 100644 index bb562e1db9..0000000000 Binary files a/scripts/system/assets/sounds/snap.wav and /dev/null differ diff --git a/scripts/system/assets/sounds/sound-print-photo.wav b/scripts/system/assets/sounds/sound-print-photo.wav new file mode 100644 index 0000000000..ecc5169968 Binary files /dev/null and b/scripts/system/assets/sounds/sound-print-photo.wav differ diff --git a/scripts/system/bubble.js b/scripts/system/bubble.js index 8d103c93de..c2a2f7af40 100644 --- a/scripts/system/bubble.js +++ b/scripts/system/bubble.js @@ -10,7 +10,7 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global Script, Users, Overlays, AvatarList, Controller, Camera, getControllerWorldLocation */ +/* global Script, Users, Overlays, AvatarList, Controller, Camera, getControllerWorldLocation, UserActivityLogger */ (function () { // BEGIN LOCAL_SCOPE var button; @@ -76,6 +76,7 @@ // Called from the C++ scripting interface to show the bubble overlay function enteredIgnoreRadius() { createOverlays(); + UserActivityLogger.bubbleActivated(); } // Used to set the state of the bubble HUD button @@ -139,10 +140,14 @@ } // When the space bubble is toggled... - function onBubbleToggled() { - var bubbleActive = Users.getIgnoreRadiusEnabled(); - writeButtonProperties(bubbleActive); - if (bubbleActive) { + // NOTE: the c++ calls this with just the first param -- we added a second + // just for not logging the initial state of the bubble when we startup. + function onBubbleToggled(enabled, doNotLog) { + writeButtonProperties(enabled); + if (doNotLog !== true) { + UserActivityLogger.bubbleToggled(enabled); + } + if (enabled) { createOverlays(); } else { hideOverlays(); @@ -163,7 +168,7 @@ sortOrder: 4 }); - onBubbleToggled(); + onBubbleToggled(Users.getIgnoreRadiusEnabled(), true); // pass in true so we don't log this initial one in the UserActivity table button.clicked.connect(Users.toggleIgnoreRadius); Users.ignoreRadiusEnabledChanged.connect(onBubbleToggled); diff --git a/unpublishedScripts/marketplace/chat/Chat.js b/scripts/system/chat.js similarity index 99% rename from unpublishedScripts/marketplace/chat/Chat.js rename to scripts/system/chat.js index 33bfcfeb4d..d03c6aae98 100644 --- a/unpublishedScripts/marketplace/chat/Chat.js +++ b/scripts/system/chat.js @@ -9,7 +9,7 @@ (function() { - var webPageURL = "ChatPage.html"; // URL of tablet web page. + var webPageURL = Script.resolvePath("html/ChatPage.html"); // URL of tablet web page. var randomizeWebPageURL = true; // Set to true for debugging. var lastWebPageURL = ""; // Last random URL of tablet web page. var onChatPage = false; // True when chat web page is opened. diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 993cf22d83..04921fe14d 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -3334,7 +3334,8 @@ function MyController(hand) { // If both secondary triggers squeezed, and the non-holding hand is empty, start scaling if (this.secondarySqueezed() && this.getOtherHandController().secondarySqueezed() && - this.getOtherHandController().state === STATE_OFF) { + this.grabbedThingID && this.getOtherHandController().grabbedThingID && + this.grabbedThingID == this.getOtherHandController().grabbedThingID) { this.scalingStartDistance = Vec3.length(Vec3.subtract(this.getHandPosition(), this.getOtherHandController().getHandPosition())); this.scalingStartDimensions = props.dimensions; diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index dcbcaeb621..b058ec670f 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -366,45 +366,25 @@ function Teleporter() { } // related to repositioning the avatar after you teleport +var FOOT_JOINT_NAMES = ["RightToe_End", "RightToeBase", "RightFoot"]; +var DEFAULT_ROOT_TO_FOOT_OFFSET = 0.5; function getAvatarFootOffset() { - var data = getJointData(); - var upperLeg, lowerLeg, foot, toe, toeTop; - data.forEach(function(d) { - var jointName = d.joint; - if (jointName === "RightUpLeg") { - upperLeg = d.translation.y; - } else if (jointName === "RightLeg") { - lowerLeg = d.translation.y; - } else if (jointName === "RightFoot") { - foot = d.translation.y; - } else if (jointName === "RightToeBase") { - toe = d.translation.y; - } else if (jointName === "RightToe_End") { - toeTop = d.translation.y; + // find a valid foot jointIndex + var footJointIndex = -1; + var i, l = FOOT_JOINT_NAMES.length; + for (i = 0; i < l; i++) { + footJointIndex = MyAvatar.getJointIndex(FOOT_JOINT_NAMES[i]); + if (footJointIndex != -1) { + break; } - }); - - var offset = upperLeg + lowerLeg + foot + toe + toeTop; - offset = offset / 100; - return offset; -} - -function getJointData() { - var allJointData = []; - var jointNames = MyAvatar.jointNames; - jointNames.forEach(function(joint, index) { - var translation = MyAvatar.getJointTranslation(index); - var rotation = MyAvatar.getJointRotation(index); - allJointData.push({ - joint: joint, - index: index, - translation: translation, - rotation: rotation - }); - }); - - return allJointData; + } + if (footJointIndex != -1) { + // default vertical offset from foot to avatar root. + return -MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(footJointIndex).y; + } else { + return DEFAULT_ROOT_TO_FOOT_OFFSET * MyAvatar.scale; + } } var leftPad = new ThumbPad('left'); diff --git a/scripts/system/edit.js b/scripts/system/edit.js index f39165f3df..a3583e7808 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -12,7 +12,7 @@ // 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, +/* 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 @@ -80,27 +80,7 @@ selectionManager.addEventListener(function () { } var type = Entities.getEntityProperties(selectedEntityID, "type").type; if (type === "ParticleEffect") { - // Destroy the old particles web view first - particleExplorerTool.destroyWebView(); - particleExplorerTool.createWebView(); - var properties = Entities.getEntityProperties(selectedEntityID); - var particleData = { - messageType: "particle_settings", - currentProperties: properties - }; - selectedParticleEntityID = selectedEntityID; - particleExplorerTool.setActiveParticleEntity(selectedParticleEntityID); - - particleExplorerTool.webView.webEventReceived.connect(function (data) { - data = JSON.parse(data); - if (data.messageType === "page_loaded") { - 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'}}); + selectParticleEntity(selectedEntityID); } else { needToDestroyParticleExplorer = true; } @@ -217,6 +197,32 @@ function hideMarketplace() { // } // } +function adjustPositionPerBoundingBox(position, direction, registration, dimensions, orientation) { + // Adjust the position such that the bounding box (registration, dimenions, and orientation) lies behind the original + // position in the given direction. + var CORNERS = [ + { x: 0, y: 0, z: 0 }, + { x: 0, y: 0, z: 1 }, + { x: 0, y: 1, z: 0 }, + { x: 0, y: 1, z: 1 }, + { x: 1, y: 0, z: 0 }, + { x: 1, y: 0, z: 1 }, + { x: 1, y: 1, z: 0 }, + { x: 1, y: 1, z: 1 }, + ]; + + // Go through all corners and find least (most negative) distance in front of position. + var distance = 0; + for (var i = 0, length = CORNERS.length; i < length; i++) { + var cornerVector = + Vec3.multiplyQbyV(orientation, Vec3.multiplyVbyV(Vec3.subtract(CORNERS[i], registration), dimensions)); + var cornerDistance = Vec3.dot(cornerVector, direction); + distance = Math.min(cornerDistance, distance); + } + position = Vec3.sum(Vec3.multiply(distance, direction), position); + return position; +} + var TOOLS_PATH = Script.resolvePath("assets/images/tools/"); var GRABBABLE_ENTITIES_MENU_CATEGORY = "Edit"; var GRABBABLE_ENTITIES_MENU_ITEM = "Create Entities As Grabbable"; @@ -234,6 +240,32 @@ var toolBar = (function () { var position = getPositionToCreateEntity(); var entityID = null; if (position !== null && position !== undefined) { + var direction; + if (Camera.mode === "entity" || Camera.mode === "independent") { + direction = Camera.orientation; + } else { + direction = MyAvatar.orientation; + } + direction = Vec3.multiplyQbyV(direction, Vec3.UNIT_Z); + + var PRE_ADJUST_ENTITY_TYPES = ["Box", "Sphere", "Shape", "Text", "Web"]; + if (PRE_ADJUST_ENTITY_TYPES.indexOf(properties.type) !== -1) { + // Adjust position of entity per bounding box prior to creating it. + var registration = properties.registration; + if (registration === undefined) { + var DEFAULT_REGISTRATION = { x: 0.5, y: 0.5, z: 0.5 }; + registration = DEFAULT_REGISTRATION; + } + + var orientation = properties.orientation; + if (orientation === undefined) { + var DEFAULT_ORIENTATION = Quat.fromPitchYawRollDegrees(0, 0, 0); + orientation = DEFAULT_ORIENTATION; + } + + position = adjustPositionPerBoundingBox(position, direction, registration, dimensions, orientation); + } + position = grid.snapToSurface(grid.snapToGrid(position, false, dimensions), dimensions); properties.position = position; if (Menu.isOptionChecked(GRABBABLE_ENTITIES_MENU_ITEM)) { @@ -243,6 +275,32 @@ var toolBar = (function () { if (properties.type == "ParticleEffect") { selectParticleEntity(entityID); } + + var POST_ADJUST_ENTITY_TYPES = ["Model"]; + if (POST_ADJUST_ENTITY_TYPES.indexOf(properties.type) !== -1) { + // Adjust position of entity per bounding box after it has been created and auto-resized. + var initialDimensions = Entities.getEntityProperties(entityID, ["dimensions"]).dimensions; + var DIMENSIONS_CHECK_INTERVAL = 200; + var MAX_DIMENSIONS_CHECKS = 10; + var dimensionsCheckCount = 0; + var dimensionsCheckFunction = function () { + dimensionsCheckCount++; + var properties = Entities.getEntityProperties(entityID, ["dimensions", "registrationPoint", "rotation"]); + if (!Vec3.equal(properties.dimensions, initialDimensions)) { + position = adjustPositionPerBoundingBox(position, direction, properties.registrationPoint, + properties.dimensions, properties.rotation); + position = grid.snapToSurface(grid.snapToGrid(position, false, properties.dimensions), + properties.dimensions); + Entities.editEntity(entityID, { + position: position + }); + selectionManager._update(); + } else if (dimensionsCheckCount < MAX_DIMENSIONS_CHECKS) { + Script.setTimeout(dimensionsCheckFunction, DIMENSIONS_CHECK_INTERVAL); + } + }; + Script.setTimeout(dimensionsCheckFunction, DIMENSIONS_CHECK_INTERVAL); + } } else { Window.notifyEditError("Can't create " + properties.type + ": " + properties.type + " would be out of bounds."); @@ -540,7 +598,6 @@ var toolBar = (function () { that.toggle = function () { that.setActive(!isActive); - activeButton.editProperties({isActive: isActive}); if (!isActive) { tablet.gotoHomeScreen(); } @@ -564,6 +621,10 @@ var toolBar = (function () { enabled: active })); isActive = active; + activeButton.editProperties({isActive: isActive}); + + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + if (!isActive) { entityListTool.setVisible(false); gridTool.setVisible(false); @@ -572,8 +633,8 @@ var toolBar = (function () { selectionManager.clearSelections(); cameraManager.disable(); selectionDisplay.triggerMapping.disable(); + tablet.landscape = false; } else { - var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); tablet.loadQMLSource("Edit.qml"); UserActivityLogger.enabledEdit(); entityListTool.setVisible(true); @@ -581,6 +642,8 @@ var toolBar = (function () { grid.setEnabled(true); propertiesTool.setVisible(true); selectionDisplay.triggerMapping.enable(); + print("starting tablet in landscape mode") + tablet.landscape = true; // Not sure what the following was meant to accomplish, but it currently causes // everybody else to think that Interface has lost focus overall. fogbugzid:558 // Window.setFocus(); @@ -1290,7 +1353,7 @@ function parentSelectedEntities() { } }); - if(parentCheck) { + if (parentCheck) { Window.notify("Entities parented"); }else { Window.notify("Entities are already parented to last"); @@ -1407,40 +1470,26 @@ function handeMenuEvent(menuItem) { } tooltip.show(false); } -function getPositionToCreateEntity() { - var HALF_TREE_SCALE = 16384; - var direction = Quat.getForward(MyAvatar.orientation); - var distance = 1; - var position = Vec3.sum(MyAvatar.position, Vec3.multiply(direction, distance)); +var HALF_TREE_SCALE = 16384; + +function getPositionToCreateEntity(extra) { + var CREATE_DISTANCE = 2; + var position; + var delta = extra !== undefined ? extra : 0; if (Camera.mode === "entity" || Camera.mode === "independent") { - position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getForward(Camera.orientation), distance)); + position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getForward(Camera.orientation), CREATE_DISTANCE + delta)); + } else { + position = Vec3.sum(MyAvatar.position, Vec3.multiply(Quat.getForward(MyAvatar.orientation), CREATE_DISTANCE + delta)); + position.y += 0.5; } - position.y += 0.5; + if (position.x > HALF_TREE_SCALE || position.y > HALF_TREE_SCALE || position.z > HALF_TREE_SCALE) { return null; } return position; } -function getPositionToImportEntity() { - var dimensions = Clipboard.getContentsDimensions(); - var HALF_TREE_SCALE = 16384; - var direction = Quat.getForward(MyAvatar.orientation); - var longest = 1; - longest = Math.sqrt(Math.pow(dimensions.x, 2) + Math.pow(dimensions.z, 2)); - var position = Vec3.sum(MyAvatar.position, Vec3.multiply(direction, longest)); - - if (Camera.mode === "entity" || Camera.mode === "independent") { - position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getForward(Camera.orientation), longest)); - } - - if (position.x > HALF_TREE_SCALE || position.y > HALF_TREE_SCALE || position.z > HALF_TREE_SCALE) { - return null; - } - - return position; -} function importSVO(importURL) { if (!Entities.canRez() && !Entities.canRezTmp()) { Window.notifyEditError(INSUFFICIENT_PERMISSIONS_IMPORT_ERROR_MSG); @@ -1458,22 +1507,73 @@ function importSVO(importURL) { if (success) { var VERY_LARGE = 10000; - var position = { - x: 0, - y: 0, - z: 0 - }; - if (Clipboard.getClipboardContentsLargestDimension() < VERY_LARGE) { - position = getPositionToImportEntity(); + var isLargeImport = Clipboard.getClipboardContentsLargestDimension() >= VERY_LARGE; + var position = Vec3.ZERO; + if (!isLargeImport) { + position = getPositionToCreateEntity(Clipboard.getClipboardContentsLargestDimension() / 2); } if (position !== null && position !== undefined) { var pastedEntityIDs = Clipboard.pasteEntities(position); + if (!isLargeImport) { + // The first entity in Clipboard gets the specified position with the rest being relative to it. Therefore, move + // entities after they're imported so that they're all the correct distance in front of and with geometric mean + // centered on the avatar/camera direction. + var deltaPosition = Vec3.ZERO; + + var properties = Entities.getEntityProperties(pastedEntityIDs[0], ["type"]); + var NO_ADJUST_ENTITY_TYPES = ["Zone", "Light", "ParticleEffect"]; + if (NO_ADJUST_ENTITY_TYPES.indexOf(properties.type) === -1) { + var targetDirection; + if (Camera.mode === "entity" || Camera.mode === "independent") { + targetDirection = Camera.orientation; + } else { + targetDirection = MyAvatar.orientation; + } + targetDirection = Vec3.multiplyQbyV(targetDirection, Vec3.UNIT_Z); + + var targetPosition = getPositionToCreateEntity(); + var deltaParallel = HALF_TREE_SCALE; // Distance to move entities parallel to targetDirection. + var deltaPerpendicular = Vec3.ZERO; // Distance to move entities perpendicular to targetDirection. + var entityPositions = []; + for (var i = 0, length = pastedEntityIDs.length; i < length; i++) { + var properties = Entities.getEntityProperties(pastedEntityIDs[i], ["position", "dimensions", + "registrationPoint", "rotation"]); + var adjustedPosition = adjustPositionPerBoundingBox(targetPosition, targetDirection, + properties.registrationPoint, properties.dimensions, properties.rotation); + var delta = Vec3.subtract(adjustedPosition, properties.position); + var distance = Vec3.dot(delta, targetDirection); + deltaParallel = Math.min(distance, deltaParallel); + deltaPerpendicular = Vec3.sum(Vec3.subtract(delta, Vec3.multiply(distance, targetDirection)), + deltaPerpendicular); + entityPositions[i] = properties.position; + } + deltaPerpendicular = Vec3.multiply(1 / pastedEntityIDs.length, deltaPerpendicular); + deltaPosition = Vec3.sum(Vec3.multiply(deltaParallel, targetDirection), deltaPerpendicular); + } + + if (grid.getSnapToGrid()) { + var properties = Entities.getEntityProperties(pastedEntityIDs[0], ["position", "dimensions", + "registrationPoint"]); + var position = Vec3.sum(deltaPosition, properties.position); + position = grid.snapToSurface(grid.snapToGrid(position, false, properties.dimensions, + properties.registrationPoint), properties.dimensions, properties.registrationPoint); + deltaPosition = Vec3.subtract(position, properties.position); + } + + if (!Vec3.equal(deltaPosition, Vec3.ZERO)) { + for (var i = 0, length = pastedEntityIDs.length; i < length; i++) { + Entities.editEntity(pastedEntityIDs[i], { + position: Vec3.sum(deltaPosition, entityPositions[i]) + }); + } + } + } if (isActive) { selectionManager.setSelections(pastedEntityIDs); } } else { - Window.notifyEditError("Can't import objects: objects would be out of bounds."); + Window.notifyEditError("Can't import entities: entities would be out of bounds."); } } else { Window.notifyEditError("There was an error importing the entity file."); @@ -1787,11 +1887,11 @@ var PropertiesTool = function (opts) { } pushCommandForSelections(); selectionManager._update(); - } else if(data.type === 'parent') { + } else if (data.type === 'parent') { parentSelectedEntities(); - } else if(data.type === 'unparent') { + } else if (data.type === 'unparent') { unparentSelectedEntities(); - } else if(data.type === 'saveUserData'){ + } else if (data.type === 'saveUserData'){ //the event bridge and json parsing handle our avatar id string differently. var actualID = data.id.split('"')[1]; Entities.editEntity(actualID, data.properties); @@ -2083,6 +2183,10 @@ var selectedParticleEntityID = null; function selectParticleEntity(entityID) { var properties = Entities.getEntityProperties(entityID); + + if (properties.emitOrientation) { + properties.emitOrientation = Quat.safeEulerAngles(properties.emitOrientation); + } var particleData = { messageType: "particle_settings", currentProperties: properties @@ -2092,6 +2196,7 @@ function selectParticleEntity(entityID) { selectedParticleEntity = entityID; particleExplorerTool.setActiveParticleEntity(entityID); + particleExplorerTool.webView.emitScriptEvent(JSON.stringify(particleData)); // Switch to particle explorer @@ -2100,10 +2205,16 @@ function selectParticleEntity(entityID) { } entityListTool.webView.webEventReceived.connect(function (data) { - data = JSON.parse(data); + try { + data = JSON.parse(data); + } catch(e) { + print("edit.js: Error parsing JSON: " + e.name + " data " + data) + return; + } + if (data.type === 'parent') { parentSelectedEntities(); - } else if(data.type === 'unparent') { + } else if (data.type === 'unparent') { unparentSelectedEntities(); } else if (data.type === "selectionUpdate") { var ids = data.entityIds; @@ -2124,4 +2235,3 @@ entityListTool.webView.webEventReceived.connect(function (data) { }); }()); // END LOCAL_SCOPE - diff --git a/unpublishedScripts/marketplace/chat/ChatPage.html b/scripts/system/html/ChatPage.html similarity index 100% rename from unpublishedScripts/marketplace/chat/ChatPage.html rename to scripts/system/html/ChatPage.html diff --git a/scripts/system/html/SnapshotReview.html b/scripts/system/html/SnapshotReview.html index fb40c04d05..f080cd204a 100644 --- a/scripts/system/html/SnapshotReview.html +++ b/scripts/system/html/SnapshotReview.html @@ -28,7 +28,11 @@ -
+
+ +
diff --git a/scripts/system/html/css/SnapshotReview.css b/scripts/system/html/css/SnapshotReview.css index 218eb8c9a5..54d39aaad3 100644 --- a/scripts/system/html/css/SnapshotReview.css +++ b/scripts/system/html/css/SnapshotReview.css @@ -286,6 +286,43 @@ input[type=button].naked:active { // END styling of snapshot controls (bottom panel) and its contents */ + +/* +// START polaroid styling +*/ + +#print-button { + width: 72px; + height: 72px; + margin-left: 30px; + margin-top: -10px; + box-sizing: content-box; + display: inline; + outline:none; +} + +.print-icon { + margin: auto; +} + +.print-icon-default { + background: url(../img/button-snap-print.svg) no-repeat; + margin-right: -1px; + width: 64px; + height: 64px; +} + +.print-icon-loading { + background: url(../img/loader.gif) no-repeat; + width: 32px; + height: 32px; +} + +/* +// END polaroid styling +*/ + + /* // START misc styling */ diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index fcb1815ca4..40f3d682cd 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -610,6 +610,7 @@ hr { .dropdown dl[dropped="true"] { color: #404040; background: linear-gradient(#afafaf, #afafaf); + z-index: 998; } .dropdown dt { @@ -657,7 +658,8 @@ hr { font-family: FiraSans-SemiBold; font-size: 15px; color: #404040; - background-color: #afafaf + background-color: #afafaf; + z-index: 999; } .dropdown li:hover { background-color: #00b4ef; diff --git a/scripts/system/html/css/hifi-style.css b/scripts/system/html/css/hifi-style.css index ec6cd1a402..e1e4f67723 100644 --- a/scripts/system/html/css/hifi-style.css +++ b/scripts/system/html/css/hifi-style.css @@ -172,4 +172,4 @@ input[type=radio]:active + label > span > span{ } .blueButton:disabled { background-image: linear-gradient(#FFFFFF, #AFAFAF); -} \ No newline at end of file +} diff --git a/scripts/system/html/css/jsoneditor.css b/scripts/system/html/css/jsoneditor.css index 4d63380a62..ce83b45ff3 100644 --- a/scripts/system/html/css/jsoneditor.css +++ b/scripts/system/html/css/jsoneditor.css @@ -503,7 +503,7 @@ div.jsoneditor-contextmenu-root { div.jsoneditor-contextmenu { position: absolute; box-sizing: content-box; - z-index: 99999; + z-index: 998; } div.jsoneditor-contextmenu ul, diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 35accdd0df..bf65bdad32 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -51,6 +51,9 @@ + + +
diff --git a/scripts/system/html/img/button-snap-print.svg b/scripts/system/html/img/button-snap-print.svg new file mode 100644 index 0000000000..d1570711d7 --- /dev/null +++ b/scripts/system/html/img/button-snap-print.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/html/js/SnapshotReview.js b/scripts/system/html/js/SnapshotReview.js index 6c5829d64f..a3d1923aa9 100644 --- a/scripts/system/html/js/SnapshotReview.js +++ b/scripts/system/html/js/SnapshotReview.js @@ -21,6 +21,11 @@ var blastShareText = "Blast to my Connections", hifiAlreadySharedText = "Already Shared to Snaps Feed", facebookShareText = "Share to Facebook", twitterShareText = "Share to Twitter"; + +function fileExtensionMatches(filePath, extension) { + return filePath.split('.').pop().toLowerCase() === extension; +} + function showSetupInstructions() { var snapshotImagesDiv = document.getElementById("snapshot-images"); snapshotImagesDiv.className = "snapshotInstructions"; @@ -276,10 +281,10 @@ function addImage(image_data, isLoggedIn, canShare, isGifLoading, isShowingPrevi if (!image_data.localPath) { return; } - var id = "p" + (idCounter++), - imageContainer = document.createElement("DIV"), + var imageContainer = document.createElement("DIV"), img = document.createElement("IMG"), - isGif; + isGif = fileExtensionMatches(image_data.localPath, "gif"), + id = "p" + (isGif ? "1" : "0"); imageContainer.id = id; imageContainer.style.width = "95%"; imageContainer.style.height = "240px"; @@ -290,22 +295,34 @@ function addImage(image_data, isLoggedIn, canShare, isGifLoading, isShowingPrevi imageContainer.style.position = "relative"; img.id = id + "img"; img.src = image_data.localPath; - isGif = img.src.split('.').pop().toLowerCase() === "gif"; imageContainer.appendChild(img); document.getElementById("snapshot-images").appendChild(imageContainer); paths.push(image_data.localPath); - if (isGif) { - imageContainer.innerHTML += 'GIF'; - } - if (!isGifLoading) { - appendShareBar(id, isLoggedIn, canShare, isGif, blastButtonDisabled, hifiButtonDisabled, canBlast); - } - if (!isGifLoading || (isShowingPreviousImages && !image_data.story_id)) { - shareForUrl(id); - } - if (isShowingPreviousImages && isLoggedIn && image_data.story_id) { - updateShareInfo(id, image_data.story_id); - } + img.onload = function () { + if (isGif) { + imageContainer.innerHTML += 'GIF'; + } + if (!isGifLoading) { + appendShareBar(id, isLoggedIn, canShare, isGif, blastButtonDisabled, hifiButtonDisabled, canBlast); + } + if ((!isShowingPreviousImages && ((isGif && !isGifLoading) || !isGif)) || (isShowingPreviousImages && !image_data.story_id)) { + shareForUrl(id); + } + if (isShowingPreviousImages && isLoggedIn && image_data.story_id) { + updateShareInfo(id, image_data.story_id); + } + if (isShowingPreviousImages) { + requestPrintButtonUpdate(); + } + }; + img.onerror = function () { + img.onload = null; + img.src = image_data.errorPath; + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "alertSnapshotLoadFailed" + })); + }; } function showConfirmationMessage(selectedID, destination) { if (selectedID.id) { @@ -632,9 +649,8 @@ window.onload = function () { // 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. - - if (messageOptions.containsGif) { - if (messageOptions.processingGif) { + if (messageOptions.containsGif === true) { + if (messageOptions.processingGif === true) { imageCount = message.image_data.length + 1; // "+1" for the GIF that'll finish processing soon message.image_data.push({ localPath: messageOptions.loadingGifPath }); message.image_data.forEach(function (element, idx) { @@ -661,10 +677,22 @@ window.onload = function () { break; case 'captureSettings': handleCaptureSetting(message.setting); + break; + case 'setPrintButtonEnabled': + setPrintButtonEnabled(); + break; + case 'setPrintButtonLoading': + setPrintButtonLoading(); + break; + case 'setPrintButtonDisabled': + setPrintButtonDisabled(); break; case 'snapshotUploadComplete': - var isGif = message.image_url.split('.').pop().toLowerCase() === "gif"; + var isGif = fileExtensionMatches(message.image_url, "gif"); updateShareInfo(isGif ? "p1" : "p0", message.story_id); + if (isPrintProcessing()) { + setPrintButtonEnabled(); + } break; default: console.log("Unknown message action received in SnapshotReview.js."); @@ -694,6 +722,59 @@ function takeSnapshot() { } } +function isPrintDisabled() { + var printElement = document.getElementById('print-icon'); + + return printElement.classList.contains("print-icon") && + printElement.classList.contains("print-icon-default") && + document.getElementById('print-button').disabled; +} +function isPrintProcessing() { + var printElement = document.getElementById('print-icon'); + + return printElement.classList.contains("print-icon") && + printElement.classList.contains("print-icon-loading") && + document.getElementById('print-button').disabled; +} +function isPrintEnabled() { + var printElement = document.getElementById('print-icon'); + + return printElement.classList.contains("print-icon") && + printElement.classList.contains("print-icon-default") && + !document.getElementById('print-button').disabled; +} + +function setPrintButtonLoading() { + document.getElementById('print-icon').className = "print-icon print-icon-loading"; + document.getElementById('print-button').disabled = true; +} +function setPrintButtonDisabled() { + document.getElementById('print-icon').className = "print-icon print-icon-default"; + document.getElementById('print-button').disabled = true; +} +function setPrintButtonEnabled() { + document.getElementById('print-button').disabled = false; + document.getElementById('print-icon').className = "print-icon print-icon-default"; +} + +function requestPrintButtonUpdate() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "requestPrintButtonUpdate" + })); +} + +function printToPolaroid() { + if (isPrintEnabled()) { + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "printToPolaroid" + })); + } else { + setPrintButtonLoading(); + } +} + function testInBrowser(test) { if (test === 0) { showSetupInstructions(); diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index e000e14aec..777ef54085 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -20,7 +20,8 @@ var ICON_FOR_TYPE = { Light: "p", Zone: "o", PolyVox: "", - Multiple: "" + Multiple: "", + PolyLine: "" } var EDITOR_TIMEOUT_DURATION = 1500; @@ -82,11 +83,23 @@ function showElements(els, show) { } } +function updateProperty(propertyName, propertyValue) { + var properties = {}; + properties[propertyName] = propertyValue; + updateProperties(properties); +} + +function updateProperties(properties) { + EventBridge.emitWebEvent(JSON.stringify({ + id: lastEntityID, + type: "update", + properties: properties + })); +} + function createEmitCheckedPropertyUpdateFunction(propertyName) { return function() { - EventBridge.emitWebEvent( - '{"id":' + lastEntityID + ', "type":"update", "properties":{"' + propertyName + '":' + this.checked + '}}' - ); + updateProperty(propertyName, this.checked); }; } @@ -105,13 +118,7 @@ function createEmitGroupCheckedPropertyUpdateFunction(group, propertyName) { var properties = {}; properties[group] = {}; properties[group][propertyName] = this.checked; - EventBridge.emitWebEvent( - JSON.stringify({ - id: lastEntityID, - type: "update", - properties: properties - }) - ); + updateProperties(properties); }; } @@ -119,10 +126,7 @@ function createEmitNumberPropertyUpdateFunction(propertyName, decimals) { decimals = decimals == undefined ? 4 : decimals; return function() { var value = parseFloat(this.value).toFixed(decimals); - - EventBridge.emitWebEvent( - '{"id":' + lastEntityID + ', "type":"update", "properties":{"' + propertyName + '":' + value + '}}' - ); + updateProperty(propertyName, value); }; } @@ -131,28 +135,14 @@ function createEmitGroupNumberPropertyUpdateFunction(group, propertyName) { var properties = {}; properties[group] = {}; properties[group][propertyName] = this.value; - EventBridge.emitWebEvent( - JSON.stringify({ - id: lastEntityID, - type: "update", - properties: properties, - }) - ); + updateProperties(properties); }; } function createEmitTextPropertyUpdateFunction(propertyName) { return function() { - var properties = {}; - properties[propertyName] = this.value; - EventBridge.emitWebEvent( - JSON.stringify({ - id: lastEntityID, - type: "update", - properties: properties, - }) - ); + updateProperty(propertyName, this.value); }; } @@ -161,62 +151,44 @@ function createEmitGroupTextPropertyUpdateFunction(group, propertyName) { var properties = {}; properties[group] = {}; properties[group][propertyName] = this.value; - EventBridge.emitWebEvent( - JSON.stringify({ - id: lastEntityID, - type: "update", - properties: properties, - }) - ); + updateProperties(properties); }; } function createEmitVec3PropertyUpdateFunction(property, elX, elY, elZ) { return function() { - var data = { - id: lastEntityID, - type: "update", - properties: {} - }; - data.properties[property] = { + var properties = {}; + properties[property] = { x: elX.value, y: elY.value, z: elZ.value, }; - EventBridge.emitWebEvent(JSON.stringify(data)); + updateProperties(properties); } }; function createEmitGroupVec3PropertyUpdateFunction(group, property, elX, elY, elZ) { return function() { - var data = { - id: lastEntityID, - type: "update", - properties: {} - }; - data.properties[group] = {}; - data.properties[group][property] = { + var properties = {}; + properties[group] = {}; + properties[group][property] = { x: elX.value, y: elY.value, z: elZ ? elZ.value : 0, }; - EventBridge.emitWebEvent(JSON.stringify(data)); + updateProperties(properties); } }; function createEmitVec3PropertyUpdateFunctionWithMultiplier(property, elX, elY, elZ, multiplier) { return function() { - var data = { - id: lastEntityID, - type: "update", - properties: {} - }; - data.properties[property] = { + var properties = {}; + properties[property] = { x: elX.value * multiplier, y: elY.value * multiplier, z: elZ.value * multiplier, }; - EventBridge.emitWebEvent(JSON.stringify(data)); + updateProperties(properties); } }; @@ -227,44 +199,35 @@ function createEmitColorPropertyUpdateFunction(property, elRed, elGreen, elBlue) }; function emitColorPropertyUpdate(property, red, green, blue, group) { - var data = { - id: lastEntityID, - type: "update", - properties: {} - }; + var properties = {}; if (group) { - data.properties[group] = {}; - data.properties[group][property] = { + properties[group] = {}; + properties[group][property] = { red: red, green: green, blue: blue, }; } else { - data.properties[property] = { + properties[property] = { red: red, green: green, blue: blue, }; } - EventBridge.emitWebEvent(JSON.stringify(data)); + updateProperties(properties); }; function createEmitGroupColorPropertyUpdateFunction(group, property, elRed, elGreen, elBlue) { return function() { - var data = { - id: lastEntityID, - type: "update", - properties: {} - }; - data.properties[group] = {}; - - data.properties[group][property] = { + var properties = {}; + properties[group] = {}; + properties[group][property] = { red: elRed.value, green: elGreen.value, blue: elBlue.value, }; - EventBridge.emitWebEvent(JSON.stringify(data)); + updateProperties(properties); } }; @@ -277,18 +240,7 @@ function updateCheckedSubProperty(propertyName, propertyValue, subPropertyElemen // We've unchecked, so remove propertyValue = propertyValue.replace(subPropertyString + ",", ""); } - - var _properties = {} - _properties[propertyName] = propertyValue; - - EventBridge.emitWebEvent( - JSON.stringify({ - id: lastEntityID, - type: "update", - properties: _properties - }) - ); - + updateProperty(propertyName, propertyValue); } function setUserDataFromEditor(noUpdate) { @@ -314,18 +266,11 @@ function setUserDataFromEditor(noUpdate) { ); return; } else { - EventBridge.emitWebEvent( - JSON.stringify({ - id: lastEntityID, - type: "update", - properties: { - userData: text - }, - }) - ); + updateProperty('userData', text); } } } + function multiDataUpdater(groupName, updateKeyPair, userDataElement, defaults) { var properties = {}; var parsedData = {}; @@ -372,13 +317,7 @@ function multiDataUpdater(groupName, updateKeyPair, userDataElement, defaults) { userDataElement.value = properties['userData']; - EventBridge.emitWebEvent( - JSON.stringify({ - id: lastEntityID, - type: "update", - properties: properties, - }) - ); + updateProperties(properties); } function userDataChanger(groupName, keyName, values, userDataElement, defaultValue) { var val = {}, def = {}; @@ -900,7 +839,6 @@ function loaded() { elCloneable.checked = parsedUserData["grabbableKey"].cloneable; elCloneableGroup.style.display = elCloneable.checked ? "block": "none"; elCloneableDynamic.checked = parsedUserData["grabbableKey"].cloneDynamic ? parsedUserData["grabbableKey"].cloneDynamic : properties.dynamic; - elDynamic.checked = elCloneable.checked ? false: properties.dynamic; if (elCloneable.checked) { if ("cloneLifetime" in parsedUserData["grabbableKey"]) { elCloneableLifetime.value = parsedUserData["grabbableKey"].cloneLifetime ? parsedUserData["grabbableKey"].cloneLifetime : 300; @@ -1202,8 +1140,8 @@ function loaded() { }); elGrabbable.addEventListener('change', function() { - if(elCloneable.checked) { - elGrabbable.checked = false; + if (elCloneable.checked) { + elGrabbable.checked = false; } userDataChanger("grabbableKey", "grabbable", elGrabbable, elUserData, properties.dynamic); }); @@ -1213,17 +1151,22 @@ function loaded() { elCloneable.addEventListener('change', function (event) { var checked = event.target.checked; if (checked) { - multiDataUpdater("grabbableKey", - {cloneLifetime: elCloneableLifetime, cloneLimit: elCloneableLimit, cloneDynamic: elCloneableDynamic, cloneable: event.target}, - elUserData, {}); + multiDataUpdater("grabbableKey", { + cloneLifetime: elCloneableLifetime, + cloneLimit: elCloneableLimit, + cloneDynamic: elCloneableDynamic, + cloneable: event.target, + grabbable: null + }, elUserData, {}); elCloneableGroup.style.display = "block"; - EventBridge.emitWebEvent( - '{"id":' + lastEntityID + ', "type":"update", "properties":{"dynamic":false, "grabbable": false}}' - ); + updateProperty('dynamic', false); } else { - multiDataUpdater("grabbableKey", - {cloneLifetime: null, cloneLimit: null, cloneDynamic: null, cloneable: false}, - elUserData, {}); + multiDataUpdater("grabbableKey", { + cloneLifetime: null, + cloneLimit: null, + cloneDynamic: null, + cloneable: false + }, elUserData, {}); elCloneableGroup.style.display = "none"; } }); @@ -1258,15 +1201,7 @@ function loaded() { showUserDataTextArea(); showNewJSONEditorButton(); hideSaveUserDataButton(); - var properties = {}; - properties['userData'] = elUserData.value; - EventBridge.emitWebEvent( - JSON.stringify({ - id: lastEntityID, - type: "update", - properties: properties, - }) - ); + updateProperty('userData', elUserData.value) }); elSaveUserData.addEventListener("click", function() { diff --git a/scripts/system/html/js/eventBridgeLoader.js b/scripts/system/html/js/eventBridgeLoader.js index 0e95345b40..411780853b 100644 --- a/scripts/system/html/js/eventBridgeLoader.js +++ b/scripts/system/html/js/eventBridgeLoader.js @@ -13,7 +13,7 @@ var WebChannel; openEventBridge = function(callback) { WebChannel = new QWebChannel(qt.webChannelTransport, function (channel) { - EventBridge = WebChannel.objects.eventBridgeWrapper.eventBridge; + EventBridge = WebChannel.objects.eventBridge; callback(EventBridge); }); } diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index d4937ac9db..3b3d4b4937 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -25,9 +25,7 @@ var canWriteAssets = false; var xmlHttpRequest = null; var isPreparing = false; // Explicitly track download request status. - - var lastPage = "https://metaverse.highfidelity.com/marketplace?"; - + function injectCommonCode(isDirectoryPage) { // Supporting styles from marketplaces.css. @@ -67,7 +65,7 @@ // Footer actions. $("#back-button").on("click", function () { - window.location = lastPage; + window.history.back(); }); $("#all-markets").on("click", function () { EventBridge.emitWebEvent(GOTO_DIRECTORY); @@ -344,12 +342,7 @@ } } - function locationChanged() { - lastPage = location.href; - } - // Load / unload. window.addEventListener("load", onLoad); // More robust to Web site issues than using $(document).ready(). - window.addEventListener("hashchange", locationChanged); }()); diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js index 757743accc..b429a9f3ae 100644 --- a/scripts/system/libraries/WebTablet.js +++ b/scripts/system/libraries/WebTablet.js @@ -22,7 +22,9 @@ var DEFAULT_WIDTH = 0.4375; var DEFAULT_VERTICAL_FIELD_OF_VIEW = 45; // degrees var SENSOR_TO_ROOM_MATRIX = -2; var CAMERA_MATRIX = -7; -var ROT_Y_180 = {x: 0, y: 1, z: 0, w: 0}; +var ROT_Y_180 = {x: 0.0, y: 1.0, z: 0, w: 0}; +var ROT_LANDSCAPE = {x: 1.0, y: 1.0, z: 0, w: 0}; +var ROT_LANDSCAPE_WINDOW = {x: 0.0, y: 0.0, z: 0.0, w: 0}; var ROT_IDENT = {x: 0, y: 0, z: 0, w: 1}; var TABLET_TEXTURE_RESOLUTION = { x: 480, y: 706 }; var INCHES_TO_METERS = 1 / 39.3701; @@ -41,7 +43,7 @@ var LOCAL_TABLET_MODEL_PATH = Script.resourcesPath() + "meshes/tablet-with-home- // returns object with two fields: // * position - position in front of the user // * rotation - rotation of entity so it faces the user. -function calcSpawnInfo(hand, height) { +function calcSpawnInfo(hand, tabletHeight) { var finalPosition; var headPos = (HMD.active && Camera.mode === "first person") ? HMD.position : Camera.position; @@ -51,30 +53,35 @@ function calcSpawnInfo(hand, height) { hand = NO_HANDS; } + var handController = null; if (HMD.active && hand !== NO_HANDS) { - var handController = getControllerWorldLocation(hand, true); + handController = getControllerWorldLocation(hand, true); + } - var TABLET_UP_OFFSET = 0.1; - var TABLET_FORWARD_OFFSET = 0.1; - var normal = Vec3.multiplyQbyV(handController.rotation, {x: 0, y: -1, z: 0}); - var pitch = Math.asin(normal.y); - var MAX_PITCH = Math.PI / 4; - if (pitch < -MAX_PITCH) { - pitch = -MAX_PITCH; - } else if (pitch > MAX_PITCH) { - pitch = MAX_PITCH; + if (handController && handController.valid) { + // Orient tablet per hand pitch and yaw. + // Angle it back similar to holding it like a book. + // Move tablet up so that hand is at bottom. + // Move tablet back so that hand is in front. + + var position = handController.position; + var rotation = handController.rotation; + + if (hand === Controller.Standard.LeftHand) { + rotation = Quat.multiply(rotation, Quat.fromPitchYawRollDegrees(0, 90, 0)); + } else { + rotation = Quat.multiply(rotation, Quat.fromPitchYawRollDegrees(0, -90, 0)); } + var normal = Vec3.multiplyQbyV(rotation, Vec3.UNIT_NEG_Y); + var lookAt = Quat.lookAt(Vec3.ZERO, normal, Vec3.multiplyQbyV(MyAvatar.orientation, Vec3.UNIT_Y)); + var TABLET_RAKE_ANGLE = 30; + rotation = Quat.multiply(Quat.angleAxis(TABLET_RAKE_ANGLE, Vec3.multiplyQbyV(lookAt, Vec3.UNIT_X)), lookAt); - // rebuild normal from pitch and heading. - var heading = Math.atan2(normal.z, normal.x); - normal = {x: Math.cos(heading), y: Math.sin(pitch), z: Math.sin(heading)}; - - var position = Vec3.sum(handController.position, {x: 0, y: TABLET_UP_OFFSET, z: 0}); - var rotation = Quat.lookAt({x: 0, y: 0, z: 0}, normal, Y_AXIS); - var offset = Vec3.multiplyQbyV(rotation, {x: 0, y: height / 2, z: TABLET_FORWARD_OFFSET}); + var RELATIVE_SPAWN_OFFSET = { x: 0, y: 0.4, z: 0.05 }; + position = Vec3.sum(position, Vec3.multiplyQbyV(rotation, Vec3.multiply(tabletHeight, RELATIVE_SPAWN_OFFSET))); return { - position: Vec3.sum(offset, position), + position: position, rotation: rotation }; } else { @@ -243,29 +250,29 @@ WebTablet = function (url, width, dpi, hand, clientOnly, location, visible) { }; WebTablet.prototype.getDimensions = function() { - if (this.landscape) { - return { x: this.width * 2, y: this.height, z: this.depth }; - } else { - return { x: this.width, y: this.height, z: this.depth }; - } + return { x: this.width, y: this.height, z: this.depth }; }; WebTablet.prototype.getTabletTextureResolution = function() { if (this.landscape) { - return { x: TABLET_TEXTURE_RESOLUTION.x * 2, y: TABLET_TEXTURE_RESOLUTION.y }; + return { x: TABLET_TEXTURE_RESOLUTION.y , y: TABLET_TEXTURE_RESOLUTION.x }; } else { return TABLET_TEXTURE_RESOLUTION; } }; WebTablet.prototype.setLandscape = function(newLandscapeValue) { - if (this.landscape == newLandscapeValue) { + if (this.landscape === newLandscapeValue) { return; } + this.landscape = newLandscapeValue; - Overlays.editOverlay(this.tabletEntityID, { dimensions: this.getDimensions() }); + Overlays.editOverlay(this.tabletEntityID, + { rotation: this.landscape ? Quat.multiply(Camera.orientation, ROT_LANDSCAPE) : + Quat.multiply(Camera.orientation, ROT_Y_180) }); Overlays.editOverlay(this.webOverlayID, { - resolution: this.getTabletTextureResolution() + resolution: this.getTabletTextureResolution(), + rotation: Quat.multiply(Camera.orientation, ROT_LANDSCAPE_WINDOW) }); }; @@ -407,7 +414,7 @@ WebTablet.prototype.calculateWorldAttitudeRelativeToCamera = function (windowPos return { position: worldMousePosition, - rotation: Quat.multiply(Camera.orientation, ROT_Y_180) + rotation: this.landscape ? Quat.multiply(Camera.orientation, ROT_LANDSCAPE) : Quat.multiply(Camera.orientation, ROT_Y_180) }; }; diff --git a/scripts/system/libraries/entityList.js b/scripts/system/libraries/entityList.js index 3b6d32ec1c..64a05fcebf 100644 --- a/scripts/system/libraries/entityList.js +++ b/scripts/system/libraries/entityList.js @@ -109,7 +109,13 @@ EntityListTool = function(opts) { }; webView.webEventReceived.connect(function(data) { - data = JSON.parse(data); + try { + data = JSON.parse(data); + } catch(e) { + print("entityList.js: Error parsing JSON: " + e.name + " data " + data) + return; + } + if (data.type == "selectionUpdate") { var ids = data.entityIds; var entityIDs = []; diff --git a/scripts/system/libraries/gridTool.js b/scripts/system/libraries/gridTool.js index 0290674a0f..2c417a9dde 100644 --- a/scripts/system/libraries/gridTool.js +++ b/scripts/system/libraries/gridTool.js @@ -79,7 +79,7 @@ Grid = function(opts) { } } - that.snapToSurface = function(position, dimensions) { + that.snapToSurface = function(position, dimensions, registration) { if (!snapToGrid) { return position; } @@ -88,14 +88,18 @@ Grid = function(opts) { dimensions = { x: 0, y: 0, z: 0 }; } + if (registration === undefined) { + registration = { x: 0.5, y: 0.5, z: 0.5 }; + } + return { x: position.x, - y: origin.y + (dimensions.y / 2), + y: origin.y + (registration.y * dimensions.y), z: position.z }; } - that.snapToGrid = function(position, majorOnly, dimensions) { + that.snapToGrid = function(position, majorOnly, dimensions, registration) { if (!snapToGrid) { return position; } @@ -104,6 +108,10 @@ Grid = function(opts) { dimensions = { x: 0, y: 0, z: 0 }; } + if (registration === undefined) { + registration = { x: 0.5, y: 0.5, z: 0.5 }; + } + var spacing = majorOnly ? majorGridEvery : minorGridEvery; position = Vec3.subtract(position, origin); @@ -112,7 +120,7 @@ Grid = function(opts) { position.y = Math.round(position.y / spacing) * spacing; position.z = Math.round(position.z / spacing) * spacing; - return Vec3.sum(Vec3.sum(position, Vec3.multiply(0.5, dimensions)), origin); + return Vec3.sum(Vec3.sum(position, Vec3.multiplyVbyV(registration, dimensions)), origin); } that.snapToSpacing = function(delta, majorOnly) { @@ -161,9 +169,9 @@ Grid = function(opts) { if (data.origin) { var pos = data.origin; - pos.x = pos.x === undefined ? origin.x : pos.x; - pos.y = pos.y === undefined ? origin.y : pos.y; - pos.z = pos.z === undefined ? origin.z : pos.z; + pos.x = pos.x === undefined ? origin.x : parseFloat(pos.x); + pos.y = pos.y === undefined ? origin.y : parseFloat(pos.y); + pos.z = pos.z === undefined ? origin.z : parseFloat(pos.z); that.setPosition(pos, true); } @@ -238,7 +246,13 @@ GridTool = function(opts) { }); webView.webEventReceived.connect(function(data) { - data = JSON.parse(data); + try { + data = JSON.parse(data); + } catch(e) { + print("gridTool.js: Error parsing JSON: " + e.name + " data " + data) + return; + } + if (data.type == "init") { horizontalGrid.emitUpdate(); } else if (data.type == "update") { diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index 37a334bd70..d95ad919b6 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -613,7 +613,6 @@ 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; @@ -668,8 +667,8 @@ // 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. We pass in - // whether or not the connecting id is actually logged in, as now we - // will allow to start the connection process but have it stop with a + // whether or not the connecting id is actually logged in, as now we + // will allow to start the connection process but have it stop with a // fail message before trying to call the backend if the other guy isn't // logged in. function startConnecting(id, jointIndex, isLoggedIn) { diff --git a/scripts/system/particle_explorer/dat.gui.min.js b/scripts/system/particle_explorer/dat.gui.min.js deleted file mode 100644 index 8ea141a966..0000000000 --- a/scripts/system/particle_explorer/dat.gui.min.js +++ /dev/null @@ -1,95 +0,0 @@ -/** - * dat-gui JavaScript Controller Library - * http://code.google.com/p/dat-gui - * - * Copyright 2011 Data Arts Team, Google Creative Lab - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - */ -var dat=dat||{};dat.gui=dat.gui||{};dat.utils=dat.utils||{};dat.controllers=dat.controllers||{};dat.dom=dat.dom||{};dat.color=dat.color||{};dat.utils.css=function(){return{load:function(f,a){a=a||document;var d=a.createElement("link");d.type="text/css";d.rel="stylesheet";d.href=f;a.getElementsByTagName("head")[0].appendChild(d)},inject:function(f,a){a=a||document;var d=document.createElement("style");d.type="text/css";d.innerHTML=f;a.getElementsByTagName("head")[0].appendChild(d)}}}(); -dat.utils.common=function(){var f=Array.prototype.forEach,a=Array.prototype.slice;return{BREAK:{},extend:function(d){this.each(a.call(arguments,1),function(a){for(var c in a)this.isUndefined(a[c])||(d[c]=a[c])},this);return d},defaults:function(d){this.each(a.call(arguments,1),function(a){for(var c in a)this.isUndefined(d[c])&&(d[c]=a[c])},this);return d},compose:function(){var d=a.call(arguments);return function(){for(var e=a.call(arguments),c=d.length-1;0<=c;c--)e=[d[c].apply(this,e)];return e[0]}}, -each:function(a,e,c){if(a)if(f&&a.forEach&&a.forEach===f)a.forEach(e,c);else if(a.length===a.length+0)for(var b=0,p=a.length;bthis.__max&&(a=this.__max);void 0!==this.__step&&0!=a%this.__step&&(a=Math.round(a/this.__step)*this.__step);return e.superclass.prototype.setValue.call(this,a)},min:function(a){this.__min=a;return this},max:function(a){this.__max=a;return this},step:function(a){this.__impliedStep=this.__step=a;this.__precision=d(a);return this}});return e}(dat.controllers.Controller,dat.utils.common); -dat.controllers.NumberControllerBox=function(f,a,d){var e=function(c,b,f){function q(){var a=parseFloat(n.__input.value);d.isNaN(a)||n.setValue(a)}function l(a){var b=u-a.clientY;n.setValue(n.getValue()+b*n.__impliedStep);u=a.clientY}function r(){a.unbind(window,"mousemove",l);a.unbind(window,"mouseup",r)}this.__truncationSuspended=!1;e.superclass.call(this,c,b,f);var n=this,u;this.__input=document.createElement("input");this.__input.setAttribute("type","text");a.bind(this.__input,"change",q);a.bind(this.__input, -"blur",function(){q();n.__onFinishChange&&n.__onFinishChange.call(n,n.getValue())});a.bind(this.__input,"mousedown",function(b){a.bind(window,"mousemove",l);a.bind(window,"mouseup",r);u=b.clientY});a.bind(this.__input,"keydown",function(a){13===a.keyCode&&(n.__truncationSuspended=!0,this.blur(),n.__truncationSuspended=!1)});this.updateDisplay();this.domElement.appendChild(this.__input)};e.superclass=f;d.extend(e.prototype,f.prototype,{updateDisplay:function(){var a=this.__input,b;if(this.__truncationSuspended)b= -this.getValue();else{b=this.getValue();var d=Math.pow(10,this.__precision);b=Math.round(b*d)/d}a.value=b;return e.superclass.prototype.updateDisplay.call(this)}});return e}(dat.controllers.NumberController,dat.dom.dom,dat.utils.common); -dat.controllers.NumberControllerSlider=function(f,a,d,e,c){function b(a,b,c,e,d){return e+(a-b)/(c-b)*(d-e)}var p=function(c,e,d,f,u){function A(c){c.preventDefault();var e=a.getOffset(k.__background),d=a.getWidth(k.__background);k.setValue(b(c.clientX,e.left,e.left+d,k.__min,k.__max));return!1}function g(){a.unbind(window,"mousemove",A);a.unbind(window,"mouseup",g);k.__onFinishChange&&k.__onFinishChange.call(k,k.getValue())}p.superclass.call(this,c,e,{min:d,max:f,step:u});var k=this;this.__background= -document.createElement("div");this.__foreground=document.createElement("div");a.bind(this.__background,"mousedown",function(b){a.bind(window,"mousemove",A);a.bind(window,"mouseup",g);A(b)});a.addClass(this.__background,"slider");a.addClass(this.__foreground,"slider-fg");this.updateDisplay();this.__background.appendChild(this.__foreground);this.domElement.appendChild(this.__background)};p.superclass=f;p.useDefaultStyles=function(){d.inject(c)};e.extend(p.prototype,f.prototype,{updateDisplay:function(){var a= -(this.getValue()-this.__min)/(this.__max-this.__min);this.__foreground.style.width=100*a+"%";return p.superclass.prototype.updateDisplay.call(this)}});return p}(dat.controllers.NumberController,dat.dom.dom,dat.utils.css,dat.utils.common,"/**\n * dat-gui JavaScript Controller Library\n * http://code.google.com/p/dat-gui\n *\n * Copyright 2011 Data Arts Team, Google Creative Lab\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n */\n\n.slider {\n box-shadow: inset 0 2px 4px rgba(0,0,0,0.15);\n height: 1em;\n border-radius: 1em;\n background-color: #eee;\n padding: 0 0.5em;\n overflow: hidden;\n}\n\n.slider-fg {\n padding: 1px 0 2px 0;\n background-color: #aaa;\n height: 1em;\n margin-left: -0.5em;\n padding-right: 0.5em;\n border-radius: 1em 0 0 1em;\n}\n\n.slider-fg:after {\n display: inline-block;\n border-radius: 1em;\n background-color: #fff;\n border: 1px solid #aaa;\n content: '';\n float: right;\n margin-right: -1em;\n margin-top: -1px;\n height: 0.9em;\n width: 0.9em;\n}"); -dat.controllers.FunctionController=function(f,a,d){var e=function(c,b,d){e.superclass.call(this,c,b);var f=this;this.__button=document.createElement("div");this.__button.innerHTML=void 0===d?"Fire":d;a.bind(this.__button,"click",function(a){a.preventDefault();f.fire();return!1});a.addClass(this.__button,"button");this.domElement.appendChild(this.__button)};e.superclass=f;d.extend(e.prototype,f.prototype,{fire:function(){this.__onChange&&this.__onChange.call(this);this.getValue().call(this.object); -this.__onFinishChange&&this.__onFinishChange.call(this,this.getValue())}});return e}(dat.controllers.Controller,dat.dom.dom,dat.utils.common); -dat.controllers.BooleanController=function(f,a,d){var e=function(c,b){e.superclass.call(this,c,b);var d=this;this.__prev=this.getValue();this.__checkbox=document.createElement("input");this.__checkbox.setAttribute("type","checkbox");a.bind(this.__checkbox,"change",function(){d.setValue(!d.__prev)},!1);this.domElement.appendChild(this.__checkbox);this.updateDisplay()};e.superclass=f;d.extend(e.prototype,f.prototype,{setValue:function(a){a=e.superclass.prototype.setValue.call(this,a);this.__onFinishChange&& -this.__onFinishChange.call(this,this.getValue());this.__prev=this.getValue();return a},updateDisplay:function(){!0===this.getValue()?(this.__checkbox.setAttribute("checked","checked"),this.__checkbox.checked=!0):this.__checkbox.checked=!1;return e.superclass.prototype.updateDisplay.call(this)}});return e}(dat.controllers.Controller,dat.dom.dom,dat.utils.common); -dat.color.toString=function(f){return function(a){if(1==a.a||f.isUndefined(a.a)){for(a=a.hex.toString(16);6>a.length;)a="0"+a;return"#"+a}return"rgba("+Math.round(a.r)+","+Math.round(a.g)+","+Math.round(a.b)+","+a.a+")"}}(dat.utils.common); -dat.color.interpret=function(f,a){var d,e,c=[{litmus:a.isString,conversions:{THREE_CHAR_HEX:{read:function(a){a=a.match(/^#([A-F0-9])([A-F0-9])([A-F0-9])$/i);return null===a?!1:{space:"HEX",hex:parseInt("0x"+a[1].toString()+a[1].toString()+a[2].toString()+a[2].toString()+a[3].toString()+a[3].toString())}},write:f},SIX_CHAR_HEX:{read:function(a){a=a.match(/^#([A-F0-9]{6})$/i);return null===a?!1:{space:"HEX",hex:parseInt("0x"+a[1].toString())}},write:f},CSS_RGB:{read:function(a){a=a.match(/^rgb\(\s*(.+)\s*,\s*(.+)\s*,\s*(.+)\s*\)/); -return null===a?!1:{space:"RGB",r:parseFloat(a[1]),g:parseFloat(a[2]),b:parseFloat(a[3])}},write:f},CSS_RGBA:{read:function(a){a=a.match(/^rgba\(\s*(.+)\s*,\s*(.+)\s*,\s*(.+)\s*\,\s*(.+)\s*\)/);return null===a?!1:{space:"RGB",r:parseFloat(a[1]),g:parseFloat(a[2]),b:parseFloat(a[3]),a:parseFloat(a[4])}},write:f}}},{litmus:a.isNumber,conversions:{HEX:{read:function(a){return{space:"HEX",hex:a,conversionName:"HEX"}},write:function(a){return a.hex}}}},{litmus:a.isArray,conversions:{RGB_ARRAY:{read:function(a){return 3!= -a.length?!1:{space:"RGB",r:a[0],g:a[1],b:a[2]}},write:function(a){return[a.r,a.g,a.b]}},RGBA_ARRAY:{read:function(a){return 4!=a.length?!1:{space:"RGB",r:a[0],g:a[1],b:a[2],a:a[3]}},write:function(a){return[a.r,a.g,a.b,a.a]}}}},{litmus:a.isObject,conversions:{RGBA_OBJ:{read:function(b){return a.isNumber(b.r)&&a.isNumber(b.g)&&a.isNumber(b.b)&&a.isNumber(b.a)?{space:"RGB",r:b.r,g:b.g,b:b.b,a:b.a}:!1},write:function(a){return{r:a.r,g:a.g,b:a.b,a:a.a}}},RGB_OBJ:{read:function(b){return a.isNumber(b.r)&& -a.isNumber(b.g)&&a.isNumber(b.b)?{space:"RGB",r:b.r,g:b.g,b:b.b}:!1},write:function(a){return{r:a.r,g:a.g,b:a.b}}},HSVA_OBJ:{read:function(b){return a.isNumber(b.h)&&a.isNumber(b.s)&&a.isNumber(b.v)&&a.isNumber(b.a)?{space:"HSV",h:b.h,s:b.s,v:b.v,a:b.a}:!1},write:function(a){return{h:a.h,s:a.s,v:a.v,a:a.a}}},HSV_OBJ:{read:function(b){return a.isNumber(b.h)&&a.isNumber(b.s)&&a.isNumber(b.v)?{space:"HSV",h:b.h,s:b.s,v:b.v}:!1},write:function(a){return{h:a.h,s:a.s,v:a.v}}}}}];return function(){e=!1; -var b=1\n\n Here\'s the new load parameter for your GUI\'s constructor:\n\n \n\n
\n\n Automatically save\n values to localStorage on exit.\n\n
The values saved to localStorage will\n override those passed to dat.GUI\'s constructor. This makes it\n easier to work incrementally, but localStorage is fragile,\n and your friends may not see the same values you do.\n \n
\n \n
\n\n
', -".dg {\n /** Clear list styles */\n /* Auto-place container */\n /* Auto-placed GUI's */\n /* Line items that don't contain folders. */\n /** Folder names */\n /** Hides closed items */\n /** Controller row */\n /** Name-half (left) */\n /** Controller-half (right) */\n /** Controller placement */\n /** Shorter number boxes when slider is present. */\n /** Ensure the entire boolean and function row shows a hand */ }\n .dg ul {\n list-style: none;\n margin: 0;\n padding: 0;\n width: 100%;\n clear: both; }\n .dg.ac {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n height: 0;\n z-index: 0; }\n .dg:not(.ac) .main {\n /** Exclude mains in ac so that we don't hide close button */\n overflow: hidden; }\n .dg.main {\n -webkit-transition: opacity 0.1s linear;\n -o-transition: opacity 0.1s linear;\n -moz-transition: opacity 0.1s linear;\n transition: opacity 0.1s linear; }\n .dg.main.taller-than-window {\n overflow-y: auto; }\n .dg.main.taller-than-window .close-button {\n opacity: 1;\n /* TODO, these are style notes */\n margin-top: -1px;\n border-top: 1px solid #2c2c2c; }\n .dg.main ul.closed .close-button {\n opacity: 1 !important; }\n .dg.main:hover .close-button,\n .dg.main .close-button.drag {\n opacity: 1; }\n .dg.main .close-button {\n /*opacity: 0;*/\n -webkit-transition: opacity 0.1s linear;\n -o-transition: opacity 0.1s linear;\n -moz-transition: opacity 0.1s linear;\n transition: opacity 0.1s linear;\n border: 0;\n position: absolute;\n line-height: 19px;\n height: 20px;\n /* TODO, these are style notes */\n cursor: pointer;\n text-align: center;\n background-color: #000; }\n .dg.main .close-button:hover {\n background-color: #111; }\n .dg.a {\n float: right;\n margin-right: 15px;\n overflow-x: hidden; }\n .dg.a.has-save > ul {\n margin-top: 27px; }\n .dg.a.has-save > ul.closed {\n margin-top: 0; }\n .dg.a .save-row {\n position: fixed;\n top: 0;\n z-index: 1002; }\n .dg li {\n -webkit-transition: height 0.1s ease-out;\n -o-transition: height 0.1s ease-out;\n -moz-transition: height 0.1s ease-out;\n transition: height 0.1s ease-out; }\n .dg li:not(.folder) {\n cursor: auto;\n height: 27px;\n line-height: 27px;\n overflow: hidden;\n padding: 0 4px 0 5px; }\n .dg li.folder {\n padding: 0;\n border-left: 4px solid rgba(0, 0, 0, 0); }\n .dg li.title {\n cursor: pointer;\n margin-left: -4px; }\n .dg .closed li:not(.title),\n .dg .closed ul li,\n .dg .closed ul li > * {\n height: 0;\n overflow: hidden;\n border: 0; }\n .dg .cr {\n clear: both;\n padding-left: 3px;\n height: 27px; }\n .dg .property-name {\n cursor: default;\n float: left;\n clear: left;\n width: 40%;\n overflow: hidden;\n text-overflow: ellipsis; }\n .dg .c {\n float: left;\n width: 60%; }\n .dg .c input[type=text] {\n border: 0;\n margin-top: 4px;\n padding: 3px;\n width: 100%;\n float: right; }\n .dg .has-slider input[type=text] {\n width: 30%;\n /*display: none;*/\n margin-left: 0; }\n .dg .slider {\n float: left;\n width: 66%;\n margin-left: -5px;\n margin-right: 0;\n height: 19px;\n margin-top: 4px; }\n .dg .slider-fg {\n height: 100%; }\n .dg .c input[type=checkbox] {\n margin-top: 9px; }\n .dg .c select {\n margin-top: 5px; }\n .dg .cr.function,\n .dg .cr.function .property-name,\n .dg .cr.function *,\n .dg .cr.boolean,\n .dg .cr.boolean * {\n cursor: pointer; }\n .dg .selector {\n display: none;\n position: absolute;\n margin-left: -9px;\n margin-top: 23px;\n z-index: 10; }\n .dg .c:hover .selector,\n .dg .selector.drag {\n display: block; }\n .dg li.save-row {\n padding: 0; }\n .dg li.save-row .button {\n display: inline-block;\n padding: 0px 6px; }\n .dg.dialogue {\n background-color: #222;\n width: 460px;\n padding: 15px;\n font-size: 13px;\n line-height: 15px; }\n\n/* TODO Separate style and structure */\n#dg-new-constructor {\n padding: 10px;\n color: #222;\n font-family: Monaco, monospace;\n font-size: 10px;\n border: 0;\n resize: none;\n box-shadow: inset 1px 1px 1px #888;\n word-wrap: break-word;\n margin: 12px 0;\n display: block;\n width: 440px;\n overflow-y: scroll;\n height: 100px;\n position: relative; }\n\n#dg-local-explain {\n display: none;\n font-size: 11px;\n line-height: 17px;\n border-radius: 3px;\n background-color: #333;\n padding: 8px;\n margin-top: 10px; }\n #dg-local-explain code {\n font-size: 10px; }\n\n#dat-gui-save-locally {\n display: none; }\n\n/** Main type */\n.dg {\n color: #eee;\n font: 11px 'Lucida Grande', sans-serif;\n text-shadow: 0 -1px 0 #111;\n /** Auto place */\n /* Controller row,
  • */\n /** Controllers */ }\n .dg.main {\n /** Scrollbar */ }\n .dg.main::-webkit-scrollbar {\n width: 5px;\n background: #1a1a1a; }\n .dg.main::-webkit-scrollbar-corner {\n height: 0;\n display: none; }\n .dg.main::-webkit-scrollbar-thumb {\n border-radius: 5px;\n background: #676767; }\n .dg li:not(.folder) {\n background: #1a1a1a;\n border-bottom: 1px solid #2c2c2c; }\n .dg li.save-row {\n line-height: 25px;\n background: #dad5cb;\n border: 0; }\n .dg li.save-row select {\n margin-left: 5px;\n width: 108px; }\n .dg li.save-row .button {\n margin-left: 5px;\n margin-top: 1px;\n border-radius: 2px;\n font-size: 9px;\n line-height: 7px;\n padding: 4px 4px 5px 4px;\n background: #c5bdad;\n color: #fff;\n text-shadow: 0 1px 0 #b0a58f;\n box-shadow: 0 -1px 0 #b0a58f;\n cursor: pointer; }\n .dg li.save-row .button.gears {\n background: #c5bdad url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAANCAYAAAB/9ZQ7AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQJJREFUeNpiYKAU/P//PwGIC/ApCABiBSAW+I8AClAcgKxQ4T9hoMAEUrxx2QSGN6+egDX+/vWT4e7N82AMYoPAx/evwWoYoSYbACX2s7KxCxzcsezDh3evFoDEBYTEEqycggWAzA9AuUSQQgeYPa9fPv6/YWm/Acx5IPb7ty/fw+QZblw67vDs8R0YHyQhgObx+yAJkBqmG5dPPDh1aPOGR/eugW0G4vlIoTIfyFcA+QekhhHJhPdQxbiAIguMBTQZrPD7108M6roWYDFQiIAAv6Aow/1bFwXgis+f2LUAynwoIaNcz8XNx3Dl7MEJUDGQpx9gtQ8YCueB+D26OECAAQDadt7e46D42QAAAABJRU5ErkJggg==) 2px 1px no-repeat;\n height: 7px;\n width: 8px; }\n .dg li.save-row .button:hover {\n background-color: #bab19e;\n box-shadow: 0 -1px 0 #b0a58f; }\n .dg li.folder {\n border-bottom: 0; }\n .dg li.title {\n padding-left: 16px;\n background: black url(data:image/gif;base64,R0lGODlhBQAFAJEAAP////Pz8////////yH5BAEAAAIALAAAAAAFAAUAAAIIlI+hKgFxoCgAOw==) 6px 10px no-repeat;\n cursor: pointer;\n border-bottom: 1px solid rgba(255, 255, 255, 0.2); }\n .dg .closed li.title {\n background-image: url(data:image/gif;base64,R0lGODlhBQAFAJEAAP////Pz8////////yH5BAEAAAIALAAAAAAFAAUAAAIIlGIWqMCbWAEAOw==); }\n .dg .cr.boolean {\n border-left: 3px solid #806787; }\n .dg .cr.function {\n border-left: 3px solid #e61d5f; }\n .dg .cr.number {\n border-left: 3px solid #2fa1d6; }\n .dg .cr.number input[type=text] {\n color: #2fa1d6; }\n .dg .cr.string {\n border-left: 3px solid #1ed36f; }\n .dg .cr.string input[type=text] {\n color: #1ed36f; }\n .dg .cr.function:hover, .dg .cr.boolean:hover {\n background: #111; }\n .dg .c input[type=text] {\n background: #303030;\n outline: none; }\n .dg .c input[type=text]:hover {\n background: #3c3c3c; }\n .dg .c input[type=text]:focus {\n background: #494949;\n color: #fff; }\n .dg .c .slider {\n background: #303030;\n cursor: ew-resize; }\n .dg .c .slider-fg {\n background: #2fa1d6; }\n .dg .c .slider:hover {\n background: #3c3c3c; }\n .dg .c .slider:hover .slider-fg {\n background: #44abda; }\n", -dat.controllers.factory=function(f,a,d,e,c,b,p){return function(q,l,r,n){var u=q[l];if(p.isArray(r)||p.isObject(r))return new f(q,l,r);if(p.isNumber(u))return p.isNumber(r)&&p.isNumber(n)?new d(q,l,r,n):new a(q,l,{min:r,max:n});if(p.isString(u))return new e(q,l);if(p.isFunction(u))return new c(q,l,"");if(p.isBoolean(u))return new b(q,l)}}(dat.controllers.OptionController,dat.controllers.NumberControllerBox,dat.controllers.NumberControllerSlider,dat.controllers.StringController=function(f,a,d){var e= -function(c,b){function d(){f.setValue(f.__input.value)}e.superclass.call(this,c,b);var f=this;this.__input=document.createElement("input");this.__input.setAttribute("type","text");a.bind(this.__input,"keyup",d);a.bind(this.__input,"change",d);a.bind(this.__input,"blur",function(){f.__onFinishChange&&f.__onFinishChange.call(f,f.getValue())});a.bind(this.__input,"keydown",function(a){13===a.keyCode&&this.blur()});this.updateDisplay();this.domElement.appendChild(this.__input)};e.superclass=f;d.extend(e.prototype, -f.prototype,{updateDisplay:function(){a.isActive(this.__input)||(this.__input.value=this.getValue());return e.superclass.prototype.updateDisplay.call(this)}});return e}(dat.controllers.Controller,dat.dom.dom,dat.utils.common),dat.controllers.FunctionController,dat.controllers.BooleanController,dat.utils.common),dat.controllers.Controller,dat.controllers.BooleanController,dat.controllers.FunctionController,dat.controllers.NumberControllerBox,dat.controllers.NumberControllerSlider,dat.controllers.OptionController, -dat.controllers.ColorController=function(f,a,d,e,c){function b(a,b,d,e){a.style.background="";c.each(l,function(c){a.style.cssText+="background: "+c+"linear-gradient("+b+", "+d+" 0%, "+e+" 100%); "})}function p(a){a.style.background="";a.style.cssText+="background: -moz-linear-gradient(top, #ff0000 0%, #ff00ff 17%, #0000ff 34%, #00ffff 50%, #00ff00 67%, #ffff00 84%, #ff0000 100%);";a.style.cssText+="background: -webkit-linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);"; -a.style.cssText+="background: -o-linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);";a.style.cssText+="background: -ms-linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);";a.style.cssText+="background: linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);"}var q=function(f,n){function u(b){v(b);a.bind(window,"mousemove",v);a.bind(window, -"mouseup",l)}function l(){a.unbind(window,"mousemove",v);a.unbind(window,"mouseup",l)}function g(){var a=e(this.value);!1!==a?(t.__color.__state=a,t.setValue(t.__color.toOriginal())):this.value=t.__color.toString()}function k(){a.unbind(window,"mousemove",w);a.unbind(window,"mouseup",k)}function v(b){b.preventDefault();var c=a.getWidth(t.__saturation_field),d=a.getOffset(t.__saturation_field),e=(b.clientX-d.left+document.body.scrollLeft)/c;b=1-(b.clientY-d.top+document.body.scrollTop)/c;1 -b&&(b=0);1e&&(e=0);t.__color.v=b;t.__color.s=e;t.setValue(t.__color.toOriginal());return!1}function w(b){b.preventDefault();var c=a.getHeight(t.__hue_field),d=a.getOffset(t.__hue_field);b=1-(b.clientY-d.top+document.body.scrollTop)/c;1b&&(b=0);t.__color.h=360*b;t.setValue(t.__color.toOriginal());return!1}q.superclass.call(this,f,n);this.__color=new d(this.getValue());this.__temp=new d(0);var t=this;this.domElement=document.createElement("div");a.makeSelectable(this.domElement,!1); -this.__selector=document.createElement("div");this.__selector.className="selector";this.__saturation_field=document.createElement("div");this.__saturation_field.className="saturation-field";this.__field_knob=document.createElement("div");this.__field_knob.className="field-knob";this.__field_knob_border="2px solid ";this.__hue_knob=document.createElement("div");this.__hue_knob.className="hue-knob";this.__hue_field=document.createElement("div");this.__hue_field.className="hue-field";this.__input=document.createElement("input"); -this.__input.type="text";this.__input_textShadow="0 1px 1px ";a.bind(this.__input,"keydown",function(a){13===a.keyCode&&g.call(this)});a.bind(this.__input,"blur",g);a.bind(this.__selector,"mousedown",function(b){a.addClass(this,"drag").bind(window,"mouseup",function(b){a.removeClass(t.__selector,"drag")})});var y=document.createElement("div");c.extend(this.__selector.style,{width:"122px",height:"102px",padding:"3px",backgroundColor:"#222",boxShadow:"0px 1px 3px rgba(0,0,0,0.3)"});c.extend(this.__field_knob.style, -{position:"absolute",width:"12px",height:"12px",border:this.__field_knob_border+(.5>this.__color.v?"#fff":"#000"),boxShadow:"0px 1px 3px rgba(0,0,0,0.5)",borderRadius:"12px",zIndex:1});c.extend(this.__hue_knob.style,{position:"absolute",width:"15px",height:"2px",borderRight:"4px solid #fff",zIndex:1});c.extend(this.__saturation_field.style,{width:"100px",height:"100px",border:"1px solid #555",marginRight:"3px",display:"inline-block",cursor:"pointer"});c.extend(y.style,{width:"100%",height:"100%", -background:"none"});b(y,"top","rgba(0,0,0,0)","#000");c.extend(this.__hue_field.style,{width:"15px",height:"100px",display:"inline-block",border:"1px solid #555",cursor:"ns-resize"});p(this.__hue_field);c.extend(this.__input.style,{outline:"none",textAlign:"center",color:"#fff",border:0,fontWeight:"bold",textShadow:this.__input_textShadow+"rgba(0,0,0,0.7)"});a.bind(this.__saturation_field,"mousedown",u);a.bind(this.__field_knob,"mousedown",u);a.bind(this.__hue_field,"mousedown",function(b){w(b);a.bind(window, -"mousemove",w);a.bind(window,"mouseup",k)});this.__saturation_field.appendChild(y);this.__selector.appendChild(this.__field_knob);this.__selector.appendChild(this.__saturation_field);this.__selector.appendChild(this.__hue_field);this.__hue_field.appendChild(this.__hue_knob);this.domElement.appendChild(this.__input);this.domElement.appendChild(this.__selector);this.updateDisplay()};q.superclass=f;c.extend(q.prototype,f.prototype,{updateDisplay:function(){var a=e(this.getValue());if(!1!==a){var f=!1; -c.each(d.COMPONENTS,function(b){if(!c.isUndefined(a[b])&&!c.isUndefined(this.__color.__state[b])&&a[b]!==this.__color.__state[b])return f=!0,{}},this);f&&c.extend(this.__color.__state,a)}c.extend(this.__temp.__state,this.__color.__state);this.__temp.a=1;var l=.5>this.__color.v||.5a&&(a+=1);return{h:360*a,s:c/b,v:b/255}},rgb_to_hex:function(a,d,e){a=this.hex_with_component(0,2,a);a=this.hex_with_component(a,1,d);return a=this.hex_with_component(a,0,e)},component_from_hex:function(a,d){return a>>8*d&255},hex_with_component:function(a,d,e){return e<<(f=8*d)|a&~(255< 0) { + json[key] = document.getElementById(key) + .value; + } + } + + return json; + }, + fillFields: function (currentProperties) { + var self = this; + var fields = document.getElementsByTagName("input"); + + if (!currentProperties.locked) { + for (var i = 0; i < fields.length; i++) { + fields[i].removeAttribute("disabled"); + } + } + if (self.onSelect) { + self.onSelect(); + } + var keys = Object.keys(currentProperties); + + + for (var e in keys) { + if (keys.hasOwnProperty(e)) { + var value = keys[e]; + + var property = currentProperties[value]; + var field = self.builtRows[value]; + if (field) { + var el = document.getElementById(value); + + if (field.className.indexOf("radian") !== -1) { + el.value = property / RADIANS_PER_DEGREE; + el.onchange({ + target: el + }); + } else if (field.className.indexOf("range") !== -1 || field.className.indexOf("texture") !== -1) { + el.value = property; + el.onchange({ + target: el + }); + } else if (field.className.indexOf("checkbox") !== -1) { + if (property) { + el.setAttribute("checked", property); + } else { + el.removeAttribute("checked"); + } + } else if (field.className.indexOf("vector-section") !== -1) { + if (field.className.indexOf("rgb") !== -1) { + var red = document.getElementById(value + "-red"); + var blue = document.getElementById(value + "-blue"); + var green = document.getElementById(value + "-green"); + red.value = parseInt(property.red); + blue.value = parseInt(property.blue); + green.value = parseInt(property.green); + + red.oninput({ + target: red + }); + } else if (field.className.indexOf("xyz") !== -1) { + var x = document.getElementById(value + "-x"); + var y = document.getElementById(value + "-y"); + var z = document.getElementById(value + "-z"); + + x.value = roundFloat(property.x, 100); + y.value = roundFloat(property.y, 100); + z.value = roundFloat(property.z, 100); + } else if (field.className.indexOf("pyr") !== -1) { + var pitch = document.getElementById(value + "-Pitch"); + var yaw = document.getElementById(value + "-Yaw"); + var roll = document.getElementById(value + "-Roll"); + + pitch.value = roundFloat(property.x, 100); + yaw.value = roundFloat(property.y, 100); + roll.value = roundFloat(property.z, 100); + + } + } + } + } + } + }, + connect: function (EventBridge) { + this.EventBridge = EventBridge; + + var self = this; + + EventBridge.emitWebEvent(JSON.stringify({ + messageType: 'page_loaded' + })); + + EventBridge.scriptEventReceived.connect(function (data) { + data = JSON.parse(data); + + if (data.messageType === 'particle_settings') { + // Update settings + var currentProperties = data.currentProperties; + self.fillFields(currentProperties); + // Do expected property match with structure; + } else if (data.messageType === 'particle_close') { + self.disableFields(); + } + }); + }, + build: function () { + var self = this; + var sections = Object.keys(this.structure); + this.builtRows = {}; + sections.forEach(function (section, index) { + var properties = self.structure[section]; + self.addSection(self.parent, section, properties, index); + }); + }, + addSection: function (parent, section, properties, index) { + var self = this; + + var sectionDivHeader = document.createElement("div"); + var title = document.createElement("label"); + var dropDown = document.createElement("span"); + + dropDown.className = "arrow"; + sectionDivHeader.className = "section-header"; + title.innerHTML = section; + sectionDivHeader.appendChild(title); + sectionDivHeader.appendChild(dropDown); + var collapsed = index !== 0; + + dropDown.innerHTML = collapsed ? "L" : "M"; + sectionDivHeader.setAttribute("collapsed", collapsed); + parent.appendChild(sectionDivHeader); + + var sectionDivBody = document.createElement("div"); + sectionDivBody.className = "property-group"; + + var animationWrapper = document.createElement("div"); + animationWrapper.className = "section-wrap"; + + for (var property in properties) { + if (properties.hasOwnProperty(property)) { + var builtRow = self.addElement(animationWrapper, properties[property]); + var id = properties[property].id; + if (id) { + self.builtRows[id] = builtRow; + } + } + } + sectionDivBody.appendChild(animationWrapper); + parent.appendChild(sectionDivBody); + _.defer(function () { + var height = (animationWrapper.clientHeight) + "px"; + if (collapsed) { + sectionDivBody.classList.remove("visible"); + sectionDivBody.style.maxHeight = "0px"; + } else { + sectionDivBody.classList.add("visible"); + sectionDivBody.style.maxHeight = height; + } + + sectionDivHeader.onclick = function () { + collapsed = !collapsed; + if (collapsed) { + sectionDivBody.classList.remove("visible"); + sectionDivBody.style.maxHeight = "0px"; + } else { + sectionDivBody.classList.add("visible"); + sectionDivBody.style.maxHeight = (animationWrapper.clientHeight) + "px"; + } + // sectionDivBody.style.display = collapsed ? "none": "block"; + dropDown.innerHTML = collapsed ? "L" : "M"; + sectionDivHeader.setAttribute("collapsed", collapsed); + }; + }); + }, + addLabel: function (parent, group) { + var label = document.createElement("label"); + label.innerHTML = group.name; + parent.appendChild(label); + if (group.unit) { + var span = document.createElement("span"); + span.innerHTML = group.unit; + span.className = "unit"; + label.appendChild(span); + } + return label; + }, + addVector: function (parent, group, labels, domArray) { + var self = this; + var inputs = labels ? labels : ["x", "y", "z"]; + domArray = domArray ? domArray : []; + parent.id = group.id; + for (var index in inputs) { + var element = document.createElement("input"); + + element.setAttribute("type", "number"); + element.className = inputs[index]; + element.id = group.id + "-" + inputs[index]; + + if (group.defaultRange) { + if (group.defaultRange.min) { + element.setAttribute("min", group.defaultRange.min); + } + if (group.defaultRange.max) { + element.setAttribute("max", group.defaultRange.max); + } + if (group.defaultRange.step) { + element.setAttribute("step", group.defaultRange.step); + } + } + if (group.oninput) { + element.oninput = group.oninput; + } else { + element.oninput = function (event) { + self.webBridgeSync(group.id, { + x: domArray[0].value, + y: domArray[1].value, + z: domArray[2].value + }); + }; + } + element.onchange = element.oninput; + domArray.push(element); + } + + this.addLabel(parent, group); + var className = ""; + for (var i = 0; i < inputs.length; i++) { + className += inputs[i].charAt(0) + .toLowerCase(); + } + parent.className += " property vector-section " + className; + + // Add Tuple and the rest + var tupleContainer = document.createElement("div"); + tupleContainer.className = "tuple"; + for (var domIndex in domArray) { + var container = domArray[domIndex]; + var div = document.createElement("div"); + var label = document.createElement("label"); + label.innerHTML = inputs[domIndex] + ":"; + label.setAttribute("for", container.id); + div.appendChild(container); + div.appendChild(label); + tupleContainer.appendChild(div); + } + parent.appendChild(tupleContainer); + }, + addVectorQuaternion: function (parent, group) { + this.addVector(parent, group, ["Pitch", "Yaw", "Roll"]); + }, + addColorPicker: function (parent, group) { + var self = this; + var $colPickContainer = $('
    ', { + id: group.id, + class: "color-picker" + }); + var updateColors = function (red, green, blue) { + $colPickContainer.css('background-color', "rgb(" + + red + "," + + green + "," + + blue + ")"); + }; + + var inputs = ["red", "green", "blue"]; + var domArray = []; + group.oninput = function (event) { + $colPickContainer.colpickSetColor( + { + r: domArray[0].value, + g: domArray[1].value, + b: domArray[2].value + }, + true); + }; + group.defaultRange = { + min: 0, + max: 255, + step: 1 + }; + + parent.appendChild($colPickContainer[0]); + self.addVector(parent, group, inputs, domArray); + + updateColors(domArray[0].value, domArray[1].value, domArray[2].value); + + // Could probably write a custom one for this to completely write out jquery, + // but for now, using the same as earlier. + + /* Color Picker Logic Here */ + + + $colPickContainer.colpick({ + colorScheme: 'dark', + layout: 'hex', + color: { + r: domArray[0].value, + g: domArray[1].value, + b: domArray[2].value + }, + onChange: function (hsb, hex, rgb, el) { + updateColors(rgb.r, rgb.g, rgb.b); + + domArray[0].value = rgb.r; + domArray[1].value = rgb.g; + domArray[2].value = rgb.b; + self.webBridgeSync(group.id, { + red: rgb.r, + green: rgb.g, + blue: rgb.b + }); + }, + onSubmit: function (hsb, hex, rgb, el) { + $(el) + .css('background-color', '#' + hex); + $(el) + .colpickHide(); + domArray[0].value = rgb.r; + domArray[1].value = rgb.g; + domArray[2].value = rgb.b; + self.webBridgeSync(group.id, { + red: rgb.r, + green: rgb.g, + blue: rgb.b + }); + } + }); + }, + addTextureField: function (parent, group) { + var self = this; + this.addLabel(parent, group); + parent.className += " property texture"; + var textureImage = document.createElement("div"); + var textureUrl = document.createElement("input"); + textureUrl.setAttribute("type", "text"); + textureUrl.id = group.id; + textureImage.className = "texture-image no-texture"; + var image = document.createElement("img"); + var imageLoad = _.debounce(function (url) { + if (url.length > 0) { + textureImage.classList.remove("no-texture"); + textureImage.classList.add("with-texture"); + image.src = url; + image.style.display = "block"; + } else { + image.src = ""; + image.style.display = "none"; + textureImage.classList.add("no-texture"); + } + self.webBridgeSync(group.id, url); + }, 250); + + textureUrl.oninput = function (event) { + // Add throttle + var url = event.target.value; + imageLoad(url); + }; + textureUrl.onchange = textureUrl.oninput; + textureImage.appendChild(image); + parent.appendChild(textureImage); + parent.appendChild(textureUrl); + }, + addSlider: function (parent, group) { + var self = this; + this.addLabel(parent, group); + parent.className += " property range"; + var container = document.createElement("div"); + container.className = "slider-wrapper"; + var slider = document.createElement("input"); + slider.setAttribute("type", "range"); + + var inputField = document.createElement("input"); + inputField.setAttribute("type", "number"); + + container.appendChild(slider); + container.appendChild(inputField); + parent.appendChild(container); + + if (group.type === "SliderInteger") { + inputField.setAttribute("min", group.min !== undefined ? group.min : 0); + inputField.setAttribute("step", 1); + + slider.setAttribute("min", group.min !== undefined ? group.min : 0); + slider.setAttribute("max", group.max !== undefined ? group.max : 10000); + slider.setAttribute("step", 1); + + inputField.oninput = function (event) { + + if (parseInt(event.target.value) > parseInt(slider.getAttribute("max")) && group.max !== 1) { + slider.setAttribute("max", event.target.value); + } + slider.value = event.target.value; + + self.webBridgeSync(group.id, slider.value); + }; + inputField.onchange = inputField.oninput; + slider.oninput = function (event) { + inputField.value = event.target.value; + self.webBridgeSync(group.id, slider.value); + }; + + inputField.id = group.id; + } else if (group.type === "SliderRadian") { + slider.setAttribute("min", group.min !== undefined ? group.min : 0); + slider.setAttribute("max", group.max !== undefined ? group.max : 180); + slider.setAttribute("step", 1); + parent.className += " radian"; + inputField.setAttribute("min", (group.min !== undefined ? group.min : 0)); + inputField.setAttribute("max", (group.max !== undefined ? group.max : 180)); + + inputField.oninput = function (event) { + slider.value = event.target.value; + self.webBridgeSync(group.id, slider.value * RADIANS_PER_DEGREE); + }; + inputField.onchange = inputField.oninput; + + inputField.id = group.id; + slider.oninput = function (event) { + if (event.target.value > 0) { + inputField.value = Math.floor(event.target.value); + } else { + inputField.value = Math.ceil(event.target.value); + } + self.webBridgeSync(group.id, slider.value * RADIANS_PER_DEGREE); + }; + var degrees = document.createElement("label"); + degrees.innerHTML = "°"; + degrees.style.fontSize = "1.4rem"; + degrees.style.display = "inline"; + degrees.style.verticalAlign = "top"; + degrees.style.paddingLeft = "0.4rem"; + container.appendChild(degrees); + + } else { + // Must then be Float + inputField.setAttribute("min", group.min !== undefined ? group.min : 0); + slider.setAttribute("step", 0.01); + + slider.setAttribute("min", group.min !== undefined ? group.min : 0); + slider.setAttribute("max", group.max !== undefined ? group.max : 1); + slider.setAttribute("step", 0.01); + + inputField.oninput = function (event) { + if (parseFloat(event.target.value) > parseFloat(slider.getAttribute("max")) && group.max !== 1) { + slider.setAttribute("max", event.target.value); + } + + slider.value = event.target.value; + self.webBridgeSync(group.id, slider.value); + // bind web sock update here. + }; + inputField.onchange = inputField.oninput; + slider.oninput = function (event) { + inputField.value = event.target.value; + self.webBridgeSync(group.id, inputField.value); + }; + + inputField.id = group.id; + } + + // UpdateBinding + }, + addCheckBox: function (parent, group) { + var checkBox = document.createElement("input"); + checkBox.setAttribute("type", "checkbox"); + var self = this; + checkBox.onchange = function (event) { + self.webBridgeSync(group.id, event.target.checked); + }; + checkBox.id = group.id; + parent.appendChild(checkBox); + var label = this.addLabel(parent, group); + label.setAttribute("for", checkBox.id); + parent.className += " property checkbox"; + }, + addElement: function (parent, group) { + var self = this; + var property = document.createElement("div"); + property.id = group.id; + + var row = document.createElement("div"); + switch (group.type) { + case "Button": + var button = document.createElement("input"); + button.setAttribute("type", "button"); + button.id = group.id; + if (group.disabled) { + button.disabled = group.disabled; + } + button.className = group.class; + button.value = group.name; + + button.onclick = group.callback; + parent.appendChild(button); + break; + case "Row": + var hr = document.createElement("hr"); + hr.className = "splitter"; + if (group.id) { + hr.id = group.id; + } + parent.appendChild(hr); + break; + case "Boolean": + self.addCheckBox(row, group); + parent.appendChild(row); + break; + case "SliderFloat": + case "SliderInteger": + case "SliderRadian": + self.addSlider(row, group); + parent.appendChild(row); + break; + case "Texture": + self.addTextureField(row, group); + parent.appendChild(row); + break; + case "Color": + self.addColorPicker(row, group); + parent.appendChild(row); + break; + case "Vector": + self.addVector(row, group); + parent.appendChild(row); + break; + case "VectorQuaternion": + self.addVectorQuaternion(row, group); + parent.appendChild(row); + break; + default: + console.log("not defined"); + } + return row; + } +}; diff --git a/scripts/system/particle_explorer/particle-style.css b/scripts/system/particle_explorer/particle-style.css new file mode 100644 index 0000000000..e8b71fdba0 --- /dev/null +++ b/scripts/system/particle_explorer/particle-style.css @@ -0,0 +1,124 @@ +/* +// particle-style.css +// +// Created by Matti 'Menithal' Lahtinen on 21 May 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 +*/ + + +.property-group { + max-height: 0; + -webkit-transition: max-height 0.15s ease-out; + transition: max-height 0.15s ease-out; + overflow: hidden; +} +.property-group.visible { + transition: max-height 0.25s ease-in; +} +.section-wrap { + width: 100%; +} +.property { + padding: 0.4rem 0; + margin: 0; +} +.property.checkbox { + margin: 0; +} +.property.range label{ + display: block; +} + +input[type="button"] { + margin: 0.4rem; + min-width: 6rem; +} +input[type="text"] { + margin: 0; +} +.property.range input[type=number]{ + margin-left: 0.8rem; + width: 5.4rem; + height: 1.8rem; +} +input[type=range] { + -webkit-appearance: none; + background: #2e2e2e; + height: 1.8rem; + border-radius: 1rem; +} +input[type=range]::-webkit-slider-thumb { + -webkit-appearance:none; + width: 0.6rem; + height: 1.8rem; + padding:0; + margin: 0; + background-color: #696969; + border-radius: 1rem; +} +input[type=range]::-webkit-slider-thumb:hover { + background-color: white; +} +input[type=range]:focus { /*#252525*/ + outline: none; +} +.tuple label { + text-transform: capitalize; +} +.slider-wrapper { + display: table; + padding: 0.4rem 0; +} +hr.splitter{ + width: 100%; + padding: 0.2rem 0 0 0; + margin: 0; + position: relative; + clear: both; +} +hr.splitter:last-of-type{ + padding:0; +} +#rem { + height: 1rem; + width: 1rem; +} +.property { + min-height: 2rem; +} +.property.vector-section{ + + width: 24rem; +} + +.property.texture { + display: block; +} +.property.texture input{ + margin: 0.4rem 0; +} +.texture-image img{ + padding: 0; + margin: 0; + width: 100%; + height: 100%; + display: none; +} +.texture-image { + display: block; + position: relative; + background-repeat: no-repeat; + background-position: center; + background-size: 100% 100%; + margin-top: 0.4rem; + height:128px; + width: 128px; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAABhNJREFUeNrsnVFy4joQRVsSCwAqBMwqsrRsIavMEkICoeAf2+8j1R5ZGDBgpLzoUDVVmTT2dc8It/paOpi3t7faOSciImVZyn6/l6qqRETEWivj8VistYPFd7ud1HUtIiLGGBmPx5JKX0RkMplIzvmPnHNijBERaS7Ef1lrB40bY1oXgH5a/ZH+8P7+LlVVycfHR/MGa60URdGcYOi4MUaKomhGaGx9EZHlcplMP2X+Ly8vPwOgLEtxzklVVVJVVfOznqAsy9YFXhuvqqq5AF/Lj+srtr7+LpV+yvz1mNF+vxcRkdVqJdZaeXp6ap1ws9m0TjibzVoj6lJ8vV6fjJdlKev1ujViU+j7t8tc8p9Op1KWpYw06L9JL0Av0r9l+jXl3nhd11JV1VE8tn5YM3PI3xjzoxVOGvyDU7zQj6s/0tGmI++amtNV087F9Wf/FnVPzRtCXz8RdV1nlb/efUbaJy4Wi0FqzjU1yRgjs9ls0Jp3jb6IyPPzczL9lPkvFot/dwCtB/om/x9oyJoXxps65NW8mPpdNTeX/JtBEtYE/+AUL/Tj6g/qA3TVnD41a6g++Bp9rYOp9FPnH80HOBcvy1I2m81D++BL+o/2AX5r/vgA+AD4AOif8AH8EdpVcy71sX3jWp/8W2AKff/TkUv+Oufr9AF0YuKc66xJ18T7eNP3nP9WfZ0EzufzJPqp8y+KQuq67vYBdETqCDpVU/rEw5oUnr+rD46h73/qUuinzh8fAP22D6AjxznXcqq6akrf+KmaFB6vf4+t7/sAelfIJf/GB9jtdmKMkdVq1dQM3zg4VVNU/NY+1Bgjh8Oh6YM1+dj6X19fzXwgp/wbH0DFtS7oyf0RdKqmhPFr+1RdseKfP7a+Px/IKX98APTbPoDOJrv60L417d54TH3V8lfS5pT/yfUA6/X6qOZcqkm3xrUm6X9CTH3fB0ihnzr/Ix9A/3T1qbfWpGvjMfX9T0UK/dT54wOg/88H8EfGPTVr6D740frhLDmn/Hv5AH1qku9t31KTzh3/aP1LPsBfzr+XDxCO0K6ack/N6qp5MfUv+QB/Of/ePsCQfWmfc6EfV3/kjzZrrRwOh9YtKHSm/LjOH3yrMTzej4c1y//51PHoP0a/tR7AOSdFURw9rz5VU049zw7jl2qWrosP++BY+iI/+wJS6afMv9kXoA6gvimsieHzZr/m6MTp3PPuc3G9SP95OPpx9JtOgT4cHwA+QCJ9+ADwAeADsC+AfQHo/4b1APAB4APAB4APAB8APgB9OD4AfAD4AFFqEnwA+AD4APgA6P86HwA+AHyAZhIBHwA+AHwA+AD04X/eB4APAB8APgB8APgA8AHow/P0AeADwAeADwAfAD4AfAD68Px8APgA8AHgA8AHgA8AH0DO70/v6lHvjaOfVn8U/iLcXx5OUML96X49vRTX3/nPw9FPo9+sB5hMJuKck+VyeVRTrLWtdfNdcf95eldNCuOfn5+tSYy/Pz+2voi0fICc8p/P5z93gJAPEN4+wufN4evaePj99eH+ePTj6p/1Abp60kt9Ksf/v46HDwAfAD6A/6gUPgD7AtgXwPP4DNcDwAeADwAfAD4AfAD4ADyPz289AHyA+Pqp84cPIPAB8AHwAfAB8AHgA7Q+HfAB4APAB4APAB+APjw3HwA+AHwA+ADwAeADwAegD8/TB4APAB8APgB8APgA8AHow/PzAeADwAeADwAfAD4AfACJ//316KfVH/mjLeb31+vx/kWhH0+/tR7AOSdFUUT9/nq9oK4+OJa+iLT25+eUf7MvIOQDxPr+en2F++PRj6PfdAr04fgA8AES6cMHgA8AH4B9AewLQP83rAeADwAfAD4AfAD4APAB6MPxAeADwAeIUpPgA8AHwAfAB0D/1/kA8AHgAzSTCPgA8AHgA8AHoA//8z4AfAD4APAB4APAB4APQB+epw8AHwA+AHwA+ADwAeAD0Ifn5wPAB4APAB8APgB8gBz5AOb19bX2TYLpdNpqQ7bbbctJGjJeVZVst9vWLSu2/vf3t+Sc/yicFIRr0C7Fu76f/lw8XBePflr9/wYAqWwWUSLcO54AAAAASUVORK5CYII='); +} + +.texture-image.no-texture{ + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAB81JREFUeNrsnTGPm0oXht97FWm2Ch2pTEeHpUihsyvTuXO67Ta/IPkr+Qfp3MWdO7Zad0SKZDo6XIWOrTzV9xVXZ8SygGHXG4/t96lW68GGw8vMmZlzDv98+/btfyBXy780wXXzTv74/fs3rXFFfPz4kT0AoQAoAJqAAiAUAKEACAVAKABCARAKgFAAhAIgFAChAAgFQCgAQgEQCoBQAIQCIBQAoQAIBUAoAHLmvDv3C7i7u4PjOMiyDOv1+mC75XKJoiga2wRBAN/34TgOHMdBWZYoigJpmiLPcwrARhzHAQD4vg/P81pvlLRrwvM8zGYz00ZrbY5xHAe+7yPPc9zf36MsSwrAVmazGX78+DHoGM/zsFgsAAB5nmOz2ZgeQimF8XiMMAxNu+VyaQRCH8Ai8jyH4zgIw7D3MUopzOdzAECaplitVk+GB601kiTBz58/obWG4ziIoohOoI38+vULABCGYWd3X2U6nUIphbIsEcdxa7uiKPDw8GCGGtd1KQDbKMsSWZZBKYXJZNLrGN/3zdN/iDRNTdcvx1EAFqGUwmazeeIQduG6LpRSAIAsy3r9hrRjD2BxL5AkiXEI+8wetNa9PXtp13eIoQBOQJIkxmHrcgjlJkov8JKpJwVgIVpr47CFYdh6g/f7/ZM5/9CehgKwmDRNURQFlFKYTqeNN/rx8dH0AH2faBn7KYAzQKZ1QRCYZd0qf/78MX+PRqNe3ymO5W63owBsR9bwZShoGirEq++zeBQEweBZAwVwYh4eHqC1RhAErQ6jOHVdK3yu65qhJE1TDgHn5BDKTW6auxdFYdYOgiDAYrF40k4phTAM8fnzZyilUBRF54rhOfIOF06SJMYPaPt8v99jOp3C8zx4nget9bPZQ5ZlF3fzL0IAZVke9OLv7+/Njl/brCHLMozHY4xGI3z48MH0EEVRIMuyi40H+EdqBbNS6HXBSqGEAiAUAAVAE1AAhAIgFAChAAgFQCgAQgGQq+Eom0GLxeJgGHYVSdCUhM02yrI0qV5hGGIymaAsy9b0LNd1cXt7CwDYbDa98wOA/zKLVquVSQGr/nYTbe2iKDIh53JtZVmiLEvsdjtst9tn5z7EDmfXA3QFXdaTMbvYbrdm568tgkdueJ7njbt3QwJA+8YJ1tsFQQDXdXFzc2N2E0Uwk8kEX758eXbMEDtY2QOsVqtn//v69SsAYL1eH9xK7dNGgjuiKMJ4PH4WmSN7+QBMFu/3798bn1oAzz47NvVrqmYgz2azRpv1scNV+wDVaN969y6JIEmSWBmyJenlIgZbcgvOzgmUqJxqkmY18ldCvGwkz/MntQcogBcgETrVMV98Aptvfh1JTKEAXsBms4HWGp7nYT6fw3Ec5Hlufbi253lQSkFr3VqmhgLoQVmW2G63ZigQx8/2my/FKCR17WLWAV7LfD5vzOFLkqS1W0/T1HT9RVFY5/jNZjMz3ouvorVGHMet9QheYoer7AGq478Y2LaiDTc3N3Bd90megSwG2YQVPcDQ+a/ccK01ttutWSWsetl/i7bfq16TzP1lGFgul0exw9X2AJLGJV3joRXCl3rnXbUDhmQKl2WJ9XoNrbV1vdXZCUCWWqvVQGR8HFIgqmuaKUiCSJcA+nrzWmvzdA/ZN6EAKlTz/eXmA3iSuXOoNEzfBRsA+PTpU+PnUjxSfnvo9/ZNR6cAakjFj2rqd3VtQJ6u1z5h1e+SdYbqdK5aWHLImC0OoFQgpRN4YPoD/LfRVC8C2TQlkhVC3/dfVDG0/l1xHCOKIvi+b572atJoURSdtYnbfAHxV0aj0TP/oY8dzqYH6OscHXK26tO+rqcujmNTIKqtJkDfc0vTFMvl8smu436/R57niOO4NSbh0HfLkFHtpYbY4dgwOfRKYXIooQAIBUAB0AQUAKEACAVAKABCARAKgFAA5Gp4s93AKIrw/v17ExsnFEWB/X6P3W6HLMtaN0+GJkwOad+W2FlPLq3GHFSRdq85h2PYyGoByG6cvJOnHiEryZJSg7e+s1ZNmOyzSza0ffWYJsIwbMzk7Tp+6Dm81kZWC0BoCnSU7dowDE2K12q1alT60EDJYwVWKqUQRdHgPf9jnfMQG52dDyA5fLKnLlGztiB5Bn1eP3fuNvr31IaWZM9jhHIdEwk5G1Jk4hxtdPJZQJZlJrLWlnBpx3FMmrnrup3RReduIyumgXJxtryRUxw4mQXIO4Yv0UZWCMDWN3I2vX7u0mxk1RtDmp6yoQmTbe27kjK7iOMYt7e3CIIA2+22VyLIWyZ5Hrsnsmol0Jac+fo51QtSXJKNrOgBuvLsTrUOUO8FxAP3ff/gTXiLc3irt5aevAdQSpmpja0vZqq+fm4ymfz18i5vaaOTC0DSvapv8rQRmRY6joPxeHwxNjqpAGSpUwx8ikKJQ5AyNFKb4BJsdBIfwPM8BEFgFjXSNG3debMJSUv7GyuWf8tGby6Aaq2c+qvaJce/a3p2ioTJQ73A3d3di6aBbef8WhtZKQDJ6K1fTJ7neHx8PFjWTcbbvvPePm8QbVtc6ft/+UwKUdfbDT3n19roGDA59EphciihAAgFQAHQBBQAoQAIBUAoAEIBEAqAUACEAiAUAKEACAVAKABCARAKgFAAhAIgFAChAAgFQC4CkxgiceKEPQC5Iv4/APgB2O7x8IXXAAAAAElFTkSuQmCC'); +} diff --git a/scripts/system/particle_explorer/particleExplorer.html b/scripts/system/particle_explorer/particleExplorer.html index d0d86d79da..0f014e9fa8 100644 --- a/scripts/system/particle_explorer/particleExplorer.html +++ b/scripts/system/particle_explorer/particleExplorer.html @@ -1,95 +1,42 @@ +// + --> - - - - - - - - - - + + + - -
    - -
    -
    -
    -
    -
    - + +
    +
    + +
    + +
    +
    + diff --git a/scripts/system/particle_explorer/particleExplorer.js b/scripts/system/particle_explorer/particleExplorer.js index 5f66fe7ae6..ca6a873b73 100644 --- a/scripts/system/particle_explorer/particleExplorer.js +++ b/scripts/system/particle_explorer/particleExplorer.js @@ -2,550 +2,410 @@ // particleExplorer.js // // Created by James B. Pollack @imgntn on 9/26/2015 -// Copyright 2015 High Fidelity, Inc. +// Copyright 2017 High Fidelity, Inc. +// +// Reworked by Menithal on 20/5/2017 +// // Web app side of the App - contains GUI. -// This is an example of a new, easy way to do two way bindings between dynamically created GUI and in-world entities. +// This is an example of a new, easy way to do two way bindings between dynamically created GUI and in-world entities. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global window, alert, EventBridge, dat, listenForSettingsUpdates, createVec3Folder, createQuatFolder, writeVec3ToInterface, writeDataToInterface, - $, document, _, openEventBridge */ +/* global HifiEntityUI, openEventBridge, console, EventBridge, document, window */ +/* eslint no-console: 0, no-global-assign: 0 */ -var Settings = function () { - this.exportSettings = function () { - // copyExportSettingsToClipboard(); - showPreselectedPrompt(); - }; - this.importSettings = function () { - importSettings(); - }; -}; +(function () { -// 2-way bindings-aren't quite ready yet. see bottom of file. -var AUTO_UPDATE = false; -var UPDATE_ALL_FREQUENCY = 100; + var root = document.getElementById("particle-explorer"); -var controllers = []; -var colpickKeys = []; -var folders = []; -var gui = null; -var settings = new Settings(); -var updateInterval; - -var active = false; - -var currentInputField; -var storedController; -// CHANGE TO WHITELIST -var keysToAllow = [ - 'isEmitting', - 'maxParticles', - 'lifespan', - 'emitRate', - 'emitSpeed', - 'speedSpread', - 'emitOrientation', - 'emitDimensions', - 'polarStart', - 'polarFinish', - 'azimuthStart', - 'azimuthFinish', - 'emitAcceleration', - 'accelerationSpread', - 'particleRadius', - 'radiusSpread', - 'radiusStart', - 'radiusFinish', - 'color', - 'colorSpread', - 'colorStart', - 'colorFinish', - 'alpha', - 'alphaSpread', - 'alphaStart', - 'alphaFinish', - 'emitterShouldTrail', - 'textures' -]; - -var individualKeys = []; -var vec3Keys = []; -var quatKeys = []; -var colorKeys = []; - -window.onload = function () { - openEventBridge(function () { - var stringifiedData = JSON.stringify({ - messageType: 'page_loaded' + window.onload = function () { + var ui = new HifiEntityUI(root); + var textarea = document.createElement("textarea"); + var properties = ""; + var menuStructure = { + General: [ + { + type: "Row", + id: "export-import-field" + }, + { + id: "show-properties-button", + name: "Show Properties", + type: "Button", + class: "blue", + disabled: true, + callback: function (event) { + var insertZone = document.getElementById("export-import-field"); + var json = ui.getSettings(); + properties = JSON.stringify(json); + textarea.value = properties; + if (!insertZone.contains(textarea)) { + insertZone.appendChild(textarea); + insertZone.parentNode.parentNode.style.maxHeight = + insertZone.parentNode.clientHeight + "px"; + document.getElementById("export-properties-button").removeAttribute("disabled"); + textarea.onchange = function (e) { + if (e.target.value !== properties) { + document.getElementById("import-properties-button").removeAttribute("disabled"); + } + }; + textarea.oninput = textarea.onchange; + } else { + textarea.onchange = function () {}; + textarea.oninput = textarea.onchange; + textarea.value = ""; + textarea.remove(); + insertZone.parentNode.parentNode.style.maxHeight = + insertZone.parentNode.clientHeight + "px"; + document.getElementById("export-properties-button").setAttribute("disabled", true); + document.getElementById("import-properties-button").setAttribute("disabled", true); + } + } + }, + { + id: "import-properties-button", + name: "Import", + type: "Button", + class: "blue", + disabled: true, + callback: function (event) { + ui.fillFields(JSON.parse(textarea.value)); + ui.submitChanges(JSON.parse(textarea.value)); + } + }, + { + id: "export-properties-button", + name: "Export", + type: "Button", + class: "red", + disabled: true, + callback: function (event) { + textarea.select(); + try { + var success = document.execCommand('copy'); + if (!success) { + throw "Not success :("; + } + } catch (e) { + print("couldnt copy field"); + } + } + }, + { + type: "Row" + }, + { + id: "isEmitting", + name: "Is Emitting", + type: "Boolean" + }, + { + type: "Row" + }, + { + id: "lifespan", + name: "Lifespan", + type: "SliderFloat", + min: 0.01, + max: 10 + }, + { + type: "Row" + }, + { + id: "maxParticles", + name: "Max Particles", + type: "SliderInteger", + min: 1, + max: 10000 + }, + { + type: "Row" + }, + { + id: "textures", + name: "Textures", + type: "Texture" + }, + { + type: "Row" + } + ], + Emit: [ + { + id: "emitRate", + name: "Emit Rate", + type: "SliderInteger", + max: 1000, + min: 1 + }, + { + type: "Row" + }, + { + id: "emitSpeed", + name: "Emit Speed", + type: "SliderFloat", + max: 5 + }, + { + type: "Row" + }, + { + id: "emitDimensions", + name: "Emit Dimension", + type: "Vector", + defaultRange: { + min: 0, + step: 0.01 + } + }, + { + type: "Row" + }, + { + id: "emitOrientation", + unit: "deg", + name: "Emit Orientation", + type: "VectorQuaternion", + defaultRange: { + min: 0, + step: 0.01 + } + }, + { + type: "Row" + }, + { + id: "emitShouldTrail", + name: "Emit Should Trail", + type: "Boolean" + }, + { + type: "Row" + } + ], + Radius: [ + { + id: "particleRadius", + name: "Particle Radius", + type: "SliderFloat", + max: 4.0 + }, + { + type: "Row" + }, + { + id: "radiusSpread", + name: "Radius Spread", + type: "SliderFloat", + max: 4.0 + }, + { + type: "Row" + }, + { + id: "radiusStart", + name: "Radius Start", + type: "SliderFloat", + max: 4.0 + }, + { + type: "Row" + }, + { + id: "radiusFinish", + name: "Radius Finish", + type: "SliderFloat", + max: 4.0 + }, + { + type: "Row" + } + ], + Color: [ + { + id: "color", + name: "Color", + type: "Color", + defaultColor: { + red: 255, + green: 255, + blue: 255 + } + }, + { + type: "Row" + }, + { + id: "colorSpread", + name: "Color Spread", + type: "Color", + defaultColor: { + red: 0, + green: 0, + blue: 0 + } + }, + { + type: "Row" + }, + { + id: "colorStart", + name: "Color Start", + type: "Color", + defaultColor: { + red: 255, + green: 255, + blue: 255 + } + }, + { + type: "Row" + }, + { + id: "colorFinish", + name: "Color Finish", + type: "Color", + defaultColor: { + red: 255, + green: 255, + blue: 255 + } + }, + { + type: "Row" + } + ], + Acceleration: [ + { + id: "emitAcceleration", + name: "Emit Acceleration", + type: "Vector", + defaultRange: { + step: 0.01 + } + }, + { + type: "Row" + }, + { + id: "accelerationSpread", + name: "Acceleration Spread", + type: "Vector", + defaultRange: { + step: 0.01 + } + }, + { + type: "Row" + } + ], + Alpha: [ + { + id: "alpha", + name: "Alpha", + type: "SliderFloat" + }, + { + type: "Row" + }, + { + id: "alphaSpread", + name: "Alpha Spread", + type: "SliderFloat" + }, + { + type: "Row" + }, + { + id: "alphaStart", + name: "Alpha Start", + type: "SliderFloat" + }, + { + type: "Row" + }, + { + id: "alphaFinish", + name: "Alpha Finish", + type: "SliderFloat" + }, + { + type: "Row" + } + ], + Polar: [ + { + id: "polarStart", + name: "Polar Start", + unit: "deg", + type: "SliderRadian" + }, + { + type: "Row" + }, + { + id: "polarFinish", + name: "Polar Finish", + unit: "deg", + type: "SliderRadian" + }, + { + type: "Row" + } + ], + Azimuth: [ + { + id: "azimuthStart", + name: "Azimuth Start", + unit: "deg", + type: "SliderRadian", + min: -180, + max: 0 + }, + { + type: "Row" + }, + { + id: "azimuthFinish", + name: "Azimuth Finish", + unit: "deg", + type: "SliderRadian" + }, + { + type: "Row" + } + ] + }; + ui.setUI(menuStructure); + ui.setOnSelect(function () { + document.getElementById("show-properties-button").removeAttribute("disabled"); + document.getElementById("export-properties-button").setAttribute("disabled", true); + document.getElementById("import-properties-button").setAttribute("disabled", true); }); + ui.build(); + var overrideLoad = false; + if (openEventBridge === undefined) { + overrideLoad = true, + openEventBridge = function (callback) { + callback({ + emitWebEvent: function () {}, + submitChanges: function () {}, + scriptEventReceived: { + connect: function () { - EventBridge.emitWebEvent( - stringifiedData - ); - - listenForSettingsUpdates(); - window.onresize = setGUIWidthToWindowWidth; - }); - -}; - -function loadGUI() { - // whether or not to autoplace - gui = new dat.GUI({ - autoPlace: false - }); - - // if not autoplacing, put gui in a custom container - if (gui.autoPlace === false) { - var customContainer = document.getElementById('my-gui-container'); - customContainer.appendChild(gui.domElement); - } - - // presets for the GUI itself. a little confusing and import/export is mostly what we want to do at the moment. - // gui.remember(settings); - - colpickKeys = []; - var keys = _.keys(settings); - - _.each(keys, function(key) { - var shouldAllow = _.contains(keysToAllow, key); - - if (shouldAllow) { - var subKeys = _.keys(settings[key]); - var hasX = _.contains(subKeys, 'x'); - var hasY = _.contains(subKeys, 'y'); - var hasZ = _.contains(subKeys, 'z'); - var hasW = _.contains(subKeys, 'w'); - var hasRed = _.contains(subKeys, 'red'); - var hasGreen = _.contains(subKeys, 'green'); - var hasBlue = _.contains(subKeys, 'blue'); - - if ((hasX && hasY && hasZ) && hasW === false) { - vec3Keys.push(key); - } else if (hasX && hasY && hasZ && hasW) { - quatKeys.push(key); - } else if (hasRed || hasGreen || hasBlue) { - colorKeys.push(key); - - } else { - individualKeys.push(key); - } + } + } + }); + }; } - }); - - // alphabetize our keys - individualKeys.sort(); - vec3Keys.sort(); - quatKeys.sort(); - colorKeys.sort(); - - // add to gui in the order they should appear - gui.add(settings, 'importSettings'); - gui.add(settings, 'exportSettings'); - addIndividualKeys(); - addFolders(); - - // set the gui width to match the web window width - gui.width = window.innerWidth; - - // 2-way binding stuff - // if (AUTO_UPDATE) { - // setInterval(manuallyUpdateDisplay, UPDATE_ALL_FREQUENCY); - // registerDOMElementsForListenerBlocking(); - // } - -} - -function addIndividualKeys() { - _.each(individualKeys, function(key) { - // temporary patch for not crashing when this goes below 0 - var controller; - - if (key.indexOf('emitRate') > -1) { - controller = gui.add(settings, key).min(0); - } else { - controller = gui.add(settings, key); - } - - // 2-way - need to fix not being able to input exact values if constantly listening - // controller.listen(); - - // keep track of our controller - controllers.push(controller); - - // hook into change events for this gui controller - controller.onChange(function(value) { - // Fires on every change, drag, keypress, etc. - writeDataToInterface(this.property, value); + openEventBridge(function (EventBridge) { + ui.connect(EventBridge); }); - - }); -} - -function addFolders() { - _.each(colorKeys, function(key) { - createColorPicker(key); - }); - _.each(vec3Keys, function(key) { - createVec3Folder(key); - }); - _.each(quatKeys, function(key) { - createQuatFolder(key); - }); -} - -function createColorPicker(key) { - var colorObject = settings[key]; - - // Embed colpick's color picker into dat.GUI - var name = document.createElement('span'); - name.className = 'property-name'; - name.innerHTML = key; - - var container = document.createElement('div'); - container.appendChild(name); - - var $colPickContainer = $('
    ', { - id: key.toString(), - class: "color-box" - }); - $colPickContainer.css('background-color', "rgb(" + colorObject.red + "," + colorObject.green + "," + colorObject.blue + ")"); - container.appendChild($colPickContainer[0]); - - var $li = $('
  • ', { class: 'cr object color' }); - $li.append(container); - gui.__ul.appendChild($li[0]); - gui.onResize(); - - $colPickContainer.colpick({ - colorScheme: 'dark', - layout: 'hex', - color: { r: colorObject.red, g: colorObject.green, b: colorObject.blue }, - onSubmit: function (hsb, hex, rgb, el) { - $(el).css('background-color', '#' + hex); - $(el).colpickHide(); - - var obj = {}; - obj[key] = { red: rgb.r, green: rgb.g, blue: rgb.b }; - writeVec3ToInterface(obj); + if (overrideLoad) { + openEventBridge(); } - }); - - colpickKeys.push(key); -} - -function createVec3Folder(category) { - var folder = gui.addFolder(category); - - folder.add(settings[category], 'x').step(0.1).onChange(function(value) { - // Fires when a controller loses focus. - var obj = {}; - obj[category] = {}; - obj[category][this.property] = value; - obj[category].y = settings[category].y; - obj[category].z = settings[category].z; - writeVec3ToInterface(obj); - }); - - folder.add(settings[category], 'y').step(0.1).onChange(function(value) { - // Fires when a controller loses focus. - var obj = {}; - obj[category] = {}; - obj[category].x = settings[category].x; - obj[category][this.property] = value; - obj[category].z = settings[category].z; - writeVec3ToInterface(obj); - }); - - folder.add(settings[category], 'z').step(0.1).onChange(function(value) { - // Fires when a controller loses focus. - var obj = {}; - obj[category] = {}; - obj[category].y = settings[category].y; - obj[category].x = settings[category].x; - obj[category][this.property] = value; - writeVec3ToInterface(obj); - }); - - folders.push(folder); - folder.open(); -} - -function createQuatFolder(category) { - var folder = gui.addFolder(category); - - folder.add(settings[category], 'x').step(0.1).onChange(function(value) { - // Fires when a controller loses focus. - var obj = {}; - obj[category] = {}; - obj[category][this.property] = value; - obj[category].y = settings[category].y; - obj[category].z = settings[category].z; - obj[category].w = settings[category].w; - writeVec3ToInterface(obj); - }); - - folder.add(settings[category], 'y').step(0.1).onChange(function(value) { - // Fires when a controller loses focus. - var obj = {}; - obj[category] = {}; - obj[category].x = settings[category].x; - obj[category][this.property] = value; - obj[category].z = settings[category].z; - obj[category].w = settings[category].w; - writeVec3ToInterface(obj); - }); - - folder.add(settings[category], 'z').step(0.1).onChange(function(value) { - // Fires when a controller loses focus. - var obj = {}; - obj[category] = {}; - obj[category].x = settings[category].x; - obj[category].y = settings[category].y; - obj[category][this.property] = value; - obj[category].w = settings[category].w; - writeVec3ToInterface(obj); - }); - - folder.add(settings[category], 'w').step(0.1).onChange(function(value) { - // Fires when a controller loses focus. - var obj = {}; - obj[category] = {}; - obj[category].x = settings[category].x; - obj[category].y = settings[category].y; - obj[category].z = settings[category].z; - obj[category][this.property] = value; - writeVec3ToInterface(obj); - }); - - folders.push(folder); - folder.open(); -} - -function convertColorObjectToArray(colorObject) { - var colorArray = []; - - _.each(colorObject, function(singleColor) { - colorArray.push(singleColor); - }); - - return colorArray; -} - -function convertColorArrayToObject(colorArray) { - var colorObject = { - red: colorArray[0], - green: colorArray[1], - blue: colorArray[2] }; - - return colorObject; -} - -function writeDataToInterface(property, value) { - var data = {}; - data[property] = value; - - var sendData = { - messageType: "settings_update", - updatedSettings: data - }; - - var stringifiedData = JSON.stringify(sendData); - - EventBridge.emitWebEvent(stringifiedData); -} - -function writeVec3ToInterface(obj) { - var sendData = { - messageType: "settings_update", - updatedSettings: obj - }; - - var stringifiedData = JSON.stringify(sendData); - - EventBridge.emitWebEvent(stringifiedData); -} - -function listenForSettingsUpdates() { - EventBridge.scriptEventReceived.connect(function(data) { - data = JSON.parse(data); - if (data.messageType === 'particle_settings') { - _.each(data.currentProperties, function(value, key) { - settings[key] = {}; - settings[key] = value; - }); - - if (gui) { - manuallyUpdateDisplay(); - } else { - loadGUI(); - } - if (!active) { - // gui.toggleHide(); - gui.closed = false; - } - active = true; - - } else if (data.messageType === "particle_close") { - // none of this seems to work. - // if (active) { - // gui.toggleHide(); - // } - active = false; - gui.closed = true; - } - }); -} - -function manuallyUpdateDisplay() { - // Iterate over all controllers - // this is expensive, write a method for indiviudal controllers and use it when the value is different than a cached value, perhaps. - var i; - for (i in gui.__controllers) { - gui.__controllers[i].updateDisplay(); - } - - // Update color pickers - for (i in colpickKeys) { - var color = settings[colpickKeys[i]]; - var $object = $('#' + colpickKeys[i]); - $object.css('background-color', "rgb(" + color.red + "," + color.green + "," + color.blue + ")"); - $object.colpickSetColor({ r: color.red, g: color.green, b: color.blue }, true); - } -} - -function setGUIWidthToWindowWidth() { - if (gui !== null) { - gui.width = window.innerWidth; - } -} - -function handleInputKeyPress(e) { - if (e.keyCode === 13) { - importSettings(); - } - return false; -} - -function importSettings() { - var importInput = document.getElementById('importer-input'); - - try { - var importedSettings = JSON.parse(importInput.value); - - var keys = _.keys(importedSettings); - - _.each(keys, function(key) { - var shouldAllow = _.contains(keysToAllow, key); - - if (!shouldAllow) { - return; - } - - settings[key] = importedSettings[key]; - }); - - writeVec3ToInterface(settings); - - manuallyUpdateDisplay(); - } catch (e) { - alert('Not properly formatted JSON'); - } -} - -function prepareSettingsForExport() { - var keys = _.keys(settings); - - var exportSettings = {}; - - _.each(keys, function(key) { - var shouldAllow = _.contains(keysToAllow, key); - - if (!shouldAllow) { - return; - } - - if (key.indexOf('color') > -1) { - var colorObject = convertColorArrayToObject(settings[key]); - settings[key] = colorObject; - } - - exportSettings[key] = settings[key]; - }); - - return JSON.stringify(exportSettings, null, 4); -} - -function showPreselectedPrompt() { - var elem = document.getElementById("exported-props"); - var exportSettings = prepareSettingsForExport(); - elem.innerHTML = ""; - var buttonnode = document.createElement('input'); - buttonnode.setAttribute('type', 'button'); - buttonnode.setAttribute('value', 'close'); - elem.appendChild(document.createTextNode("COPY THE BELOW FIELD TO CLIPBOARD:")); - elem.appendChild(document.createElement("br")); - var textAreaNode = document.createElement("textarea"); - textAreaNode.value = exportSettings; - elem.appendChild(textAreaNode); - elem.appendChild(document.createElement("br")); - elem.appendChild(buttonnode); - - buttonnode.onclick = function() { - console.log("click"); - elem.innerHTML = ""; - }; - - // window.alert("Ctrl-C to copy, then Enter.", prepareSettingsForExport()); -} - -function removeContainerDomElement() { - var elem = document.getElementById("my-gui-container"); - elem.parentNode.removeChild(elem); -} - -function removeListenerFromGUI(key) { - _.each(gui.__listening, function(controller, index) { - if (controller.property === key) { - storedController = controller; - gui.__listening.splice(index, 1); - } - }); -} - -// the section below is to try to work at achieving two way bindings; - -function addListenersBackToGUI() { - gui.__listening.push(storedController); - storedController = null; -} - -function registerDOMElementsForListenerBlocking() { - _.each(gui.__controllers, function(controller) { - var input = controller.domElement.childNodes[0]; - input.addEventListener('focus', function() { - console.log('INPUT ELEMENT GOT FOCUS!' + controller.property); - removeListenerFromGUI(controller.property); - }); - }); - - _.each(gui.__controllers, function(controller) { - var input = controller.domElement.childNodes[0]; - input.addEventListener('blur', function() { - console.log('INPUT ELEMENT GOT BLUR!' + controller.property); - addListenersBackToGUI(); - }); - }); - - // also listen to inputs inside of folders - _.each(gui.__folders, function(folder) { - _.each(folder.__controllers, function(controller) { - var input = controller.__input; - input.addEventListener('focus', function() { - console.log('FOLDER ELEMENT GOT FOCUS!' + controller.property); - }); - }); - }); -} +})(); diff --git a/scripts/system/particle_explorer/particleExplorerTool.js b/scripts/system/particle_explorer/particleExplorerTool.js index b3db475ab0..d85fc169b1 100644 --- a/scripts/system/particle_explorer/particleExplorerTool.js +++ b/scripts/system/particle_explorer/particleExplorerTool.js @@ -4,23 +4,22 @@ // Created by Eric Levin on 2/15/16 // Copyright 2016 High Fidelity, Inc. // Adds particleExplorer tool to the edit panel when a user selects a particle entity from the edit tool window -// This is an example of a new, easy way to do two way bindings between dynamically created GUI and in-world entities. +// This is an example of a new, easy way to do two way bindings between dynamically created GUI and in-world entities. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/*global window, alert, EventBridge, dat, listenForSettingsUpdates,createVec3Folder,createQuatFolder,writeVec3ToInterface,writeDataToInterface*/ +/* global window, alert, ParticleExplorerTool, EventBridge, dat, listenForSettingsUpdates,createVec3Folder,createQuatFolder,writeVec3ToInterface,writeDataToInterface*/ var PARTICLE_EXPLORER_HTML_URL = Script.resolvePath('particleExplorer.html'); ParticleExplorerTool = function() { var that = {}; - that.createWebView = function() { that.webView = Tablet.getTablet("com.highfidelity.interface.tablet.system"); that.webView.setVisible = function(value) {}; - that.webView.webEventReceived.connect(that.webEventReceived); + that.webView.webEventReceived.connect(that.webEventReceived); } that.destroyWebView = function() { @@ -38,6 +37,9 @@ ParticleExplorerTool = function() { that.webEventReceived = function(data) { var data = JSON.parse(data); if (data.messageType === "settings_update") { + if (data.updatedSettings.emitOrientation) { + data.updatedSettings.emitOrientation = Quat.fromVec3Degrees(data.updatedSettings.emitOrientation); + } Entities.editEntity(that.activeParticleEntity, data.updatedSettings); } } diff --git a/scripts/system/progress.js b/scripts/system/progress.js index 81da38c8c2..f4741c5b6a 100644 --- a/scripts/system/progress.js +++ b/scripts/system/progress.js @@ -261,7 +261,7 @@ } } - gpuTextures = Render.getConfig("Stats").textureGPUTransferCount; + gpuTextures = Render.getConfig("Stats").texturePendingGPUTransferCount; // Update state if (!visible) { // Not visible because no recent downloads @@ -290,7 +290,7 @@ }, FADE_OUT_WAIT); } } else { - if (displayProgress < 100 || gpuTextures > 0) { // Was finished and waiting to fade out but have resumed so + if (displayProgress < 100 || gpuTextures > 0) { // Was finished and waiting to fade out but have resumed so // don't fade out Script.clearInterval(fadeWaitTimer); fadeWaitTimer = null; diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 4c661482fc..4e3fb95140 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -36,6 +36,8 @@ var shareAfterLogin = false; var snapshotToShareAfterLogin = []; var METAVERSE_BASE = location.metaverseServerUrl; var isLoggedIn; +var numGifSnapshotUploadsPending = 0; +var numStillSnapshotUploadsPending = 0; // It's totally unnecessary to return to C++ to perform many of these requests, such as DELETEing an old story, // POSTING a new one, PUTTING a new audience, or GETTING story data. It's far more efficient to do all of that within JS @@ -56,6 +58,10 @@ function removeFromStoryIDsToMaybeDelete(story_id) { print('storyIDsToMaybeDelete[] now:', JSON.stringify(storyIDsToMaybeDelete)); } +function fileExtensionMatches(filePath, extension) { + return filePath.split('.').pop().toLowerCase() === extension; +} + function onMessage(message) { // Receives message from the html dialog via the qwebchannel EventBridge. This is complicated by the following: // 1. Although we can send POJOs, we cannot receive a toplevel object. (Arrays of POJOs are fine, though.) @@ -93,11 +99,13 @@ function onMessage(message) { Settings.setValue("previousStillSnapStoryID", ""); Settings.setValue("previousStillSnapBlastingDisabled", false); Settings.setValue("previousStillSnapHifiSharingDisabled", false); + Settings.setValue("previousStillSnapUrl", ""); Settings.setValue("previousAnimatedSnapPath", ""); Settings.setValue("previousAnimatedSnapStoryID", ""); Settings.setValue("previousAnimatedSnapBlastingDisabled", false); Settings.setValue("previousAnimatedSnapHifiSharingDisabled", false); } + updatePrintPermissions(); break; case 'login': openLoginWindow(); @@ -134,16 +142,24 @@ function onMessage(message) { break; case 'shareSnapshotForUrl': isDomainOpen(Settings.getValue("previousSnapshotDomainID"), function (canShare) { - if (canShare) { - isLoggedIn = Account.isLoggedIn(); + var isGif = fileExtensionMatches(message.data, "gif"); + isLoggedIn = Account.isLoggedIn(); + isUploadingPrintableStill = canShare && Account.isLoggedIn() && !isGif; + if (canShare) { if (isLoggedIn) { print('Sharing snapshot with audience "for_url":', message.data); - Window.shareSnapshot(message.data, Settings.getValue("previousSnapshotHref")); + Window.shareSnapshot(message.data, Settings.getValue("previousSnapshotHref")); + if (isGif) { + numGifSnapshotUploadsPending++; + } else { + numStillSnapshotUploadsPending++; + } } else { shareAfterLogin = true; snapshotToShareAfterLogin.push({ path: message.data, href: Settings.getValue("previousSnapshotHref") }); } } + updatePrintPermissions(); }); break; case 'blastToConnections': @@ -203,6 +219,18 @@ function onMessage(message) { }); } break; + case 'requestPrintButtonUpdate': + updatePrintPermissions(); + break; + case 'printToPolaroid': + if (Entities.canRez() || Entities.canRezTmp()) { + printToPolaroid(Settings.getValue("previousStillSnapUrl")); + removeFromStoryIDsToMaybeDelete(Settings.getValue("previousStillSnapStoryID")); + } + break; + case 'alertSnapshotLoadFailed': + snapshotFailedToLoad = true; + break; case 'shareSnapshotWithEveryone': isLoggedIn = Account.isLoggedIn(); if (isLoggedIn) { @@ -250,6 +278,54 @@ function onMessage(message) { } } +var POLAROID_PRINT_SOUND = SoundCache.getSound(Script.resolvePath("assets/sounds/sound-print-photo.wav")); +var POLAROID_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/alan/dev/Test/snapshot.fbx'; + +function printToPolaroid(image_url) { + var polaroid_url = image_url; + + var model_pos = Vec3.sum(MyAvatar.position, Vec3.multiply(1.25, Quat.getForward(MyAvatar.orientation))); + + var model_q1 = MyAvatar.orientation; + var model_q2 = Quat.angleAxis(90, Quat.getRight(model_q1)); + var model_rot = Quat.multiply(model_q2, model_q1); + + var properties = { + "type": 'Model', + "shapeType": 'box', + + "name": "New Snapshot", + "description": "Printed from Snaps", + "modelURL": POLAROID_MODEL_URL, + + "position": model_pos, + "rotation": model_rot, + + "textures": JSON.stringify( { "tex.picture": polaroid_url } ), + + "density": 200, + "restitution": 0.15, + "gravity": { "x": 0, "y": -4.5, "z": 0 }, + + "velocity": { "x": 0, "y": 3.5, "z": 0 }, + "angularVelocity": { "x": -1.0, "y": 0, "z": -1.3 }, + + "dynamic": true, + "collisionsWillMove": true, + + "userData": { + "grabbableKey": { "grabbable" : true } + } + }; + + var polaroid = Entities.addEntity(properties); + Audio.playSound(POLAROID_PRINT_SOUND, { + position: model_pos, + localOnly: false, + volume: 0.2 + }); +} + function fillImageDataFromPrevious() { isLoggedIn = Account.isLoggedIn(); var previousStillSnapPath = Settings.getValue("previousStillSnapPath"); @@ -273,7 +349,8 @@ function fillImageDataFromPrevious() { localPath: previousStillSnapPath, story_id: previousStillSnapStoryID, blastButtonDisabled: previousStillSnapBlastingDisabled, - hifiButtonDisabled: previousStillSnapHifiSharingDisabled + hifiButtonDisabled: previousStillSnapHifiSharingDisabled, + errorPath: Script.resolvePath(Script.resourcesPath() + 'snapshot/img/no-image.jpg') }); } if (previousAnimatedSnapPath !== "") { @@ -281,7 +358,8 @@ function fillImageDataFromPrevious() { localPath: previousAnimatedSnapPath, story_id: previousAnimatedSnapStoryID, blastButtonDisabled: previousAnimatedSnapBlastingDisabled, - hifiButtonDisabled: previousAnimatedSnapHifiSharingDisabled + hifiButtonDisabled: previousAnimatedSnapHifiSharingDisabled, + errorPath: Script.resolvePath(Script.resourcesPath() + 'snapshot/img/no-image.jpg') }); } } @@ -305,26 +383,45 @@ function onButtonClicked() { function snapshotUploaded(isError, reply) { if (!isError) { - var replyJson = JSON.parse(reply); - var storyID = replyJson.user_story.id; + var replyJson = JSON.parse(reply), + storyID = replyJson.user_story.id, + imageURL = replyJson.user_story.details.image_url, + isGif = fileExtensionMatches(imageURL, "gif"), + ignoreGifSnapshotData = false, + ignoreStillSnapshotData = false; storyIDsToMaybeDelete.push(storyID); - var imageURL = replyJson.user_story.details.image_url; - var isGif = imageURL.split('.').pop().toLowerCase() === "gif"; - print('SUCCESS: Snapshot uploaded! Story with audience:for_url created! ID:', storyID); - tablet.emitScriptEvent(JSON.stringify({ - type: "snapshot", - action: "snapshotUploadComplete", - story_id: storyID, - image_url: imageURL, - })); if (isGif) { - Settings.setValue("previousAnimatedSnapStoryID", storyID); + numGifSnapshotUploadsPending--; + if (numGifSnapshotUploadsPending !== 0) { + ignoreGifSnapshotData = true; + } } else { - Settings.setValue("previousStillSnapStoryID", storyID); + numStillSnapshotUploadsPending--; + if (numStillSnapshotUploadsPending !== 0) { + ignoreStillSnapshotData = true; + } + } + if ((isGif && !ignoreGifSnapshotData) || (!isGif && !ignoreStillSnapshotData)) { + print('SUCCESS: Snapshot uploaded! Story with audience:for_url created! ID:', storyID); + tablet.emitScriptEvent(JSON.stringify({ + type: "snapshot", + action: "snapshotUploadComplete", + story_id: storyID, + image_url: imageURL, + })); + if (isGif) { + Settings.setValue("previousAnimatedSnapStoryID", storyID); + } else { + Settings.setValue("previousStillSnapStoryID", storyID); + Settings.setValue("previousStillSnapUrl", imageURL); + } + } else { + print('Ignoring snapshotUploaded() callback for stale ' + (isGif ? 'GIF' : 'Still' ) + ' snapshot. Stale story ID:', storyID); } } else { print(reply); } + isUploadingPrintableStill = false; } var href, domainId; function takeSnapshot() { @@ -336,10 +433,16 @@ function takeSnapshot() { Settings.setValue("previousStillSnapStoryID", ""); Settings.setValue("previousStillSnapBlastingDisabled", false); Settings.setValue("previousStillSnapHifiSharingDisabled", false); + Settings.setValue("previousStillSnapUrl", ""); Settings.setValue("previousAnimatedSnapPath", ""); Settings.setValue("previousAnimatedSnapStoryID", ""); Settings.setValue("previousAnimatedSnapBlastingDisabled", false); Settings.setValue("previousAnimatedSnapHifiSharingDisabled", false); + + // Since we are taking a snapshot, we should make the print button appear to be loading/processing + snapshotFailedToLoad = false; + isUploadingPrintableStill = true; + updatePrintPermissions(); // Raising the desktop for the share dialog at end will interact badly with clearOverlayWhenMoving. // Turn it off now, before we start futzing with things (and possibly moving). @@ -379,8 +482,15 @@ function takeSnapshot() { Menu.setIsOptionChecked("Overlays", false); } + var snapActivateSound = SoundCache.getSound(Script.resolvePath("../../resources/sounds/snap.wav")); + // take snapshot (with no notification) Script.setTimeout(function () { + Audio.playSound(snapActivateSound, { + position: { x: MyAvatar.position.x, y: MyAvatar.position.y, z: MyAvatar.position.z }, + localOnly: true, + volume: 1.0 + }); HMD.closeTablet(); Script.setTimeout(function () { Window.takeSnapshot(false, includeAnimated, 1.91); @@ -566,8 +676,16 @@ function onUsernameChanged() { snapshotToShareAfterLogin.forEach(function (element) { print('Uploading snapshot after login:', element.path); Window.shareSnapshot(element.path, element.href); + var isGif = fileExtensionMatches(element.path, "gif"); + if (isGif) { + numGifSnapshotUploadsPending++; + } else { + numStillSnapshotUploadsPending++; + } }); } + isUploadingPrintableStill = canShare; + updatePrintPermissions(); }); shareAfterLogin = false; @@ -575,6 +693,7 @@ function onUsernameChanged() { } } } + function snapshotLocationSet(location) { if (location !== "") { tablet.emitScriptEvent(JSON.stringify({ @@ -584,12 +703,44 @@ function snapshotLocationSet(location) { } } +function updatePrintPermissions() { + processRezPermissionChange(Entities.canRez() || Entities.canRezTmp()); +} + +var snapshotFailedToLoad = false; +var isUploadingPrintableStill = false; +function processRezPermissionChange(canRez) { + var action = ""; + + if (canRez && !snapshotFailedToLoad) { + if (Settings.getValue("previousStillSnapUrl")) { + action = 'setPrintButtonEnabled'; + } else if (isUploadingPrintableStill) { + action = 'setPrintButtonLoading'; + } else { + action = 'setPrintButtonDisabled'; + } + } else { + action = 'setPrintButtonDisabled'; + } + + tablet.emitScriptEvent(JSON.stringify({ + type: "snapshot", + action : action + })); +} + button.clicked.connect(onButtonClicked); buttonConnected = true; + Window.snapshotShared.connect(snapshotUploaded); tablet.screenChanged.connect(onTabletScreenChanged); GlobalServices.myUsernameChanged.connect(onUsernameChanged); Snapshot.snapshotLocationSet.connect(snapshotLocationSet); + +Entities.canRezChanged.connect(updatePrintPermissions); +Entities.canRezTmpChanged.connect(updatePrintPermissions); + Script.scriptEnding.connect(function () { if (buttonConnected) { button.clicked.disconnect(onButtonClicked); @@ -601,6 +752,9 @@ Script.scriptEnding.connect(function () { Window.snapshotShared.disconnect(snapshotUploaded); tablet.screenChanged.disconnect(onTabletScreenChanged); Snapshot.snapshotLocationSet.disconnect(snapshotLocationSet); + + Entities.canRezChanged.disconnect(processRezPermissionChange); + Entities.canRezTmpChanged.disconnect(processRezPermissionChange); }); }()); // END LOCAL_SCOPE diff --git a/scripts/system/tablet-ui/tabletUI.js b/scripts/system/tablet-ui/tabletUI.js index af093551e1..b7324ed28c 100644 --- a/scripts/system/tablet-ui/tabletUI.js +++ b/scripts/system/tablet-ui/tabletUI.js @@ -188,16 +188,12 @@ gTablet.updateAudioBar(getMicLevel()); } - if (validCheckTime - now > MSECS_PER_SEC/4) { - //each 250ms should be just fine + if (now - validCheckTime > MSECS_PER_SEC) { + validCheckTime = now; updateTabletWidthFromSettings(); if (UIWebTablet) { UIWebTablet.setLandscape(landscape); } - } - - if (validCheckTime - now > MSECS_PER_SEC) { - validCheckTime = now; if (tabletRezzed && UIWebTablet && !tabletIsValid()) { // when we switch domains, the tablet entity gets destroyed and recreated. this causes // the overlay to be deleted, but not recreated. If the overlay is deleted for this or any diff --git a/server-console/src/main.js b/server-console/src/main.js index 98bda9a10f..408a17bd56 100644 --- a/server-console/src/main.js +++ b/server-console/src/main.js @@ -821,6 +821,17 @@ for (var key in trayIcons) { const notificationIcon = path.join(__dirname, '../resources/console-notification.png'); +function isProcessRunning(pid) { + try { + // Sending a signal of 0 is effectively a NOOP. + // If sending the signal is successful, kill will return true. + // If the process is not running, an exception will be thrown. + return process.kill(pid, 0); + } catch (e) { + } + return false; +} + function onContentLoaded() { // Disable splash window for now. // maybeShowSplash(); @@ -882,31 +893,18 @@ function onContentLoaded() { startInterface(); } - // If we were launched with the shutdownWatcher option, then we need to watch for the interface app - // shutting down. The interface app will regularly update a running state file which we will check. - // If the file doesn't exist or stops updating for a significant amount of time, we will shut down. - if (argv.shutdownWatcher) { - log.debug("Shutdown watcher requested... argv.shutdownWatcher:", argv.shutdownWatcher); - var MAX_TIME_SINCE_EDIT = 5000; // 5 seconds between updates - var firstAttemptToCheck = new Date().getTime(); - var shutdownWatchInterval = setInterval(function(){ - var stats = fs.stat(argv.shutdownWatcher, function(err, stats) { - if (err) { - var sinceFirstCheck = new Date().getTime() - firstAttemptToCheck; - if (sinceFirstCheck > MAX_TIME_SINCE_EDIT) { - log.debug("Running state file is missing, assume interface has shutdown... shutting down snadbox."); - forcedShutdown(); - clearTimeout(shutdownWatchInterval); - } - } else { - var sinceEdit = new Date().getTime() - stats.mtime.getTime(); - if (sinceEdit > MAX_TIME_SINCE_EDIT) { - log.debug("Running state of interface hasn't updated in MAX time... shutting down."); - forcedShutdown(); - clearTimeout(shutdownWatchInterval); - } - } - }); + // If we were launched with the shutdownWith option, then we need to shutdown when that process (pid) + // is no longer running. + if (argv.shutdownWith) { + let pid = argv.shutdownWith; + console.log("Shutting down with process: ", pid); + let checkProcessInterval = setInterval(function() { + let isRunning = isProcessRunning(pid); + if (!isRunning) { + log.debug("Watched process is no longer running, shutting down"); + clearTimeout(checkProcessInterval); + forcedShutdown(); + } }, 1000); } } diff --git a/tests/controllers/src/main.cpp b/tests/controllers/src/main.cpp index e697bd501f..81f2f8d581 100644 --- a/tests/controllers/src/main.cpp +++ b/tests/controllers/src/main.cpp @@ -122,6 +122,10 @@ int main(int argc, char** argv) { glm::mat4(), glm::mat4(), glm::mat4(), + glm::mat4(), + glm::mat4(), + glm::mat4(), + glm::mat4(), glm::mat4() }; @@ -144,6 +148,10 @@ int main(int argc, char** argv) { glm::mat4(), glm::mat4(), glm::mat4(), + glm::mat4(), + glm::mat4(), + glm::mat4(), + glm::mat4(), glm::mat4() }; diff --git a/tests/ktx/CMakeLists.txt b/tests/ktx/CMakeLists.txt index 7133c30898..599435a90b 100644 --- a/tests/ktx/CMakeLists.txt +++ b/tests/ktx/CMakeLists.txt @@ -1,15 +1,9 @@ +# Declare dependencies +macro (SETUP_TESTCASE_DEPENDENCIES) + # link in the shared libraries + link_hifi_libraries(shared ktx gpu image) -set(TARGET_NAME ktx-test) - -if (WIN32) - SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /ignore:4049 /ignore:4217") -endif() + package_libraries_for_deployment() +endmacro () -# This is not a testcase -- just set it up as a regular hifi project -setup_hifi_project(Quick Gui OpenGL) -set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") - -# link in the shared libraries -link_hifi_libraries(shared octree ktx gl gpu gpu-gl render model model-networking networking render-utils fbx entities entities-renderer animation audio avatars script-engine physics image) - -package_libraries_for_deployment() +setup_hifi_testcase() diff --git a/tests/ktx/src/KtxTests.cpp b/tests/ktx/src/KtxTests.cpp new file mode 100644 index 0000000000..94e5d7e8e7 --- /dev/null +++ b/tests/ktx/src/KtxTests.cpp @@ -0,0 +1,195 @@ +// +// Created by Bradley Austin Davis on 2016/07/01 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "KtxTests.h" + +#include + +#include + +#include +#include +#include + + +QTEST_GUILESS_MAIN(KtxTests) + +QString getRootPath() { + static std::once_flag once; + static QString result; + std::call_once(once, [&] { + QFileInfo file(__FILE__); + QDir parent = file.absolutePath(); + result = QDir::cleanPath(parent.currentPath() + "/../../.."); + }); + return result; +} + +void KtxTests::initTestCase() { +} + +void KtxTests::cleanupTestCase() { +} + +void KtxTests::testKhronosCompressionFunctions() { + using namespace khronos::gl::texture; + QCOMPARE(evalAlignedCompressedBlockCount<4>(0), (uint32_t)0x0); + QCOMPARE(evalAlignedCompressedBlockCount<4>(1), (uint32_t)0x1); + QCOMPARE(evalAlignedCompressedBlockCount<4>(4), (uint32_t)0x1); + QCOMPARE(evalAlignedCompressedBlockCount<4>(5), (uint32_t)0x2); + QCOMPARE(evalCompressedBlockCount(InternalFormat::COMPRESSED_SRGB_S3TC_DXT1_EXT, 0x00), (uint32_t)0x00); + QCOMPARE(evalCompressedBlockCount(InternalFormat::COMPRESSED_SRGB_S3TC_DXT1_EXT, 0x01), (uint32_t)0x01); + QCOMPARE(evalCompressedBlockCount(InternalFormat::COMPRESSED_SRGB_S3TC_DXT1_EXT, 0x04), (uint32_t)0x01); + QCOMPARE(evalCompressedBlockCount(InternalFormat::COMPRESSED_SRGB_S3TC_DXT1_EXT, 0x05), (uint32_t)0x02); + QCOMPARE(evalCompressedBlockCount(InternalFormat::COMPRESSED_SRGB_S3TC_DXT1_EXT, 0x1000), (uint32_t)0x400); + + QVERIFY_EXCEPTION_THROWN(evalCompressedBlockCount(InternalFormat::RGBA8, 0x00), std::runtime_error); +} + +void KtxTests::testKtxEvalFunctions() { + QCOMPARE(sizeof(ktx::Header), (size_t)64); + QCOMPARE(ktx::evalPadding(0x0), (uint8_t)0); + QCOMPARE(ktx::evalPadding(0x1), (uint8_t)3); + QCOMPARE(ktx::evalPadding(0x2), (uint8_t)2); + QCOMPARE(ktx::evalPadding(0x3), (uint8_t)1); + QCOMPARE(ktx::evalPadding(0x4), (uint8_t)0); + QCOMPARE(ktx::evalPadding(0x400), (uint8_t)0); + QCOMPARE(ktx::evalPadding(0x401), (uint8_t)3); + QCOMPARE(ktx::evalPaddedSize(0x0), 0x0); + QCOMPARE(ktx::evalPaddedSize(0x1), 0x4); + QCOMPARE(ktx::evalPaddedSize(0x2), 0x4); + QCOMPARE(ktx::evalPaddedSize(0x3), 0x4); + QCOMPARE(ktx::evalPaddedSize(0x4), 0x4); + QCOMPARE(ktx::evalPaddedSize(0x400), 0x400); + QCOMPARE(ktx::evalPaddedSize(0x401), 0x404); + QCOMPARE(ktx::evalAlignedCount((uint32_t)0x0), (uint32_t)0x0); + QCOMPARE(ktx::evalAlignedCount((uint32_t)0x1), (uint32_t)0x1); + QCOMPARE(ktx::evalAlignedCount((uint32_t)0x4), (uint32_t)0x1); + QCOMPARE(ktx::evalAlignedCount((uint32_t)0x5), (uint32_t)0x2); +} + +void KtxTests::testKtxSerialization() { + const QString TEST_IMAGE = getRootPath() + "/scripts/developer/tests/cube_texture.png"; + QImage image(TEST_IMAGE); + gpu::TexturePointer testTexture = image::TextureUsage::process2DTextureColorFromImage(image, TEST_IMAGE.toStdString(), true); + auto ktxMemory = gpu::Texture::serialize(*testTexture); + QVERIFY(ktxMemory.get()); + + // Serialize the image to a file + QTemporaryFile TEST_IMAGE_KTX; + { + const auto& ktxStorage = ktxMemory->getStorage(); + QVERIFY(ktx::KTX::validate(ktxStorage)); + QVERIFY(ktxMemory->isValid()); + + auto& outFile = TEST_IMAGE_KTX; + if (!outFile.open()) { + QFAIL("Unable to open file"); + } + auto ktxSize = ktxStorage->size(); + outFile.resize(ktxSize); + auto dest = outFile.map(0, ktxSize); + memcpy(dest, ktxStorage->data(), ktxSize); + outFile.unmap(dest); + outFile.close(); + } + + + { + auto ktxStorage = std::make_shared(TEST_IMAGE_KTX.fileName()); + QVERIFY(ktx::KTX::validate(ktxStorage)); + auto ktxFile = ktx::KTX::create(ktxStorage); + QVERIFY(ktxFile.get()); + QVERIFY(ktxFile->isValid()); + { + const auto& memStorage = ktxMemory->getStorage(); + const auto& fileStorage = ktxFile->getStorage(); + QVERIFY(memStorage->size() == fileStorage->size()); + QVERIFY(memStorage->data() != fileStorage->data()); + QVERIFY(0 == memcmp(memStorage->data(), fileStorage->data(), memStorage->size())); + QVERIFY(ktxFile->_images.size() == ktxMemory->_images.size()); + auto imageCount = ktxFile->_images.size(); + auto startMemory = ktxMemory->_storage->data(); + auto startFile = ktxFile->_storage->data(); + for (size_t i = 0; i < imageCount; ++i) { + auto memImages = ktxMemory->_images[i]; + auto fileImages = ktxFile->_images[i]; + QVERIFY(memImages._padding == fileImages._padding); + QVERIFY(memImages._numFaces == fileImages._numFaces); + QVERIFY(memImages._imageSize == fileImages._imageSize); + QVERIFY(memImages._faceSize == fileImages._faceSize); + QVERIFY(memImages._faceBytes.size() == memImages._numFaces); + QVERIFY(fileImages._faceBytes.size() == fileImages._numFaces); + auto faceCount = fileImages._numFaces; + for (uint32_t face = 0; face < faceCount; ++face) { + auto memFace = memImages._faceBytes[face]; + auto memOffset = memFace - startMemory; + auto fileFace = fileImages._faceBytes[face]; + auto fileOffset = fileFace - startFile; + QVERIFY(memOffset % 4 == 0); + QVERIFY(memOffset == fileOffset); + } + } + } + } + testTexture->setKtxBacking(TEST_IMAGE_KTX.fileName().toStdString()); +} + +#if 0 + +static const QString TEST_FOLDER { "H:/ktx_cacheold" }; +//static const QString TEST_FOLDER { "C:/Users/bdavis/Git/KTX/testimages" }; + +//static const QString EXTENSIONS { "4bbdf8f786470e4ab3e672d44b8e8df2.ktx" }; +static const QString EXTENSIONS { "*.ktx" }; + +int mainTemp(int, char**) { + qInstallMessageHandler(messageHandler); + auto fileInfoList = QDir { TEST_FOLDER }.entryInfoList(QStringList { EXTENSIONS }); + for (auto fileInfo : fileInfoList) { + qDebug() << fileInfo.filePath(); + std::shared_ptr storage { new storage::FileStorage { fileInfo.filePath() } }; + + if (!ktx::KTX::validate(storage)) { + qDebug() << "KTX invalid"; + } + + auto ktxFile = ktx::KTX::create(storage); + ktx::KTXDescriptor ktxDescriptor = ktxFile->toDescriptor(); + + qDebug() << "Contains " << ktxDescriptor.keyValues.size() << " key value pairs"; + for (const auto& kv : ktxDescriptor.keyValues) { + qDebug() << "\t" << kv._key.c_str(); + } + + auto offsetToMinMipKV = ktxDescriptor.getValueOffsetForKey(ktx::HIFI_MIN_POPULATED_MIP_KEY); + if (offsetToMinMipKV) { + auto data = storage->data() + ktx::KTX_HEADER_SIZE + offsetToMinMipKV; + auto minMipLevelAvailable = *data; + qDebug() << "\tMin mip available " << minMipLevelAvailable; + assert(minMipLevelAvailable < ktxDescriptor.header.numberOfMipmapLevels); + } + auto storageSize = storage->size(); + for (const auto& faceImageDesc : ktxDescriptor.images) { + //assert(0 == (faceImageDesc._faceSize % 4)); + for (const auto& faceOffset : faceImageDesc._faceOffsets) { + assert(0 == (faceOffset % 4)); + auto faceEndOffset = faceOffset + faceImageDesc._faceSize; + assert(faceEndOffset <= storageSize); + } + } + + for (const auto& faceImage : ktxFile->_images) { + for (const ktx::Byte* faceBytes : faceImage._faceBytes) { + assert(0 == (reinterpret_cast(faceBytes) % 4)); + } + } + } + return 0; +} +#endif diff --git a/tests/ktx/src/KtxTests.h b/tests/ktx/src/KtxTests.h new file mode 100644 index 0000000000..5627dc313d --- /dev/null +++ b/tests/ktx/src/KtxTests.h @@ -0,0 +1,21 @@ +// +// Created by Bradley Austin Davis on 2016/07/01 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include + +class KtxTests : public QObject { + Q_OBJECT +private slots: + void initTestCase(); + void cleanupTestCase(); + void testKtxEvalFunctions(); + void testKhronosCompressionFunctions(); + void testKtxSerialization(); +}; + + diff --git a/tests/ktx/src/main.cpp b/tests/ktx/src/main.cpp deleted file mode 100644 index 3b62b89948..0000000000 --- a/tests/ktx/src/main.cpp +++ /dev/null @@ -1,223 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/07/01 -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -QSharedPointer logger; - -gpu::Texture* cacheTexture(const std::string& name, gpu::Texture* srcTexture, bool write = true, bool read = true); - - -void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { - QString logMessage = LogHandler::getInstance().printMessage((LogMsgType)type, context, message); - - if (!logMessage.isEmpty()) { -#ifdef Q_OS_WIN - OutputDebugStringA(logMessage.toLocal8Bit().constData()); - OutputDebugStringA("\n"); -#endif - if (logger) { - logger->addMessage(qPrintable(logMessage + "\n")); - } - } -} - -const char * LOG_FILTER_RULES = R"V0G0N( -hifi.gpu=true -)V0G0N"; - -QString getRootPath() { - static std::once_flag once; - static QString result; - std::call_once(once, [&] { - QFileInfo file(__FILE__); - QDir parent = file.absolutePath(); - result = QDir::cleanPath(parent.currentPath() + "/../../.."); - }); - return result; -} - -const QString TEST_IMAGE = getRootPath() + "/scripts/developer/tests/cube_texture.png"; -const QString TEST_IMAGE_KTX = getRootPath() + "/scripts/developer/tests/cube_texture.ktx"; - -int main(int argc, char** argv) { - QApplication app(argc, argv); - QCoreApplication::setApplicationName("KTX"); - QCoreApplication::setOrganizationName("High Fidelity"); - QCoreApplication::setOrganizationDomain("highfidelity.com"); - logger.reset(new FileLogger()); - - Q_ASSERT(ktx::evalPadding(0) == 0); - Q_ASSERT(ktx::evalPadding(1) == 3); - Q_ASSERT(ktx::evalPadding(2) == 2); - Q_ASSERT(ktx::evalPadding(3) == 1); - Q_ASSERT(ktx::evalPadding(4) == 0); - Q_ASSERT(ktx::evalPadding(1024) == 0); - Q_ASSERT(ktx::evalPadding(1025) == 3); - Q_ASSERT(ktx::evalPaddedSize(0) == 0); - Q_ASSERT(ktx::evalPaddedSize(1) == 4); - Q_ASSERT(ktx::evalPaddedSize(2) == 4); - Q_ASSERT(ktx::evalPaddedSize(3) == 4); - Q_ASSERT(ktx::evalPaddedSize(4) == 4); - Q_ASSERT(ktx::evalPaddedSize(1024) == 1024); - Q_ASSERT(ktx::evalPaddedSize(1025) == 1028); - Q_ASSERT(sizeof(ktx::Header) == 12 + (sizeof(uint32_t) * 13)); - - DependencyManager::set(); - qInstallMessageHandler(messageHandler); - QLoggingCategory::setFilterRules(LOG_FILTER_RULES); - - QImage image(TEST_IMAGE); - gpu::TexturePointer testTexture = image::TextureUsage::process2DTextureColorFromImage(image, TEST_IMAGE.toStdString(), true); - - auto ktxMemory = gpu::Texture::serialize(*testTexture); - { - const auto& ktxStorage = ktxMemory->getStorage(); - Q_ASSERT_X(ktx::KTX::validate(ktxStorage), __FUNCTION__, "KTX storage validation failed"); - Q_ASSERT_X(ktxMemory->isValid(), __FUNCTION__, "KTX self-validation failed"); - QSaveFile outFile(TEST_IMAGE_KTX); - if (!outFile.open(QFile::WriteOnly)) { - throw std::runtime_error("Unable to open file"); - } - auto ktxSize = ktxStorage->size(); - outFile.resize(ktxSize); - auto dest = outFile.map(0, ktxSize); - memcpy(dest, ktxStorage->data(), ktxSize); - outFile.unmap(dest); - outFile.commit(); - } - - { - auto ktxFile = ktx::KTX::create(std::shared_ptr(new storage::FileStorage(TEST_IMAGE_KTX))); - { - const auto& memStorage = ktxMemory->getStorage(); - const auto& fileStorage = ktxFile->getStorage(); - Q_ASSERT(memStorage->size() == fileStorage->size()); - Q_ASSERT(memStorage->data() != fileStorage->data()); - Q_ASSERT(0 == memcmp(memStorage->data(), fileStorage->data(), memStorage->size())); - Q_ASSERT(ktxFile->_images.size() == ktxMemory->_images.size()); - auto imageCount = ktxFile->_images.size(); - auto startMemory = ktxMemory->_storage->data(); - auto startFile = ktxFile->_storage->data(); - for (size_t i = 0; i < imageCount; ++i) { - auto memImages = ktxMemory->_images[i]; - auto fileImages = ktxFile->_images[i]; - Q_ASSERT(memImages._padding == fileImages._padding); - Q_ASSERT(memImages._numFaces == fileImages._numFaces); - Q_ASSERT(memImages._imageSize == fileImages._imageSize); - Q_ASSERT(memImages._faceSize == fileImages._faceSize); - Q_ASSERT(memImages._faceBytes.size() == memImages._numFaces); - Q_ASSERT(fileImages._faceBytes.size() == fileImages._numFaces); - auto faceCount = fileImages._numFaces; - for (uint32_t face = 0; face < faceCount; ++face) { - auto memFace = memImages._faceBytes[face]; - auto memOffset = memFace - startMemory; - auto fileFace = fileImages._faceBytes[face]; - auto fileOffset = fileFace - startFile; - Q_ASSERT(memOffset % 4 == 0); - Q_ASSERT(memOffset == fileOffset); - } - } - } - } - testTexture->setKtxBacking(TEST_IMAGE_KTX.toStdString()); - return 0; -} - -#if 0 -static const QString TEST_FOLDER { "H:/ktx_cacheold" }; -//static const QString TEST_FOLDER { "C:/Users/bdavis/Git/KTX/testimages" }; - -//static const QString EXTENSIONS { "4bbdf8f786470e4ab3e672d44b8e8df2.ktx" }; -static const QString EXTENSIONS { "*.ktx" }; - -int mainTemp(int, char**) { - qInstallMessageHandler(messageHandler); - auto fileInfoList = QDir { TEST_FOLDER }.entryInfoList(QStringList { EXTENSIONS }); - for (auto fileInfo : fileInfoList) { - qDebug() << fileInfo.filePath(); - std::shared_ptr storage { new storage::FileStorage { fileInfo.filePath() } }; - - if (!ktx::KTX::validate(storage)) { - qDebug() << "KTX invalid"; - } - - auto ktxFile = ktx::KTX::create(storage); - ktx::KTXDescriptor ktxDescriptor = ktxFile->toDescriptor(); - - qDebug() << "Contains " << ktxDescriptor.keyValues.size() << " key value pairs"; - for (const auto& kv : ktxDescriptor.keyValues) { - qDebug() << "\t" << kv._key.c_str(); - } - - auto offsetToMinMipKV = ktxDescriptor.getValueOffsetForKey(ktx::HIFI_MIN_POPULATED_MIP_KEY); - if (offsetToMinMipKV) { - auto data = storage->data() + ktx::KTX_HEADER_SIZE + offsetToMinMipKV; - auto minMipLevelAvailable = *data; - qDebug() << "\tMin mip available " << minMipLevelAvailable; - assert(minMipLevelAvailable < ktxDescriptor.header.numberOfMipmapLevels); - } - auto storageSize = storage->size(); - for (const auto& faceImageDesc : ktxDescriptor.images) { - //assert(0 == (faceImageDesc._faceSize % 4)); - for (const auto& faceOffset : faceImageDesc._faceOffsets) { - assert(0 == (faceOffset % 4)); - auto faceEndOffset = faceOffset + faceImageDesc._faceSize; - assert(faceEndOffset <= storageSize); - } - } - - for (const auto& faceImage : ktxFile->_images) { - for (const ktx::Byte* faceBytes : faceImage._faceBytes) { - assert(0 == (reinterpret_cast(faceBytes) % 4)); - } - } - } - return 0; -} -#endif - -#include "main.moc" - diff --git a/tests/networking/src/FileCacheTests.cpp b/tests/networking/src/FileCacheTests.cpp new file mode 100644 index 0000000000..79fe9dee54 --- /dev/null +++ b/tests/networking/src/FileCacheTests.cpp @@ -0,0 +1,187 @@ +// +// ResoruceTests.cpp +// +// 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 "FileCacheTests.h" + +#include + +QTEST_GUILESS_MAIN(FileCacheTests) + +using namespace cache; + +// Limit the file size to 10 MB +static const size_t MAX_UNUSED_SIZE { 1024 * 1024 * 10 }; +static const QByteArray TEST_DATA { 1024 * 1024, '0' }; + +static std::string getFileKey(int i) { + return QString(QByteArray { 1, (char)i }.toHex()).toStdString(); +} + +class TestFile : public File { + using Parent = File; +public: + TestFile(Metadata&& metadata, const std::string& filepath) + : Parent(std::move(metadata), filepath) { + } +}; + +class TestFileCache : public FileCache { + using Parent = FileCache; + +public: + TestFileCache(const std::string& dirname, const std::string& ext, QObject* parent = nullptr) : Parent(dirname, ext, nullptr) { + initialize(); + } + + std::unique_ptr createFile(Metadata&& metadata, const std::string& filepath) override { + qCInfo(file_cache) << "Wrote KTX" << metadata.key.c_str(); + return std::unique_ptr(new TestFile(std::move(metadata), filepath)); + } +}; + +using CachePointer = std::shared_ptr; + +// The FileCache relies on deleteLater to clear unused files, but QTest classes don't run a conventional event loop +// so we need to call this function to force any pending deletes to occur in the File destructor +static void forceDeletes() { + while (QCoreApplication::hasPendingEvents()) { + QCoreApplication::sendPostedEvents(); + QCoreApplication::sendPostedEvents(0, QEvent::DeferredDelete); + QCoreApplication::processEvents(); + } +} + +size_t FileCacheTests::getCacheDirectorySize() const { + size_t result = 0; + QDir dir(_testDir.path()); + for (const auto& file : dir.entryList({ "*.tmp" })) { + result += QFileInfo(dir.absoluteFilePath(file)).size(); + } + return result; +} + +CachePointer makeFileCache(QString& location) { + auto result = std::make_shared(location.toStdString(), "tmp"); + result->setMaxSize(MAX_UNUSED_SIZE); + return result; +} + +void FileCacheTests::initTestCase() { +} + +void FileCacheTests::testUnusedFiles() { + auto cache = makeFileCache(_testDir.path()); + std::list inUseFiles; + + { + for (int i = 0; i < 100; ++i) { + std::string key = getFileKey(i); + auto file = cache->writeFile(TEST_DATA.data(), TestFileCache::Metadata(key, TEST_DATA.size())); + inUseFiles.push_back(file); + forceDeletes(); + QThread::msleep(10); + } + QCOMPARE(cache->getNumCachedFiles(), (size_t)0); + QCOMPARE(cache->getNumTotalFiles(), (size_t)100); + // Release the in-use files + inUseFiles.clear(); + // Cache state is updated, but the directory state is unchanged, + // because the file deletes are triggered by an event loop + QCOMPARE(cache->getNumCachedFiles(), (size_t)10); + QCOMPARE(cache->getNumTotalFiles(), (size_t)10); + QVERIFY(getCacheDirectorySize() > MAX_UNUSED_SIZE); + forceDeletes(); + QCOMPARE(cache->getNumCachedFiles(), (size_t)10); + QCOMPARE(cache->getNumTotalFiles(), (size_t)10); + QVERIFY(getCacheDirectorySize() <= MAX_UNUSED_SIZE); + } + + // Reset the cache + cache = makeFileCache(_testDir.path()); + { + // Test files 0 to 89 are missing, because the LRU algorithm deleted them when we released the files + for (int i = 0; i < 90; ++i) { + std::string key = getFileKey(i); + auto file = cache->getFile(key); + QVERIFY(!file.get()); + } + + // Test files 90 to 99 are present + for (int i = 90; i < 100; ++i) { + std::string key = getFileKey(i); + auto file = cache->getFile(key); + QVERIFY(file.get()); + inUseFiles.push_back(file); + + if (i == 94) { + // Each access touches the file, so we need to sleep here to ensure that the the last 5 files + // have later times for cache ejection priority, otherwise the test runs too fast to reliably + // differentiate + QThread::msleep(1000); + } + } + + QCOMPARE(cache->getNumCachedFiles(), (size_t)0); + QCOMPARE(cache->getNumTotalFiles(), (size_t)10); + inUseFiles.clear(); + QCOMPARE(cache->getNumCachedFiles(), (size_t)10); + QCOMPARE(cache->getNumTotalFiles(), (size_t)10); + } +} + +size_t FileCacheTests::getFreeSpace() const { + return QStorageInfo(_testDir.path()).bytesFree(); +} + +// FIXME if something else is changing the amount of free space on the target drive concurrently with this test +// running, then it may fail +void FileCacheTests::testFreeSpacePreservation() { + QCOMPARE(getCacheDirectorySize(), MAX_UNUSED_SIZE); + // Set the target free space to slightly above whatever the current free space is... + size_t targetFreeSpace = getFreeSpace() + MAX_UNUSED_SIZE / 2; + + // Reset the cache + auto cache = makeFileCache(_testDir.path()); + // Setting the min free space causes it to eject the oldest files that cause the cache to exceed the minimum space + cache->setMinFreeSize(targetFreeSpace); + QVERIFY(getFreeSpace() < targetFreeSpace); + forceDeletes(); + QCOMPARE(cache->getNumCachedFiles(), (size_t)5); + QCOMPARE(cache->getNumTotalFiles(), (size_t)5); + QVERIFY(getFreeSpace() >= targetFreeSpace); + for (int i = 0; i < 95; ++i) { + std::string key = getFileKey(i); + auto file = cache->getFile(key); + QVERIFY(!file.get()); + } + for (int i = 95; i < 100; ++i) { + std::string key = getFileKey(i); + auto file = cache->getFile(key); + QVERIFY(file.get()); + } +} + +void FileCacheTests::testWipe() { + // Reset the cache + auto cache = makeFileCache(_testDir.path()); + QCOMPARE(cache->getNumCachedFiles(), (size_t)5); + QCOMPARE(cache->getNumTotalFiles(), (size_t)5); + cache->wipe(); + QCOMPARE(cache->getNumCachedFiles(), (size_t)0); + QCOMPARE(cache->getNumTotalFiles(), (size_t)0); + QVERIFY(getCacheDirectorySize() > 0); + forceDeletes(); + QCOMPARE(getCacheDirectorySize(), (size_t)0); +} + + +void FileCacheTests::cleanupTestCase() { +} + diff --git a/tests/networking/src/FileCacheTests.h b/tests/networking/src/FileCacheTests.h new file mode 100644 index 0000000000..b34b384855 --- /dev/null +++ b/tests/networking/src/FileCacheTests.h @@ -0,0 +1,31 @@ +// +// ResourceTests.h +// +// 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_ResourceTests_h +#define hifi_ResourceTests_h + +#include +#include + +class FileCacheTests : public QObject { + Q_OBJECT +private slots: + void initTestCase(); + void testUnusedFiles(); + void testFreeSpacePreservation(); + void cleanupTestCase(); + void testWipe(); + +private: + size_t getFreeSpace() const; + size_t getCacheDirectorySize() const; + QTemporaryDir _testDir; +}; + +#endif // hifi_ResourceTests_h diff --git a/tests/qt59/CMakeLists.txt b/tests/qt59/CMakeLists.txt new file mode 100644 index 0000000000..32cc125ecf --- /dev/null +++ b/tests/qt59/CMakeLists.txt @@ -0,0 +1,15 @@ + +set(TARGET_NAME qt59) + +if (WIN32) + SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /ignore:4049 /ignore:4217") +endif() + +# This is not a testcase -- just set it up as a regular hifi project +setup_hifi_project(Gui) +set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") + +# link in the shared libraries +link_hifi_libraries(shared networking) + +package_libraries_for_deployment() diff --git a/tests/qt59/src/main.cpp b/tests/qt59/src/main.cpp new file mode 100644 index 0000000000..c66a5e6f9a --- /dev/null +++ b/tests/qt59/src/main.cpp @@ -0,0 +1,78 @@ +// +// Created by Bradley Austin Davis on 2017/06/06 +// 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 + +#include + +#include +#include +#include +#include + +#include + + +class Qt59TestApp : public QCoreApplication { + Q_OBJECT +public: + Qt59TestApp(int argc, char* argv[]); + ~Qt59TestApp(); + +private: + void finish(int exitCode); +}; + + + +Qt59TestApp::Qt59TestApp(int argc, char* argv[]) : + QCoreApplication(argc, argv) +{ + + Setting::init(); + DependencyManager::registerInheritance(); + DependencyManager::set([&] { return QString("Mozilla/5.0 (HighFidelityACClient)"); }); + DependencyManager::set(); + DependencyManager::set(NodeType::Agent, 0); + auto nodeList = DependencyManager::get(); + nodeList->startThread(); + auto messagesClient = DependencyManager::set(); + messagesClient->startThread(); + QTimer::singleShot(1000, [this] { finish(0); }); +} + +Qt59TestApp::~Qt59TestApp() { +} + + +void Qt59TestApp::finish(int exitCode) { + auto nodeList = DependencyManager::get(); + + // send the domain a disconnect packet, force stoppage of domain-server check-ins + nodeList->getDomainHandler().disconnect(); + nodeList->setIsShuttingDown(true); + nodeList->getPacketReceiver().setShouldDropPackets(true); + + // remove the NodeList from the DependencyManager + DependencyManager::destroy(); + QCoreApplication::exit(exitCode); +} + + +int main(int argc, char * argv[]) { + QCoreApplication::setApplicationName("Qt59Test"); + QCoreApplication::setOrganizationName(BuildInfo::MODIFIED_ORGANIZATION); + QCoreApplication::setOrganizationDomain(BuildInfo::ORGANIZATION_DOMAIN); + QCoreApplication::setApplicationVersion(BuildInfo::VERSION); + + Qt59TestApp app(argc, argv); + + return app.exec(); +} + +#include "main.moc" \ No newline at end of file diff --git a/tests/render-perf/CMakeLists.txt b/tests/render-perf/CMakeLists.txt index 1e85dd3ea8..553e7c06e7 100644 --- a/tests/render-perf/CMakeLists.txt +++ b/tests/render-perf/CMakeLists.txt @@ -10,7 +10,7 @@ setup_hifi_project(Quick Gui OpenGL) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") # link in the shared libraries -link_hifi_libraries(shared octree ktx gl gpu gpu-gl render model model-networking networking render-utils fbx entities entities-renderer animation audio avatars script-engine physics image) +link_hifi_libraries(shared octree ktx gl gpu gpu-gl render model model-networking networking render-utils fbx entities entities-renderer animation audio avatars script-engine physics image procedural) package_libraries_for_deployment() diff --git a/tests/shaders/src/main.cpp b/tests/shaders/src/main.cpp index 8a239f0728..d10ab1ddbe 100644 --- a/tests/shaders/src/main.cpp +++ b/tests/shaders/src/main.cpp @@ -31,13 +31,9 @@ #include #include -#include #include #include -#include -#include - #include #include @@ -66,9 +62,6 @@ #include #include -#include -#include - #include #include @@ -155,11 +148,8 @@ void QTestWindow::draw() { testShaderBuild(simple_vert, simple_frag); testShaderBuild(simple_vert, simple_textured_frag); testShaderBuild(simple_vert, simple_textured_unlit_frag); - testShaderBuild(deferred_light_vert, directional_light_frag); testShaderBuild(deferred_light_vert, directional_ambient_light_frag); testShaderBuild(deferred_light_vert, directional_skybox_light_frag); - testShaderBuild(deferred_light_point_vert, point_light_frag); - testShaderBuild(deferred_light_spot_vert, spot_light_frag); testShaderBuild(standardTransformPNTC_vert, standardDrawTexture_frag); testShaderBuild(standardTransformPNTC_vert, DrawTextureOpaque_frag); @@ -190,7 +180,6 @@ void QTestWindow::draw() { testShaderBuild(ambient_occlusion_vert, ambient_occlusion_frag); testShaderBuild(ambient_occlusion_vert, occlusion_blend_frag); */ - testShaderBuild(hit_effect_vert, hit_effect_frag); testShaderBuild(overlay3D_vert, overlay3D_frag); diff --git a/tests/shared/src/CubicHermiteSplineTests.cpp b/tests/shared/src/CubicHermiteSplineTests.cpp new file mode 100644 index 0000000000..53a4ba8f6b --- /dev/null +++ b/tests/shared/src/CubicHermiteSplineTests.cpp @@ -0,0 +1,77 @@ +// +// CubicHermiteSplineTests.cpp +// tests/shared/src +// +// 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 "CubicHermiteSplineTests.h" +#include "../QTestExtensions.h" +#include +#include "CubicHermiteSpline.h" + +QTEST_MAIN(CubicHermiteSplineTests) + +void CubicHermiteSplineTests::testCubicHermiteSplineFunctor() { + glm::vec3 p0(0.0f, 0.0f, 0.0f); + glm::vec3 m0(1.0f, 0.0f, 0.0f); + glm::vec3 p1(1.0f, 1.0f, 0.0f); + glm::vec3 m1(2.0f, 0.0f, 0.0f); + CubicHermiteSplineFunctor hermiteSpline(p0, m0, p1, m1); + + const float EPSILON = 0.0001f; + + QCOMPARE_WITH_ABS_ERROR(p0, hermiteSpline(0.0f), EPSILON); + QCOMPARE_WITH_ABS_ERROR(p1, hermiteSpline(1.0f), EPSILON); + + // these values were computed offline. + const glm::vec3 oneFourth(0.203125f, 0.15625f, 0.0f); + const glm::vec3 oneHalf(0.375f, 0.5f, 0.0f); + const glm::vec3 threeFourths(0.609375f, 0.84375f, 0.0f); + + QCOMPARE_WITH_ABS_ERROR(oneFourth, hermiteSpline(0.25f), EPSILON); + QCOMPARE_WITH_ABS_ERROR(oneHalf, hermiteSpline(0.5f), EPSILON); + QCOMPARE_WITH_ABS_ERROR(threeFourths, hermiteSpline(0.75f), EPSILON); +} + +void CubicHermiteSplineTests::testCubicHermiteSplineFunctorWithArcLength() { + glm::vec3 p0(0.0f, 0.0f, 0.0f); + glm::vec3 m0(1.0f, 0.0f, 0.0f); + glm::vec3 p1(1.0f, 1.0f, 0.0f); + glm::vec3 m1(2.0f, 0.0f, 0.0f); + CubicHermiteSplineFunctorWithArcLength hermiteSpline(p0, m0, p1, m1); + + const float EPSILON = 0.001f; + + float arcLengths[5] = { + hermiteSpline.arcLength(0.0f), + hermiteSpline.arcLength(0.25f), + hermiteSpline.arcLength(0.5f), + hermiteSpline.arcLength(0.75f), + hermiteSpline.arcLength(1.0f) + }; + + // these values were computed offline + float referenceArcLengths[5] = { + 0.0f, + 0.268317f, + 0.652788f, + 1.07096f, + 1.50267f + }; + + QCOMPARE_WITH_ABS_ERROR(arcLengths[0], referenceArcLengths[0], EPSILON); + QCOMPARE_WITH_ABS_ERROR(arcLengths[1], referenceArcLengths[1], EPSILON); + QCOMPARE_WITH_ABS_ERROR(arcLengths[2], referenceArcLengths[2], EPSILON); + QCOMPARE_WITH_ABS_ERROR(arcLengths[3], referenceArcLengths[3], EPSILON); + QCOMPARE_WITH_ABS_ERROR(arcLengths[4], referenceArcLengths[4], EPSILON); + + QCOMPARE_WITH_ABS_ERROR(0.0f, hermiteSpline.arcLengthInverse(referenceArcLengths[0]), EPSILON); + QCOMPARE_WITH_ABS_ERROR(0.25f, hermiteSpline.arcLengthInverse(referenceArcLengths[1]), EPSILON); + QCOMPARE_WITH_ABS_ERROR(0.5f, hermiteSpline.arcLengthInverse(referenceArcLengths[2]), EPSILON); + QCOMPARE_WITH_ABS_ERROR(0.75f, hermiteSpline.arcLengthInverse(referenceArcLengths[3]), EPSILON); + QCOMPARE_WITH_ABS_ERROR(1.0f, hermiteSpline.arcLengthInverse(referenceArcLengths[4]), EPSILON); +} diff --git a/tests/shared/src/CubicHermiteSplineTests.h b/tests/shared/src/CubicHermiteSplineTests.h new file mode 100644 index 0000000000..1828d1aa02 --- /dev/null +++ b/tests/shared/src/CubicHermiteSplineTests.h @@ -0,0 +1,23 @@ +// +// CubicHermiteSplineTests.h +// tests/shared/src +// +// 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_CubicHermiteSplineTests_h +#define hifi_CubicHermiteSplineTests_h + +#include + +class CubicHermiteSplineTests : public QObject { + Q_OBJECT +private slots: + void testCubicHermiteSplineFunctor(); + void testCubicHermiteSplineFunctorWithArcLength(); +}; + +#endif // hifi_TransformTests_h diff --git a/tests/shared/src/GLMHelpersTests.cpp b/tests/shared/src/GLMHelpersTests.cpp index b4af4729a3..83a294ee1d 100644 --- a/tests/shared/src/GLMHelpersTests.cpp +++ b/tests/shared/src/GLMHelpersTests.cpp @@ -140,3 +140,77 @@ void GLMHelpersTests::testSimd() { } qDebug() << "Done "; } + +void GLMHelpersTests::testGenerateBasisVectors() { + { // very simple case: primary along X, secondary is linear combination of X and Y + glm::vec3 u(1.0f, 0.0f, 0.0f); + glm::vec3 v(1.0f, 1.0f, 0.0f); + glm::vec3 w; + + generateBasisVectors(u, v, u, v, w); + + QCOMPARE_WITH_ABS_ERROR(u, Vectors::UNIT_X, EPSILON); + QCOMPARE_WITH_ABS_ERROR(v, Vectors::UNIT_Y, EPSILON); + QCOMPARE_WITH_ABS_ERROR(w, Vectors::UNIT_Z, EPSILON); + } + + { // point primary along Y instead of X + glm::vec3 u(0.0f, 1.0f, 0.0f); + glm::vec3 v(1.0f, 1.0f, 0.0f); + glm::vec3 w; + + generateBasisVectors(u, v, u, v, w); + + QCOMPARE_WITH_ABS_ERROR(u, Vectors::UNIT_Y, EPSILON); + QCOMPARE_WITH_ABS_ERROR(v, Vectors::UNIT_X, EPSILON); + QCOMPARE_WITH_ABS_ERROR(w, -Vectors::UNIT_Z, EPSILON); + } + + { // pass bad data (both vectors along Y). The helper will guess X for secondary. + glm::vec3 u(0.0f, 1.0f, 0.0f); + glm::vec3 v(0.0f, 1.0f, 0.0f); + glm::vec3 w; + + generateBasisVectors(u, v, u, v, w); + + QCOMPARE_WITH_ABS_ERROR(u, Vectors::UNIT_Y, EPSILON); + QCOMPARE_WITH_ABS_ERROR(v, Vectors::UNIT_X, EPSILON); + QCOMPARE_WITH_ABS_ERROR(w, -Vectors::UNIT_Z, EPSILON); + } + + { // pass bad data (both vectors along X). The helper will guess X for secondary, fail, then guess Y. + glm::vec3 u(1.0f, 0.0f, 0.0f); + glm::vec3 v(1.0f, 0.0f, 0.0f); + glm::vec3 w; + + generateBasisVectors(u, v, u, v, w); + + QCOMPARE_WITH_ABS_ERROR(u, Vectors::UNIT_X, EPSILON); + QCOMPARE_WITH_ABS_ERROR(v, Vectors::UNIT_Y, EPSILON); + QCOMPARE_WITH_ABS_ERROR(w, Vectors::UNIT_Z, EPSILON); + } + + { // general case for arbitrary rotation + float angle = 1.234f; + glm::vec3 axis = glm::normalize(glm::vec3(1.0f, 2.0f, 3.0f)); + glm::quat rotation = glm::angleAxis(angle, axis); + + // expected values + glm::vec3 x = rotation * Vectors::UNIT_X; + glm::vec3 y = rotation * Vectors::UNIT_Y; + glm::vec3 z = rotation * Vectors::UNIT_Z; + + // primary is along x + // secondary is linear combination of x and y + // tertiary is unknown + glm::vec3 u = 1.23f * x; + glm::vec3 v = 2.34f * x + 3.45f * y; + glm::vec3 w; + + generateBasisVectors(u, v, u, v, w); + + QCOMPARE_WITH_ABS_ERROR(u, x, EPSILON); + QCOMPARE_WITH_ABS_ERROR(v, y, EPSILON); + QCOMPARE_WITH_ABS_ERROR(w, z, EPSILON); + } +} diff --git a/tests/shared/src/GLMHelpersTests.h b/tests/shared/src/GLMHelpersTests.h index acc7b533f5..030f2d477f 100644 --- a/tests/shared/src/GLMHelpersTests.h +++ b/tests/shared/src/GLMHelpersTests.h @@ -21,6 +21,7 @@ private slots: void testEulerDecomposition(); void testSixByteOrientationCompression(); void testSimd(); + void testGenerateBasisVectors(); }; float getErrorDifference(const float& a, const float& b); diff --git a/tests/shared/src/StorageTests.cpp b/tests/shared/src/StorageTests.cpp index fa538f6911..48e6b91900 100644 --- a/tests/shared/src/StorageTests.cpp +++ b/tests/shared/src/StorageTests.cpp @@ -8,6 +8,8 @@ #include "StorageTests.h" +#include + QTEST_MAIN(StorageTests) using namespace storage; @@ -32,8 +34,8 @@ void StorageTests::testConversion() { QFileInfo fileInfo(_testFile); QCOMPARE(fileInfo.exists(), false); } - StoragePointer storagePointer = std::make_unique(_testData.size(), _testData.data()); - QCOMPARE(storagePointer->size(), (quint64)_testData.size()); + StoragePointer storagePointer = std::unique_ptr(new MemoryStorage(_testData.size(), _testData.data())); + QCOMPARE(storagePointer->size(), _testData.size()); QCOMPARE(memcmp(_testData.data(), storagePointer->data(), _testData.size()), 0); // Convert to a file storagePointer = storagePointer->toFileStorage(_testFile); @@ -42,12 +44,12 @@ void StorageTests::testConversion() { QCOMPARE(fileInfo.exists(), true); QCOMPARE(fileInfo.size(), (qint64)_testData.size()); } - QCOMPARE(storagePointer->size(), (quint64)_testData.size()); + QCOMPARE(storagePointer->size(), _testData.size()); QCOMPARE(memcmp(_testData.data(), storagePointer->data(), _testData.size()), 0); // Convert to memory storagePointer = storagePointer->toMemoryStorage(); - QCOMPARE(storagePointer->size(), (quint64)_testData.size()); + QCOMPARE(storagePointer->size(), _testData.size()); QCOMPARE(memcmp(_testData.data(), storagePointer->data(), _testData.size()), 0); { // ensure the file is unaffected @@ -58,13 +60,13 @@ void StorageTests::testConversion() { // truncate the data as a new memory object auto newSize = _testData.size() / 2; - storagePointer = std::make_unique(newSize, storagePointer->data()); - QCOMPARE(storagePointer->size(), (quint64)newSize); + storagePointer = std::unique_ptr(new MemoryStorage(newSize, storagePointer->data())); + QCOMPARE(storagePointer->size(), newSize); QCOMPARE(memcmp(_testData.data(), storagePointer->data(), newSize), 0); // Convert back to file storagePointer = storagePointer->toFileStorage(_testFile); - QCOMPARE(storagePointer->size(), (quint64)newSize); + QCOMPARE(storagePointer->size(), newSize); QCOMPARE(memcmp(_testData.data(), storagePointer->data(), newSize), 0); { // ensure the file is truncated diff --git a/tools/ac-client/src/ACClientApp.cpp b/tools/ac-client/src/ACClientApp.cpp index 4c12a09388..b81d092662 100644 --- a/tools/ac-client/src/ACClientApp.cpp +++ b/tools/ac-client/src/ACClientApp.cpp @@ -90,23 +90,14 @@ ACClientApp::ACClientApp(int argc, char* argv[]) : auto nodeList = DependencyManager::get(); - // start the nodeThread so its event loop is running - QThread* nodeThread = new QThread(this); - nodeThread->setObjectName("NodeList Thread"); - nodeThread->start(); - - // make sure the node thread is given highest priority - nodeThread->setPriority(QThread::TimeCriticalPriority); + nodeList->startThread(); // setup a timer for domain-server check ins QTimer* domainCheckInTimer = new QTimer(nodeList.data()); connect(domainCheckInTimer, &QTimer::timeout, nodeList.data(), &NodeList::sendDomainServerCheckIn); domainCheckInTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS); - // put the NodeList and datagram processing on the node thread - nodeList->moveToThread(nodeThread); - const DomainHandler& domainHandler = nodeList->getDomainHandler(); connect(&domainHandler, SIGNAL(hostnameChanged(const QString&)), SLOT(domainChanged(const QString&))); @@ -239,12 +230,8 @@ void ACClientApp::finish(int exitCode) { // tell the packet receiver we're shutting down, so it can drop packets nodeList->getPacketReceiver().setShouldDropPackets(true); - QThread* nodeThread = DependencyManager::get()->thread(); // remove the NodeList from the DependencyManager DependencyManager::destroy(); - // ask the node thread to quit and wait until it is done - nodeThread->quit(); - nodeThread->wait(); printFailedServers(); QCoreApplication::exit(exitCode); diff --git a/tools/atp-get/src/ATPGetApp.cpp b/tools/atp-get/src/ATPGetApp.cpp index 30054fffea..4125582c21 100644 --- a/tools/atp-get/src/ATPGetApp.cpp +++ b/tools/atp-get/src/ATPGetApp.cpp @@ -111,23 +111,13 @@ ATPGetApp::ATPGetApp(int argc, char* argv[]) : auto nodeList = DependencyManager::get(); - - // start the nodeThread so its event loop is running - QThread* nodeThread = new QThread(this); - nodeThread->setObjectName("NodeList Thread"); - nodeThread->start(); - - // make sure the node thread is given highest priority - nodeThread->setPriority(QThread::TimeCriticalPriority); + nodeList->startThread(); // setup a timer for domain-server check ins QTimer* domainCheckInTimer = new QTimer(nodeList.data()); connect(domainCheckInTimer, &QTimer::timeout, nodeList.data(), &NodeList::sendDomainServerCheckIn); domainCheckInTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS); - // put the NodeList and datagram processing on the node thread - nodeList->moveToThread(nodeThread); - const DomainHandler& domainHandler = nodeList->getDomainHandler(); connect(&domainHandler, SIGNAL(hostnameChanged(const QString&)), SLOT(domainChanged(const QString&))); @@ -258,12 +248,8 @@ void ATPGetApp::finish(int exitCode) { // tell the packet receiver we're shutting down, so it can drop packets nodeList->getPacketReceiver().setShouldDropPackets(true); - QThread* nodeThread = DependencyManager::get()->thread(); // remove the NodeList from the DependencyManager DependencyManager::destroy(); - // ask the node thread to quit and wait until it is done - nodeThread->quit(); - nodeThread->wait(); QCoreApplication::exit(exitCode); } diff --git a/tools/oven/src/BakerCLI.cpp b/tools/oven/src/BakerCLI.cpp new file mode 100644 index 0000000000..14eb9de150 --- /dev/null +++ b/tools/oven/src/BakerCLI.cpp @@ -0,0 +1,64 @@ +// +// BakerCLI.cpp +// tools/oven/src +// +// Created by Robbie Uvanni on 6/6/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include + +#include "ModelBakingLoggingCategory.h" +#include "Oven.h" +#include "BakerCLI.h" +#include "FBXBaker.h" +#include "TextureBaker.h" + +BakerCLI::BakerCLI(Oven* parent) : QObject(parent) { +} + +void BakerCLI::bakeFile(QUrl inputUrl, const QString outputPath) { + + // if the URL doesn't have a scheme, assume it is a local file + if (inputUrl.scheme() != "http" && inputUrl.scheme() != "https" && inputUrl.scheme() != "ftp") { + inputUrl.setScheme("file"); + } + + static const QString MODEL_EXTENSION { ".fbx" }; + + // check what kind of baker we should be creating + bool isFBX = inputUrl.toDisplayString().endsWith(MODEL_EXTENSION, Qt::CaseInsensitive); + bool isSupportedImage = false; + + for (QByteArray format : QImageReader::supportedImageFormats()) { + isSupportedImage |= inputUrl.toDisplayString().endsWith(format, Qt::CaseInsensitive); + } + + // create our appropiate baker + if (isFBX) { + _baker = std::unique_ptr { new FBXBaker(inputUrl, outputPath, []() -> QThread* { return qApp->getNextWorkerThread(); }) }; + _baker->moveToThread(qApp->getFBXBakerThread()); + } else if (isSupportedImage) { + _baker = std::unique_ptr { new TextureBaker(inputUrl, image::TextureUsage::CUBE_TEXTURE, outputPath) }; + _baker->moveToThread(qApp->getNextWorkerThread()); + } else { + qCDebug(model_baking) << "Failed to determine baker type for file" << inputUrl; + QApplication::exit(1); + } + + // invoke the bake method on the baker thread + QMetaObject::invokeMethod(_baker.get(), "bake"); + + // make sure we hear about the results of this baker when it is done + connect(_baker.get(), &Baker::finished, this, &BakerCLI::handleFinishedBaker); +} + +void BakerCLI::handleFinishedBaker() { + qCDebug(model_baking) << "Finished baking file."; + QApplication::exit(_baker.get()->hasErrors()); +} \ No newline at end of file diff --git a/tools/oven/src/BakerCLI.h b/tools/oven/src/BakerCLI.h new file mode 100644 index 0000000000..cb2b908059 --- /dev/null +++ b/tools/oven/src/BakerCLI.h @@ -0,0 +1,34 @@ +// +// BakerCLI.h +// tools/oven/src +// +// Created by Robbie Uvanni on 6/6/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_BakerCLI_h +#define hifi_BakerCLI_h + +#include + +#include "Baker.h" +#include "Oven.h" + +class BakerCLI : public QObject { + Q_OBJECT + +public: + BakerCLI(Oven* parent); + void bakeFile(QUrl inputUrl, const QString outputPath); + +private slots: + void handleFinishedBaker(); + +private: + std::unique_ptr _baker; +}; + +#endif // hifi_BakerCLI_h \ No newline at end of file diff --git a/tools/oven/src/Oven.cpp b/tools/oven/src/Oven.cpp index af660e9795..a38aaa2b97 100644 --- a/tools/oven/src/Oven.cpp +++ b/tools/oven/src/Oven.cpp @@ -11,16 +11,20 @@ #include #include +#include #include #include #include "ui/OvenMainWindow.h" - #include "Oven.h" +#include "BakerCli.h" static const QString OUTPUT_FOLDER = "/Users/birarda/code/hifi/lod/test-oven/export"; +static const QString CLI_INPUT_PARAMETER = "i"; +static const QString CLI_OUTPUT_PARAMETER = "o"; + Oven::Oven(int argc, char* argv[]) : QApplication(argc, argv) { @@ -30,24 +34,43 @@ Oven::Oven(int argc, char* argv[]) : // init the settings interface so we can save and load settings Setting::init(); + // parse the command line parameters + QCommandLineParser parser; + + parser.addOptions({ + { CLI_INPUT_PARAMETER, "Path to file that you would like to bake.", "input" }, + { CLI_OUTPUT_PARAMETER, "Path to folder that will be used as output.", "output" } + }); + parser.addHelpOption(); + parser.process(*this); + // enable compression in image library, except for cube maps image::setColorTexturesCompressionEnabled(true); image::setGrayscaleTexturesCompressionEnabled(true); image::setNormalTexturesCompressionEnabled(true); image::setCubeTexturesCompressionEnabled(true); - // check if we were passed any command line arguments that would tell us just to run without the GUI - - // setup the GUI - _mainWindow = new OvenMainWindow; - _mainWindow->show(); - // setup our worker threads setupWorkerThreads(QThread::idealThreadCount() - 1); // Autodesk's SDK means that we need a single thread for all FBX importing/exporting in the same process // setup the FBX Baker thread setupFBXBakerThread(); + + // check if we were passed any command line arguments that would tell us just to run without the GUI + if (parser.isSet(CLI_INPUT_PARAMETER) || parser.isSet(CLI_OUTPUT_PARAMETER)) { + if (parser.isSet(CLI_INPUT_PARAMETER) && parser.isSet(CLI_OUTPUT_PARAMETER)) { + BakerCLI* cli = new BakerCLI(this); + cli->bakeFile(parser.value(CLI_INPUT_PARAMETER), parser.value(CLI_OUTPUT_PARAMETER)); + } else { + parser.showHelp(); + QApplication::quit(); + } + } else { + // setup the GUI + _mainWindow = new OvenMainWindow; + _mainWindow->show(); + } } Oven::~Oven() { diff --git a/tools/oven/src/Oven.h b/tools/oven/src/Oven.h index 350c615ce0..569b73a3e2 100644 --- a/tools/oven/src/Oven.h +++ b/tools/oven/src/Oven.h @@ -14,7 +14,7 @@ #include -#include +#include #include diff --git a/scripts/system/assets/sounds/countdown-tick.wav b/unpublishedScripts/marketplace/record/assets/sounds/countdown-tick.wav similarity index 100% rename from scripts/system/assets/sounds/countdown-tick.wav rename to unpublishedScripts/marketplace/record/assets/sounds/countdown-tick.wav diff --git a/scripts/system/assets/sounds/finish-recording.wav b/unpublishedScripts/marketplace/record/assets/sounds/finish-recording.wav similarity index 100% rename from scripts/system/assets/sounds/finish-recording.wav rename to unpublishedScripts/marketplace/record/assets/sounds/finish-recording.wav diff --git a/scripts/system/assets/sounds/start-recording.wav b/unpublishedScripts/marketplace/record/assets/sounds/start-recording.wav similarity index 100% rename from scripts/system/assets/sounds/start-recording.wav rename to unpublishedScripts/marketplace/record/assets/sounds/start-recording.wav diff --git a/unpublishedScripts/marketplace/record/html/css/edit-style.css b/unpublishedScripts/marketplace/record/html/css/edit-style.css new file mode 100644 index 0000000000..fcb1815ca4 --- /dev/null +++ b/unpublishedScripts/marketplace/record/html/css/edit-style.css @@ -0,0 +1,1262 @@ +/* +// edit-style.css +// +// Created by Ryan Huffman on 13 Nov 2014 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +*/ + +@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 */ + 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(../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(../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(../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(../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(../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(../fonts/hifi-glyphs.ttf); +} + +* { + margin: 0; + padding: 0; +} + +body { + padding: 21px 21px 21px 21px; + + 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; +} + +table { + font-family: FiraSans-SemiBold; + font-size: 15px; + color: #afafaf; + border-collapse: collapse; + width: 100%; + border: 2px solid #575757; + border-radius: 7px; +} + +thead { + font-family: Raleway-Regular; + font-size: 12px; + text-transform: uppercase; + background-color: #1c1c1c; + padding: 1px 0px; + border-bottom: 1px solid #575757; + width: 100%; +} + +tbody { + width: 100%; +} + +tfoot { + font-family: Raleway-Light; + font-size: 13px; + background-color: #1c1c1c; + border-top: 1px solid #575757; + width: 100%; +} + +tfoot tr { + background-color: #1c1cff; +} + +thead tr { + height: 26px; /* 28px with thead padding */ +} + +thead th { + height: 26px; + background-color: #1c1c1c; + border-right: 1px solid #575757; +} + +thead th:last-child { + border: none; +} + +tbody td { + height: 26px; +} + +tfoot td { + height: 18px; + width: 100%; + background-color: #1c1c1c; + margin-left: 12px; +} + +tr { + width: 100%; + cursor: pointer; +} + +tr:nth-child(odd) { + background-color: #2e2e2e; +} + +tr:nth-child(even) { + background-color: #1c1c1c; +} + +tr:focus { + outline: none; +} + +tr.selected { + color: #000000; + background-color: #00b4ef; +} + +tr.selected + tr.selected { + border-top: 1px solid #2e2e2e; +} + +th { + text-align: center; + word-wrap: nowrap; + white-space: nowrap; + padding-left: 12px; + padding-right: 12px; +} + +td { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + word-wrap: nowrap; + padding-left: 12px; + padding-right: 12px; +} + +td.url { + white-space: nowrap; + overflow: hidden; +} + + +input[type="text"], input[type="number"], textarea { + margin: 0; + padding: 0 12px; + color: #afafaf; + background-color: #252525; + border: none; + font-family: FiraSans-SemiBold; + font-size: 15px; +} + +textarea { + font-family: AnonymousPro-Regular; + font-size: 16px; + padding-top: 5px; + padding-bottom: 5px; + min-height: 64px; + width: 100%; + resize: vertical; +} + +input::-webkit-input-placeholder { + font-style: italic; +} + +input:focus, textarea:focus { + color: #fff; + background-color: #000; + outline: 1px solid #00b4ef; + outline-offset: -1px; +} + +input::selection, textarea::selection { + color: #000000; + background-color: #00b4ef; +} + +input.search { + border-radius: 14px; +} + +input.search:focus { + outline: none; + box-sizing: border-box; + height: 26px; + margin-top: 1px; + margin-bottom: 1px; + box-shadow: 0 0 0px 1px #00b4ef; +} + +input:disabled, textarea:disabled { + background-color: #383838; + color: #afafaf; +} + +input[type="text"] { + height: 28px; + width: 100%; +} + +input[type="number"] { + position: relative; + height: 28px; + width: 124px; +} + +input[type=number] { + padding-right: 3px; +} +input[type=number]::-webkit-inner-spin-button { + -webkit-appearance: none; + opacity: 1.0; + display: block; + position: relative; + width: 10px; + height: 100%; + overflow: hidden; + font-family: hifi-glyphs; + font-size: 46px; + color: #afafaf; + cursor: pointer; + /*background-color: #000000;*/ +} +input[type=number]::-webkit-inner-spin-button:before, +input[type=number]::-webkit-inner-spin-button:after { + position:absolute; + left: -19px; + line-height: 8px; + text-align: center; +} +input[type=number]::-webkit-inner-spin-button:before { + content: "6"; + top: 4px; +} +input[type=number]::-webkit-inner-spin-button:after { + content: "5"; + bottom: 4px; +} + +input[type=number].hover-up::-webkit-inner-spin-button:before, +input[type=number].hover-down::-webkit-inner-spin-button:after { + color: #ffffff; +} + +input.no-spin::-webkit-outer-spin-button, +input.no-spin::-webkit-inner-spin-button { + display: none; + -webkit-appearance: none; + margin: 0; /* <-- Apparently some margin are still there even though it's hidden */ + padding-right: 12px; +} + +input[type=button] { + font-family: Raleway-Bold; + font-size: 13px; + text-transform: uppercase; + vertical-align: top; + height: 28px; + min-width: 120px; + padding: 0px 18px; + margin-right: 6px; + border-radius: 5px; + border: none; + color: #fff; + background-color: #000; + background: linear-gradient(#343434 20%, #000 100%); + cursor: pointer; +} + +input[type=button].glyph { + font-family: HiFi-Glyphs; + font-size: 20px; + text-transform: none; + min-width: 32px; + padding: 0; +} + +input[type=button].red { + color: #fff; + background-color: #94132e; + background: linear-gradient(#d42043 20%, #94132e 100%); +} +input[type=button].blue { + color: #fff; + background-color: #1080b8; + background: linear-gradient(#00b4ef 20%, #1080b8 100%); +} +input[type=button].white { + color: #121212; + background-color: #afafaf; + background: linear-gradient(#fff 20%, #afafaf 100%); +} + +input[type=button]:enabled:hover { + background: linear-gradient(#000, #000); + border: none; +} +input[type=button].red:enabled:hover { + background: linear-gradient(#d42043, #d42043); + border: none; +} +input[type=button].blue:enabled:hover { + background: linear-gradient(#00b4ef, #00b4ef); + border: none; +} +input[type=button].white:enabled:hover { + background: linear-gradient(#fff, #fff); + border: none; +} + +input[type=button]:active { + background: linear-gradient(#343434, #343434); +} +input[type=button].red:active { + background: linear-gradient(#94132e, #94132e); +} +input[type=button].blue:active { + background: linear-gradient(#1080b8, #1080b8); +} +input[type=button].white:active { + background: linear-gradient(#afafaf, #afafaf); +} + +input[type=button]:disabled { + color: #252525; + background: linear-gradient(#575757 20%, #252525 100%); +} + +input[type=button][pressed=pressed] { + color: #00b4ef; +} + +input[type=checkbox] { + display: none; +} +input[type=checkbox] + label { + padding-left: 24px; + background-position-y: 6px; + background-repeat: no-repeat; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACpSURBVDhPY2xoaGD68+dPMSMjY9L////VgTQjAw4AlH8PxLOPHj1azWxjY1MBVNsBFBfBpwkEgNKcQGwtJyfHyATkF0KEiQdAzYlMQEIUyicFyDD9+/ePgRxMvsb///4zkIOZ/v0HmkAGHginYjGNGAzS+BpdkAj8mun/3//92DyPD//993cG88nTJ4+Zm5p/BSZeJYb/DEJADEzNOPF7hn8Mk69cvVIPAHN5pyfo70F5AAAAAElFTkSuQmCC); +} +input[type=checkbox]:enabled + label:hover { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAClSURBVDhPY2hoaGD6//9/6b9//64D8T8gGycASr/7+/dv5/79+1kYgIxKqDjRAKiniRFIv2JgYBAFYlLAE0aQ66AckgDjjx8/yNP44cMH8jS+fPmSPI0PHz4kT+PNmzfJ03jp0iXyNJ46dYo8jYcPHyYnAbxm+vnzZz8wLhlIwd+/f5/BrKSkdExCQuLrnz9/lIBpUAiIQekXF34PTGmTT548WQ8AokXg+rhVtPYAAAAASUVORK5CYII=); +} +input[type=checkbox]:checked + label { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFLSURBVDhPjZK9SgNBFIXvbCaQRDQq7mIhQRPBRisJKcwLWOobaCsExEaxcEEQe0trGysfwg0EwWoDsbFIJUaIBJOwus547saNP3FlPzgzzJxzL5edFbZtG77v7wkhtrXWS9gFRQC/DZ07jnOYKJfL+8ie4n7mvyIGdhpay+VyQuK8y5dPZoHuVtbpZcLi4wjJ1x4t316R9dDgBlsSi8mGu7pJjyJFzVaH+r7iqyHSELSQzVADjS0UgjlDKUUsLzVO98+9kSLGV5qaHXhjU0GWNSxk3hCIwnsfeMNCjTArLmHeUBodoLiE+R+jxuHPUZP4elGE3teonx2S/Q7lJzOUlkYQ+A4/xzyegzNhXmJpwTMXry9IFjcoa84O0r+QXpcK1cugCLREZadyoA19Ergxwf96nKjd1KqlYqmLQ540TUNwItUmRWdu3T36AODjwgpY9xqqAAAAAElFTkSuQmCC); +} +input[type=checkbox]:checked + label:hover { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEySURBVDhPnZLPSsNAEMa/XVPBCE0RhNy0OarP4Av4AD6JB0GwVBA8efBBxHsgh4CQswcRoUIpiIpVAm3zZ5M4szFSbQPBH3xkJvNNZskOer2eLIriKM/ze1JOcS1UHmdZduF5ngEKjr/fN4Z6+oKerwA2gxC4HAFPEWVLsAzgZAvYt3Q6Enw6jg7uBAaTFMNwhpnKdbXCkAJdy8ROu4XrXW2HTJIErHcFDD6nC02Mom8PwymeE2gvS0ZRBBaTlsOXEmdlrfLLOI7Bakrl/zWxCT8T/904f9QW/b06qtrCUdtFCqdjYs2Q2jAPX8c2XQd7Kr/wfV8vwIPs4Ga1ixe5Xrr/YFLTYfKIvWzM6ZtwXZdX7lxXG0L+sxXHcW5t254opRzawQ0S72+dPmjTroIgOP0CQSMt5LDn1T8AAAAASUVORK5CYII=); +} + +.icon-input input { + position: relative; + padding-left: 36px; +} +.icon-input span { + position: absolute; + left: 6px; + top: -2px; + font-family: hifi-glyphs; + font-size: 30px; + color: #afafaf; +} +.icon-input input:focus + span { + color: #ffffff; +} + +.selectable { + -webkit-touch-callout: text; + -webkit-user-select: text; + -khtml-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; + cursor: text; +} + +.color-box { + display: inline-block; + width: 15pt; + height: 15pt; + border: 0.75pt solid black; + margin: 1.5pt; + cursor: pointer; +} + +.color-box.highlight { + width: 13.5pt; + height: 13.5pt; + border: 1.5pt solid black; +} + + +.section-header, .sub-section-header, hr { + display: table; + width: 100%; + margin: 21px -21px 0 -21px; + padding: 14px 21px 0 21px; + font-family: Raleway-Regular; + font-size: 12px; + color: #afafaf; + height: 28px; + text-transform: uppercase; + outline: none; +} + +.section-header { + position: relative; + background: #404040 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAqCAIAAAAbNW1vAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAmSURBVChTY1BFAgzhSIDBAQmMcoYHRwIJMCgjAQZ9JMBgBQdWVgBh5XmBV5A2FQAAAABJRU5ErkJggg==) repeat-x top left; +} + +.sub-section-header, .no-collapse, hr { + background: #404040 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAjSURBVBhXY1RVVf3PgARYjIyMoEwIYHRwcEBRwQSloYCBAQCwjgPMiI7W2QAAAABJRU5ErkJggg==) repeat-x top left; +} + +.section-header:first-child { + margin-top: -2px; + padding-top: 0; + background: none; + height: auto; +} + +.sub-section-header { + margin-bottom: -10px; +} + +.section-header span { + font-family: HiFi-Glyphs; + font-size: 30px; + float: right; + position: absolute; + top: 4px; + right: 13px; +} + +.section-header[collapsed="true"] { + margin-bottom: -21px; +} + +hr { + border: none; + padding-top: 2px; +} + +.text-group[collapsed="true"] ~ .text-group, +.zone-group[collapsed="true"] ~ .zone-group, +.web-group[collapsed="true"] ~ .web-group, +.hyperlink-group[collapsed="true"] ~ .hyperlink-group, +.spatial-group[collapsed="true"] ~ .spatial-group, +.physical-group[collapsed="true"] ~ .physical-group, +.behavior-group[collapsed="true"] ~ .behavior-group, +.model-group[collapsed="true"] ~ .model-group, +.light-group[collapsed="true"] ~ .light-group { + display: none !important; +} + + +.property { + display: table; + width: 100%; + margin-top: 21px; + min-height: 28px; +} + +.property.checkbox { + width: auto; +} + +.property label, .number label { + display: table-cell; + vertical-align: middle; + font-family: Raleway-SemiBold; + font-size: 14px; +} +.property label .unit, .number label .unit { + margin-left: 8px; + font-family: Raleway-Light; + font-size: 13px; +} + +.value { + display: block; + min-height: 18px; +} +.value label { + display: inline-block; + vertical-align: top; + width: 48px; +} +.value span { + font-family: FiraSans-SemiBold; + font-size: 15px; +} + +.checkbox + .checkbox { + margin-top: 0; +} + +.checkbox-sub-props { + margin-top: 18px; +} + +.property .number { + float: left; +} +.property .number + .number { + margin-left: 10px; +} + +.text label, .url label, .number label, .textarea label, .rgb label, .xyz label, .pyr label, .dropdown label, .gen label { + float: left; + margin-left: 1px; + margin-bottom: 3px; + margin-top: -2px; +} + +.number > input { + clear: both; + float: left; +} +.number > span { + clear: both; + float: left; +} +.xyz > div, .pyr > div, .gen > div { + clear: both; +} + +.dropdown { + position: relative; + margin-bottom: -17px; +} + +.dropdown select { + clear: both; +} + +.dropdown dl { + clear: both; +} +.dropdown dl { + font-family: FiraSans-SemiBold; + font-size: 15px; + width: 292px; + height: 28px; + padding: 0 28px 0 12px; + color: #afafaf; + background: linear-gradient(#7d7d7d 20%, #686a68 100%); + position: relative; +} +.dropdown dl[dropped="true"] { + color: #404040; + background: linear-gradient(#afafaf, #afafaf); +} + +.dropdown dt { + height: 100%; + box-sizing: border-box; + border-right: 1px solid #121212; + width: 100%; +} +.dropdown dt:hover { + color: #404040; +} +.dropdown dt:focus { + outline: none; +} +.dropdown dt span:first-child { + display: inline-block; + position: relative; + top: 5px; +} +.dropdown dt span:last-child { + font-family: HiFi-Glyphs; + font-size: 42px; + float: right; + margin-right: -48px; + position: relative; + left: -12px; + top: -9px; +} + +.dropdown dd { + position: absolute; + top: 28px; + left: 3px; + display: none; +} +.dropdown dl[dropped="true"] dd { + display: block; +} + +.dropdown li { + list-style-type: none; + padding: 3px 0 1px 12px; + width: 320px; + height: auto; + font-family: FiraSans-SemiBold; + font-size: 15px; + color: #404040; + background-color: #afafaf +} +.dropdown li:hover { + background-color: #00b4ef; +} + +.dropdown dl[disabled="disabled"], .dropdown dl[disabled="disabled"][dropped="true"] { + color: #252525; + background: linear-gradient(#575757 20%, #252525 100%); +} +.dropdown dl[disabled="disabled"] dd { + display: none; +} +.dropdown dl[disabled="disabled"] dt:hover { + color: #252525; +} + + +div.refresh { + box-sizing: border-box; + padding-right: 44px; +} +div.refresh input[type="button"] { + float: right; + margin-right: -44px; +} + +.color-picker { + box-sizing: border-box; + float: left; + margin-bottom: 21px; + width: 36px; + height: 36px; + border: 4px solid #afafaf; + border-radius: 4px; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABTSURBVChTjcxBDoAwCETRwTs33sFjwB6uaCE1Ggvav5qQF7CSqu40dllHjYiOT3gh3yV8Ii+Fb+RNMEP9hm3sKENmBhG5P1aImWMH/EMerSAAOAFgTC/R8ZXSXAAAAABJRU5ErkJggg==); + background-position: bottom right; + background-repeat: no-repeat; +} +.color-picker:focus { + outline: none; +} +.color-picker[active="true"] { + border-color: #000; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABDSURBVChTjcoLCkAhCETRNq0tf97Y5xGZ1gVJ45TH6njThIO+xk2UwhWFcEdH6JCqOuiQiMDi/hcii3crRRb/7ggAPvIMVihQwvSXAAAAAElFTkSuQmCC); +} + +.color-picker[disabled="disabled"] { + border-color: #afafaf; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABTSURBVChTjcxBDoAwCETRwTs33sFjwB6uaCE1Ggvav5qQF7CSqu40dllHjYiOT3gh3yV8Ii+Fb+RNMEP9hm3sKENmBhG5P1aImWMH/EMerSAAOAFgTC/R8ZXSXAAAAABJRU5ErkJggg==); +} + +.colpick[disabled="disabled"] { + display: none !important; +} + + +.rgb label { + float: left; + margin-top: 10px; + margin-left: 21px; +} +.rgb label + * { + clear: both; +} + +.tuple div { + display: inline-block; + position: relative; + margin-right: 6px; +} +.tuple div:last-child { + margin-right: 0; +} + +.tuple label { + margin-right: -6px; +} + +.rgb .tuple input { + padding-left: 65px; +} +.xyz .tuple input { + padding-left: 25px; +} +.pyr .tuple input { + padding-left: 40px; +} + +.tuple div > label:first-child { + float: left; +} +.tuple div > label + input { + clear: both; + float: left; +} +.tuple div input + label { + display: inline !important; + float: none !important; + position: absolute; + margin-top: 8px; + margin-left: 6px; + left: 0; + font-family: FiraSans-SemiBold; + font-size: 12px; +} +.tuple .red + label, .tuple .x + label, .tuple .pitch + label { + color: #e2334d; +} +.tuple .green + label, .tuple .y + label, .tuple .yaw + label { + color: #1ac567; +} +.tuple .blue + label, .tuple .z + label, .tuple .roll + label { + color: #1080b8; +} + +.tuple .red:focus, .tuple .x:focus, .tuple .pitch:focus { + outline-color: #e2334d; +} +.tuple .green:focus, .tuple .y:focus, .tuple .yaw:focus { + outline-color: #1ac567; +} +tuple, .blue:focus, .tuple .z:focus, .tuple .roll:focus { + outline-color: #1080b8; +} + +.xyz .buttons input { + margin-top: 14px; +} +.xyz .buttons span { + word-wrap: nowrap; + white-space: nowrap; +} + +.row .property { + width: auto; + display: inline-block; + margin-right: 6px; +} +.row .property:last-child { + margin-right: 0; +} +.row .property input { + clear: both; + float: left; +} + +.two-column { + display: table; + width: 100%; +} +.two-column > div { + display: table-cell; + width: 50%; +} +.column { + vertical-align: top; +} + +.indent { + margin-left: 24px; +} + +::-webkit-scrollbar { + width: 20px; + height: 10px; +} +::-webkit-scrollbar-track { + background-color: #2e2e2e; +} +::-webkit-scrollbar-thumb { + background-color: #696969; + border: 2px solid #2e2e2e; + border-radius: 8px; +} + +/* FIXME: Revisit textarea resizer/corner when move to Qt 5.6 or later: see if can get resizer/corner to always be visible and +have correct background color with and without scrollbars. */ +textarea:enabled::-webkit-resizer { + background-size: 10px 10px; + background: #252525 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAXSURBVChTY1RVVWXADZigNA4wMqUZGACS3gCD5UUtKAAAAABJRU5ErkJggg==) no-repeat bottom right; +} +textarea:focus::-webkit-resizer { + background-size: 10px 10px; + background: #000000 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACOSURBVChThdC5DQQhDAXQvyRI5LQxFdABARWQElAPogYkiqEWQhLYGe8xxzJaS5a/8AuQHwDG2n+Lvee0hBDQWlO+hRvy3mNZFjDG5vCDOOeIMaL3/guPKISAWiu9n+AVSSlhraXdF86Qcw6tNdoTvEOlFOScd6iUOv3JGEMopYQx9jNvaawnoHnNr8Z4AuRLPOq2gPgnAAAAAElFTkSuQmCC) no-repeat bottom right; +} +textarea:enabled[scrolling="true"]::-webkit-resizer { + background-size: 10px 10px; + background: #2e2e2e url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACKSURBVChTjdAxDsMgDAXQT4UYuQIzCwsSKxsSJ4YDoByDY7AwUOG2aZMQqX+xhd9gzIwxA3/k8a7LCCFgraX+Fk4UY4RSCoyxNfwgzjlyzhhjXOEvSimhtUbvB3hGUkp472m2wxUKIaD3TnOCd6jWim3bvlBrfdjJOUeolEJoZj/4PMH83bl/BXgCWSs2Z09IjgoAAAAASUVORK5CYII=) no-repeat bottom right; +} + + +#entity-list-header { + margin-bottom: 36px; +} + +#entity-list-header div { + display: inline-block; + width: 65px; + margin-right: 6px; +} + +#entity-list-header div input:first-child { + margin-right: 0; + float: left; + width: 33px; + border-right: 1px solid #808080; + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +#entity-list-header div input:last-child { + margin-right: 0; + float: right; + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +#delete { + float: right; + margin-right: 0; + background-color: #ff0000; + min-width: 90px; +} + +#entity-list { + position: relative; /* New positioning context. */ +} + +#search-area { + padding-right: 168px; + padding-bottom: 24px; +} + +#filter { + width: 98%; +} + +#in-view { + position: absolute; + right: 126px; +} + +#radius-and-unit { + float: right; + margin-right: -168px; + position: relative; + top: -17px; +} +#radius-and-unit label { + margin-left: 2px; +} +#radius-and-unit input { + width: 120px; +} + +#entity-table-scroll { + /* Height is set by JavaScript. */ + width: 100%; + overflow-x: hidden; + overflow-y: auto; + box-sizing: border-box; + padding-top: 28px; /* Space for header and footer outside of scroll region. */ + margin-top: 28px; + border-left: 2px solid #575757; + border-right: 2px solid #575757; + background-color: #1c1c1c; +} + +#entity-table-scroll .glyph { + font-family: HiFi-Glyphs; + font-size: 15px; +} + +#entity-table { + margin-top: -28px; + margin-bottom: -18px; + table-layout: fixed; + border: none; + background-color: #1c1c1c; +} + +#entity-table thead tr, #entity-table thead tr th, +#entity-table tfoot tr, #entity-table tfoot tr td { + background: none; +} + +#entity-table .glyph { + margin: 0 -2px 0 -2px; + vertical-align: middle; +} + +#entity-table thead { + 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; + top: 49px; + left: 0; + width: 100%; + word-wrap: nowrap; + white-space: nowrap; + overflow: hidden; +} + +.verticesCount, .texturesCount, .texturesSize, .drawCalls { + text-align: right; +} + +#entity-table th { + display: inline-block; + box-sizing: border-box; + padding: 5px 0 0 0; + vertical-align: middle; + overflow: hidden; + text-overflow: ellipsis; +} + +#entity-table th:focus { + outline: none; +} + +#entity-table th .glyph { + position: relative; + left: 4px; +} +#entity-table th .glyph + .sort-order { + position: relative; + left: 4px; +} + +#entity-table th#entity-hasScript { + overflow: visible; +} + +#entity-table th#entity-hasScript .glyph { + text-transform: none; +} + +#entity-table thead .sort-order { + display: inline-block; + width: 8px; + margin: -5px 0 -3px 0; + vertical-align: middle; +} + +#entity-table th #info-toggle { + display: inline-block; + position: absolute; + left: initial; + right: 0; + width: 11px; + background-color: #1c1c1c; + z-index: 100; +} +#entity-table th #info-toggle span { + position: relative; + left: -2px; +} + +th#entity-hasTransparent .glyph { + font-weight: normal; + font-size: 24px !important; + margin: -6px; + position: relative; + top: -6px; +} +th#entity-hasTransparent .sort-order { + position: relative; + top: -4px; +} + +#entity-table td { + box-sizing: border-box; +} + +#entity-table td.glyph { + text-align: center; + padding: 0; +} +#entity-table td.hasTransparent.glyph { + font-size: 22px; + position: relative; + 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; + border-bottom-left-radius: 7px; + border-bottom-right-radius: 7px; + border-top: 1px solid #575757; + position: absolute; + bottom: -21px; + left: 0; + width: 100%; +} + + +#col-type { + width: 16%; +} +#col-name { + width: 34%; +} +#col-url { + width: 34%; +} +#col-locked, #col-visible { + width: 9%; +} +#col-verticesCount, #col-texturesCount, #col-texturesSize, #col-hasTransparent, #col-isBaked, #col-drawCalls, #col-hasScript { + width: 0; +} + +.showExtraInfo #col-type { + width: 10%; +} +.showExtraInfo #col-name { + width: 19%; +} +.showExtraInfo #col-url { + width: 19%; +} +.showExtraInfo #col-locked, .showExtraInfo #col-visible { + width: 4%; +} +.showExtraInfo #col-verticesCount { + width: 8%; +} +.showExtraInfo #col-texturesCount { + width: 8%; +} +.showExtraInfo #col-texturesSize { + width: 10%; +} +.showExtraInfo #col-hasTransparent { + width: 4%; +} +.showExtraInfo #col-isBaked { + width: 8%; +} +.showExtraInfo #col-drawCalls { + width: 8%; +} +.showExtraInfo #col-hasScript { + width: 6%; +} + +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, .isBaked, .drawCalls, .hasScript { + display: none; +} + +#entity-visible { + border: none; +} + +.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 .isBaked, .showExtraInfo .drawCalls, .showExtraInfo .hasScript { + display: table-cell; +} + +.showExtraInfo #entity-visible { + border-right: 1px solid #575757; +} + + +#no-entities { + display: none; + position: absolute; + top: 80px; + padding: 12px; + font-family: FiraSans-SemiBold; + font-size: 15px; + font-style: italic; + color: #afafaf; +} + + +#properties-header { + display: table-row; + height: 28px; +} + +#properties-header .property { + display: table-cell; + vertical-align: middle; +} +#properties-header .checkbox { + position: relative; + top: -1px; +} + +#properties-header #type-icon { + font-family: hifi-glyphs; + font-size: 31px; + color: #00b4ef; + margin: -4px 12px -4px -2px; + width: auto; + display: none; + vertical-align: middle; +} + +#properties-header #property-type { + padding: 5px 24px 5px 0; + border-right: 1px solid #808080; + height: 100%; + width: auto; + display: inline-block; + vertical-align: middle; +} + +#properties-header .checkbox:last-child { + padding-left: 24px; +} + +#properties-header .checkbox label { + background-position-y: 1px; +} + +#properties-header .checkbox label span { + font-family: HiFi-Glyphs; + font-size: 20px; + padding-right: 6px; + vertical-align: top; + position: relative; + top: -4px; +} + +#properties-header input[type=checkbox]:checked + label span { + color: #ffffff; +} + +#properties-header + hr { + margin-top: 12px; +} + + +#id label { + width: 24px; +} +#property-id { + display: inline-block; +} +#property-id::selection { + color: #000000; + background-color: #00b4ef; +} + +input#property-parent-id { + width: 340px; +} + +input#dimension-rescale-button { + min-width: 50px; + margin-left: 6px; +} +input#reset-to-natural-dimensions { + margin-right: 0; +} + +#animation-fps { + margin-top: 48px; +} + +#userdata-clear{ + margin-bottom: 10px; +} + + +#static-userdata{ + display: none; + z-index: 99; + position: absolute; + width: 96%; + padding-left:1%; + margin-top:5px; + margin-bottom:10px; + background-color: #2e2e2e; +} + +#userdata-saved{ + margin-top:5px; + font-size:16px; + display:none; +} diff --git a/scripts/system/html/css/record.css b/unpublishedScripts/marketplace/record/html/css/record.css similarity index 100% rename from scripts/system/html/css/record.css rename to unpublishedScripts/marketplace/record/html/css/record.css diff --git a/unpublishedScripts/marketplace/record/html/fonts/FiraSans-SemiBold.ttf b/unpublishedScripts/marketplace/record/html/fonts/FiraSans-SemiBold.ttf new file mode 100644 index 0000000000..821a43d7fd Binary files /dev/null and b/unpublishedScripts/marketplace/record/html/fonts/FiraSans-SemiBold.ttf differ diff --git a/unpublishedScripts/marketplace/record/html/fonts/FiraSans.license b/unpublishedScripts/marketplace/record/html/fonts/FiraSans.license new file mode 100644 index 0000000000..d444ea92b6 --- /dev/null +++ b/unpublishedScripts/marketplace/record/html/fonts/FiraSans.license @@ -0,0 +1,94 @@ +Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A. +with Reserved Font Name < Fira >, + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/unpublishedScripts/marketplace/record/html/fonts/Raleway-Bold.ttf b/unpublishedScripts/marketplace/record/html/fonts/Raleway-Bold.ttf new file mode 100644 index 0000000000..38c099cc85 Binary files /dev/null and b/unpublishedScripts/marketplace/record/html/fonts/Raleway-Bold.ttf differ diff --git a/unpublishedScripts/marketplace/record/html/fonts/Raleway-Light.ttf b/unpublishedScripts/marketplace/record/html/fonts/Raleway-Light.ttf new file mode 100644 index 0000000000..91aa0c701f Binary files /dev/null and b/unpublishedScripts/marketplace/record/html/fonts/Raleway-Light.ttf differ diff --git a/unpublishedScripts/marketplace/record/html/fonts/Raleway-Regular.ttf b/unpublishedScripts/marketplace/record/html/fonts/Raleway-Regular.ttf new file mode 100644 index 0000000000..e570a2d5c3 Binary files /dev/null and b/unpublishedScripts/marketplace/record/html/fonts/Raleway-Regular.ttf differ diff --git a/unpublishedScripts/marketplace/record/html/fonts/Raleway-SemiBold.ttf b/unpublishedScripts/marketplace/record/html/fonts/Raleway-SemiBold.ttf new file mode 100644 index 0000000000..ed0a8b9941 Binary files /dev/null and b/unpublishedScripts/marketplace/record/html/fonts/Raleway-SemiBold.ttf differ diff --git a/unpublishedScripts/marketplace/record/html/fonts/Raleway.license b/unpublishedScripts/marketplace/record/html/fonts/Raleway.license new file mode 100644 index 0000000000..1c9779ddcd --- /dev/null +++ b/unpublishedScripts/marketplace/record/html/fonts/Raleway.license @@ -0,0 +1,94 @@ +Copyright (c) 2010, Matt McInerney (matt@pixelspread.com), +Copyright (c) 2011, Pablo Impallari (www.impallari.com|impallari@gmail.com), +Copyright (c) 2011, Rodrigo Fuenzalida (www.rfuenzalida.com|hello@rfuenzalida.com), with Reserved Font Name Raleway +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/unpublishedScripts/marketplace/record/html/fonts/hifi-glyphs.ttf b/unpublishedScripts/marketplace/record/html/fonts/hifi-glyphs.ttf new file mode 100644 index 0000000000..93f6fe6d13 Binary files /dev/null and b/unpublishedScripts/marketplace/record/html/fonts/hifi-glyphs.ttf differ diff --git a/scripts/system/html/img/loader-red-countdown-ring.gif b/unpublishedScripts/marketplace/record/html/img/loader-red-countdown-ring.gif similarity index 100% rename from scripts/system/html/img/loader-red-countdown-ring.gif rename to unpublishedScripts/marketplace/record/html/img/loader-red-countdown-ring.gif diff --git a/scripts/system/html/js/record.js b/unpublishedScripts/marketplace/record/html/js/record.js similarity index 100% rename from scripts/system/html/js/record.js rename to unpublishedScripts/marketplace/record/html/js/record.js diff --git a/scripts/system/html/record.html b/unpublishedScripts/marketplace/record/html/record.html similarity index 100% rename from scripts/system/html/record.html rename to unpublishedScripts/marketplace/record/html/record.html diff --git a/scripts/system/playRecordingAC.js b/unpublishedScripts/marketplace/record/playRecordingAC.js similarity index 100% rename from scripts/system/playRecordingAC.js rename to unpublishedScripts/marketplace/record/playRecordingAC.js diff --git a/scripts/system/record.js b/unpublishedScripts/marketplace/record/record.js similarity index 100% rename from scripts/system/record.js rename to unpublishedScripts/marketplace/record/record.js diff --git a/unpublishedScripts/marketplace/stopwatch/models/transparent-box.fbx b/unpublishedScripts/marketplace/stopwatch/models/transparent-box.fbx new file mode 100644 index 0000000000..b1df7d962c Binary files /dev/null and b/unpublishedScripts/marketplace/stopwatch/models/transparent-box.fbx differ diff --git a/unpublishedScripts/marketplace/stopwatch/spawnStopwatch.js b/unpublishedScripts/marketplace/stopwatch/spawnStopwatch.js index e72f949163..3a0a8a506b 100644 --- a/unpublishedScripts/marketplace/stopwatch/spawnStopwatch.js +++ b/unpublishedScripts/marketplace/stopwatch/spawnStopwatch.js @@ -43,13 +43,47 @@ var minuteHandID = Entities.addEntity({ modelURL: Script.resolvePath("models/Stopwatch-min-hand.fbx"), }); +var startStopButtonID = Entities.addEntity({ + type: "Model", + name: "stopwatch/startStop", + parentID: stopwatchID, + dimensions: Vec3.multiply(scale, { x: 0.8, y: 0.8, z: 1.0 }), + localPosition: Vec3.multiply(scale, { x: 0, y: -0.1, z: -2.06 }), + modelURL: Script.resolvePath("models/transparent-box.fbx") +}); + +var resetButtonID = Entities.addEntity({ + type: "Model", + name: "stopwatch/startStop", + parentID: stopwatchID, + dimensions: Vec3.multiply(scale, { x: 0.6, y: 0.6, z: 0.8 }), + localPosition: Vec3.multiply(scale, { x: -1.5, y: -0.1, z: -1.2 }), + localRotation: Quat.fromVec3Degrees({ x: 0, y: 36, z: 0 }), + modelURL: Script.resolvePath("models/transparent-box.fbx") +}); + Entities.editEntity(stopwatchID, { userData: JSON.stringify({ secondHandID: secondHandID, - minuteHandID: minuteHandID, + minuteHandID: minuteHandID }), - script: Script.resolvePath("stopwatchClient.js"), serverScripts: Script.resolvePath("stopwatchServer.js") }); +Entities.editEntity(startStopButtonID, { + userData: JSON.stringify({ + stopwatchID: stopwatchID, + grabbableKey: { wantsTrigger: true } + }), + script: Script.resolvePath("stopwatchStartStop.js") +}); + +Entities.editEntity(resetButtonID, { + userData: JSON.stringify({ + stopwatchID: stopwatchID, + grabbableKey: { wantsTrigger: true } + }), + script: Script.resolvePath("stopwatchReset.js") +}); + Script.stop() diff --git a/unpublishedScripts/marketplace/stopwatch/stopwatchReset.js b/unpublishedScripts/marketplace/stopwatch/stopwatchReset.js new file mode 100644 index 0000000000..b65c1e7340 --- /dev/null +++ b/unpublishedScripts/marketplace/stopwatch/stopwatchReset.js @@ -0,0 +1,22 @@ +// +// stopwatchReset.js +// +// Created by David Rowe on 26 May 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 +// + +(function () { + this.preload = function (entityID) { + var properties = Entities.getEntityProperties(entityID, "userData"); + this.messageChannel = "STOPWATCH-" + JSON.parse(properties.userData).stopwatchID; + }; + function click() { + Messages.sendMessage(this.messageChannel, "reset"); + } + this.startNearTrigger = click; + this.startFarTrigger = click; + this.clickDownOnEntity = click; +}); diff --git a/unpublishedScripts/marketplace/stopwatch/stopwatchServer.js b/unpublishedScripts/marketplace/stopwatch/stopwatchServer.js index 925db565c3..6ae1b69087 100644 --- a/unpublishedScripts/marketplace/stopwatch/stopwatchServer.js +++ b/unpublishedScripts/marketplace/stopwatch/stopwatchServer.js @@ -13,6 +13,7 @@ self.equipped = false; self.isActive = false; + self.seconds = 0; self.secondHandID = null; self.minuteHandID = null; @@ -46,11 +47,19 @@ }; self.messageReceived = function(channel, message, sender) { print("Message received", channel, sender, message); - if (channel === self.messageChannel && message === 'click') { - if (self.isActive) { - self.resetTimer(); - } else { - self.startTimer(); + if (channel === self.messageChannel) { + switch (message) { + case "startStop": + if (self.isActive) { + self.stopTimer(); + } else { + self.startTimer(); + } + break; + case "reset": + self.stopTimer(); + self.resetTimer(); + break; } } }; @@ -58,14 +67,7 @@ return Entities.getEntityProperties(self.entityID, "position").position; }; self.resetTimer = function() { - print("Stopping stopwatch"); - if (self.tickInjector) { - self.tickInjector.stop(); - } - if (self.tickIntervalID !== null) { - Script.clearInterval(self.tickIntervalID); - self.tickIntervalID = null; - } + print("Resetting stopwatch"); Entities.editEntity(self.secondHandID, { localRotation: Quat.fromPitchYawRollDegrees(0, 0, 0), angularVelocity: { x: 0, y: 0, z: 0 }, @@ -74,7 +76,7 @@ localRotation: Quat.fromPitchYawRollDegrees(0, 0, 0), angularVelocity: { x: 0, y: 0, z: 0 }, }); - self.isActive = false; + self.seconds = 0; }; self.startTimer = function() { print("Starting stopwatch"); @@ -88,7 +90,6 @@ self.tickInjector.restart(); } - var seconds = 0; self.tickIntervalID = Script.setInterval(function() { if (self.tickInjector) { self.tickInjector.setOptions({ @@ -97,15 +98,15 @@ loop: true }); } - seconds++; + self.seconds++; const degreesPerTick = -360 / 60; Entities.editEntity(self.secondHandID, { - localRotation: Quat.fromPitchYawRollDegrees(0, seconds * degreesPerTick, 0), + localRotation: Quat.fromPitchYawRollDegrees(0, self.seconds * degreesPerTick, 0), }); - if (seconds % 60 == 0) { + if (self.seconds % 60 == 0) { Entities.editEntity(self.minuteHandID, { - localRotation: Quat.fromPitchYawRollDegrees(0, (seconds / 60) * degreesPerTick, 0), + localRotation: Quat.fromPitchYawRollDegrees(0, (self.seconds / 60) * degreesPerTick, 0), }); Audio.playSound(self.chimeSound, { position: self.getStopwatchPosition(), @@ -117,4 +118,15 @@ self.isActive = true; }; + self.stopTimer = function () { + print("Stopping stopwatch"); + if (self.tickInjector) { + self.tickInjector.stop(); + } + if (self.tickIntervalID !== null) { + Script.clearInterval(self.tickIntervalID); + self.tickIntervalID = null; + } + self.isActive = false; + }; }); diff --git a/unpublishedScripts/marketplace/stopwatch/stopwatchClient.js b/unpublishedScripts/marketplace/stopwatch/stopwatchStartStop.js similarity index 50% rename from unpublishedScripts/marketplace/stopwatch/stopwatchClient.js rename to unpublishedScripts/marketplace/stopwatch/stopwatchStartStop.js index 6284b86102..88c037ee36 100644 --- a/unpublishedScripts/marketplace/stopwatch/stopwatchClient.js +++ b/unpublishedScripts/marketplace/stopwatch/stopwatchStartStop.js @@ -1,20 +1,21 @@ // -// stopwatchServer.js +// stopwatchStartStop.js // -// Created by Ryan Huffman on 1/20/17. +// Created by David Rowe on 26 May 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 // -(function() { +(function () { var messageChannel; - this.preload = function(entityID) { - this.messageChannel = "STOPWATCH-" + entityID; + this.preload = function (entityID) { + var properties = Entities.getEntityProperties(entityID, "userData"); + this.messageChannel = "STOPWATCH-" + JSON.parse(properties.userData).stopwatchID; }; function click() { - Messages.sendMessage(this.messageChannel, 'click'); + Messages.sendMessage(this.messageChannel, "startStop"); } this.startNearTrigger = click; this.startFarTrigger = click;